@spikedpunch/mast 0.2.0 → 0.4.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 (150) hide show
  1. package/MAST_SPEC.md +169 -44
  2. package/README.md +266 -20
  3. package/assets/prime.md +18 -0
  4. package/assets/signals.md +51 -0
  5. package/assets/skill.md +13 -15
  6. package/dist/ast/types.d.ts +137 -1
  7. package/dist/ast/types.d.ts.map +1 -1
  8. package/dist/cli/docs-cmd.d.ts +1 -0
  9. package/dist/cli/docs-cmd.d.ts.map +1 -1
  10. package/dist/cli/docs-cmd.js +2 -1
  11. package/dist/cli/docs-cmd.js.map +1 -1
  12. package/dist/cli/hook-cmd.d.ts +3 -0
  13. package/dist/cli/hook-cmd.d.ts.map +1 -0
  14. package/dist/cli/hook-cmd.js +13 -0
  15. package/dist/cli/hook-cmd.js.map +1 -0
  16. package/dist/cli/hook-state-dir.d.ts +24 -0
  17. package/dist/cli/hook-state-dir.d.ts.map +1 -0
  18. package/dist/cli/hook-state-dir.js +57 -0
  19. package/dist/cli/hook-state-dir.js.map +1 -0
  20. package/dist/cli/hook.d.ts +44 -0
  21. package/dist/cli/hook.d.ts.map +1 -0
  22. package/dist/cli/hook.js +200 -0
  23. package/dist/cli/hook.js.map +1 -0
  24. package/dist/cli/index.js +12 -2
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/installed-artifacts.d.ts +23 -0
  27. package/dist/cli/installed-artifacts.d.ts.map +1 -0
  28. package/dist/cli/installed-artifacts.js +39 -0
  29. package/dist/cli/installed-artifacts.js.map +1 -0
  30. package/dist/cli/prime-cmd.d.ts +17 -0
  31. package/dist/cli/prime-cmd.d.ts.map +1 -0
  32. package/dist/cli/prime-cmd.js +70 -0
  33. package/dist/cli/prime-cmd.js.map +1 -0
  34. package/dist/cli/program.d.ts.map +1 -1
  35. package/dist/cli/program.js +6 -0
  36. package/dist/cli/program.js.map +1 -1
  37. package/dist/cli/query.d.ts +40 -1
  38. package/dist/cli/query.d.ts.map +1 -1
  39. package/dist/cli/query.js +36 -2
  40. package/dist/cli/query.js.map +1 -1
  41. package/dist/cli/search-cmd.d.ts +6 -1
  42. package/dist/cli/search-cmd.d.ts.map +1 -1
  43. package/dist/cli/search-cmd.js +9 -2
  44. package/dist/cli/search-cmd.js.map +1 -1
  45. package/dist/cli/serve.d.ts +6 -1
  46. package/dist/cli/serve.d.ts.map +1 -1
  47. package/dist/cli/serve.js +14 -4
  48. package/dist/cli/serve.js.map +1 -1
  49. package/dist/cli/setup-cmd.d.ts +34 -0
  50. package/dist/cli/setup-cmd.d.ts.map +1 -0
  51. package/dist/cli/setup-cmd.js +205 -0
  52. package/dist/cli/setup-cmd.js.map +1 -0
  53. package/dist/cli/setup-command.d.ts +32 -0
  54. package/dist/cli/setup-command.d.ts.map +1 -0
  55. package/dist/cli/setup-command.js +43 -0
  56. package/dist/cli/setup-command.js.map +1 -0
  57. package/dist/cli/setup-plan.d.ts +52 -0
  58. package/dist/cli/setup-plan.d.ts.map +1 -0
  59. package/dist/cli/setup-plan.js +221 -0
  60. package/dist/cli/setup-plan.js.map +1 -0
  61. package/dist/cli/setup-rules.d.ts +41 -0
  62. package/dist/cli/setup-rules.d.ts.map +1 -0
  63. package/dist/cli/setup-rules.js +76 -0
  64. package/dist/cli/setup-rules.js.map +1 -0
  65. package/dist/cli/setup-static.d.ts +13 -0
  66. package/dist/cli/setup-static.d.ts.map +1 -0
  67. package/dist/cli/setup-static.js +116 -0
  68. package/dist/cli/setup-static.js.map +1 -0
  69. package/dist/cli/skill-install.d.ts +2 -0
  70. package/dist/cli/skill-install.d.ts.map +1 -1
  71. package/dist/cli/skill-install.js +2 -0
  72. package/dist/cli/skill-install.js.map +1 -1
  73. package/dist/cli/status.d.ts +15 -0
  74. package/dist/cli/status.d.ts.map +1 -1
  75. package/dist/cli/status.js +32 -6
  76. package/dist/cli/status.js.map +1 -1
  77. package/dist/cli/upgrade-cmd.d.ts +5 -1
  78. package/dist/cli/upgrade-cmd.d.ts.map +1 -1
  79. package/dist/cli/upgrade-cmd.js +28 -3
  80. package/dist/cli/upgrade-cmd.js.map +1 -1
  81. package/dist/indexer/freshness.d.ts +10 -0
  82. package/dist/indexer/freshness.d.ts.map +1 -1
  83. package/dist/indexer/freshness.js +7 -1
  84. package/dist/indexer/freshness.js.map +1 -1
  85. package/dist/indexer/import-resolver.js +28 -16
  86. package/dist/indexer/import-resolver.js.map +1 -1
  87. package/dist/indexer/index.d.ts +38 -3
  88. package/dist/indexer/index.d.ts.map +1 -1
  89. package/dist/indexer/index.js +47 -4
  90. package/dist/indexer/index.js.map +1 -1
  91. package/dist/indexer/watcher.d.ts +94 -0
  92. package/dist/indexer/watcher.d.ts.map +1 -1
  93. package/dist/indexer/watcher.js +172 -19
  94. package/dist/indexer/watcher.js.map +1 -1
  95. package/dist/mcp/context.d.ts +16 -0
  96. package/dist/mcp/context.d.ts.map +1 -1
  97. package/dist/mcp/freshness-probe.d.ts +82 -0
  98. package/dist/mcp/freshness-probe.d.ts.map +1 -0
  99. package/dist/mcp/freshness-probe.js +111 -0
  100. package/dist/mcp/freshness-probe.js.map +1 -0
  101. package/dist/mcp/instructions.d.ts +7 -0
  102. package/dist/mcp/instructions.d.ts.map +1 -0
  103. package/dist/mcp/instructions.js +10 -0
  104. package/dist/mcp/instructions.js.map +1 -0
  105. package/dist/mcp/server.d.ts +54 -3
  106. package/dist/mcp/server.d.ts.map +1 -1
  107. package/dist/mcp/server.js +79 -4
  108. package/dist/mcp/server.js.map +1 -1
  109. package/dist/mcp/tools/_helpers.d.ts +40 -0
  110. package/dist/mcp/tools/_helpers.d.ts.map +1 -1
  111. package/dist/mcp/tools/_helpers.js +27 -0
  112. package/dist/mcp/tools/_helpers.js.map +1 -1
  113. package/dist/mcp/tools/callers.d.ts.map +1 -1
  114. package/dist/mcp/tools/callers.js +6 -1
  115. package/dist/mcp/tools/callers.js.map +1 -1
  116. package/dist/mcp/tools/dependencies.d.ts.map +1 -1
  117. package/dist/mcp/tools/dependencies.js +2 -1
  118. package/dist/mcp/tools/dependencies.js.map +1 -1
  119. package/dist/mcp/tools/exports.d.ts.map +1 -1
  120. package/dist/mcp/tools/exports.js +2 -1
  121. package/dist/mcp/tools/exports.js.map +1 -1
  122. package/dist/mcp/tools/implementors.d.ts.map +1 -1
  123. package/dist/mcp/tools/implementors.js +2 -1
  124. package/dist/mcp/tools/implementors.js.map +1 -1
  125. package/dist/mcp/tools/project-skeleton.d.ts.map +1 -1
  126. package/dist/mcp/tools/project-skeleton.js +2 -1
  127. package/dist/mcp/tools/project-skeleton.js.map +1 -1
  128. package/dist/mcp/tools/reindex.d.ts.map +1 -1
  129. package/dist/mcp/tools/reindex.js +3 -0
  130. package/dist/mcp/tools/reindex.js.map +1 -1
  131. package/dist/mcp/tools/rename-impact.d.ts.map +1 -1
  132. package/dist/mcp/tools/rename-impact.js +4 -1
  133. package/dist/mcp/tools/rename-impact.js.map +1 -1
  134. package/dist/mcp/tools/search.d.ts.map +1 -1
  135. package/dist/mcp/tools/search.js +13 -2
  136. package/dist/mcp/tools/search.js.map +1 -1
  137. package/dist/mcp/tools/signature.d.ts.map +1 -1
  138. package/dist/mcp/tools/signature.js +2 -1
  139. package/dist/mcp/tools/signature.js.map +1 -1
  140. package/dist/mcp/tools/status.d.ts.map +1 -1
  141. package/dist/mcp/tools/status.js +8 -2
  142. package/dist/mcp/tools/status.js.map +1 -1
  143. package/dist/store/config.d.ts.map +1 -1
  144. package/dist/store/config.js +4 -5
  145. package/dist/store/config.js.map +1 -1
  146. package/dist/store/defaults.d.ts +9 -0
  147. package/dist/store/defaults.d.ts.map +1 -0
  148. package/dist/store/defaults.js +12 -0
  149. package/dist/store/defaults.js.map +1 -0
  150. package/package.json +5 -2
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
  ---
@@ -142,12 +142,21 @@ mast index --incremental # reindex only what changed
142
142
  mast install-hooks # reindex automatically after commits and checkouts
143
143
  ```
144
144
 
145
+ Then tell your agent to use it. One command installs what your harness can take — hooks
146
+ that prime the agent at session start and remind it before a built-in search, or a rules
147
+ file where there are no hooks ([details](#tell-the-assistant-how-to-use-it)):
148
+
149
+ ```bash
150
+ mast setup claude # or cursor, vscode, windsurf, zed, desktop
151
+ ```
152
+
145
153
  Everything shipped with your build is readable offline, so you never have to work out
146
154
  which docs match your version:
147
155
 
148
156
  ```bash
149
157
  mast docs # list the topics
150
158
  mast docs spec # the full behavioural specification
159
+ mast docs signals # how to read the flags on an answer (stale, truncated, empty)
151
160
  mast skill # the instructions to paste into an agent prompt
152
161
  ```
153
162
 
@@ -170,6 +179,8 @@ claude mcp add mast -- mast serve
170
179
  Add `--scope project` to write `.mcp.json` into the repository so your team picks it up
171
180
  from the checkout.
172
181
 
182
+ To prime the agent at session start and remind it before a search: `mast setup claude`.
183
+
173
184
  ### Claude Desktop
174
185
 
175
186
  `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS,
@@ -190,6 +201,10 @@ from the checkout.
190
201
  Claude Desktop does not run in your project directory, so `MAST_STATE_DIR` must be
191
202
  absolute. The CLI and editor integrations below infer it from the working directory.
192
203
 
204
+ Claude Desktop has no hook system and no rules file mast can write. The instructions string
205
+ `mast serve` sends in the MCP handshake is its only channel, and it needs no setup;
206
+ `mast setup desktop` just says so.
207
+
193
208
  ### Cursor
194
209
 
195
210
  `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally:
@@ -202,6 +217,8 @@ absolute. The CLI and editor integrations below infer it from the working direct
202
217
  }
203
218
  ```
204
219
 
220
+ To add the hooks and a rules file: `mast setup cursor`.
221
+
205
222
  ### VS Code (GitHub Copilot)
206
223
 
207
224
  `.vscode/mcp.json`:
@@ -214,6 +231,8 @@ absolute. The CLI and editor integrations below infer it from the working direct
214
231
  }
215
232
  ```
216
233
 
234
+ To add the session-primer hook: `mast setup vscode`.
235
+
217
236
  ### Windsurf
218
237
 
219
238
  `~/.codeium/windsurf/mcp_config.json`:
@@ -226,6 +245,8 @@ absolute. The CLI and editor integrations below infer it from the working direct
226
245
  }
227
246
  ```
228
247
 
248
+ To add a rules file: `mast setup windsurf`.
249
+
229
250
  ### Zed
230
251
 
231
252
  `settings.json`:
@@ -238,16 +259,43 @@ absolute. The CLI and editor integrations below infer it from the working direct
238
259
  }
239
260
  ```
240
261
 
262
+ To add mast's instructions to a rules file you already keep (`.rules`, `AGENTS.md`, and the
263
+ others Zed reads): `mast setup zed`.
264
+
241
265
  ### Any other MCP client
242
266
 
243
- Run `mast serve` over stdio from the project root. It advertises eleven read tools and
244
- needs no arguments beyond `serve`.
267
+ Run `mast serve` over stdio from the project root. It advertises eleven tools — ten that
268
+ read and `mast_reindex`, which writes — and needs no arguments beyond `serve`.
245
269
 
246
270
  ### Tell the assistant how to use it
247
271
 
248
272
  Registering the server gives the model the tools; it does not tell it *when* to reach for
249
- them, or how to read a flagged answer. `mast skill` prints instructions written for that —
250
- paste them into your system prompt, `CLAUDE.md`, `.cursorrules`, or a skill file:
273
+ them. `mast setup <harness>` installs what each harness can take:
274
+
275
+ | harness | session primer | search reminder | static instructions |
276
+ |---|---|---|---|
277
+ | `claude` (Claude Code) | yes | yes, before `Grep` and `Glob` | none written |
278
+ | `cursor` | yes | yes, but after the search runs | `.cursor/rules/mast.mdc` |
279
+ | `vscode` (VS Code Copilot) | yes | no | none written |
280
+ | `windsurf` | no | no | `.windsurf/rules/mast.md`, or `.devin/rules/mast.md` if `.devin/` exists |
281
+ | `zed` | no | no | a marked block in the first rules file that exists |
282
+ | `desktop` (Claude Desktop) | no | no | none written |
283
+
284
+ The session primer is the rule set plus the live index state, given at session start. The
285
+ search reminder is one line naming `mast_search`, attached to a built-in search. Windsurf,
286
+ Zed and Claude Desktop have no hook system mast can use, so for them `mast setup` installs
287
+ the static file only, and says so. `mast serve` also sends a short instructions string when
288
+ the MCP connection opens; whether a harness passes that to the model is the harness's choice.
289
+
290
+ ```bash
291
+ mast setup claude # project scope; add --global for ~/.claude/settings.json
292
+ mast setup cursor --check # exit 0 only if the hooks and the rules file are current
293
+ mast setup windsurf --dry-run
294
+ mast setup zed --remove
295
+ ```
296
+
297
+ Anything else, or a harness not listed, can use `mast skill`, which prints the same short
298
+ instructions to paste into a system prompt, `CLAUDE.md`, or a skill file:
251
299
 
252
300
  ```bash
253
301
  mast skill # print it
@@ -261,6 +309,12 @@ marked block, so re-running after an upgrade replaces the previous copy instead
261
309
  a second one. It never runs on its own, and it never creates a config file you did not
262
310
  already keep.
263
311
 
312
+ **What is verified.** In Claude Code, both hooks were observed delivering their text to the
313
+ model in one session. For Cursor, VS Code and Windsurf, the hook formats and rules-file
314
+ locations were read from the vendor's documentation and nothing was run inside the tool.
315
+ The list of files Zed reads was not checked against Zed's documentation. Whether any of
316
+ this changes how often an agent uses mast has not been measured.
317
+
264
318
  ---
265
319
 
266
320
  ## Upgrading
@@ -279,6 +333,13 @@ it on the next `serve` or `index`. Nothing is lost that cannot be rebuilt — th
279
333
  derived state — but on a large monorepo it is minutes, and it is better known in advance
280
334
  than discovered as an unexplained stall.
281
335
 
336
+ If mast's hooks, rules files or skill blocks are installed in the project (or hook files
337
+ under your home directory), the report ends with an "After upgrading" block listing the
338
+ commands to re-run, such as `mast setup claude --check` and `mast skill --install`. It runs
339
+ on the version you have now, so it cannot know what the new version would write: it only
340
+ knows the files exist, and `--check` is what compares them. A project with none of these
341
+ gets no such block.
342
+
282
343
  ---
283
344
 
284
345
  ## Using MAST in a monorepo
@@ -289,7 +350,10 @@ tool rather than one index per package.
289
350
 
290
351
  **What is indexed.** `.ts`, `.tsx`, `.js`, `.jsx`, and `.md`, minus `node_modules`,
291
352
  `dist`, `build`, `coverage`, `.next`, `.turbo`, `.mast`, and test files. Override with
292
- `--extensions` and `--exclude` on `mast init`, or edit `.mast/config.json`.
353
+ `--extensions` and `--exclude` on `mast init`, or — for a setting the whole team should
354
+ get — `file_extensions` / `exclude_patterns` in `mast.config.json` at the project root.
355
+ Editing `.mast/config.json` also works and is read back, but that file is gitignored and
356
+ per-machine, so the change will not travel; `mast.config.json` outranks it.
293
357
 
294
358
  **Other languages are not indexed, and this matters.** MAST parses TypeScript and
295
359
  JavaScript only. A symbol defined in Python, Go, Java, or Rust is absent from the index,
@@ -297,7 +361,16 @@ which looks exactly like absent from the repository. Treat an empty result as "M
297
361
  not find it", never as "it does not exist" — `mast skill` says this to the model too.
298
362
 
299
363
  **Add `.mast/` to `.gitignore`.** It is derived state, it is large, and it is
300
- machine-specific.
364
+ machine-specific — on a 14k-file monorepo it is around 420 MB, almost all of it `graph.db`.
365
+ Ignore the whole directory, including `.mast/config.json`: that file is a *resolved*
366
+ snapshot and carries absolute paths (`project_root`, `resolved_state_dir`) that mean
367
+ nothing on anyone else's machine. The file meant to be committed is `mast.config.json` at
368
+ the project root — see the next paragraph.
369
+
370
+ **If you move the index, move the ignore rule with it.** `.mast/` is the default location,
371
+ not the only one: a `state_dir` in `mast.config.json`, a `MAST_STATE_DIR` in the
372
+ environment, or a `--state-dir` flag all put the index somewhere else, and a `.gitignore`
373
+ naming `.mast/` then protects nothing.
301
374
 
302
375
  **A custom index location is not remembered between runs.** `--state-dir` applies to the
303
376
  one command you pass it to. Path settings are deliberately never read back out of a
@@ -351,9 +424,19 @@ Options:
351
424
  -e, --exported Only exported symbols
352
425
  -f, --file <glob> Restrict to files matching a glob
353
426
  --state-dir <dir> State directory
427
+ --reindex Incrementally reindex before searching
354
428
  --json Emit the raw MCP response instead of text
355
429
  ```
356
430
 
431
+ `--reindex` exists because the CLI has neither freshness mechanism the MCP server has:
432
+ no file watcher, and no cached freshness probe (a one-shot process has no lifetime to
433
+ amortise one over). Without it, a file created since the last index run is invisible —
434
+ JIT staleness only re-parses files the index already knows. It is opt-in because it adds
435
+ a whole incremental index run to what is otherwise a single query, and it reports what it
436
+ did on **stderr**, so `--json` consumers keep a parseable stdout. A reindex that loses
437
+ `structure.lock` to a concurrent writer — likelier now that `mast serve` watches by
438
+ default — warns and queries the existing index rather than failing the search.
439
+
357
440
  **Why:** the fastest way to check what the index actually contains, and the same code path
358
441
  the MCP `mast_search` tool uses — it dispatches through the registered handler rather than
359
442
  re-implementing ranking, so CLI and assistant results cannot disagree. Staleness and
@@ -390,10 +473,16 @@ Start the MCP server over stdio.
390
473
  Options:
391
474
  --state-dir <dir> State directory
392
475
  --no-startup-reindex Skip the startup staleness check (not recommended)
476
+ --no-watch Do not watch source files (batch/container use)
393
477
  --watch Watch source files and incrementally reindex on change
394
- (interactive use; not needed in the container ladder)
478
+ (the default; accepted for compatibility)
395
479
  ```
396
480
 
481
+ Watching is **on by default**. JIT staleness only re-parses files the index already knows,
482
+ so without a watcher a file created during a session stays invisible to every read tool
483
+ until something reindexes — and nothing does. A watcher failure (EMFILE, permissions) logs
484
+ a warning and the server keeps serving, so the default cannot stop `serve` from starting.
485
+
397
486
  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
487
 
399
488
  ---
@@ -442,6 +531,7 @@ Invoke any MCP read tool directly, with byte-identical output to the MCP transpo
442
531
  ```
443
532
  Options:
444
533
  --state-dir <dir> State directory
534
+ --reindex Incrementally reindex before querying (see `mast search --reindex`)
445
535
  --json Emit the exact single-line MCP response (default pretty-prints)
446
536
  ```
447
537
 
@@ -459,8 +549,9 @@ tool that does not exist lists the ones that do.
459
549
 
460
550
  ### `mast docs [topic]`
461
551
 
462
- Print documentation shipped with the installed build — `readme`, `spec`, or `skill`. No
463
- argument lists the topics with the version they belong to.
552
+ Print documentation shipped with the installed build — `readme`, `spec`, `skill`, or
553
+ `signals` (the staleness, truncation and emptiness flags on an answer). No argument lists
554
+ the topics with the version they belong to.
464
555
 
465
556
  **Why:** removes the step where a reader looks up their version and then finds docs for a
466
557
  different one. What `mast docs` prints is what the binary in your `node_modules` does.
@@ -479,10 +570,117 @@ Options:
479
570
  ```
480
571
 
481
572
  **Why:** registering the MCP server gives a model the tools but not the judgement — when to
482
- search instead of reading, that code tokens beat prose in a query, and how to read a
483
- staleness or truncation flag. It also tells the model that an empty result means "MAST did
484
- not find it", not "it does not exist", which is the single most consequential thing to get
485
- right about a search tool.
573
+ search instead of reading, and that code tokens beat prose in a query. The text is short
574
+ (under 4,000 characters) so it fits a rules file. It also tells the model that an empty
575
+ result means "MAST did not find it", not "it does not exist", which is the single most
576
+ consequential thing to get right about a search tool, and points at `mast docs signals` for
577
+ how to read the individual flags.
578
+
579
+ ---
580
+
581
+ ### `mast prime [path]`
582
+
583
+ Print the session primer: a short rule set for using mast, plus the live index health.
584
+
585
+ ```
586
+ Options:
587
+ --state-dir <dir> State directory
588
+ ```
589
+
590
+ **Why:** it is the text an agent should be given at the start of a session. When the index
591
+ is fresh or stale it prints the rules and the file count and age of the index (a stale
592
+ index adds the changed/unindexed/deleted split and a `mast_reindex` instruction). When
593
+ there is no index, or the index describes a different tree, it withholds the rules and says
594
+ what is wrong. It exits 0 in every state, unlike `mast status`.
595
+
596
+ ---
597
+
598
+ ### `mast hook <harness> <event>`
599
+
600
+ The entry point for agent hooks. Reads the harness's hook JSON on stdin and writes that
601
+ harness's JSON envelope on stdout, or writes nothing. `<harness>` is `claude`, `cursor` or
602
+ `vscode`; `<event>` is `session-start` or `search`.
603
+
604
+ - `session-start` emits the same text `mast prime` prints for the project (the hook's `cwd`,
605
+ else the first workspace root, else the current directory), in every index state.
606
+ - `search` emits a one-line reminder to try `mast_search` first. It stays silent when no
607
+ index exists for the project, or when the search is scoped by `type`, `glob` or `path` to
608
+ a language mast does not index. "Indexes" follows the project's own `file_extensions`
609
+ where it sets one, so a project that indexes `.mjs` is reminded on a `*.mjs` search.
610
+
611
+ ```
612
+ Arguments:
613
+ harness claude | cursor | vscode
614
+ event session-start | search
615
+ ```
616
+
617
+ **Why:** a hook that fails breaks the user's session, so this command exits 0 for every
618
+ ordinary condition (empty or malformed stdin, an unknown harness or event, a missing index)
619
+ and leaves stdout empty, with one line on stderr where something went wrong. `search` runs
620
+ before every Grep, so it is dispatched before the rest of the CLI loads.
621
+
622
+ ---
623
+
624
+ ### `mast setup <harness> [path]`
625
+
626
+ Install what tells an agent to use mast: hooks that prime it at session start and remind it
627
+ before a built-in search, and rules files for harnesses that read them. `<harness>` is
628
+ `claude`, `cursor`, `vscode`, `windsurf`, `zed` or `desktop`; `[path]` is the project root
629
+ (default: the current directory).
630
+
631
+ | harness | project file (default) | `--global` |
632
+ |---|---|---|
633
+ | `claude` | `.claude/settings.json` | `~/.claude/settings.json` |
634
+ | `cursor` | `.cursor/hooks.json` and `.cursor/rules/mast.mdc` | `~/.cursor/hooks.json` only; Cursor has no user-level rules file, and a note says so |
635
+ | `vscode` | `.github/hooks/mast.json` | `~/.copilot/hooks/mast.json` |
636
+ | `windsurf` | `.windsurf/rules/mast.md`, or `.devin/rules/mast.md` if a `.devin` directory exists | refused (exit 1) |
637
+ | `zed` | a marked block in the first existing of `.rules`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` | refused (exit 1) |
638
+ | `desktop` | nothing | nothing |
639
+
640
+ ```
641
+ Options:
642
+ --global Write the user-level file instead of the project one
643
+ --check Write nothing; exit 0 only if everything is installed and current
644
+ --remove Remove what mast installed
645
+ --dry-run Print what would be written, and write nothing
646
+ ```
647
+
648
+ `claude` and `cursor` get both hooks (`mast hook <harness> session-start` and
649
+ `mast hook <harness> search`). **`vscode` gets the session primer only:** VS Code ignores hook
650
+ matchers, so a search hook would run before every tool call, and the name of its search tool
651
+ is not documented. **`windsurf`, `zed` and `desktop` have no hook system mast can use**, so
652
+ the first line of output says that only static instructions are installed. `desktop` writes
653
+ nothing: its only channel is the instructions string `mast serve` sends in the MCP handshake,
654
+ which needs no setup, and every flag exits 0 after saying so.
655
+
656
+ The rules files carry the text `mast skill` prints. `mast.mdc` (frontmatter `description` and
657
+ `alwaysApply: true`) and the Windsurf `mast.md` (frontmatter `trigger: always_on`) belong to
658
+ mast outright, and are overwritten if they differ; there is no marker, the name is the claim.
659
+ The Windsurf file stays under Windsurf's documented 12,000-character limit, checked in a test.
660
+ `zed` never creates a file: if none of Zed's candidates exists it writes nothing and exits 1,
661
+ telling you to create `.rules` and run it again, and otherwise it splices the same marked
662
+ block `mast skill --install` uses into the first one that exists. `--remove` takes out that
663
+ block, from every candidate that carries it, and leaves the file. The Zed file list is
664
+ from memory of Zed's documentation and has not been checked against it.
665
+
666
+ **Why:** the hook command depends on how mast is installed. A global install writes
667
+ `mast hook ...`; a source checkout writes `node "<path to dist/cli/index.js>" hook ...`, a
668
+ path specific to your machine, so do not commit that file; a project dependency writes a path
669
+ into `node_modules/.bin`. A project dependency cannot be installed with `--global`: a
670
+ user-level hook pointing into one project's `node_modules` breaks in every other project.
671
+ For VS Code the relative `node_modules/.bin/mast` is unverified, because its docs do not
672
+ say what directory hooks run in.
673
+
674
+ For hook files it only ever touches entries whose command ends in `hook <harness> session-start`
675
+ or `hook <harness> search`. Every other key, hook and field in the file is kept, in order, and
676
+ the file keeps its indentation and trailing newline. Re-running changes nothing and does not
677
+ write any file; a changed install (say, source to global) replaces the entry in place. A file
678
+ that is not valid JSON, or whose `hooks` have an unexpected shape, is reported and left
679
+ untouched (exit 1). `--remove` deletes only mast's entries, and for `vscode` deletes
680
+ `mast.json` once nothing else is in it. `--check` cannot be combined with `--remove` or
681
+ `--dry-run` (exit 2), nor can an unknown harness be given (exit 2). For `cursor`, `--check`
682
+ exits 0 only if both the hooks file and the rules file are current. Nothing here has been
683
+ run inside Cursor, VS Code, Windsurf or Zed.
486
684
 
487
685
  ---
488
686
 
@@ -493,7 +691,9 @@ Check for a newer release; print how to install it, and what it will cost.
493
691
  **Why:** it detects how MAST was installed and prints the matching command rather than
494
692
  running it, because a CLI cannot reliably distinguish a global install from a dev
495
693
  dependency. It also reports whether the upgrade bumps the index schema — which forces a
496
- full reindex on the next `serve` — and your package manager cannot tell you that.
694
+ full reindex on the next `serve` — and your package manager cannot tell you that. When
695
+ mast's hooks, rules files or skill blocks are installed, it lists the `mast setup <harness>
696
+ --check` and `mast skill --install` commands to re-run afterwards.
497
697
 
498
698
  ---
499
699
 
@@ -512,6 +712,31 @@ MAST registers 11 tools with the MCP server. Every read tool includes a `_stats`
512
712
  }
513
713
  ```
514
714
 
715
+ ### The signals
716
+
717
+ Beyond the results, a response carries fields describing what MAST **does not know** about
718
+ the answer it just gave. All but the last are omitted entirely when they do not apply, so
719
+ their absence carries meaning and their presence is never noise. `truncated` is the one
720
+ exception — it is a required field on every `type_context` entry and is present-and-`false`
721
+ in the ordinary case, so read it, don't test for it.
722
+
723
+ | signal | carried by | means |
724
+ |---|---|---|
725
+ | `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)) |
726
+ | `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 |
727
+ | `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 |
728
+ | `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 |
729
+ | `results_truncated` | `mast_signature`, `mast_implementors` | the list was capped at `limit`; the field carries the real, uncapped total |
730
+ | `exports_truncated` | `mast_exports` | the same, for a module's export list |
731
+ | `potential_truncated` | `mast_callers`, `mast_rename_impact` | the unresolved-candidate fetch was capped at 50; the field carries the real match count |
732
+ | `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 |
733
+
734
+ Two distinctions that are not flags but carry the same weight: in `mast_callers` and
735
+ `mast_rename_impact`, a **`verified_caller`** carries a `resolution` naming how the edge was
736
+ statically proven and is safe to act on, while a **`potential_match`** carries a `reason` and
737
+ is a name match with no proven edge — review before editing. And an empty result is never
738
+ proof of absence: MAST indexes TypeScript, JavaScript, and Markdown only.
739
+
515
740
  ---
516
741
 
517
742
  ### `mast_search`
@@ -529,7 +754,7 @@ Lexical BM25 + declaration-exact search over the indexed codebase.
529
754
  }
530
755
  ```
531
756
 
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.
757
+ **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
758
 
534
759
  **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
760
 
@@ -776,13 +1001,34 @@ with default `k = 60`. A chunk appearing at rank 1 in both lists scores twice as
776
1001
 
777
1002
  ### JIT Staleness Checks
778
1003
 
779
- Every read tool (search, exports, signature, callers, dependencies, implementors) calls `jitRefreshFile` before returning results. This function:
1004
+ Read tools handle a file that changed since it was indexed in one of **two** ways. Which one a tool
1005
+ uses is fixed per tool, not decided at runtime:
1006
+
1007
+ **Re-parse inline** — `mast_signature`, `mast_callers`, `mast_exports`, `mast_dependencies`,
1008
+ `mast_rename_impact`. Before returning, each calls `jitRefreshFile`, which:
780
1009
 
781
- 1. Reads the stored mtime for the file from the `files` table.
1010
+ 1. Reads the stored mtime for the file from the `files` table. **If there is no row, it returns
1011
+ immediately** — an unindexed file has no stored mtime to compare against, which is the whole of
1012
+ why JIT cannot discover new files.
782
1013
  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.
1014
+ 3. If the disk mtime is newer, acquires the `structure.lock` and re-parses the file immediately,
1015
+ on the request path — the answer waits for it.
1016
+
1017
+ If the lock is held by a concurrent writer the previous chunk is returned with
1018
+ `file_busy_returning_stale_cache` set, so the caller is told rather than quietly served stale data.
1019
+
1020
+ **Stat and flag** — `mast_search`, `mast_implementors`. These call `findStaleFiles`, which stats the
1021
+ files behind the results and sets `stale: true` on the affected ones. **No re-parse is attempted**:
1022
+ taking a write lock on a ranked result set would serialise the cheapest tools in the package behind
1023
+ the most expensive operation in it. The line coordinates on a flagged result may be off; the caller
1024
+ is expected to re-read or call a re-parsing tool.
1025
+
1026
+ **Neither** — `mast_project_skeleton` (documented exempt) and `mast_efficiency`.
784
1027
 
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.)
1028
+ Both mechanisms only ever touch files the index **already knows**. Neither can discover a file that
1029
+ was never indexed, so a file created during a session is invisible to every read tool until
1030
+ something reindexes it — which is why `mast serve` watches by default, and why every
1031
+ primary-result tool reports `unindexed_files` when the index is nonetheless behind.
786
1032
 
787
1033
  ### Startup Ladder
788
1034
 
@@ -0,0 +1,18 @@
1
+ # Using mast
2
+
3
+ mast indexes this repository's TypeScript, JavaScript and Markdown at the AST level.
4
+
5
+ - To find code (a symbol, its callers, a module's exports), call mast before grep, glob or
6
+ reading whole files. It returns ranked declarations, not line matches.
7
+ - Put code tokens in the query: function, type and column names. An exact symbol name
8
+ anchors its declaration to the top. If a query returns nothing, change the vocabulary.
9
+ - `mast_search`: discover code by name or token.
10
+ - `mast_signature`: a symbol's declaration and resolved parameter types.
11
+ - `mast_callers`: who calls a function. Run it before any refactor.
12
+ - `mast_exports`: a module's public API, without reading the file.
13
+ - `mast_rename_impact`: the checklist for a rename.
14
+ - Use grep for other languages, non-code files, or an exact regex over text.
15
+ - An empty result is not proof of absence. Check `index_empty`, `unindexed_files` and
16
+ `stale` on the response before concluding something does not exist.
17
+ - Call `mast_reindex` after creating or renaming files. A new file is invisible to every
18
+ read tool until an index pass runs.
@@ -0,0 +1,51 @@
1
+ # MAST signal reference
2
+
3
+ The fields MAST puts on an answer to say what it does not know, and how it treats a file that
4
+ changed after it was indexed. `mast skill` carries the short rules; this is the detail behind them.
5
+
6
+ ## How MAST handles a file that changed since it was indexed
7
+
8
+ Two mechanisms, and which one you get depends on the tool. Neither can see a file that
9
+ was never indexed at all.
10
+
11
+ - **Re-parsed for you, before the answer** — `mast_signature`, `mast_callers`,
12
+ `mast_exports`, `mast_dependencies`, `mast_rename_impact`. If the re-parse loses a
13
+ race with a writer, the result carries `file_busy_returning_stale_cache`.
14
+ - **Flagged, not re-parsed** — `mast_search`, `mast_implementors`. Affected results
15
+ carry `stale: true`. The code and line numbers shown may be out of date; call one of
16
+ the tools above, or `mast_reindex`, to get the current version.
17
+
18
+ Both only cover files already in the index. That is why creating a file needs an
19
+ explicit `mast_reindex`.
20
+
21
+ ## Reading the answers honestly
22
+
23
+ MAST reports what it does not know. These signals are load-bearing. All but the last are
24
+ **omitted entirely when they do not apply** — so their absence is meaningful, and their
25
+ presence is never `false`. `truncated` is the exception: it is always present on a
26
+ `type_context` entry, so check its value rather than its presence.
27
+
28
+ | signal | on | means |
29
+ |---|---|---|
30
+ | `stale` | per result of `mast_search`, `mast_implementors` | this result's file changed since indexing; line numbers may be wrong |
31
+ | `file_busy_returning_stale_cache` | the five re-parsing tools | a refresh was attempted and lost to a writer; retry shortly |
32
+ | `index_empty` | the empty answer of any of the eight tools that return a result set | **nothing is indexed at all.** This is not "no match" — run `mast_reindex`, or check that `mast_status` names the tree you meant |
33
+ | `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) | that many files on disk are not in the index, so this answer was computed over an incomplete corpus. **An empty result carrying this is not evidence of absence.** The set-returning tools report it even with hits — a short list of callers reads exactly like a complete one |
34
+ | `results_truncated` | `mast_signature`, `mast_implementors` | you got the first page, not the answer. The field carries the real total; raise `limit` or narrow the query |
35
+ | `exports_truncated` | `mast_exports` | the same, for a module's export list |
36
+ | `potential_truncated` | `mast_callers`, `mast_rename_impact` | the unresolved-candidate set was capped; the real count is larger |
37
+ | `truncated` | a type in `mast_signature`'s `type_context` | that declaration was clipped at 50 lines |
38
+
39
+ Two more things that are not flags:
40
+
41
+ - In `mast_callers` and `mast_rename_impact`, **`verified_callers` and
42
+ `potential_matches` are not the same claim.** A verified caller carries a `resolution`
43
+ and is safe to act on. A potential match carries a `reason` and is a name match with
44
+ no proven edge — review it before editing it.
45
+ - An **empty result is not proof of absence.** MAST indexes TypeScript, JavaScript, and
46
+ Markdown only — a symbol defined in Python, Go, Java, or any other language is absent
47
+ from the index, not absent from the repository. Check `index_empty` and
48
+ `unindexed_files` before concluding "it isn't there", and **never delete or rewrite
49
+ code on the strength of an empty result alone.** Every result-set tool carries
50
+ `unindexed_files` now, not just `mast_search` — so an empty `mast_callers` answer that
51
+ carries it is not a green light to delete the function.
package/assets/skill.md CHANGED
@@ -6,13 +6,19 @@ graph, so prefer it over reading files or grepping.
6
6
 
7
7
  ## Rules
8
8
 
9
- - Call `mast_status` at the start of a session to confirm the index is fresh.
9
+ - Call `mast_status` at the start of a session to confirm the index is fresh. Read
10
+ **`stale_breakdown.unindexed`**: any non-zero value means files exist that MAST has never
11
+ seen, and no amount of querying will find them. Read that field rather than
12
+ `freshness_cause`, which reports a single ranked cause and shows `"phase1_stale"` ahead of
13
+ `"unindexed_files"` whenever both are non-zero.
10
14
  - **Search before opening any file.** No file path without a MAST result behind it.
11
15
  - **Use code tokens in queries** — function names, type names, column names.
12
16
  `createTable uuid primaryKey` beats `migration pattern`. An exact symbol name in the
13
17
  query anchors its declaration to the top.
14
18
  - When a query returns nothing, **change vocabulary — do not repeat it**.
15
- - Call `mast_reindex` after writing files, to keep the index current.
19
+ - **Call `mast_reindex` after creating or renaming files**, before any query that depends
20
+ on what you just wrote. Editing the *body* of a file MAST already knows is handled for
21
+ you; a **new** file is not, and is invisible until an index pass runs.
16
22
 
17
23
  ## Picking the right tool
18
24
 
@@ -30,16 +36,8 @@ graph, so prefer it over reading files or grepping.
30
36
  | `mast_status` | index freshness and health |
31
37
  | `mast_reindex` | refresh the index after edits |
32
38
 
33
- ## Reading the answers honestly
34
-
35
- MAST reports what it does not know, and those signals are load-bearing:
36
-
37
- - A result carrying **`file_busy_returning_stale_cache`** or a staleness flag means the
38
- answer may predate the file on disk. Do not treat it as current.
39
- - A **`truncated`** or **`potential_truncated`** flag means the set was capped. The real
40
- count is larger than what you were shown.
41
- - An **empty result is not proof of absence.** MAST indexes TypeScript, JavaScript, and
42
- Markdown only — a symbol defined in Python, Go, Java, or any other language is absent
43
- from the index, not absent from the repository. Check the flags before concluding
44
- "it isn't there", and never delete or rewrite code on the strength of an empty result
45
- alone.
39
+ An empty result is not proof of absence. MAST indexes TypeScript, JavaScript, and Markdown
40
+ only, so a symbol in any other language is absent from the index, not from the repository.
41
+ Check `index_empty` and `unindexed_files` on the response before concluding "it isn't
42
+ there", and never delete or rewrite code on an empty result alone. `mast docs signals`
43
+ prints the full signal reference.