superwiki 0.1.3 → 0.1.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sw",
3
3
  "description": "An LLM-maintained wiki and task tracker in docs/ for coding agents. Obsidian friendly, with a static viewer.",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "wiki",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sw",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "An LLM-maintained wiki and task tracker in docs/ for coding agents. Obsidian friendly, with a static viewer.",
5
5
  "license": "MIT",
6
6
  "skills": "./skills/",
package/README.md CHANGED
@@ -17,19 +17,23 @@ Then, in a project: `/sw-init`.
17
17
 
18
18
  ## Why
19
19
 
20
- Superwiki follows the LLM Wiki pattern described by Andrej Karpathy: raw sources you curate, a wiki the agent owns, and a short schema that tells the agent how to maintain it. On top of that it adds what a software project needs: tasks with dependencies, plans, and a record of decisions and lessons.
20
+ Superwiki follows the LLM Wiki pattern described by Andrej Karpathy: raw sources you curate, a wiki the agent owns, and a short schema that tells the agent how to maintain it. On top of that it adds what a software project needs: tasks with dependencies, plans, reviews, and a record of decisions and lessons.
21
21
 
22
- It is built to be cheap for the agent. One small index to read, one file per task, and a script that answers "what is ready?", "what blocks this?" or "is anything broken?" without the agent reading the vault.
22
+ It is built to be cheap for the agent:
23
+
24
+ - **Little to read.** One small index, one file per task, and a script that answers "what is ready?", "what blocks this?" or "is anything broken?" without the agent reading the vault.
25
+ - **Work in clean contexts.** Planning, implementing and reviewing run in subagents, each on the model you choose for it. The main session only keeps the task's status true, so it stays small.
26
+ - **Cost you can see.** `sw-stats` shows what a session used, per agent; `sw-doctor` shows what every session carries before it starts, and what can go.
23
27
 
24
28
  Measured on a real project with 165 tasks, converted from a single markdown index:
25
29
 
26
30
  | | Before | After |
27
31
  | --- | --- | --- |
28
- | Read at the start of every session | 197 KB index | 94-byte catalog + 1.7 KB of rules |
32
+ | Read at the start of every session | 197 KB index | 94-byte catalog + 2.7 KB of rules |
29
33
  | Read to start one task | the index, then the task's section | one file, 2 KB at the median |
30
34
  | Marking a task done | a status cell, plus a ✅ at every reference to it (median 12 places) | one frontmatter line |
31
35
 
32
- > Status: early. The CLI, the viewer, `sw-init` and the migration script are tested, and the planning flow has been run in all three agents; some skills have only been exercised once. [DESIGN.md](DESIGN.md) lists what has and has not been proven.
36
+ > Status: early. The CLI, the viewer, `sw-init` and the migration script are tested. Planning, implementing and reviewing have been run end to end on real projects in Claude Code, and the planning flow in Codex and Copilot CLI; some skills have only been exercised once. [DESIGN.md](DESIGN.md) lists what has and has not been proven.
33
37
 
34
38
  ## What you get
35
39
 
@@ -84,9 +88,13 @@ git clone https://github.com/mhmtsrfglu/superwiki ~/.superwiki
84
88
 
85
89
  A linked install follows the clone: `git pull` updates every agent. `install.sh --copy` copies instead, `--uninstall` removes.
86
90
 
87
- All three agents below were checked the same way: the agent found the skills, refused to start a task with an unfinished dependency, and ran `sw-plan` end to end with the planner subagent.
91
+ ### Per agent
92
+
93
+ All three agents were checked the same way: the agent found the skills, refused to start a task with an unfinished dependency, and ran `sw-plan` end to end with the planner subagent.
94
+
95
+ The model for each role (planning, implementing, reviewing) is set with `sw-config`, which writes one agent file per role into the project. The skills dispatch those agents by name.
88
96
 
89
- ### Claude Code
97
+ #### Claude Code
90
98
 
91
99
  ```bash
92
100
  npx superwiki install claude
@@ -94,11 +102,11 @@ npx superwiki install claude
94
102
 
95
103
  Invoke with a slash: `/sw-init`, `/sw-plan T-01`.
96
104
 
97
- Claude Code reads `~/.claude/skills/` (and a project's `.claude/skills/`); it does not read `~/.agents/skills/`, so `global` is not enough for it.
105
+ - Claude Code reads `~/.claude/skills/` (and a project's `.claude/skills/`). It does not read `~/.agents/skills/`, so `global` is not enough for it.
106
+ - `sw-plan` enters plan mode when the session offers it.
107
+ - Agent files: `.claude/agents/sw-planner.md`, `sw-implementer.md` and `sw-reviewer.md`. They load when a session starts; in a session that began before they existed, the skills fall back to built-in agents with the same model.
98
108
 
99
- `sw-plan` enters plan mode when the session offers it. A model set with `sw-config` for `claude` applies to the planner and implementer subagents, written to `.claude/agents/`; if those agents are not loaded, the skills fall back to built-in agents with the same model.
100
-
101
- ### Codex CLI
109
+ #### Codex CLI
102
110
 
103
111
  ```bash
104
112
  npx superwiki install codex
@@ -106,9 +114,10 @@ npx superwiki install codex
106
114
 
107
115
  Invoke with a dollar sign, or by name in a sentence: `$sw-init`, `$sw-plan T-01`, "use the sw-plan skill for T-01". Checked with CLI 0.153.
108
116
 
109
- A skill cannot switch Codex into plan mode; start planning yourself with `/plan` if you want the mode, or let `sw-plan` proceed without it (it changes no file before you approve). A model set with `sw-config` for `codex` is written to `.codex/agents/sw-planner.toml` and `sw-implementer.toml`, and the skills spawn those agents by name. Subagents must be enabled (they are by default in current releases).
117
+ - A skill cannot switch Codex into plan mode. Start planning yourself with `/plan` if you want the mode, or let `sw-plan` proceed without it: it changes no file before you approve.
118
+ - Agent files: `.codex/agents/sw-planner.toml`, `sw-implementer.toml` and `sw-reviewer.toml`. Subagents must be enabled; they are by default in current releases.
110
119
 
111
- ### GitHub Copilot CLI
120
+ #### GitHub Copilot CLI
112
121
 
113
122
  ```bash
114
123
  npx superwiki install copilot
@@ -116,11 +125,11 @@ npx superwiki install copilot
116
125
 
117
126
  Invoke with a slash, or by name in a sentence: `/sw-init`, "use the sw-plan skill for T-01". Checked with CLI 1.0.31.
118
127
 
119
- Copilot CLI also reads `~/.agents/skills/`, so if you installed `codex` or `global` it already has the skills.
120
-
121
- A skill cannot switch Copilot into plan mode; start with `copilot --mode plan` or `/plan` if you want it. A model set with `sw-config` for `copilot` is written to `.github/agents/sw-planner.agent.md` and `sw-implementer.agent.md`; the skills dispatch them with the `task` tool. Whether Copilot honours the `model:` field of those files has not been checked.
128
+ - Copilot CLI also reads `~/.agents/skills/`, so if you installed `codex` or `global` it already has the skills.
129
+ - A skill cannot switch Copilot into plan mode. Start with `copilot --mode plan` or `/plan` if you want it.
130
+ - Agent files: `.github/agents/sw-planner.agent.md`, `sw-implementer.agent.md` and `sw-reviewer.agent.md`, dispatched with the `task` tool. Whether Copilot honours the `model:` field of those files has not been checked.
122
131
 
123
- ### Other agents
132
+ #### Other agents
124
133
 
125
134
  ```bash
126
135
  npx superwiki install global
@@ -128,8 +137,9 @@ npx superwiki install global
128
137
 
129
138
  Agents that load `SKILL.md` folders from `~/.agents/skills` pick the skills up from there. For an agent with its own skills folder (Cursor, Gemini CLI, OpenCode and others), copy the `skills/sw-*` folders from a clone into it by hand. Nothing has been run in these agents. What will differ:
130
139
 
131
- - the skills name Claude Code, Codex and Copilot tools when they dispatch subagents; elsewhere they fall back to doing the planning or implementing in the main session, and say so;
132
- - `sw-config` writes agent files only for `claude`, `codex` and `copilot`, so a per-role model cannot be set.
140
+ - the skills name Claude Code, Codex and Copilot tools when they dispatch subagents; elsewhere they fall back to doing the work in the main session, and say so;
141
+ - `sw-config` writes agent files only for `claude`, `codex` and `copilot`, so a per-role model cannot be set;
142
+ - `sw-stats` and `sw-doctor` read the session records of those three tools only.
133
143
 
134
144
  Everything else (the vault, the CLI, the viewer, ingest, lint, explain, triage) depends only on Node and on the agent following the skill text.
135
145
 
@@ -147,7 +157,7 @@ npx superwiki@latest install claude # update: same command, newest release
147
157
  npx superwiki uninstall all
148
158
  ```
149
159
 
150
- After an update, run `sw-init` again in each project: it replaces `docs/.sw/sw.mjs`, the templates and `docs/viewer.html` with the new version and keeps your content.
160
+ After an update, run `sw-init` again in each project: it replaces `docs/.sw/sw.mjs`, the templates and `docs/viewer.html` with the new version and keeps your content. Until then the project keeps the script it was set up with, and a skill that needs a newer command says so.
151
161
 
152
162
  A project's `docs/` folder is plain markdown and keeps working as an Obsidian vault without Superwiki.
153
163
 
@@ -156,7 +166,7 @@ A project's `docs/` folder is plain markdown and keeps working as an Obsidian va
156
166
  | Skill | What it does |
157
167
  | --- | --- |
158
168
  | `sw-init` | set up `docs/` in the current project, or upgrade it |
159
- | `sw-migrate` | convert an existing table-based task index, on a new git branch |
169
+ | `sw-migrate` | convert an existing table-based task index, on a git branch of its own |
160
170
  | `sw-ingest` | file a source into the wiki |
161
171
  | `sw-plan` | plan a task with the planner subagent and get your approval |
162
172
  | `sw-implement` | run a task with the implementer subagent, have it reviewed if the task asks for that, and record the result |
@@ -164,6 +174,8 @@ A project's `docs/` folder is plain markdown and keeps working as an Obsidian va
164
174
  | `sw-triage` | for a problem: seen before? lessons, likely causes |
165
175
  | `sw-lint` | structural checks by script, semantic review on request |
166
176
  | `sw-visualize` | open the viewer |
177
+ | `sw-stats` | what the current session has cost: tokens, context, steps and tool calls, per agent |
178
+ | `sw-doctor` | what a session carries before any work, and what to remove to make every step cheaper |
167
179
  | `sw-config` | the model each tool uses for planning, implementing and reviewing; task areas |
168
180
 
169
181
  ### Examples
@@ -191,6 +203,8 @@ Shown as typed in Claude Code. In Codex, write `$sw-plan` instead of `/sw-plan`.
191
203
  /sw-config plan with opus, implement with sonnet, review with opus
192
204
  /sw-lint check links, frontmatter and task dependencies
193
205
  /sw-visualize open the task board and the wiki in the browser
206
+ /sw-stats what this session has cost so far, per agent
207
+ /sw-doctor what fills the context before any work, and what can go
194
208
  ```
195
209
 
196
210
  You do not have to type a command. The rules `sw-init` adds to `AGENTS.md` tell the agent which skill fits, so a plain request should reach the same skill:
@@ -204,6 +218,40 @@ Users get the magic-link email twice. Have we seen this before?
204
218
 
205
219
  A filled-in example vault is in [examples/demo/docs](examples/demo/docs).
206
220
 
221
+ ### What a session cost
222
+
223
+ `sw-stats` reads the record your agent keeps of the session and prints one row for the main session and one for each subagent. It writes nothing. `sw-implement` ends its report with the same table. This one is a real task: planned, implemented and reviewed in 37 minutes.
224
+
225
+ ```text
226
+ session claude fe4cbd6c-8abf-4db6-9755-469dc7321dc7 2026-10-05 10:27 to 11:04, 37 min
227
+ agent model steps first peak sent cached output tools min
228
+ main claude-opus-5-5 27 79k 126k 2.8M 96% 18k 23 37
229
+ sw-planner claude-opus-5-5 31 59k 154k 3.5M 96% 8k 32 6
230
+ sw-implementer claude-sonnet-5-5 68 59k 252k 12.2M 96% 16k 76 26
231
+ sw-reviewer claude-opus-5-5 23 60k 137k 2.4M 89% 372 24 12
232
+ total 149 - - 20.9M 95% 43k 155
233
+ ```
234
+
235
+ `first` and `peak` are the tokens sent with one request. `sent` is that, summed over every step: each step sends the whole context again, which is why a long session in one context is expensive. In Copilot CLI the token columns fill in once the session has closed.
236
+
237
+ ### What a session starts with
238
+
239
+ Every agent above began at 59k to 79k tokens before it had read anything, and paid for that on each of its steps. `sw-doctor` shows what that start is made of, from the same session record, and proposes what to switch off for this project. The same session:
240
+
241
+ ```text
242
+ context at session start claude fe4cbd6c-8abf-4db6-9755-469dc7321dc7
243
+ first request: 79k tokens
244
+ part size holds
245
+ rule and memory files 30 KB memory/MEMORY.md 18 KB, my-app/AGENTS.md 11 KB, ...
246
+ skill list 29 KB 213: marketing-skills 41, (none) 37, claude-seo 25, claude-ads 23, +11 more
247
+ tool names (loaded on demand) 17 KB 381: claude_ai_higgsfield 116, claude_ai_meta_ads 98, +9 more
248
+ agent list 14 KB 46: claude-seo 18, (none) 12, claude-ads 10, +4 more
249
+ MCP server instructions 8.3 KB claude.ai higgsfield 2.0 KB, notebooklm 2.0 KB, ...
250
+ session-start hooks 3.3 KB
251
+ ```
252
+
253
+ Here most of the skills, agents and tools came from advertising and SEO plugins that this project never uses. The skill proposes changes and asks before making any; it touches project-local settings only, and never uninstalls a plugin or deletes a memory. Sizes are characters of text: a skill cannot run your agent's own context command (`/context` in Claude Code and Copilot CLI, `/status` in Codex), which shows the same in tokens.
254
+
207
255
  ### The CLI
208
256
 
209
257
  The skills call a small script that answers questions without the agent reading the vault. You can run it yourself, from the project root:
@@ -211,11 +259,13 @@ The skills call a small script that answers questions without the agent reading
211
259
  ```bash
212
260
  node docs/.sw/sw.mjs status # counts per area
213
261
  node docs/.sw/sw.mjs ready # tasks that can start now
214
- node docs/.sw/sw.mjs check P-15 # can it start or finish, and what is open
262
+ node docs/.sw/sw.mjs check P-15 # can it start or finish, what is open, is a review required
215
263
  node docs/.sw/sw.mjs explain P-15 # dependencies, what it unblocks, plan, area guide
216
264
  node docs/.sw/sw.mjs search sync timeout # where something is mentioned
217
265
  node docs/.sw/sw.mjs next-id P # next free id in an area
218
266
  node docs/.sw/sw.mjs lint # broken links, bad frontmatter, dependency errors
267
+ node docs/.sw/sw.mjs stats # tokens, context and steps of the agent session here
268
+ node docs/.sw/sw.mjs doctor # what that session carried before it read anything
219
269
  node docs/.sw/sw.mjs serve --open # the viewer, reading files live
220
270
  node docs/.sw/sw.mjs snapshot # or: freeze the vault into docs/viewer.html, no server
221
271
  ```
@@ -223,14 +273,25 @@ node docs/.sw/sw.mjs snapshot # or: freeze the vault into docs/vie
223
273
  ## Develop
224
274
 
225
275
  ```bash
226
- npm test # builds skills/sw-init/assets/sw.mjs, then runs the tests
276
+ npm test # builds skills/sw-init/assets/sw.mjs and viewer.html, then runs the tests
227
277
  ```
228
278
 
229
- Releases are cut by the `Release` workflow (Actions → Release → Run workflow): it tests, bumps the version in `package.json` and the plugin manifests, publishes to npm, tags, and creates a GitHub release. It publishes through npm trusted publishing, so no token is stored: the package's settings on npmjs.com name this repository and `release.yml` as its trusted publisher.
279
+ Edit sources in `src/`:
280
+
281
+ | File | What it is |
282
+ | --- | --- |
283
+ | `src/core.js` | the vault model, derived task state, lint and search; shared by the CLI and the viewer |
284
+ | `src/sessions.js` | finds the record an agent keeps of a session |
285
+ | `src/stats.js` | reduces a session record to cost per agent |
286
+ | `src/doctor.js` | reduces a session record to what the session started with |
287
+ | `src/cli.js` | the commands |
288
+ | `src/viewer.html` | the viewer |
289
+
290
+ `scripts/build.mjs` bundles them into `skills/sw-init/assets/sw.mjs` and `viewer.html`. Those two files are generated: do not edit them.
230
291
 
231
292
  `node scripts/build-demo.mjs` builds the public demo into `site/` (the viewer with the example vault baked in); the Pages workflow deploys it on every push to `main`.
232
293
 
233
- `src/core.js` is shared by the CLI and the viewer. Edit sources in `src/`; the files in `skills/sw-init/assets/` named `sw.mjs` and `viewer.html` are generated.
294
+ Releases are cut by the `Release` workflow (Actions → Release → Run workflow): it tests, bumps the version in `package.json` and the plugin manifests, publishes to npm, tags, and creates a GitHub release. It publishes with the repository secret `NPM_TOKEN`, an npm access token allowed to publish `superwiki`.
234
295
 
235
296
  ## License
236
297
 
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Show what fills the context before any work (rules, memory, skills, plugins, tools) and propose what to remove
3
+ ---
4
+
5
+ Use the sw-doctor skill. User arguments: $ARGUMENTS
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Show what this session has cost so far: tokens, context, steps and tool calls per agent
3
+ ---
4
+
5
+ Use the sw-stats skill. User arguments: $ARGUMENTS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superwiki",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Agent skills that turn docs/ into an LLM-maintained wiki and task tracker. Obsidian-friendly. Works with Claude Code, Codex and Copilot CLI.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: sw-doctor
3
+ description: Use when the user wants to make agent sessions lighter or cheaper to start, asks what fills the context window before any work (rules, memory, skills, plugins, MCP tools), why a session starts with so many tokens, or invokes sw-doctor or sw:doctor.
4
+ ---
5
+
6
+ # sw-doctor
7
+
8
+ Shows what a session carries before it has read anything, and proposes what to remove. Rule and memory files, the lists of skills, agents and tools, and what plugins and hooks add are sent again with every step of every agent, subagents included, so a smaller start makes every step cheaper.
9
+
10
+ You cannot run the tool's own context command: it is typed by the user. A script reads the same blocks from the record the tool keeps of this session. You read nothing yourself and write no report.
11
+
12
+ Run from the project root:
13
+
14
+ ```bash
15
+ node docs/.sw/sw.mjs doctor
16
+ ```
17
+
18
+ It reports the session it runs in. `--session <id or its first characters>` picks another session of this project, `--tool claude|codex|copilot` looks only at one tool's sessions.
19
+
20
+ ## Steps
21
+
22
+ 1. **Show the table as printed**, in a code block. Sizes are characters of text, not tokens.
23
+ 2. **Propose changes**, largest saving first, at most five. For each: the part and its size from the table, what to change, and where. Use the table under "What to propose". Propose only what the output supports, and say plainly when a part cannot be made smaller.
24
+ 3. **Ask before changing anything**, one proposal at a time. Which plugin or server a project needs is the user's call: say what a group appears to be for and let them decide.
25
+ 4. **Apply what the user approved**, within the limits under "Limits".
26
+ 5. **Have it measured.** A change takes effect in a new session. Ask the user to open one and run the tool's own command there: `/context` in Claude Code and Copilot CLI, `/status` in Codex. Compare its figures with the ones from before; a setting that changed nothing is reported as such and taken out again.
27
+
28
+ If the user pastes the output of that command, use its token figures in place of the character sizes; the parts are the same.
29
+
30
+ ## What to propose
31
+
32
+ | In the table | Propose | What it saves |
33
+ | --- | --- | --- |
34
+ | `rule and memory files`: one file is most of the part | name the file. A rules file (`AGENTS.md`, `CLAUDE.md`): move sections that are needed only for some work into a document the agent opens on demand, and leave a one-line pointer. A memory index: one line per entry holding one lesson; dates, counts and status belong in the entry's own file | its full size |
35
+ | `agent list`: groups that have nothing to do with this project | switch those plugins off for this project (how: "Limits"). The group label is the plugin's name; `(none)` is the user's own and built-in entries | the lines of those agents |
36
+ | `skill list`: groups that have nothing to do with this project | the same switch. Say what to expect: the tool gives the list a fixed budget and shortens descriptions to fit, so fewer skills usually means fuller descriptions for the rest, not a smaller list. Worth doing so the agent picks skills better; not a token saving | little or nothing |
37
+ | `tool names (loaded on demand)`: servers the project does not use | the user disables those MCP servers for this project. Only names are loaded, so the saving is the size shown, not the size of the tool definitions | the names of those tools |
38
+ | `MCP server instructions` | goes away with the server; no separate change | with the server |
39
+ | `session-start hooks` | name the hook and the plugin it belongs to, if the user can say; it goes away with that plugin | its full size |
40
+ | Codex: `skills instructions` is large | skills are listed from `~/.agents/skills` and from plugins; remove or move the ones this user does not use | not measured |
41
+ | Copilot CLI: `custom instruction` is large | the project's instruction files; same advice as for a rules file | its full size |
42
+ | `first request` is far above what the parts add up to | the rest is the tool's own system prompt and built-in tool definitions. Say so; it cannot be changed from here | nothing |
43
+
44
+ Give a saving as the size in the table. Do not turn it into a token or money figure unless the user pasted token numbers.
45
+
46
+ ## Limits
47
+
48
+ - **Project-local settings only.** Never edit the user's global settings, and never uninstall a plugin or remove a server: switching it off for this project is enough and is undone by deleting a line.
49
+ - **Claude Code**:
50
+ - a plugin off: in `.claude/settings.local.json` (personal, normally git-ignored; keep what is already there), `"enabledPlugins": { "<plugin>@<marketplace>": false }`. The full key is in `~/.claude/settings.json` under `enabledPlugins`; read only that key.
51
+ - an MCP server or a claude.ai connector off: the user does it with `/mcp` in the session. Do not edit `~/.claude.json`, and do not set environment variables for it in the project's settings: that was tried and changed nothing.
52
+ - plugins synced from the user's claude.ai account are not in that settings file; they are removed in the account's settings.
53
+ - **Codex and Copilot CLI**: propose, and let the user change their configuration; this skill does not edit it.
54
+ - **Memory and rule files are the user's.** Shorten an index or move a section only when asked, keep a copy of the previous version next to the file, and never delete an entry.
55
+ - If the project's own rules forbid a change, they win.
56
+
57
+ ## If it fails
58
+
59
+ | Output | Do |
60
+ | --- | --- |
61
+ | `unknown command doctor` | the project's `docs/.sw/sw.mjs` is older than this skill; offer to run sw-init, which updates it |
62
+ | `no agent session record found` | say so and ask the user to run the tool's context command and paste the output; work from that |
63
+ | a note that the session has not made a request yet | another session of this project is newer than yours; run again with `--session` and the id of the one you mean |
64
+ | a note that Copilot has not counted its tool definitions | pass it on; the other parts are valid |
@@ -7,7 +7,7 @@ description: Use when the user wants to implement, build, execute, start or cont
7
7
 
8
8
  Runs one task. You keep the task's status true and judge the result. The work is done by subagents that start from a clean context, on the models set in sw-config: an implementer, and a reviewer when the task asks for one. You do not read the code or the plan: their reports are your input.
9
9
 
10
- That split is what keeps a task cheap. A long session re-sends its whole context on every step; work done in a fresh context does not carry yours, and yours stays small because the work never enters it.
10
+ That split is what keeps a task cheap. A long session sends its whole context again on every step; work done in a fresh context does not carry yours, and yours stays small because the work never enters it.
11
11
 
12
12
  Run commands from the project root. `<skill-dir>` is the directory this SKILL.md is in.
13
13
 
@@ -15,12 +15,16 @@ Run commands from the project root. `<skill-dir>` is the directory this SKILL.md
15
15
 
16
16
  1. **Pick the task.** Id given: use it. Otherwise `node docs/.sw/sw.mjs ready` and let the user choose; tasks already in progress come first.
17
17
  2. **Gate**: `node docs/.sw/sw.mjs check <ID>`.
18
- - `can start: no open deps: ...`: stop. Tell the user which tasks block it and offer to run the first blocker instead; do not run it unasked. Do not start the task anyway, and do not edit `deps` to get past this.
19
- - `can start: n/a, status is in-progress`: this is a continuation; skip step 3.
20
- - `can start: n/a, status is done` or `cancelled`: stop and ask what the user wants.
21
- - `plan: ... (draft, not approved)`: stop; the plan needs the user's approval (sw-plan).
22
- - `plan: none`: fine for a small task (one area, three "Done when" items or fewer, nothing open in its notes, a few files). For anything larger, recommend sw-plan first and let the user choose.
23
- - `review: required (...)`: remember it for step 7.
18
+
19
+ | `check` says | Do |
20
+ | --- | --- |
21
+ | `can start: no open deps: ...` | stop. Tell the user which tasks block it and offer to run the first blocker instead; do not run it unasked. Do not start the task anyway, and do not edit `deps` to get past this |
22
+ | `can start: n/a, status is in-progress` | this is a continuation; skip step 3 |
23
+ | `can start: n/a, status is done` or `cancelled` | stop and ask what the user wants |
24
+ | `plan: ... (draft, not approved)` | stop; the plan needs the user's approval (sw-plan) |
25
+ | `plan: none` | fine for a small task: one area, three "Done when" items or fewer, nothing open in its notes, a few files. For anything larger, recommend sw-plan first and let the user choose |
26
+ | `review: required (...)` | remember it for step 7 |
27
+
24
28
  3. **Mark it started** before any work: in the frontmatter of `docs/tasks/<ID>.md` set `status: in-progress` and `started:` today. Append `## [date] task | <ID> started` to `docs/log.md`, in the layout its last entries use.
25
29
  4. **Checks that need the environment.** If the task has a plan, look for `needs:` in it: `grep -n 'needs:' docs/plans/<ID>-plan.md`. Each hit is a check that starts services or changes data. Ask the user which of them may run; without a yes, none.
26
30
  5. **Dispatch the implementer** (how: "Dispatching" below). Its prompt is the task id, the project root if it is not your working directory, and which `needs:` checks it may run. Do not paste the plan into the prompt; it reads the files.
@@ -43,8 +47,18 @@ Run commands from the project root. `<skill-dir>` is the directory this SKILL.md
43
47
  9. **Keep the area guide, if the area has one.** `node docs/.sw/sw.mjs explain <ID>` prints `area guide:` with a path or `none`.
44
48
  - A guide exists: add the reports' `Guide:` lines to it, one line per fact under Layout, Patterns, Verify or Gotchas. Replace a line the new fact corrects, and keep the page under 60 lines.
45
49
  - No guide: do nothing. A guide is worth starting once several tasks in an area have needed the same facts; if the user asks for one, create `docs/wiki/guide-<area, lowercase>.md` from `docs/.sw/templates/guide.md` and list it in `index.md`.
46
- 10. **File what else was learned.** If a report held a decision or constraint the wiki should keep, offer to save it as a wiki page (`type: decision` or `concept`) and add it to `index.md`. If the task fixed a problem whose cause is now known, or the review caught a defect worth remembering, offer a `type: lesson` page (Symptom, Cause, Fix, How to notice it earlier); sw-triage finds these later. If a report named follow-up work, offer to create the tasks. These are separate offers: act on each only when the user says yes to that one.
47
- 11. **Report** to the user: outcome, each requirement with its evidence, the review verdict and findings, anything that differs from the task, files changed, and which tasks this unblocked (`node docs/.sw/sw.mjs ready`). Commit only if the user asks. End with one line: the task is recorded, so the next task is cheapest in a new session.
50
+ 10. **File what else was learned.** These are separate offers: act on each only when the user says yes to that one.
51
+ - A report held a decision or constraint the wiki should keep: offer a wiki page (`type: decision` or `concept`), added to `index.md`.
52
+ - The task fixed a problem whose cause is now known, or the review caught a defect worth remembering: offer a `type: lesson` page (Symptom, Cause, Fix, How to notice it earlier); sw-triage finds these later.
53
+ - A report named follow-up work: offer to create the tasks.
54
+ 11. **Report** to the user, in this order. Commit only if the user asks.
55
+ - the outcome;
56
+ - each requirement with its evidence, and anything that differs from the task;
57
+ - the review verdict and its findings;
58
+ - the files changed;
59
+ - the tasks this unblocked (`node docs/.sw/sw.mjs ready`);
60
+ - what the task cost: run `node docs/.sw/sw.mjs stats` and show its table as printed;
61
+ - one last line: the task is recorded, so the next task is cheapest in a new session.
48
62
 
49
63
  ## Dispatching
50
64
 
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Generated by scripts/build.mjs from src/core.js and src/cli.js. Do not edit.
2
+ // Generated by scripts/build.mjs from src/core.js, src/sessions.js, src/stats.js, src/doctor.js, src/cli.js. Do not edit.
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, join, resolve as resolvePath, sep } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { createServer } from 'node:http';
8
+ import { fileURLToPath } from 'node:url';
3
9
  // Superwiki core: vault model, derived task state and lint. Pure: no fs, no DOM.
4
10
  // Runs in Node (docs/.sw/sw.mjs) and inlined in the viewer, so both report the same findings.
5
11
 
@@ -316,13 +322,588 @@ export function guideFor(vault, area) {
316
322
  return vault.pages.find(p => p.folder === 'wiki' && p.data.type === 'guide' && key(p.data.area ?? '') === key(area)) || null;
317
323
  }
318
324
 
325
+ // Finds the record an agent tool keeps of the session working in a project.
326
+ // Claude Code, Codex and Copilot CLI each write every session to disk in their own layout; this
327
+ // lists a project's sessions as { tool, id, modified, current, source } and picks one. What a
328
+ // record means is left to the readers in stats.js and doctor.js.
329
+
330
+ export const SESSION_TOOLS = ['claude', 'codex', 'copilot'];
331
+
332
+ // Codex keeps every project's sessions in one tree; only the most recent files are opened.
333
+ const CODEX_FILES_SCANNED = 100;
334
+
335
+ // A record still being written can end in half a line; lines that do not parse are skipped.
336
+ export function jsonLines(path) {
337
+ const records = [];
338
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
339
+ if (!line) continue;
340
+ try {
341
+ records.push(JSON.parse(line));
342
+ } catch {}
343
+ }
344
+ return records;
345
+ }
346
+
347
+ export function jsonFile(path) {
348
+ try {
349
+ return JSON.parse(readFileSync(path, 'utf8'));
350
+ } catch {
351
+ return {};
352
+ }
353
+ }
354
+
355
+ const modifiedAt = path => statSync(path).mtimeMs;
356
+
357
+ // The project root as typed and as resolved: tools record the working directory either way.
358
+ function rootForms(root) {
359
+ const forms = new Set([root]);
360
+ try {
361
+ forms.add(realpathSync(root));
362
+ } catch {}
363
+ return [...forms];
364
+ }
365
+
366
+ const isWithin = (roots, dir) => Boolean(dir) && roots.some(root => dir === root || dir.startsWith(root + sep));
367
+
368
+ // ---------- Claude Code ----------
369
+ // ~/.claude/projects/<working directory, non-alphanumerics as dashes>/<session>.jsonl, and next to
370
+ // it <session>/subagents/agent-<id>.jsonl with a .meta.json naming the agent type.
371
+ // source: { path, subagentDir }
372
+
373
+ const claudeHome = () => process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
374
+
375
+ function claudeSessions(root) {
376
+ const sessions = [];
377
+ for (const form of rootForms(root)) {
378
+ const dir = join(claudeHome(), 'projects', form.replace(/[^A-Za-z0-9]/g, '-'));
379
+ if (!existsSync(dir)) continue;
380
+ for (const name of readdirSync(dir)) {
381
+ if (!name.endsWith('.jsonl')) continue;
382
+ const id = name.slice(0, -'.jsonl'.length);
383
+ const path = join(dir, name);
384
+ sessions.push({
385
+ tool: 'claude',
386
+ id,
387
+ modified: modifiedAt(path),
388
+ current: id === process.env.CLAUDE_CODE_SESSION_ID,
389
+ source: { path, subagentDir: join(dir, id, 'subagents') },
390
+ });
391
+ }
392
+ }
393
+ return sessions;
394
+ }
395
+
396
+ // ---------- Codex ----------
397
+ // ~/.codex/sessions/<year>/<month>/<day>/rollout-*.jsonl. The first line is the session's meta:
398
+ // its working directory and, for a subagent, the session that spawned it and its role.
399
+ // source: { files: [{ meta, records }] }, one file per agent
400
+
401
+ const codexHome = () => process.env.CODEX_HOME || join(homedir(), '.codex');
402
+
403
+ function rolloutFiles(dir, found = []) {
404
+ if (!existsSync(dir)) return found;
405
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
406
+ const path = join(dir, entry.name);
407
+ if (entry.isDirectory()) rolloutFiles(path, found);
408
+ else if (entry.name.endsWith('.jsonl')) found.push({ path, modified: modifiedAt(path) });
409
+ }
410
+ return found;
411
+ }
412
+
413
+ function codexSessions(root) {
414
+ const roots = rootForms(root);
415
+ const recent = rolloutFiles(join(codexHome(), 'sessions')).sort((a, b) => b.modified - a.modified).slice(0, CODEX_FILES_SCANNED);
416
+ const byId = new Map();
417
+ for (const { path, modified } of recent) {
418
+ const records = jsonLines(path);
419
+ const meta = records[0]?.type === 'session_meta' ? records[0].payload : null;
420
+ if (!meta || !isWithin(roots, meta.cwd)) continue;
421
+ const id = meta.session_id || meta.id;
422
+ if (!byId.has(id)) byId.set(id, { modified: 0, files: [] });
423
+ const group = byId.get(id);
424
+ group.modified = Math.max(group.modified, modified);
425
+ group.files.push({ meta, records });
426
+ }
427
+ return [...byId].map(([id, { modified, files }]) => ({ tool: 'codex', id, modified, current: false, source: { files } }));
428
+ }
429
+
430
+ // ---------- Copilot CLI ----------
431
+ // ~/.copilot/session-state/<session>/events.jsonl, with the working directory in workspace.yaml.
432
+ // source: { path }
433
+
434
+ const copilotHome = () => join(homedir(), '.copilot');
435
+
436
+ function copilotSessions(root) {
437
+ const roots = rootForms(root);
438
+ const base = join(copilotHome(), 'session-state');
439
+ if (!existsSync(base)) return [];
440
+ const sessions = [];
441
+ for (const id of readdirSync(base)) {
442
+ const path = join(base, id, 'events.jsonl');
443
+ const workspace = join(base, id, 'workspace.yaml');
444
+ if (!existsSync(path) || !existsSync(workspace)) continue;
445
+ const cwd = (readFileSync(workspace, 'utf8').match(/^cwd: (.*)$/m) || [])[1];
446
+ if (!isWithin(roots, cwd)) continue;
447
+ sessions.push({ tool: 'copilot', id, modified: modifiedAt(path), current: false, source: { path } });
448
+ }
449
+ return sessions;
450
+ }
451
+
452
+ // ---------- Choosing ----------
453
+
454
+ const LISTERS = { claude: claudeSessions, codex: codexSessions, copilot: copilotSessions };
455
+
456
+ // The session to report: the one named by `id` (a prefix is enough), else the session this command
457
+ // runs in when the tool says which one that is, else the most recently written one.
458
+ export function findSession(root, { tool, id } = {}) {
459
+ let sessions = (tool ? [tool] : SESSION_TOOLS).flatMap(name => LISTERS[name](root));
460
+ if (id) sessions = sessions.filter(session => session.id.startsWith(id));
461
+ sessions.sort((a, b) => b.modified - a.modified);
462
+ return (!id && sessions.find(session => session.current)) || sessions[0] || null;
463
+ }
464
+
465
+ // Session statistics: what an agent session has cost so far, as one row per agent: the main
466
+ // session and each subagent it started. Reads the session record found by sessions.js.
467
+
468
+ const MAIN = 'main';
469
+
470
+ // ---------- The summary being built ----------
471
+
472
+ const newStats = session => ({ tool: session.tool, id: session.id, agents: [], tools: {}, note: '' });
473
+
474
+ // Token fields stay null when the record does not hold them, so "unknown" never prints as 0.
475
+ const newAgent = name => ({
476
+ name, model: '', steps: 0, first: null, peak: null, sent: null, cached: null, output: null, toolCalls: 0, start: null, end: null,
477
+ });
478
+
479
+ const plus = (sum, n) => (sum ?? 0) + (n || 0);
480
+
481
+ // One model request. `context` is everything sent with it, `cached` the part read from the cache.
482
+ function addStep(agent, { context, cached, output }) {
483
+ agent.steps++;
484
+ agent.first ??= context;
485
+ agent.peak = Math.max(agent.peak ?? 0, context);
486
+ agent.sent = plus(agent.sent, context);
487
+ agent.cached = plus(agent.cached, cached);
488
+ agent.output = plus(agent.output, output);
489
+ }
490
+
491
+ function addToolCall(stats, agent, name) {
492
+ agent.toolCalls++;
493
+ stats.tools[name] = (stats.tools[name] || 0) + 1;
494
+ }
495
+
496
+ // Widens the agent's working period to include this record.
497
+ function touch(agent, timestamp) {
498
+ const time = Date.parse(timestamp);
499
+ if (Number.isNaN(time)) return;
500
+ agent.start = Math.min(agent.start ?? time, time);
501
+ agent.end = Math.max(agent.end ?? time, time);
502
+ }
503
+
504
+ // Subagents are listed in the order they were started.
505
+ const byStart = (a, b) => (a.start ?? 0) - (b.start ?? 0);
506
+
507
+ // ---------- Claude Code ----------
508
+
509
+ function claudeAgent(stats, name, path) {
510
+ const agent = newAgent(name);
511
+ // A reply is written as one line per content block. Each line repeats the reply's usage, and
512
+ // only the last one has the final output count, so the last line of a reply is the one kept.
513
+ const replies = new Map();
514
+ for (const record of jsonLines(path)) {
515
+ touch(agent, record.timestamp);
516
+ const message = record.type === 'assistant' && record.message;
517
+ if (!message) continue;
518
+ for (const block of message.content || []) {
519
+ if (block.type === 'tool_use') addToolCall(stats, agent, block.name);
520
+ }
521
+ if (message.usage) replies.set(message.id, message);
522
+ }
523
+ for (const { model, usage } of replies.values()) {
524
+ agent.model = model || agent.model;
525
+ const cached = usage.cache_read_input_tokens || 0;
526
+ addStep(agent, {
527
+ context: (usage.input_tokens || 0) + cached + (usage.cache_creation_input_tokens || 0),
528
+ cached,
529
+ output: usage.output_tokens,
530
+ });
531
+ }
532
+ return agent;
533
+ }
534
+
535
+ function claudeStats(session) {
536
+ const { path, subagentDir } = session.source;
537
+ const stats = newStats(session);
538
+ stats.agents.push(claudeAgent(stats, MAIN, path));
539
+ if (!existsSync(subagentDir)) return stats;
540
+ const subagents = readdirSync(subagentDir)
541
+ .filter(name => name.endsWith('.jsonl'))
542
+ .map(name => {
543
+ const meta = jsonFile(join(subagentDir, name.replace(/\.jsonl$/, '.meta.json')));
544
+ return claudeAgent(stats, meta.agentType || 'subagent', join(subagentDir, name));
545
+ });
546
+ stats.agents.push(...subagents.sort(byStart));
547
+ return stats;
548
+ }
549
+
550
+ // ---------- Codex ----------
551
+
552
+ function codexAgent(stats, name, records) {
553
+ const agent = newAgent(name);
554
+ let lastTotal = null;
555
+ for (const record of records) {
556
+ touch(agent, record.timestamp);
557
+ const payload = record.payload || {};
558
+ if (record.type === 'turn_context') agent.model = payload.model || agent.model;
559
+ if (record.type === 'response_item' && /_call$/.test(payload.type || '')) addToolCall(stats, agent, payload.name || payload.type);
560
+ if (record.type !== 'event_msg' || payload.type !== 'token_count' || !payload.info) continue;
561
+ // The count is also repeated when only the rate limits change; a new request moves the total.
562
+ const total = payload.info.total_token_usage?.total_tokens;
563
+ if (total === lastTotal) continue;
564
+ lastTotal = total;
565
+ const last = payload.info.last_token_usage || {};
566
+ addStep(agent, { context: last.input_tokens || 0, cached: last.cached_input_tokens, output: last.output_tokens });
567
+ }
568
+ return agent;
569
+ }
570
+
571
+ function codexStats(session) {
572
+ const stats = newStats(session);
573
+ const subagents = [];
574
+ for (const { meta, records } of session.source.files) {
575
+ if (meta.thread_source === 'subagent') subagents.push(codexAgent(stats, meta.agent_role || meta.agent_nickname || 'subagent', records));
576
+ else stats.agents.push(codexAgent(stats, MAIN, records));
577
+ }
578
+ stats.agents.push(...subagents.sort(byStart));
579
+ return stats;
580
+ }
581
+
582
+ // ---------- Copilot CLI ----------
583
+ // Events of a subagent carry its agentId. Token counts are written only when the session closes.
584
+
585
+ function copilotStats(session) {
586
+ const stats = newStats(session);
587
+ const agents = new Map();
588
+ const agentOf = key => {
589
+ if (!agents.has(key)) agents.set(key, newAgent(key));
590
+ return agents.get(key);
591
+ };
592
+ agentOf(MAIN);
593
+ let closed = false;
594
+ for (const event of jsonLines(session.source.path)) {
595
+ const data = event.data || {};
596
+ const agent = agentOf(event.agentId || MAIN);
597
+ touch(agent, event.timestamp);
598
+ if (event.type === 'subagent.started') {
599
+ agent.name = data.agentName || agent.name;
600
+ } else if (event.type === 'assistant.message') {
601
+ agent.steps++;
602
+ agent.model = data.model || agent.model;
603
+ } else if (event.type === 'tool.execution_start') {
604
+ addToolCall(stats, agent, data.toolName);
605
+ } else if (event.type === 'session.shutdown' && data.agentMetrics) {
606
+ // A resumed session closes more than once; each close reports the run that ended with it.
607
+ closed = true;
608
+ for (const [key, metrics] of Object.entries(data.agentMetrics)) {
609
+ const reported = agentOf(key);
610
+ for (const { usage = {} } of Object.values(metrics.modelMetrics || {})) {
611
+ reported.sent = plus(reported.sent, usage.inputTokens);
612
+ reported.cached = plus(reported.cached, usage.cacheReadTokens);
613
+ reported.output = plus(reported.output, usage.outputTokens);
614
+ }
615
+ }
616
+ }
617
+ }
618
+ stats.agents = [...agents.values()];
619
+ if (!closed) stats.note = 'Copilot writes token counts when the session closes; until then /usage shows them.';
620
+ return stats;
621
+ }
622
+
623
+ const STATS_READERS = { claude: claudeStats, codex: codexStats, copilot: copilotStats };
624
+
625
+ export const sessionStats = session => STATS_READERS[session.tool](session);
626
+
627
+ // ---------- Printing ----------
628
+
629
+ const COLUMNS = ['agent', 'model', 'steps', 'first', 'peak', 'sent', 'cached', 'output', 'tools', 'min'];
630
+ const TEXT_COLUMNS = 2; // agent and model align left; the numbers after them align right
631
+ const LEGEND = 'first, peak: tokens sent with one request. sent: that, summed over every step. cached: the share of sent read from the cache.';
632
+
633
+ export function tokenCount(n) {
634
+ if (n == null) return '-';
635
+ if (n < 1000) return String(n);
636
+ if (n < 1e6) return `${Math.round(n / 1000)}k`;
637
+ return `${(n / 1e6).toFixed(1)}M`;
638
+ }
639
+
640
+ const cachedShare = agent => (agent.sent ? `${Math.round((100 * (agent.cached || 0)) / agent.sent)}%` : '-');
641
+ const minutes = (start, end) => Math.round((end - start) / 60000);
642
+
643
+ function clock(time) {
644
+ const d = new Date(time);
645
+ const two = n => String(n).padStart(2, '0');
646
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ${two(d.getHours())}:${two(d.getMinutes())}`;
647
+ }
648
+
649
+ // "2026-10-05 10:27 to 11:04, 37 min"; the end repeats the date only when it is another day.
650
+ function period(start, end) {
651
+ const [from, to] = [clock(start), clock(end)];
652
+ const sameDay = from.slice(0, 10) === to.slice(0, 10);
653
+ return `${from} to ${sameDay ? to.slice(11) : to}, ${minutes(start, end)} min`;
654
+ }
655
+
656
+ function sessionPeriod(agents) {
657
+ const timed = agents.filter(agent => agent.start != null);
658
+ if (!timed.length) return '';
659
+ return period(Math.min(...timed.map(agent => agent.start)), Math.max(...timed.map(agent => agent.end)));
660
+ }
661
+
662
+ // The sum of the agents. It has no context of its own and no period, so those cells stay empty.
663
+ function totalOf(agents) {
664
+ const total = newAgent('total');
665
+ for (const agent of agents) {
666
+ total.steps += agent.steps;
667
+ total.toolCalls += agent.toolCalls;
668
+ for (const field of ['sent', 'cached', 'output']) {
669
+ if (agent[field] != null) total[field] = plus(total[field], agent[field]);
670
+ }
671
+ }
672
+ return total;
673
+ }
674
+
675
+ const cells = agent => [
676
+ agent.name, agent.model, String(agent.steps), tokenCount(agent.first), tokenCount(agent.peak), tokenCount(agent.sent), cachedShare(agent),
677
+ tokenCount(agent.output), String(agent.toolCalls), agent.start == null ? '' : String(minutes(agent.start, agent.end)),
678
+ ];
679
+
680
+ // Rows of text cells as aligned lines: the first `textColumns` align left, the rest right.
681
+ export function alignedRows(rows, textColumns) {
682
+ const widths = rows[0].map((_, column) => Math.max(...rows.map(row => row[column].length)));
683
+ const pad = (cell, column) => (column < textColumns ? cell.padEnd(widths[column]) : cell.padStart(widths[column]));
684
+ return rows.map(row => row.map(pad).join(' ').trimEnd());
685
+ }
686
+
687
+ export function formatStats(stats) {
688
+ const { agents } = stats;
689
+ const rows = [COLUMNS, ...agents.map(cells)];
690
+ if (agents.length > 1) rows.push(cells(totalOf(agents)));
691
+ const calls = Object.entries(stats.tools).sort((a, b) => b[1] - a[1]).map(([name, count]) => `${name} ${count}`);
692
+ return [
693
+ ['session', stats.tool, stats.id, sessionPeriod(agents)].filter(Boolean).join(' '),
694
+ ...alignedRows(rows, TEXT_COLUMNS),
695
+ `tool calls ${calls.join(' ') || 'none'}`,
696
+ LEGEND,
697
+ ...(stats.note ? [stats.note] : []),
698
+ ].join('\n');
699
+ }
700
+
701
+ // Start context: what an agent session carries before it has read anything. Rule and memory
702
+ // files, the lists of skills, agents and tools, and what plugins and hooks add are sent again with
703
+ // every step of every agent, so this is the cheapest place to make a session lighter.
704
+ // Reads the session record found by sessions.js and reduces it to parts with a size and a source.
705
+
706
+ // The command that shows the same context in tokens, in each tool.
707
+ const CONTEXT_COMMANDS = { claude: '/context', codex: '/status', copilot: '/context' };
708
+ const SMALL_PART_CHARS = 512; // smaller parts are summed into "other"
709
+ const SOURCES_SHOWN = 6;
710
+ const UNGROUPED = '(none)';
711
+ const NOT_STARTED = 'This session has not made a request yet; its context is recorded with the first one.';
712
+
713
+ // ---------- The report being built ----------
714
+
715
+ const newReport = session => ({ tool: session.tool, id: session.id, firstRequest: null, parts: [], note: '' });
716
+
717
+ // A part's sources are either texts with a size or groups with a count: { label, amount }.
718
+ // `chars` is null for a part the record counts in tokens instead.
719
+ const newPart = (name, measure) => ({ name, chars: 0, tokens: null, count: null, measure, sources: [] });
720
+
721
+ // A part made of texts, each from a named source.
722
+ function sized(name, texts) {
723
+ const part = newPart(name, 'size');
724
+ for (const [label, text] of texts) {
725
+ part.chars += text.length;
726
+ part.sources.push({ label, amount: text.length });
727
+ }
728
+ return part;
729
+ }
730
+
731
+ // A part that is one list of names, grouped by where each name comes from.
732
+ function listed(name, names, text, groupOf) {
733
+ const part = newPart(name, 'count');
734
+ part.chars = text.length;
735
+ part.count = names.length;
736
+ const groups = new Map();
737
+ for (const item of names) groups.set(groupOf(item), (groups.get(groupOf(item)) || 0) + 1);
738
+ part.sources = [...groups].map(([label, amount]) => ({ label, amount }));
739
+ return part;
740
+ }
741
+
742
+ // "plugin:skill" and "plugin:agent" belong to the plugin; "mcp__server__tool" to the server.
743
+ const pluginOf = name => (name.includes(':') ? name.slice(0, name.indexOf(':')) : UNGROUPED);
744
+ const serverOf = name => (name.match(/^mcp__(.+?)__/) || [])[1] || UNGROUPED;
745
+
746
+ // "memory/MEMORY.md": the last folder says which of several same-named files this is.
747
+ const shortPath = path => `${basename(dirname(path))}/${basename(path)}`;
748
+
749
+ // ---------- Claude Code ----------
750
+ // Before the first reply the record holds one "attachment" per block of context the session was
751
+ // given: the instruction files, the skill, agent and tool lists, server instructions, hook output.
752
+
753
+ function claudePart(attachment) {
754
+ switch (attachment.type) {
755
+ case 'instructions':
756
+ return sized('rule and memory files', (attachment.files || []).map(file => [shortPath(file.path), file.content || '']));
757
+ case 'skill_listing':
758
+ return listed('skill list', attachment.names || [], attachment.content || '', pluginOf);
759
+ case 'agent_listing_delta':
760
+ return listed('agent list', attachment.addedTypes || [], (attachment.addedLines || []).join('\n'), pluginOf);
761
+ case 'deferred_tools_delta':
762
+ return listed('tool names (loaded on demand)', attachment.addedNames || [], (attachment.addedLines || []).join('\n'), serverOf);
763
+ case 'mcp_instructions_delta':
764
+ return sized('MCP server instructions', (attachment.addedNames || []).map((name, i) => [name, attachment.addedBlocks?.[i] || '']));
765
+ case 'hook_additional_context':
766
+ return sized('session-start hooks', [[attachment.hookName || 'hook', [attachment.content].flat().join('\n')]]);
767
+ default:
768
+ return null;
769
+ }
770
+ }
771
+
772
+ function claudeStart(session) {
773
+ const report = newReport(session);
774
+ for (const record of jsonLines(session.source.path)) {
775
+ if (record.type === 'attachment') {
776
+ const part = claudePart(record.attachment || {});
777
+ if (part) report.parts.push(part);
778
+ }
779
+ const usage = record.type === 'assistant' && record.message?.usage;
780
+ if (!usage) continue;
781
+ report.firstRequest = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0);
782
+ break;
783
+ }
784
+ if (report.firstRequest == null) report.note = NOT_STARTED;
785
+ return report;
786
+ }
787
+
788
+ // ---------- Codex ----------
789
+ // The session meta holds the base instructions. The messages before the first request hold the
790
+ // rest, one tagged block each: <skills_instructions>, <plugins_instructions>, the AGENTS.md text.
791
+
792
+ function codexBlockName(text) {
793
+ if (text.startsWith('# AGENTS.md')) return 'AGENTS.md';
794
+ const tag = (text.match(/^<([a-z_ ]+)>/) || [])[1];
795
+ return tag ? tag.replace(/_/g, ' ') : null; // untagged text is the user's own request
796
+ }
797
+
798
+ function codexStart(session) {
799
+ const report = newReport(session);
800
+ const main = session.source.files.find(file => file.meta.thread_source !== 'subagent') || session.source.files[0];
801
+ report.parts.push(sized('base instructions', [['codex', main.meta.base_instructions?.text || '']]));
802
+ for (const record of main.records) {
803
+ const payload = record.payload || {};
804
+ if (record.type === 'response_item' && payload.type === 'message') {
805
+ for (const block of payload.content || []) {
806
+ const name = codexBlockName(block.text || '');
807
+ if (name) report.parts.push(sized(name, [[payload.role, block.text]]));
808
+ }
809
+ }
810
+ if (record.type !== 'event_msg' || payload.type !== 'token_count' || !payload.info) continue;
811
+ report.firstRequest = payload.info.last_token_usage?.input_tokens ?? null;
812
+ break;
813
+ }
814
+ if (report.firstRequest == null) report.note = NOT_STARTED;
815
+ return report;
816
+ }
817
+
818
+ // ---------- Copilot CLI ----------
819
+ // The first system message holds the context as tagged blocks (<tools>, <custom_instruction>).
820
+ // A usage checkpoint, written later in the session, counts the tool definitions in tokens.
821
+
822
+ function copilotStart(session) {
823
+ const report = newReport(session);
824
+ const events = jsonLines(session.source.path);
825
+ const system = events.find(event => event.type === 'system.message')?.data?.content || '';
826
+ for (const [, tag, body] of system.matchAll(/<([a-z_]+)>(.*?)<\/\1>/gs)) report.parts.push(sized(tag.replace(/_/g, ' '), [['system', body]]));
827
+ const checkpoint = events.find(event => event.type === 'session.usage_checkpoint')?.data?.promptCacheBreakState?.[0]?.models;
828
+ const usage = checkpoint && Object.values(checkpoint)[0];
829
+ if (usage?.tool_tokens == null) {
830
+ report.note = 'Copilot counts its tool definitions at the first usage checkpoint; this session has not reached one.';
831
+ return report;
832
+ }
833
+ report.parts.push({ ...newPart('tool definitions', 'count'), chars: null, tokens: usage.tool_tokens, count: usage.tool_count ?? null });
834
+ return report;
835
+ }
836
+
837
+ const START_READERS = { claude: claudeStart, codex: codexStart, copilot: copilotStart };
838
+
839
+ // ---------- Tidying ----------
840
+
841
+ // Blocks with the same name become one part.
842
+ function mergedByName(parts) {
843
+ const byName = new Map();
844
+ for (const part of parts) {
845
+ const known = byName.get(part.name);
846
+ if (!known) {
847
+ byName.set(part.name, part);
848
+ continue;
849
+ }
850
+ known.chars += part.chars;
851
+ known.sources.push(...part.sources);
852
+ }
853
+ return [...byName.values()];
854
+ }
855
+
856
+ // Largest first; parts too small to matter are summed into "other", which comes last.
857
+ function ranked(parts) {
858
+ const other = newPart('other', 'size');
859
+ const kept = [];
860
+ for (const part of parts) {
861
+ if (part.chars == null || part.chars >= SMALL_PART_CHARS) kept.push(part);
862
+ else other.chars += part.chars;
863
+ }
864
+ kept.sort((a, b) => (b.chars ?? 0) - (a.chars ?? 0));
865
+ return other.chars ? [...kept, other] : kept;
866
+ }
867
+
868
+ export function startContext(session) {
869
+ const report = START_READERS[session.tool](session);
870
+ report.parts = ranked(mergedByName(report.parts));
871
+ for (const part of report.parts) part.sources.sort((a, b) => b.amount - a.amount);
872
+ return report;
873
+ }
874
+
875
+ // ---------- Printing ----------
876
+
877
+ const kilobytes = chars => `${(chars / 1024).toFixed(chars < 10 * 1024 ? 1 : 0)} KB`;
878
+ const sizeOf = part => (part.chars == null ? `${tokenCount(part.tokens)} tokens` : kilobytes(part.chars));
879
+
880
+ // "213: marketing-skills 41, claude-seo 25, +9 more" for a list, "MEMORY.md 18 KB, AGENTS.md 11 KB"
881
+ // for texts. A part that is a single text says nothing its name and size have not said.
882
+ function sourcesOf(part) {
883
+ if (part.measure === 'size' && part.sources.length < 2) return '';
884
+ const amount = source => (part.measure === 'size' ? kilobytes(source.amount) : String(source.amount));
885
+ const shown = part.sources.slice(0, SOURCES_SHOWN).map(source => `${source.label} ${amount(source)}`);
886
+ const hidden = part.sources.length - shown.length;
887
+ const list = shown.join(', ') + (hidden > 0 ? `, +${hidden} more` : '');
888
+ return part.count == null ? list : [String(part.count), list].filter(Boolean).join(': ');
889
+ }
890
+
891
+ export function formatStartContext(report) {
892
+ const rows = [['part', 'size', 'holds'], ...report.parts.map(part => [part.name, sizeOf(part), sourcesOf(part)])];
893
+ const [nameWidth, sizeWidth] = [0, 1].map(column => Math.max(...rows.map(row => row[column].length)));
894
+ return [
895
+ `context at session start ${report.tool} ${report.id}`,
896
+ ...(report.firstRequest == null ? [] : [`first request: ${tokenCount(report.firstRequest)} tokens`]),
897
+ ...rows.map(([name, size, holds]) => `${name.padEnd(nameWidth)} ${size.padStart(sizeWidth)} ${holds}`.trimEnd()),
898
+ `sizes are characters of text; ${CONTEXT_COMMANDS[report.tool]} shows this session's context in tokens`,
899
+ ...(report.note ? [report.note] : []),
900
+ ].join('\n');
901
+ }
902
+
319
903
  // Superwiki CLI. Lives in a project at docs/.sw/sw.mjs and prints short answers,
320
904
  // so agents do not have to read the vault to get them.
321
- import { spawn } from 'node:child_process';
322
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
323
- import { createServer } from 'node:http';
324
- import { basename, dirname, join, resolve as resolvePath } from 'node:path';
325
- import { fileURLToPath } from 'node:url';
905
+
906
+ const SESSION_FLAGS = `[--session <id>] [--tool ${SESSION_TOOLS.join('|')}]`;
326
907
 
327
908
  const HELP = `sw <command> [--docs <dir>] [--json]
328
909
 
@@ -333,6 +914,9 @@ const HELP = `sw <command> [--docs <dir>] [--json]
333
914
  search <words> pages and log entries that mention the words, best match first
334
915
  next-id <AREA> next free task id for an area (numbers are never reused)
335
916
  lint structural checks; exit code 1 on errors
917
+ stats what the agent session here has cost so far: steps, context and tokens per agent
918
+ doctor what that session carried before it read anything: rule files, skill and tool lists
919
+ both take ${SESSION_FLAGS} to look at another session of this project
336
920
  serve [--open] start (or reuse) a local viewer at http://127.0.0.1:<port>/ that reads the files live
337
921
  snapshot write docs/.sw/data.js so docs/viewer.html opens as a file, frozen at this moment`;
338
922
 
@@ -393,9 +977,9 @@ export function vaultData(docs) {
393
977
  // "<" is escaped so page text can never close the script element the viewer loads this with.
394
978
  export const dataScript = data => `window.SW_DATA = ${JSON.stringify(data).replace(/</g, '\\u003c')};\n`;
395
979
 
396
- // ---------- Commands ----------
397
- // Each command gets { docs, vault, args, flags } and returns { data, text, code? }.
398
- // `data` is what --json prints; `text` is the default output.
980
+ // ---------- Vault commands ----------
981
+ // Each command gets { docs, vault, args, flags } and returns { data, text, code? } or
982
+ // { error, code? }. `data` is what --json prints; `text` is the default output.
399
983
 
400
984
  function status({ vault }) {
401
985
  const s = summary(vault);
@@ -565,6 +1149,31 @@ function snapshot({ docs }) {
565
1149
  return { data: { files: data.files.length }, text };
566
1150
  }
567
1151
 
1152
+ // ---------- Session commands ----------
1153
+ // These read the record an agent tool keeps of a session, not the vault. Tools file a session
1154
+ // under the folder it ran in: the project root, which holds docs/.
1155
+
1156
+ function sessionArg(command, { docs, flags }) {
1157
+ if (flags.tool && !SESSION_TOOLS.includes(flags.tool)) return { error: `usage: sw ${command} ${SESSION_FLAGS}` };
1158
+ const root = dirname(docs);
1159
+ const session = findSession(root, { tool: flags.tool, id: flags.session });
1160
+ return session || { error: `no ${flags.tool || 'agent'} session record found for ${root}`, code: 1 };
1161
+ }
1162
+
1163
+ function statsCommand(ctx) {
1164
+ const session = sessionArg('stats', ctx);
1165
+ if (session.error) return session;
1166
+ const stats = sessionStats(session);
1167
+ return { data: stats, text: formatStats(stats) };
1168
+ }
1169
+
1170
+ function doctorCommand(ctx) {
1171
+ const session = sessionArg('doctor', ctx);
1172
+ if (session.error) return session;
1173
+ const report = startContext(session);
1174
+ return { data: report, text: formatStartContext(report) };
1175
+ }
1176
+
568
1177
  // ---------- Viewer server ----------
569
1178
  // A file:// page cannot read local files unless the user picks a folder. Served from localhost it
570
1179
  // can: every Refresh asks this process, which reads the files as they are now.
@@ -664,20 +1273,25 @@ const COMMANDS = {
664
1273
  search: { run: searchCommand, needsVault: true },
665
1274
  'next-id': { run: nextIdCommand, needsVault: true },
666
1275
  lint: { run: lintCommand, needsVault: true },
1276
+ stats: { run: statsCommand, needsVault: false },
1277
+ doctor: { run: doctorCommand, needsVault: false },
667
1278
  snapshot: { run: snapshot, needsVault: false },
668
1279
  serve: { run: serve, needsVault: false },
669
1280
  };
670
1281
 
1282
+ // Flags that are on or off, and flags that take the next argument as their value.
1283
+ const SWITCHES = ['json', 'open', 'foreground'];
1284
+ const OPTIONS = ['docs', 'tool', 'session'];
1285
+
1286
+ // Anything that is not a known flag is positional, so search words may start with dashes.
671
1287
  function parseArgs(argv) {
672
- const flags = { json: false, open: false, foreground: false, docs: null };
1288
+ const flags = Object.fromEntries([...SWITCHES.map(name => [name, false]), ...OPTIONS.map(name => [name, null])]);
673
1289
  const positional = [];
674
1290
  for (let i = 0; i < argv.length; i++) {
675
- const arg = argv[i];
676
- if (arg === '--docs') flags.docs = argv[++i] || '.';
677
- else if (arg === '--json') flags.json = true;
678
- else if (arg === '--open') flags.open = true;
679
- else if (arg === '--foreground') flags.foreground = true;
680
- else positional.push(arg);
1291
+ const name = argv[i].startsWith('--') ? argv[i].slice(2) : null;
1292
+ if (SWITCHES.includes(name)) flags[name] = true;
1293
+ else if (OPTIONS.includes(name)) flags[name] = argv[++i] ?? null;
1294
+ else positional.push(argv[i]);
681
1295
  }
682
1296
  return { command: positional[0], args: positional.slice(1), flags };
683
1297
  }
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: sw-stats
3
+ description: Use when the user asks what the current agent session has cost or used (tokens, context size, steps, tool calls, time, per subagent), wants a session summary or statistics, or invokes sw-stats or sw:stats.
4
+ ---
5
+
6
+ # sw-stats
7
+
8
+ Shows what this session has cost so far: one row for the main session and one for each subagent it started. A script reads the record your tool keeps of the session; you read nothing yourself and write no file.
9
+
10
+ Run from the project root:
11
+
12
+ ```bash
13
+ node docs/.sw/sw.mjs stats
14
+ ```
15
+
16
+ It reports the session it runs in. For another session of this project, add `--session <id or its first characters>`; to look only at one tool's sessions, `--tool claude|codex|copilot`.
17
+
18
+ ## Answer
19
+
20
+ 1. **Show the table as printed**, in a code block. Do not round, reorder or translate it.
21
+ 2. **Add at most three observations**, in the user's language, each one a number from the table and what it means. Pick the ones that apply:
22
+
23
+ | In the table | Say |
24
+ | --- | --- |
25
+ | one agent's `sent` is most of the total | which agent, and its share. That is where the session's cost is |
26
+ | an agent's `peak` is several times its `first` | its context grew during the work; `steps` times a large context is what makes `sent` large |
27
+ | `first` is large, for `main` or for the subagents | every agent starts heavy before it reads anything: rules, memory, and the lists of skills and tools. sw-doctor shows what fills it and what can go |
28
+ | `cached` is well below the others for one agent | much of what it sent was not served from the cache, which costs more per token. Long pauses do that |
29
+ | `main` has many `steps` or a `peak` far above its `first` | work is running in the main session that a subagent could do in a clean context |
30
+
31
+ 3. Nothing else: no advice the table does not support, and no price. The table counts tokens; what a token costs depends on the user's plan.
32
+
33
+ ## Columns
34
+
35
+ So that you can answer a question about them:
36
+
37
+ - `steps`: model requests. `tools`: tool calls. `min`: minutes between the agent's first and last record.
38
+ - `first`, `peak`: tokens sent with the first request and with the largest one.
39
+ - `sent`: tokens sent, summed over every step. A step sends the whole context again, so this is far larger than `peak`.
40
+ - `cached`: the share of `sent` that was read from the cache.
41
+ - `output`: tokens the model wrote.
42
+ - `-`: the tool's record does not hold that number.
43
+
44
+ ## If it fails
45
+
46
+ | Output | Do |
47
+ | --- | --- |
48
+ | `unknown command stats` | the project's `docs/.sw/sw.mjs` is older than this skill; offer to run sw-init, which updates it |
49
+ | `no agent session record found` | say so, and name the tool's own command instead: `/cost` or `/context` (Claude Code), `/status` (Codex), `/usage` (Copilot CLI). A session started in a subfolder of the project is recorded under that folder and is not found |
50
+ | a note that Copilot has not written token counts yet | pass the note on; steps and tool calls are still valid |