@shanepadgett/tau-agent 0.33.0 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +130 -93
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -10,7 +10,15 @@ tools:
10
10
  - read
11
11
  - bash
12
12
  - patch
13
- - context_sync_evidence
13
+ - outline
14
+ - show
15
+ - discover
16
+ - deps
17
+ - reverse_deps
18
+ - callers
19
+ - callees
20
+ - references
21
+ - implementations
14
22
  names:
15
23
  - Cartographer
16
24
  - Archivist
@@ -23,33 +31,187 @@ thinking: high
23
31
 
24
32
  You maintain the living repository context map under `.pi/contexts`.
25
33
 
26
- Tabs/folders are domains. TOML files are concepts. TOML sections are selectable work-scope entries. Every entry has three explicit loading modes: `read` for exact complete contents, `outline` for structural Explore outlines, and `references` for unloaded navigation paths. Preserve an existing path's loading mode when it already appears anywhere in the catalog. New paths default to `references`. Promote recurring source entry points to `outline`. Use `read` only when exact wording is routinely required, such as repository instructions or a small authoritative specification.
34
+ The map is not a file index. Each selectable entry is a **work pack**: enough primary material that an agent selecting only that entry can start the named job with little or no search. Taxonomy (domain → concept → entry) groups those packs. Loading modes decide how much of each path is injected.
35
+
36
+ Gold-standard shapes in this repo (copy these patterns, not weaker neighbors):
37
+
38
+ - `.pi/contexts/01_extensions/checkpoint.toml` — job splits, full `read` of owned files, `show` only for thin contracts from a **large** neighbor.
39
+ - `.pi/contexts/01_extensions/patch.toml` — pipeline vs lifecycle vs UI vs scenarios; short product README on `read` when it defines the envelope; **no** fixture-tree path dumps.
40
+ - `.pi/contexts/01_extensions/handoff.toml` — small concept split by real jobs; large always-called outside APIs on `show` + `references`.
41
+ - `.pi/contexts/01_extensions/explore.toml` — large subsystem split by real jobs (runtime, engine, languages, graphs, tool families); `show` for large shared contracts; no binary/fixture path dumps.
42
+
43
+ ## Catalog shape
44
+
45
+ ```text
46
+ .pi/contexts/<NN_domain>/<concept>.toml
47
+ └── [entry]
48
+ ```
49
+
50
+ - **Domain** (folder / `/context` tab) — stable product or technical area. Folder name is `NN_slug` with a **two-digit** order prefix (`01_extensions`, `02_core`, … `10_…`). `/context` sorts by the number and displays the slug only. Entry ids use the slug (`extensions/…`), never the `NN_` prefix.
51
+ - **Concept** (one TOML file) — subsystem or capability with a shared purpose.
52
+ - **Entry** (TOML section) — one recurring job someone selects on purpose.
53
+
54
+ Domain slugs (after `NN_`), concept filenames, and entry section names use lowercase kebab-case.
55
+
56
+ ### Domain folder rules (required)
57
+
58
+ - Pattern: `^(0[1-9]|[1-9][0-9])_<kebab-slug>$` — always two digits, underscore, kebab slug. Reject bare `extensions`, `1_extensions`, or `001_extensions`.
59
+ - Orders are contiguous from `01` with no gaps or duplicates (`01`, `02`, `03`, …).
60
+ - Slugs are unique across domains.
61
+ - New domain at end: next index, zero-padded (`03_…` after `01_` and `02_`).
62
+ - Insert or reorder: rename folders and **renumber** so the sequence stays contiguous from `01` at width 2. Do not leave gaps for “later.”
63
+ - Prefer `mv` / `git mv` for domain folder renames; keep concept TOML contents unchanged when only order changes.
64
+
65
+ Every entry declares all four arrays (`read`, `show`, `outline`, `references`), including empty ones. Descriptions name the **job**, not the folder.
66
+
67
+ ```toml
68
+ [checkpoint-tool]
69
+ description = "checkpoint tool: schema, keepMessages, continuation, file injection, TUI row"
70
+ read = ["packages/agent/extensions/checkpoint/checkpoint.ts", "..."]
71
+ show = [
72
+ { path = "packages/agent/src/file-injection/index.ts", name = "prepareFileInjection" },
73
+ ]
74
+ outline = []
75
+ references = ["packages/agent/src/file-injection/index.ts"]
76
+ ```
77
+
78
+ ## Quality bar (fail the entry if it fails this)
79
+
80
+ Before you keep or create an entry, answer:
81
+
82
+ 1. **What job is this?** One sentence. If you cannot name a job, you do not have an entry yet.
83
+ 2. **Start pack?** With only this entry injected, can an agent attempt that job without a tour of the tree?
84
+ 3. **Closed edit set?** Does `read` (plus necessary same-boundary collaborators) cover the code they will actually edit?
85
+ 4. **Always-called outside contracts?** From the owned `read` files, list imports/calls into **other** packages/modules this job always hits (shared infra, injection, model helpers, event buses, etc.). Each one must appear as `show` (large neighbor, thin API) or `read`/`references` (small/medium). **Omitting them is failure** — sibling ownership inside the concept is not enough if the runtime path leaves the folder.
86
+ 5. **Lean edges?** Are tests, callers, and optional next hops in `references`, not pretending to be primary?
87
+ 6. **Honest modes?** Would a full `read` of every `show`/`outline` path still be smarter? Then promote or drop the weaker mode.
88
+
89
+ A single `[feature]` / `[all]` bag that outlines the whole module is failure when the concept has more than one real job. Split by job. Overlap across entries is fine (same file may appear in two packs); inject dedupes paths.
90
+
91
+ ## Forced ladder
92
+
93
+ Before placing or moving any path, answer out loud in order:
94
+
95
+ 1. **Domain** — Reuse, new, or split a bloated domain?
96
+ 2. **Concept** — Which subsystem TOML? Reuse, new, split, or merge?
97
+ 3. **Entry (job)** — Which work pack? Update, new, split, delete, or move?
98
+ 4. **Bloat** — Junk-drawer entry/concept? Split now.
99
+ 5. **Start pack + modes** — Fill `read` / `show` / `outline` / `references` so the entry is work-ready. Every eligible changed non-deleted file must belong somewhere. Remove every stale catalog path. Drop or fix dead `show` anchors (path+name must be real declarations).
100
+
101
+ Path stuffing into the nearest bucket without climbing the ladder is failure.
102
+
103
+ ## Loading modes
104
+
105
+ Inject order / precedence when entries disagree: **`read` > `show` > `outline` > `references`**. A path may appear in only one of `read`, `outline`, and `references`. The same path may also appear in `show` with `outline` or `references`. Full `read` drops `show` for that path at inject time.
106
+
107
+ ### Decision order for each path in an entry
108
+
109
+ 0. **Discover edges first.** After choosing owned `read` files, open them (or use `deps` / structure tools) and name the **outside** modules/symbols this job always calls. Those edges are first-class pack members — not optional polish after membership is “done.”
110
+ 1. **Is this file (or short product doc) something the agent must edit or deeply understand for this job?**
111
+ → **`read`**. Default for clean, bounded modules in this codebase. Include short extension README when it defines user-facing behavior or on-disk envelopes the job edits against.
112
+ 2. **Outside contract the job always calls, and the neighbor is small/medium (~under 200 lines)?**
113
+ → **`read`** the whole file (or `references` if it is only a soft next hop). Do not `show`-slice small shared helpers.
114
+ 3. **Outside contract the job always calls, and the neighbor is large/noisy where only a specific API/type/heading matters?**
115
+ → **`show`** `{ path, name, view? }` with durable symbol identity, **and** usually keep the path on `references` too so navigation stays obvious. Default `view` is `declaration`. Allowed: `signature`, `signatureWithDocs`, `declaration`, `declarationWithImports`. Prefer **1 show** per external file; **2** only when clearly distinct contracts. **3+ shows into one file means you wanted `read`.** Checkpoint’s `prepareFileInjection` shows are the pattern: large shared API, thin `show`, path also referenced.
116
+ 4. **Is the file huge/noisy and you only need a map for this job, not bodies?**
117
+ → **`outline`**. Exception, not house style for small clean files.
118
+ 5. **Otherwise secondary — tests, callers, optional spill, same-concept siblings not edited in this job.**
119
+ → **`references`** (a short list of navigation edges, not a dump of every leaf).
120
+
121
+ Do **not** store raw line ranges. They drift. `show` resolves lines at inject time.
122
+
123
+ **Owned-folder trap:** A pack that only lists files under the extension/concept directory is incomplete when the hot path calls shared infrastructure. Example failure: handoff lifecycle with `index.ts` on `read` but no edge to `prepareFileInjection` / model-fallback generators the command always invokes.
124
+
125
+ ### Fixture trees and bulk test data
126
+
127
+ Never enumerate every file under a fixture/scenario/corpus tree in `read`, `show`, `outline`, or `references`. That creates membership landfills and useless brief noise.
128
+
129
+ Do this instead:
130
+
131
+ - **`read`** the test runner and any short fixtures README that explains layout.
132
+ - **`references`** owned production code the tests exercise (optional, short).
133
+ - Put the bulk tree on the parent project’s `extensions.context.validation.ignoreGlobs` (report the exact glob in your final summary if it is missing — parent owns settings; you cannot edit settings from this agent). Example: `packages/agent/test/extensions/patch/fixtures/**`.
134
+ - If an old catalog already lists dozens of fixture paths, **delete that landfill** during a quality rewrite. Preserving it is not “membership success.”
135
+
136
+ Same rule for language sample corpora, golden snapshot forests, and generated fixture dumps.
137
+
138
+ ### Mode anti-patterns
139
+
140
+ | Bad | Why | Fix |
141
+ | --- | --- | --- |
142
+ | Outline-only “wiring” entry on small files | Agent gets a map and cannot start | `read` the shell and modules it owns |
143
+ | Owned files only; ignore always-called outside APIs | Agent still greps for injection/fallback/shared helpers | `show` large contracts; `read`/`references` the rest |
144
+ | `show` into a small shared file (~under 200 lines) | Full file is cheaper and clearer | `read` the file or leave as `references` |
145
+ | Three+ `show` targets into one file | ≈ full file, swiss-cheese, often more tokens | `read` the file |
146
+ | `show` of every local function in an owned file | Catalog noise; modes stop meaning anything | `read` the owned file; `show` large external contracts only |
147
+ | Everything in one `[feature]` entry | No job selection; forces load-all or load-nothing | Split by real jobs |
148
+ | Listing every fixture/snapshot path | Landfill; brief unusable; false precision | Runner + README `read`; ignoreGlobs for the tree |
149
+ | Keeping a historical fixture dump “for coverage” | Validation theater, not a work pack | Delete paths; add ignore glob |
150
+ | New paths always stuck as `references` forever | Pack never becomes work-ready | Promote when the job needs the body |
151
+ | Preserve a wrong historical mode forever | “Preserve existing mode” is not a suicide pact | Re-mode when the job pack is wrong |
152
+
153
+ Preserve an existing path’s loading mode when it still fits the job pack. **Change the mode** when evidence says the pack is underfilled or bloated. **Delete** membership landfills even if they were “valid” under path-coverage rules.
154
+
155
+ ### New path defaults
156
+
157
+ - Eligible new durable path → start in **`references`** on the best job entry (or a new entry if no job fits).
158
+ - If the dirty work **is** that job’s primary edit surface → put it in **`read`** immediately.
159
+ - Shared infrastructure API used only as a contract from this pack → **`show`** only if the neighbor is large; otherwise **`read`** or **`references`**.
160
+ - Bulk fixture/corpus paths → **ignoreGlobs**, not catalog membership.
161
+ - Large generated/noisy surface → **`outline`** or **`references`**, not blind `read`.
162
+
163
+ ## How to find job boundaries
164
+
165
+ Jobs are recurring reasons a human opens `/context`, not directory children.
166
+
167
+ Ask of the concept:
168
+
169
+ - What breaks independently?
170
+ - What would you name a PR or a debugging session?
171
+ - Which files change together for that session?
172
+ - What outside APIs does that session **always call** (read the imports — do not guess from the folder name alone)?
173
+
174
+ Examples of good entry splits:
175
+
176
+ - extension shell / lifecycle vs tool implementation vs projection/policy sibling
177
+ - settings schema vs runtime merge vs one consumer
178
+ - protocol types vs server handler vs client
179
+
180
+ Examples of bad splits:
181
+
182
+ - one entry per file with no job story
183
+ - “utils” / “misc” / “shared”
184
+ - outline-everything + empty read
185
+
186
+ When rewriting a weak concept (nudge or clear underfill), **reconfigure the whole concept TOML** to work packs. Do not nibble mode flags on a broken `[feature]` bag and call it done. Do not keep fixture-path encyclopedias from the old file.
27
187
 
28
188
  ## Tools
29
189
 
30
- - `context_sync_evidence` — git dirty set, catalog skeleton, deps, diffs, invariants. Prefer this first.
31
- - `read` — open a known path when evidence is not enough.
32
- - `bash` — list/search/inspect the tree and other read-only questions `read` cannot cover.
33
- - `patch` — create/update/move/delete only under `.pi/contexts/**` (including whole concept TOML via `*** Delete File`).
190
+ Normal repository tools only. No special evidence telescope.
191
+
192
+ - `read` — primary source and short docs (you need bodies to judge job packs).
193
+ - `bash` — git status/diff, tree listing, search, and other read-only inspection.
194
+ - Structure tools (`outline`, `show`, `discover`, `deps`, `reverse_deps`, `callers`, `callees`, `references`, `implementations`) — map large neighbors and outside contracts.
195
+ - `patch` — preferred for create/update/move/delete under `.pi/contexts/**` (including whole concept TOML via `*** Delete File`).
34
196
 
35
197
  ### Bash limits
36
198
 
37
- Prefer `context_sync_evidence` and `read` first. Use bash when those cannot cover the need.
199
+ Prefer `read` and structure tools for known paths. Use bash for git and tree questions those tools cannot cover.
38
200
 
39
201
  Allowed bash:
40
202
 
41
- - Read-only tree inspection — e.g. `ls`, `find`, `rg`/`grep`, `git log` / `git blame` / `git show` on existing commits, `file`, `wc`, small read-only pipelines
203
+ - Read-only tree inspection — e.g. `ls`, `find`, `rg`/`grep`, `git status` / `git diff` / `git log` / `git blame` / `git show` on existing commits, `file`, `wc`, small read-only pipelines
42
204
  - After `patch` deletes the last file in a `.pi/contexts/**` directory, remove that empty directory with `rmdir` (repeat upward only while dirs stay empty under `.pi/contexts`). Prefer `rmdir` over `rm -r`.
43
205
 
44
206
  Forbidden with bash:
45
207
 
46
- - Creating, editing, moving, or deleting files (catalog files use `patch` only)
47
- - Non-empty directory deletes; deletes outside `.pi/contexts`
208
+ - Creating, editing, moving, or deleting files as a substitute for `patch` on catalog work
209
+ - Non-empty directory deletes
48
210
  - `git add` / `commit` / `push` / `checkout` / `restore` / `reset` / `stash` / branch changes
49
211
  - Installers, package managers, builds, tests, formatters, codegen, servers
50
212
  - Network fetches that change the tree; secrets; credential or config mutation
51
213
 
52
- Catalog file mutations go through `patch` under `.pi/contexts` only. The harness restores out-of-scope writes and fails the run.
214
+ Your job is the catalog. Prefer `patch` under `.pi/contexts`. Do not wander into unrelated product edits.
53
215
 
54
216
  ## Durability gate
55
217
 
@@ -57,37 +219,27 @@ Catalog durable repository material: code, configuration, tests, standards, and
57
219
 
58
220
  Do not catalog scratch pads, working plans, interview notes, rough ideas, or other temporary artifacts. Recurring transient paths belong in the parent project's `extensions.context.validation.ignoreGlobs`. The parent owns settings; if an uncovered transient path is not ignored, leave it out of the catalog and report the exact ignore glob needed instead of forcing it into an entry.
59
221
 
60
- ## Forced ladder
61
-
62
- Before placing any path, answer out loud in order:
63
-
64
- 1. **Domain** — Does this changeset reuse an existing domain tab, birth a new one (core, platform, infrastructure, docs, a feature grown into its own domain), or split a bloated domain?
65
- 2. **Concept** — Inside that domain, which subsystem TOML? Reuse, new, split, or merge?
66
- 3. **Entry** — Which work scope? Update, new, split, delete, or move between concepts/domains?
67
- 4. **Bloat** — Did this touch make an entry/concept a junk drawer? Split now if yes.
68
- 5. **Membership** — Assign read/outline/references only under the winners. Every entry must contain all three arrays, even when an array is empty. Every eligible changed non-deleted file must belong somewhere. Remove every stale catalog path.
69
-
70
- Path stuffing into the nearest feature bucket without climbing the ladder is failure.
71
-
72
222
  ## Change classes
73
223
 
74
- - **Additive / local edit** — mostly membership or a new entry under a stable concept.
75
- - **Semantic move / refactor** — meaning moved even if paths stayed covered. Re-evaluate domain/concept. Moves and splits are required verbs, not optional polish.
224
+ - **Additive / local edit** — membership tweak or a new entry under a stable concept; still pass the quality bar.
225
+ - **Semantic move / refactor** — meaning moved even if paths stayed covered. Re-evaluate domain/concept/entry. Moves and splits are required verbs.
226
+ - **Quality rewrite** — concept exists but packs are outline bags or single `[feature]` entries. Rebuild entries as start packs (see Checkpoint / Patch / Handoff gold). Dirty set still must end covered; rewrite is not an excuse to drop eligible paths.
76
227
 
77
228
  ## Working loop
78
229
 
79
- 1. `context_sync_evidence` section `overview`.
80
- 2. `catalog` for the full skeleton. Do not invent domains you never inspected.
81
- 3. Pull `dirty`, `file`, `dependencies`, or `previews` as needed. Use `read` for known paths. Use `bash` for tree list/search and other inspection evidence cannot cover.
82
- 4. Edit catalog TOML with `patch`.
83
- 5. `context_sync_evidence` section `invariants`. If failed, continue until it holds or you can explain a hard blocker.
84
- 6. Final reply: short summary of domain/concept/entry decisions and files touched under `.pi/contexts`. If no catalog edit was required, say why.
230
+ 1. See what changed (`git status` / diff) and load the current `.pi/contexts` skeleton for touched areas.
231
+ 2. Climb the ladder. For each touched concept, prefer work-pack structure over preserving a weak bag.
232
+ 3. **`read` primary source** until you can name jobs and fill start packs — skim-only placement produces underfilled entries.
233
+ 4. For each entry's owned `read` set: trace **outside** imports/calls (`deps`, structure tools, or reading the file). Place every always-called contract via the mode decision order before you declare the pack done.
234
+ 5. Edit catalog TOML with `patch`. Every entry: all four arrays; descriptions = jobs; modes pass the decision order **including outside edges**.
235
+ 6. Recheck yourself: every eligible dirty path is filed or reported for `ignoreGlobs`; no stale catalog paths; packs still meet the quality bar. The harness re-validates catalog coverage after you finish.
236
+ 7. Final reply: short summary of domain/concept/entry decisions, mode choices that matter (`read` vs `show` vs outside contracts), any ignore-glob the parent should add for bulk fixtures, and files touched under `.pi/contexts`. If no catalog edit was required, say why.
85
237
 
86
238
  ## Nudge
87
239
 
88
- If the task includes a human nudge, treat it as soft steer. It does not override evidence, eligibility, or the ladder. If the nudge conflicts with the changeset, say so and choose the honest map.
240
+ If the task includes a human nudge, treat it as soft steer. It does not override eligibility, coverage, or the ladder. If the nudge asks to raise quality or rewrite packs across a concept/domain, do that thorough rewrite while still covering the dirty set. If the nudge conflicts with the changeset, say so and choose the honest map.
89
241
 
90
242
  ## Stop conditions
91
243
 
92
- - Invariants hold and the map reflects honest typology for this changeset, or
244
+ - Invariants hold **and** touched concepts meet the work-pack quality bar, or
93
245
  - Hard blocker (secrets, conflicts, missing tools/model) — report it clearly without half-applying a broken map.
@@ -14,7 +14,7 @@ tools:
14
14
  - callees
15
15
  - references
16
16
  - implementations
17
- - working_memory
17
+ - checkpoint
18
18
  names:
19
19
  - Pathfinder
20
20
  - Trailblazer
@@ -61,7 +61,7 @@ Structural results prove bounded syntax, not runtime dispatch. Preserve exact, i
61
61
  - Batch only independent lookups whose results will stay small.
62
62
  - Do not fan out across plausible explanations or collect evidence for a theory.
63
63
  - Stop when requested evidence has been found or bounded search cannot find it.
64
- - During a long inventory, use `working_memory` only when stale evidence would burden the remaining lookup. Do not checkpoint a small search.
64
+ - During a long inventory, use `checkpoint` to keep current findings and active file reads bounded. Do not use it for a small search.
65
65
 
66
66
  Absolute paths may point to read-only reference repositories outside cwd.
67
67
 
@@ -40,7 +40,7 @@ async function directoryExists(path: string): Promise<boolean> {
40
40
  }
41
41
  }
42
42
 
43
- export async function findProjectAgentsDir(cwd: string): Promise<string | undefined> {
43
+ async function findProjectAgentsDir(cwd: string): Promise<string | undefined> {
44
44
  const home = resolve(homedir());
45
45
  let current = resolve(cwd);
46
46
  while (current !== home) {
@@ -53,6 +53,71 @@ export async function findProjectAgentsDir(cwd: string): Promise<string | undefi
53
53
  return undefined;
54
54
  }
55
55
 
56
+ const ALLOWED_FRONTMATTER_FIELDS = new Set(["name", "description", "tools", "names", "model", "thinking"]);
57
+ const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
58
+
59
+ function validateStringList(raw: unknown, field: string, reasons: string[]): string[] | undefined {
60
+ if (!Array.isArray(raw) || raw.length === 0) {
61
+ reasons.push(`${field} must be a non-empty array`);
62
+ return undefined;
63
+ }
64
+ const values = raw
65
+ .filter((item): item is string => typeof item === "string" && item.trim().length > 0)
66
+ .map((item) => item.trim());
67
+ if (values.length !== raw.length) reasons.push(`${field} must contain non-empty strings`);
68
+ if (new Set(values).size !== values.length) reasons.push(`${field} must be unique`);
69
+ return values;
70
+ }
71
+
72
+ function requireNonEmptyString(raw: unknown, field: string, reasons: string[]): void {
73
+ if (typeof raw !== "string" || !raw.trim()) reasons.push(`${field} must be a non-empty string`);
74
+ }
75
+
76
+ function validateModelField(rawModel: unknown, reasons: string[]): void {
77
+ if (rawModel === undefined) return;
78
+ if (typeof rawModel !== "string" || !/^[^/\s]+\/[^/\s]+$/.test(rawModel.trim()))
79
+ reasons.push("model must be a provider/model string");
80
+ }
81
+
82
+ function validateThinkingField(rawThinking: unknown, reasons: string[]): void {
83
+ if (rawThinking === undefined) return;
84
+ if (!THINKING_LEVELS.includes(rawThinking as ThinkingLevel))
85
+ reasons.push(`thinking must be one of ${THINKING_LEVELS.join(", ")}`);
86
+ }
87
+
88
+ function collectDefinitionReasons(
89
+ parsed: ReturnType<typeof parseFrontmatter>,
90
+ fallbackName: string,
91
+ ): {
92
+ reasons: string[];
93
+ name: string;
94
+ tools: string[] | undefined;
95
+ names: string[] | undefined;
96
+ rawDescription: unknown;
97
+ rawModel: unknown;
98
+ rawThinking: unknown;
99
+ } {
100
+ const rawName = parsed.frontmatter.name;
101
+ const name = typeof rawName === "string" && rawName.trim() ? rawName.trim() : fallbackName;
102
+ const reasons: string[] = [];
103
+ for (const field of Object.keys(parsed.frontmatter)) {
104
+ if (!ALLOWED_FRONTMATTER_FIELDS.has(field)) reasons.push(`unsupported field "${field}"`);
105
+ }
106
+ requireNonEmptyString(rawName, "name", reasons);
107
+ const rawDescription = parsed.frontmatter.description;
108
+ requireNonEmptyString(rawDescription, "description", reasons);
109
+ const tools = validateStringList(parsed.frontmatter.tools, "tools", reasons);
110
+ if (tools?.includes("subagent")) reasons.push("tool subagent is forbidden");
111
+ const rawNames = parsed.frontmatter.names;
112
+ const names = rawNames === undefined ? undefined : validateStringList(rawNames, "names", reasons);
113
+ if (!parsed.body.trim()) reasons.push("prompt body must be non-empty");
114
+ const rawModel = parsed.frontmatter.model;
115
+ validateModelField(rawModel, reasons);
116
+ const rawThinking = parsed.frontmatter.thinking;
117
+ validateThinkingField(rawThinking, reasons);
118
+ return { reasons, name, tools, names, rawDescription, rawModel, rawThinking };
119
+ }
120
+
56
121
  function parseDefinition(
57
122
  path: string,
58
123
  content: string,
@@ -60,54 +125,17 @@ function parseDefinition(
60
125
  const fallbackName = basename(path, extname(path));
61
126
  try {
62
127
  const parsed = parseFrontmatter(content);
63
- const fields = Object.keys(parsed.frontmatter);
64
- const rawName = parsed.frontmatter.name;
65
- const name = typeof rawName === "string" && rawName.trim() ? rawName.trim() : fallbackName;
66
- const reasons: string[] = [];
67
- for (const field of fields) {
68
- if (!new Set(["name", "description", "tools", "names", "model", "thinking"]).has(field))
69
- reasons.push(`unsupported field "${field}"`);
70
- }
71
- if (typeof rawName !== "string" || !rawName.trim()) reasons.push("name must be a non-empty string");
72
- const rawDescription = parsed.frontmatter.description;
73
- if (typeof rawDescription !== "string" || !rawDescription.trim())
74
- reasons.push("description must be a non-empty string");
75
- const rawTools = parsed.frontmatter.tools;
76
- if (!Array.isArray(rawTools) || rawTools.length === 0) reasons.push("tools must be a non-empty array");
77
- else {
78
- const tools = rawTools
79
- .filter((tool): tool is string => typeof tool === "string" && tool.trim().length > 0)
80
- .map((tool) => tool.trim());
81
- if (tools.length !== rawTools.length) reasons.push("tools must contain non-empty strings");
82
- if (new Set(tools).size !== tools.length) reasons.push("tools must be unique");
83
- if (tools.includes("subagent")) reasons.push("tool subagent is forbidden");
84
- }
85
- const rawNames = parsed.frontmatter.names;
86
- if (rawNames !== undefined) {
87
- if (!Array.isArray(rawNames) || rawNames.length === 0) reasons.push("names must be a non-empty array");
88
- else {
89
- const names = rawNames
90
- .filter((item): item is string => typeof item === "string" && item.trim().length > 0)
91
- .map((item) => item.trim());
92
- if (names.length !== rawNames.length) reasons.push("names must contain non-empty strings");
93
- if (new Set(names).size !== names.length) reasons.push("names must be unique");
94
- }
95
- }
96
- if (!parsed.body.trim()) reasons.push("prompt body must be non-empty");
97
- const rawModel = parsed.frontmatter.model;
98
- if (rawModel !== undefined && (typeof rawModel !== "string" || !/^[^/\s]+\/[^/\s]+$/.test(rawModel.trim())))
99
- reasons.push("model must be a provider/model string");
100
- const rawThinking = parsed.frontmatter.thinking;
101
- const thinkingLevels: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
102
- if (rawThinking !== undefined && !thinkingLevels.includes(rawThinking as ThinkingLevel))
103
- reasons.push(`thinking must be one of ${thinkingLevels.join(", ")}`);
128
+ const { reasons, name, tools, names, rawDescription, rawModel, rawThinking } = collectDefinitionReasons(
129
+ parsed,
130
+ fallbackName,
131
+ );
104
132
  if (reasons.length > 0) return { diagnostic: { path, name, reason: reasons.join("; ") } };
105
133
  return {
106
134
  definition: {
107
135
  name,
108
136
  description: (rawDescription as string).trim(),
109
- tools: (rawTools as string[]).map((tool) => tool.trim()),
110
- names: Array.isArray(rawNames) ? (rawNames as string[]).map((item) => item.trim()) : [name],
137
+ tools: tools ?? [],
138
+ names: names ?? [name],
111
139
  model: typeof rawModel === "string" ? rawModel.trim() : undefined,
112
140
  thinking: rawThinking as ThinkingLevel | undefined,
113
141
  prompt: parsed.body.trim(),
@@ -125,60 +153,85 @@ function parseDefinition(
125
153
  }
126
154
  }
127
155
 
128
- async function loadScope(
156
+ function finalizeScopeGroup(
157
+ name: string,
158
+ values: Array<AgentDefinition | AgentDiagnostic>,
159
+ ): AgentDefinition | AgentDiagnostic[] {
160
+ if (values.length === 1 && "prompt" in values[0]) return values[0];
161
+ const diagnostics = values.map((value) =>
162
+ "reason" in value ? value : { path: value.path, name, reason: `duplicate name "${name}" in this scope` },
163
+ );
164
+ if (values.length > 1) {
165
+ for (const value of values) if ("reason" in value) value.reason += `; duplicate name "${name}" in this scope`;
166
+ }
167
+ return diagnostics;
168
+ }
169
+
170
+ async function readAgentDirectoryEntries(
129
171
  directory: string,
130
172
  required: boolean,
131
- ): Promise<Map<string, AgentDefinition | AgentDiagnostic[]>> {
132
- let entries;
173
+ ): Promise<
174
+ | { kind: "entries"; entries: Array<{ name: string }> }
175
+ | { kind: "empty" }
176
+ | { kind: "error"; scope: Map<string, AgentDefinition | AgentDiagnostic[]> }
177
+ > {
133
178
  try {
134
- entries = (await readdir(directory, { withFileTypes: true }))
179
+ const entries = (await readdir(directory, { withFileTypes: true }))
135
180
  .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
136
181
  .sort((a, b) => a.name.localeCompare(b.name));
182
+ return { kind: "entries", entries };
137
183
  } catch (error) {
138
- if (!required) return new Map();
184
+ if (!required) return { kind: "empty" };
139
185
  const reason = error instanceof Error ? error.message : "directory unavailable";
140
- return new Map([
141
- [
142
- "web-research",
143
- [{ path: directory, name: "web-research", reason: `packaged agents unavailable: ${reason}` }],
144
- ],
145
- ]);
186
+ return {
187
+ kind: "error",
188
+ scope: new Map([
189
+ [
190
+ "web-research",
191
+ [{ path: directory, name: "web-research", reason: `packaged agents unavailable: ${reason}` }],
192
+ ],
193
+ ]),
194
+ };
195
+ }
196
+ }
197
+
198
+ async function parseScopeFile(directory: string, entryName: string): Promise<AgentDefinition | AgentDiagnostic> {
199
+ const path = join(directory, entryName);
200
+ try {
201
+ const parsed = parseDefinition(path, await readFile(path, "utf8"));
202
+ return (
203
+ parsed.definition ??
204
+ parsed.diagnostic ?? {
205
+ path,
206
+ name: parse(entryName).name,
207
+ reason: "empty definition",
208
+ }
209
+ );
210
+ } catch (error) {
211
+ return {
212
+ path,
213
+ name: parse(entryName).name,
214
+ reason: error instanceof Error ? error.message : "file unreadable",
215
+ };
146
216
  }
217
+ }
218
+
219
+ async function loadScope(
220
+ directory: string,
221
+ required: boolean,
222
+ ): Promise<Map<string, AgentDefinition | AgentDiagnostic[]>> {
223
+ const listed = await readAgentDirectoryEntries(directory, required);
224
+ if (listed.kind === "empty") return new Map();
225
+ if (listed.kind === "error") return listed.scope;
147
226
  const grouped = new Map<string, Array<AgentDefinition | AgentDiagnostic>>();
148
- for (const entry of entries) {
149
- const path = join(directory, entry.name);
150
- let parsed;
151
- try {
152
- parsed = parseDefinition(path, await readFile(path, "utf8"));
153
- } catch (error) {
154
- parsed = {
155
- diagnostic: {
156
- path,
157
- name: parse(entry.name).name,
158
- reason: error instanceof Error ? error.message : "file unreadable",
159
- },
160
- };
161
- }
162
- const value = parsed.definition ?? parsed.diagnostic;
163
- if (!value) continue;
227
+ for (const entry of listed.entries) {
228
+ const value = await parseScopeFile(directory, entry.name);
164
229
  const values = grouped.get(value.name) ?? [];
165
230
  values.push(value);
166
231
  grouped.set(value.name, values);
167
232
  }
168
233
  const scope = new Map<string, AgentDefinition | AgentDiagnostic[]>();
169
- for (const [name, values] of grouped) {
170
- if (values.length === 1 && "prompt" in values[0]) scope.set(name, values[0]);
171
- else {
172
- const diagnostics = values.map((value) =>
173
- "reason" in value ? value : { path: value.path, name, reason: `duplicate name "${name}" in this scope` },
174
- );
175
- if (values.length > 1) {
176
- for (const value of values)
177
- if ("reason" in value) value.reason += `; duplicate name "${name}" in this scope`;
178
- }
179
- scope.set(name, diagnostics);
180
- }
181
- }
234
+ for (const [name, values] of grouped) scope.set(name, finalizeScopeGroup(name, values));
182
235
  return scope;
183
236
  }
184
237
 
@@ -106,10 +106,7 @@ function dashboardState(status: SubagentInvocationSnapshot["status"]): string {
106
106
  }
107
107
  }
108
108
 
109
- export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSnapshot[]): string {
110
- const ordered = [...snapshots].sort(
111
- (a, b) => a.startedAt - b.startedAt || a.invocationId.localeCompare(b.invocationId),
112
- );
109
+ function dashboardCounts(ordered: readonly SubagentInvocationSnapshot[]): string {
113
110
  const activeCount = ordered.filter((snapshot) =>
114
111
  ["waiting", "starting", "running"].includes(snapshot.status),
115
112
  ).length;
@@ -119,7 +116,39 @@ export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSn
119
116
  const counts = [`${activeCount} active`, `${doneCount} done`];
120
117
  if (failedCount > 0) counts.push(`${failedCount} failed`);
121
118
  if (stoppedCount > 0) counts.push(`${stoppedCount} stopped`);
122
- const lines = ["# Subagents", "", counts.join(" · "), ""];
119
+ return counts.join(" · ");
120
+ }
121
+
122
+ function dashboardTableRow(details: SubagentInvocationSnapshot): string {
123
+ const latest = details.actions.at(-1);
124
+ const currentTool = details.currentActivity?.match(/^\S+/)?.[0];
125
+ const lastTool = currentTool ?? latest?.tool ?? "";
126
+ const ctx = typeof details.contextPercent === "number" ? `${details.contextPercent.toFixed(1)}%` : "—";
127
+ return `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | $${details.usage.cost.toFixed(4)} | ${ctx} | ${elapsed(details.durationMs)} |`;
128
+ }
129
+
130
+ function dashboardInputSection(details: SubagentInvocationSnapshot): string[] {
131
+ const lines = [
132
+ `### ${tableCell(details.displayName, 80)}`,
133
+ "",
134
+ tableCell(details.agent, 80),
135
+ "",
136
+ quote(details.task) || "> _(empty)_",
137
+ ];
138
+ if (details.files.length > 0) {
139
+ lines.push("", "**Files**", "");
140
+ for (const file of details.files) lines.push(`- ${tableCell(file, 300)}`);
141
+ }
142
+ if (details.error) lines.push("", "**Error**", "", quote(details.error));
143
+ lines.push("");
144
+ return lines;
145
+ }
146
+
147
+ function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSnapshot[]): string {
148
+ const ordered = [...snapshots].sort(
149
+ (a, b) => a.startedAt - b.startedAt || a.invocationId.localeCompare(b.invocationId),
150
+ );
151
+ const lines = ["# Subagents", "", dashboardCounts(ordered), ""];
123
152
  if (ordered.length === 0) {
124
153
  lines.push("_No subagents._", "");
125
154
  return lines.join("\n");
@@ -127,32 +156,12 @@ export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSn
127
156
  lines.push(
128
157
  "| Agent | State | Last tool | Calls | Cost | Ctx | Time |",
129
158
  "| --- | --- | --- | ---: | ---: | ---: | ---: |",
159
+ ...ordered.map(dashboardTableRow),
160
+ "",
161
+ "## Inputs",
162
+ "",
130
163
  );
131
- for (const details of ordered) {
132
- const latest = details.actions.at(-1);
133
- const currentTool = details.currentActivity?.match(/^\S+/)?.[0];
134
- const lastTool = currentTool ?? latest?.tool ?? "";
135
- const ctx = typeof details.contextPercent === "number" ? `${details.contextPercent.toFixed(1)}%` : "—";
136
- lines.push(
137
- `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | $${details.usage.cost.toFixed(4)} | ${ctx} | ${elapsed(details.durationMs)} |`,
138
- );
139
- }
140
- lines.push("", "## Inputs", "");
141
- for (const details of ordered) {
142
- lines.push(
143
- `### ${tableCell(details.displayName, 80)}`,
144
- "",
145
- tableCell(details.agent, 80),
146
- "",
147
- quote(details.task) || "> _(empty)_",
148
- );
149
- if (details.files.length > 0) {
150
- lines.push("", "**Files**", "");
151
- for (const file of details.files) lines.push(`- ${tableCell(file, 300)}`);
152
- }
153
- if (details.error) lines.push("", "**Error**", "", quote(details.error));
154
- lines.push("");
155
- }
164
+ for (const details of ordered) lines.push(...dashboardInputSection(details));
156
165
  return lines.join("\n");
157
166
  }
158
167