spexcode 0.6.1 → 0.6.2

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 (72) hide show
  1. package/README.md +97 -149
  2. package/package.json +1 -1
  3. package/spec-cli/hooks/harness.sh +26 -6
  4. package/spec-cli/src/anchors.ts +27 -5
  5. package/spec-cli/src/cli.ts +171 -28
  6. package/spec-cli/src/client.ts +112 -13
  7. package/spec-cli/src/delivery-queue.ts +62 -4
  8. package/spec-cli/src/execution-trace.ts +444 -0
  9. package/spec-cli/src/gateway-hub.ts +10 -8
  10. package/spec-cli/src/gateway.ts +99 -6
  11. package/spec-cli/src/git.ts +477 -101
  12. package/spec-cli/src/graph.ts +56 -6
  13. package/spec-cli/src/graphCache.ts +86 -17
  14. package/spec-cli/src/graphStream.ts +1 -1
  15. package/spec-cli/src/guide.ts +93 -4
  16. package/spec-cli/src/harness-select.ts +2 -2
  17. package/spec-cli/src/harness.ts +344 -66
  18. package/spec-cli/src/help.ts +22 -10
  19. package/spec-cli/src/host-resources.ts +62 -7
  20. package/spec-cli/src/identity-presets.js +16 -6
  21. package/spec-cli/src/index.ts +101 -13
  22. package/spec-cli/src/layout.ts +116 -33
  23. package/spec-cli/src/lint.ts +37 -7
  24. package/spec-cli/src/materialize.ts +10 -5
  25. package/spec-cli/src/plugin-harness.ts +10 -10
  26. package/spec-cli/src/reviews.ts +57 -28
  27. package/spec-cli/src/session-execution.ts +68 -0
  28. package/spec-cli/src/session-files.ts +121 -0
  29. package/spec-cli/src/session-follow.ts +4 -4
  30. package/spec-cli/src/session-reparent.ts +33 -0
  31. package/spec-cli/src/session-timeline.ts +163 -14
  32. package/spec-cli/src/session-web.ts +135 -0
  33. package/spec-cli/src/sessions.ts +584 -104
  34. package/spec-cli/src/uninstall.ts +1 -1
  35. package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +2 -21
  36. package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -13
  37. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +5 -22
  38. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -1
  39. package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +4 -6
  40. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +3 -9
  41. package/spec-dashboard/dist/assets/{App-F9uaAVcH.js → App-B6IbFbGu.js} +2 -2
  42. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +27 -0
  43. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +2 -0
  44. package/spec-dashboard/dist/assets/{IssuesPage-CuKLFhH3.js → IssuesPage-BG9rNOgV.js} +1 -1
  45. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +2 -0
  46. package/spec-dashboard/dist/assets/{Modal-CQgYymmr.js → Modal-B3EcgeA3.js} +1 -1
  47. package/spec-dashboard/dist/assets/{PageScroll-hT7UTLvD.js → PageScroll-CNZOugWV.js} +1 -1
  48. package/spec-dashboard/dist/assets/{ProjectsPage-CtXxakF9.js → ProjectsPage-C2CRHmvs.js} +1 -1
  49. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +40 -0
  50. package/spec-dashboard/dist/assets/{SessionWindow-CixDi4PI.js → SessionWindow-1nYgZ4CL.js} +1 -1
  51. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +1 -0
  52. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +13 -0
  53. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +1 -0
  54. package/spec-dashboard/dist/assets/{index-DrVao0Ep.js → index-DLVeOHL-.js} +10 -10
  55. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +1 -0
  56. package/spec-dashboard/dist/assets/{launch-BBH02b1v.js → launch-yxCYr64x.js} +29 -29
  57. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +1 -0
  58. package/spec-dashboard/dist/index.html +2 -2
  59. package/spec-eval/src/cli.ts +6 -1
  60. package/spec-eval/src/evaltab.ts +7 -5
  61. package/spec-eval/src/freshness.ts +383 -104
  62. package/spec-eval/src/scenariofresh.ts +2 -2
  63. package/spec-eval/src/sessioneval.ts +126 -132
  64. package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
  65. package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
  66. package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
  67. package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
  68. package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
  69. package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
  70. package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
  71. package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
  72. package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
package/README.md CHANGED
@@ -18,211 +18,159 @@
18
18
 
19
19
  </div>
20
20
 
21
- Spec-driven development with AI agents in the loop. SpexCode keeps a versioned tree of specs inside
21
+ Spec-driven orchestration for your coding agents. SpexCode keeps a versioned tree of specs inside
22
22
  your git repo, links every spec to the code it governs, and runs a session manager that dispatches
23
23
  coding agents into isolated worktrees. You review and merge; the tool keeps intent and
24
24
  implementation from drifting apart.
25
25
 
26
- [![Watch: this repo's spec tree grow from its git history](docs/spec-tree-poster.jpg)](https://spexcode.net/assets/spec-tree-growth.mp4)
27
-
28
- <sub>▶ This repo's own spec tree, replayed from its git history — 160 spec nodes growing over three weeks. Click for the [full video](https://spexcode.net/assets/spec-tree-growth.mp4).</sub>
29
-
30
26
  English | [中文](./docs/README.zh-CN.md) · Docs: [spexcode.net](https://spexcode.net) · License: MIT
31
27
 
32
- Quick links: [the model](#the-model) · [quick start](#quick-start) ·
33
- [agents](#working-with-agents) · [eval](#measuring-behavior-eval) · [config](#configuration)
28
+ | Feature | Description |
29
+ |---|---|
30
+ | **Computable spec–code drift** | Every spec pins the file it governs, down to the function. Whether code moved without its spec is computed from commits and line ranges, the same way on every machine: advisory for the file, blocking when an anchored function changes. |
31
+ | **Session & worktree management** | Each task runs in its own worktree and branch; independent tasks run in parallel. Sessions form a hierarchy: a session can dispatch and supervise workers of its own, so a worker can have a supervisor and that supervisor another. Workers propose; you review once, at merge time. |
32
+ | **Shareable URLs** | Spec nodes, sessions, evals, live terminals: every dashboard view has a stable address you can send to a colleague. Two people can watch the same session board. |
33
+ | **Modular layers** | Three separable layers: the spec↔code data asset (L0), the session substrate (L1), the dashboard (L2). Take what you need; L0 and L1 are building bricks for your own software factory. |
34
+ | **Cross-harness support** | Claude Code, Codex, OpenCode, pi, each interactive or headless. One materialized workflow contract serves them all; adding a harness is a config entry. |
34
35
 
35
36
  ## The model
36
37
 
37
- <div align="center"><img src="docs/sdd-tuxedo-pooh.png" alt="spec-driven development meme" width="260"></div>
38
-
39
- A spec node is a directory under `.spec/` containing a `spec.md`: frontmatter (title, status, a
40
- `code:` pointer to the file it governs, a `related:` list for files it references) plus a prose
41
- body stating what that part of the system is supposed to do, right now. Nodes nest, so the tree
42
- mirrors how you think about the project rather than the file layout. The body can split into two
43
- labelled parts. The short **raw source** states the intent; changing it takes explicit
44
- human approval (an agent can draft it, a human signs off). The **expanded spec** is the agent's
45
- detailed reading of that intent; it iterates freely but must always match the raw source.
38
+ A spec node is a directory under `.spec/` holding a `spec.md`: frontmatter naming the one file it
39
+ governs (`code:` — optionally pinned to specific functions with `path#symbol`) and the files it
40
+ references (`related:`), then a prose body stating what that part of the system is supposed to do,
41
+ right now. Nodes nest, so the tree mirrors how you think about the project rather than the file
42
+ layout. The body can split into two labelled parts: a short **raw source** a human signs off on, and
43
+ an **expanded spec** the agent iterates freely as long as it keeps matching the raw source.
46
44
 
47
- <img src="docs/readme-node.png" alt="spec node popup">
45
+ <img src="docs/readme-model.svg" alt="a spec node governs one file, anchored at function level; related files are referenced; git is the only database">
48
46
 
49
- Two rules make this workable:
47
+ Git is the only database: a node's versions are the commits that touched its
48
+ `spec.md`. A change is one commit that updates both the spec and the code it justifies. When code
49
+ moves alone, the linter notices:
50
50
 
51
- 1. **Git is the database.** There is no separate store. A node's version count is the number of
52
- commits that changed its `spec.md`, its history view is `git log` on that file, and each version
53
- is attributed to an agent session through a `Session:` commit trailer. This is also why a spec
54
- body always describes present intent and gets rewritten in place: changelog headings inside the
55
- body are banned (the linter enforces it), because git already keeps the history.
56
- 2. **Spec and code land together.** A change is one commit that updates both the `spec.md` and the
57
- code it justifies. When code moves without its spec, the linter flags it,
51
+ <img src="docs/readme-drift-flow.svg" alt="one real drift: spec and code land together, six days of code-only commits later a rename hits the anchored function — the commit is flagged and blocked">
58
52
 
59
- ```
60
- drift: spec-cli/src/graph.ts is 1 commit(s) ahead of spec 'graph-lean' (v12) may be stale
61
- ```
53
+ The check compares plain git facts: which commits came after the spec's last version, and whether
54
+ they intersect the anchored unit's lines. It cannot judge whether the new behavior is better, only
55
+ that the spec stopped describing it. The commit in the diagram updated seven other specs in the
56
+ same rename and missed this one; that kind of miss is normal work, and it is what a mechanical
57
+ check catches.
62
58
 
63
- and keeps flagging until the spec catches up.
59
+ ## Software as heuristic learning
64
60
 
65
- ## The optimization loop
61
+ Specs, commits, and evals compose into one optimization loop. The spec is the loss function: it
62
+ states what you want, and it is the half a human signs off on. Commits are the optimizer. **eval**,
63
+ the measurement subsystem, scores how far live behavior currently sits from the spec — an agent runs
64
+ each scenario against the product's real surface, the way an end user would touch it, and files the
65
+ result with evidence (a screenshot, a recording). The score's history lives in git like everything
66
+ else, and a bug fix is expected to bracket: a failing eval that reproduces the bug, then a passing
67
+ one on the same scenario.
66
68
 
67
- Specs, commits, and evals compose into one loop. The spec is the loss function: it states what you
68
- want, and it's the half a human signs off on. Commits are the optimizer. **eval**, the measurement
69
- subsystem, scores how far live behavior currently sits from the spec, and the
70
- score's history lives in git like everything else.
69
+ <div align="center"><img src="docs/readme-loop.svg" alt="the spec/code optimization loop" width="560"></div>
71
70
 
72
- <img src="docs/readme-loop.png" alt="the spec/code optimization loop">
73
-
74
- It also settles where the human stands day to day: nobody reads a neural net by staring at its
75
- weights, and between merge gates you don't have to stare at agent diffs either. Attention goes to
76
- the spec and the evals; the diff gets read once, at merge time.
71
+ Nobody reads a neural net by staring at its weights, and between merge gates you don't stare at
72
+ agent diffs either. Attention goes to the two ends — the spec and the evals; the diff gets read
73
+ once, at merge time.
77
74
 
78
75
  ## Quick start
79
76
 
80
- Requires Node ≥ 22 and git. This part is plain tooling — no AI involved yet.
77
+ Requires Node ≥ 22 and git.
81
78
 
82
79
  ```sh
83
80
  npm i -g spexcode # installs the `spex` command
84
81
  cd your-repo
85
- spex init --harness claude,codex,opencode,pi,claude-headless,opencode-headless,pi-headless,codex-headless # seeds .spec/, installs hooks, materializes the agent contracts
82
+ spex init --harness claude,codex,opencode,pi,zcode,claude-headless,opencode-headless,pi-headless,codex-headless # seeds .spec/, installs hooks, materializes the agent contracts
86
83
  ```
87
84
 
88
- That's the whole adoption. The example lists all the built-in harnesses remove the ones you don't
89
- use: `--harness` is required, has no default, and takes any one id or comma-separated subset.
90
- `spex init` is additive: it works on any existing git repo and never
91
- overwrites your files it creates a root `.spec/project/spec.md` and a starter `spexcode.json`,
92
- installs the git hooks, and writes the selected harness's managed contract, so any agent working in
93
- the repo discovers the workflow on its own.
85
+ That's the whole adoption. The example lists all the built-in harnesses; remove the ones you don't
86
+ use (`--harness` is required and takes any one id or comma-separated subset).
87
+ `spex init` is additive: it works on any existing git repo, never overwrites your files, and does
88
+ three things. It seeds a root `.spec/project/spec.md` plus a starter `spexcode.json`, installs the
89
+ git hooks, and **materializes** the workflow rules into the files your agent already reads
90
+ (`CLAUDE.md`, `AGENTS.md`): read the governing spec before the code, land spec and code in one
91
+ commit, propose merges instead of performing them. Any agent that opens the repo discovers the
92
+ workflow on its own.
94
93
 
95
- When you want the live board the graph, sessions, evals start the runtime:
94
+ When you want the live board (the graph, sessions, evals), start the runtime:
96
95
 
97
96
  ```sh
98
- spex serve # this project's backend — prints its URL, registers itself for your user
99
- spex dashboard # once per user, any directory: the one dashboardopen the URL it prints
97
+ spex serve # this project's backend — prints its URL
98
+ spex dashboard # the machine's one gatewayevery project behind one URL
100
99
  ```
101
100
 
102
- Run `spex serve` from each project you want online. Every backend registers itself, and the single
103
- `spex dashboard` finds them all backends already running and ones you start later, in any order.
104
- `/projects` switches and manages projects; each project's board lives under `/p/:id/`. There is no
105
- per-project dashboard process and no port pairing to remember: if a port is taken, give that
106
- backend its own with `spex serve --port <n>`, and trust the URL each command prints.
107
-
108
- Those are installed-user commands. Contributors working from this source checkout use `npm run api`
109
- for the reloadable backend and `npm run web` for the Vite/HMR frontend; see
110
- [Contributing](#contributing).
111
-
112
- Then grow the tree:
113
-
114
- 1. Edit `.spec/project/spec.md` to describe the project.
115
- 2. Add child nodes for the parts you want governed, each with a `code:` entry pointing at an
116
- existing file (`related:` for the files it touches but doesn't own).
117
- 3. Run `spex spec lint`. Coverage warnings list the source files no spec claims yet; that list is your
118
- adoption TODO.
101
+ One `spex dashboard` per machine is enough: every project you serve shows up behind it, and its
102
+ `/projects` page manages them from the browser.
103
+ [Getting started](https://spexcode.net/getting-started/) walks the rest of the setup.
119
104
 
120
- You are not expected to hand-author all of this. The intended workflow is to have an agent do most
121
- of the spec writing; `spex guide spec` prints the exact file format it needs.
122
- [Getting started](https://spexcode.net/getting-started/) on the docs site walks the setup end to
123
- end.
105
+ ## How does this system work
124
106
 
125
- <img src="docs/readme-graph.png" alt="dashboard screenshot">
107
+ Three stacked layers, and each is worth having without the ones above it:
126
108
 
127
- *SpexCode's own repo on its own board; the sessions top-left are agents building the tool.*
109
+ <img src="docs/readme-layers.svg" alt="L0 the spec-code data asset, L1 the agent session substrate, L2 the dashboard workspace — an adoption ladder">
128
110
 
129
- ## Working with agents
111
+ L0 is the asset an organisation adopts and keeps: plain files in plain git, useful offline.
112
+ ([Watch this repo's own L0 grow from its git
113
+ history](https://spexcode.net/assets/spec-tree-growth.mp4), 160 spec nodes over three weeks.)
114
+ L1 puts agents to work on that asset: the session state machine below. L2 is the workspace you
115
+ watch it all from, and because it is only a consumer of L1, anything the dashboard does, your
116
+ scripts and agents can do through the same CLI.
130
117
 
131
- This part needs tmux and a logged-in [Claude Code](https://www.anthropic.com/claude-code) or Codex
132
- on the machine.
118
+ ## Working with agents (L1)
133
119
 
134
- ```sh
135
- spex session new "[[settings]] make the settings page remember the last tab"
136
- ```
137
-
138
- launches a worker session in its own worktree on branch `node/settings-…`. The prompt's first
139
- `[[settings]]` mention sets the branch name and board attribution; the
140
- worker still finds and reads the governing spec itself before touching code. It makes the change,
141
- rewrites the spec body to match, commits
142
- both (a hook stamps the `Session:` trailer), then proposes a merge and stops. Workers never merge
143
- themselves. The merge stays with the manager: when you fire it, the session's own agent runs the
144
- actual `git merge`, so conflicts land on the one who knows the work. The same dispatch is a
145
- button on the dashboard (the new-session box on the board); the command form is what agents
146
- themselves use when they delegate.
147
-
148
- You supervise from outside — on the board, or with the same commands your agent uses:
120
+ This part needs tmux on the machine (on Windows, run inside WSL2).
149
121
 
150
122
  ```sh
151
- spex session watch # stream session transitions: launched / review / done / needs-input ...
152
- spex session review settings # commits ahead of trunk, merge-base diff, merge-conflict/lint gates
153
- spex session merge settings # gated merge into the trunk
154
- spex session close settings
123
+ spex session new "[[uploader]] retry failed chunks with backoff"
155
124
  ```
156
125
 
157
- Independent tasks run in parallel. Each worker is isolated in its own worktree, git serializes the
158
- merges, and a pre-commit guard blocks direct commits on the trunk, so everything flows through
159
- reviewable node branches.
126
+ launches a worker session in its own worktree on branch `node/uploader-…`. The prompt's first
127
+ `[[uploader]]` mention sets the branch name and board attribution; the worker finds and reads
128
+ the governing spec before it changes code. It makes the change, rewrites the spec body to
129
+ match, puts both in one commit, then files a merge proposal and stops:
160
130
 
161
- The process is enforced by mechanism, not prompt engineering: the backend creates the branch and a
162
- hook stamps the attribution; the materialized contract block carries the rest, so your dispatch
163
- prompt stays task-only. More on this mode of working:
164
- [working with agents](https://spexcode.net/working-with-agents/).
165
-
166
- ## Measuring behavior: eval
167
-
168
- eval is the measuring half of
169
- [the loop](#the-optimization-loop), built on the YATU discipline (**You As The User**): you measure
170
- behavior from the product's real surface, the way a real end user would touch it, not through an
171
- internal helper or shortcut that makes the
172
- proof easy. A spec says what a part should do; an
173
- `eval.md` beside it says how to check. Each scenario is a plain description plus an expected
174
- result. eval itself runs nothing (no DSL, no runner). An agent runs the scenario however it can:
175
- a test file, a real browser, or just clicking through by hand and screenshotting. It compares
176
- actual to expected and files the eval with evidence:
131
+ <img src="docs/readme-worker-flow.svg" alt="the eight-step worker loop: dispatch, read the spec, do the work, run evals, clear drift, propose a merge, human review, close">
177
132
 
178
133
  ```sh
179
- spex eval add settings --scenario remembers-tab --pass --image evidence.png
134
+ spex session ls # the living table below
135
+ spex session watch stream # follow transitions: working → review → done …
136
+ spex session review uploader # commits ahead of trunk, merge-base diff, merge/lint gates
137
+ spex session merge uploader # hands the gated merge to the session's own agent
138
+ spex session close uploader # retire the worktree, branch, and record
180
139
  ```
181
140
 
182
- Evals live in a git-tracked ndjson next to the spec, so measurements get the same attribution
183
- and history as spec versions. Bug fixes are expected to bracket: file a failing eval that
184
- reproduces the bug, fix, then file a passing eval on the same scenario.
141
+ <img src="docs/readme-sessions.svg" alt="animated terminal: spex session ls listing five sessions across working, review, asking and done states">
185
142
 
186
- <img src="docs/readme-eval.png" alt="eval view screenshot">
143
+ Each rule has a mechanism enforcing it: the backend creates the branch, a git hook attributes
144
+ every commit to its session, a pre-commit guard rejects direct commits to the trunk, and the
145
+ remaining conventions live in the materialized `CLAUDE.md`/`AGENTS.md`. Your dispatch prompt only
146
+ has to state the task. More: [working with agents](https://spexcode.net/working-with-agents/).
187
147
 
188
- *The eval view: scenario evals on the left; the selected eval's expected result, staleness,
189
- and recorded video evidence in the middle.*
148
+ ## The dashboard (L2)
190
149
 
191
- ## What's in the repo
192
-
193
- | Package | Role |
194
- |---|---|
195
- | `spec-cli` | The `spex` CLI and the HTTP backend (Hono, runs via tsx, no build step). Reads `.spec` and git live; owns the session state machine and the linter. |
196
- | `spec-dashboard` | React board: the node graph, per-node spec/history/issues panes, and a real terminal onto each live agent session. |
197
- | `spec-eval` | Scenario definitions, evals, evidence. |
198
- | `spec-forge` | Read-only tracer that resolves a forge's open issues and PRs to the spec nodes they serve (GitHub and GitLab drivers today). An issue links itself with a `Spec: <node-id>` line in its body; a PR from a `node/<id>` branch links for free. |
150
+ The spec tree, the sessions, and the evals each have a live page on the dashboard. Start `spex serve` and `spex dashboard`, then:
199
151
 
200
- ## The linter
152
+ <img src="docs/readme-graph.png" alt="the spec map: SpexCode's own repo on its own board — per-node version and eval chips, an agent avatar hovering on the node it is editing">
201
153
 
202
- `spex spec lint` checks the spec↔code graph and is the real gate (the git hook is fast local feedback):
154
+ *Your whole repo as one map SpexCode's own board shown. Each node carries its version and eval
155
+ state, an agent's avatar hovers on the node it is editing right now, and the rail top-left is the
156
+ live session console.*
203
157
 
204
- - **integrity** (error): a `code:` or `related:` path that doesn't exist
205
- - **living** (error): a changelog heading in a spec body
206
- - **coverage** (warn): unclaimed source files
207
- - **drift** (warn): governed code changed after its spec's last version, derived live from git
158
+ <img src="docs/readme-node.png" alt="a node opened on the board: the raw source callout, the expanded spec body, the governed file, a drift badge, and tabs for history, issues, eval">
208
159
 
209
- plus naming and ownership rules (`one-govern`, `id-format`, `mention` as errors;
210
- `related-drift`, `owners`, `confusable-id` as warns) `spex guide spec` lists them all.
160
+ *Click a node: the raw source on top, the expanded spec below it, the file it governs, its current
161
+ drift state, and tabs for the version history git already kept, its issues, and its evals.*
211
162
 
212
- `spex doctor` is the opt-in, read-only health diagnosis. Its altitude and breadth checks report bodies that
213
- look like implementation dumps and nodes with wide child fan-out, with per-node evidence and repair, without
214
- putting heuristic judgment in the lint or commit gate.
163
+ <img src="docs/readme-eval.png" alt="an eval reading under review: verdict banner, the scenario's expected result, the agent's note, recorded video evidence, and the review queue">
215
164
 
216
- ## Configuration
165
+ *An eval reading under review: the verdict, the scenario's expected result, the agent's note and
166
+ recorded video evidence. You can draw a region on the video to annotate it; the annotation is
167
+ matched to its step automatically, and the timestamp and step are sent to the agent with your
168
+ comment. The queue on the right leads to the next reading.*
217
169
 
218
- `spexcode.json` (committed, portable: layout, lint policy, doctor health budgets, project dashboard identity, launcher names) and
219
- `spexcode.local.json` (gitignored, host-specific: absolute launcher paths, cert paths) cover every
220
- setting. There is no imperative settings verb: you edit the two files by hand (or ask your agent
221
- to), and `spex guide settings` documents every field. The Projects admin UI writes a project's icon back
222
- to that same `dashboard.icon` field; the global gateway icon is the one separate host fact at
223
- `$SPEXCODE_HOME/config.json` `gateway.icon`, never copied into a repo. The other
224
- manuals are `spex guide` (the workflow), `spex guide spec`, `spex guide eval`, and
225
- `spex guide footprint`; `spex help` maps the commands.
170
+ The whole workspace is served over HTTP, so every view (a spec node, a session, an eval reading, a
171
+ live terminal) is a stable URL you can hand to a colleague; you can sit on the same board together.
172
+ The terminal pane is a real tmux session: copy the printed command and attach from your own
173
+ terminal.
226
174
 
227
175
  ## Contributing
228
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
6
6
  "license": "MIT",
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env bash
2
2
  # @@@ harness.sh - the SHELL face of the [[harness-adapter]] (spec-cli/src/harness.ts). The hook scripts run
3
3
  # as pure shell, so they cannot import the TS adapter; this sourced library is its mirror. dispatch.sh sources
4
- # it and exports SPEXCODE_HARNESS (claude|codex|opencode) — baked into the shim by each adapter, so a hook learns its
4
+ # it and exports SPEXCODE_HARNESS (claude|codex|opencode|zcode) — baked into the shim by each adapter, so a hook learns its
5
5
  # harness deterministically, never by sniffing the payload shape. EVERY harness-divergent payload-parse lives
6
6
  # HERE; the hook scripts stay harness-agnostic and just call hp_* (the one place Claude's tool names appear in
7
7
  # shell, plus codex's Bash-command mapping). The session-id + global-store resolution is harness-agnostic and
8
8
  # lives here too, so the six hooks no longer each repeat the git-common-dir → project-key dance.
9
- # SPEXCODE_HARNESS is claude|codex|opencode|pi|plugin: `plugin` is the bundle form ([[plugin-harness]]) and its
10
- # host (z-code/Claude) shares Claude's payload shape; `opencode`'s generated plugin ([[opencode-harness]]) and
11
- # `pi`'s generated extension ([[pi-harness]]) SYNTHESIZE claude-shaped payloads by construction — so every
12
- # `case "$SPEXCODE_HARNESS"` below routes all three through the claude branch via the default case; only codex
9
+ # SPEXCODE_HARNESS is claude|codex|opencode|pi|zcode|plugin: `plugin` is the bundle form ([[plugin-harness]]) and its
10
+ # host (adopter-a/Claude) shares Claude's payload shape; `opencode`'s generated plugin ([[opencode-harness]]),
11
+ # `pi`'s generated extension ([[pi-harness]]), and z-code's native hook payload all share that Claude shape — so every
12
+ # `case "$SPEXCODE_HARNESS"` below routes all four through the claude branch via the default case; only codex
13
13
  # keeps parse arms of its own.
14
14
 
15
15
 
@@ -68,11 +68,28 @@ hp_field() {
68
68
  # every child tool call fired mark-active against the PARENT's record — the parent read `working` forever and
69
69
  # every park/done declaration was clobbered within seconds (measured). Same staleness class the codex branch
70
70
  # already guards against (payload-first below); claude now follows the same rule.
71
+ # The claude preference is RESOLUTION-AWARE, not blind. "Claude's payload id equals its governed record id"
72
+ # holds only while the launched conversation lives: a compaction/continuation MINTS A NEW conversation id
73
+ # while the record keeps the launched one, and a blindly-preferred payload id then resolves to no record at
74
+ # all — hp_store_dir echoes a path nothing sits at, so every record-dependent hook (stop-gate, mark-active,
75
+ # idle, session-fail) silently no-ops and the session reads `working` forever (measured: a live session froze
76
+ # at `active` while its terminal still answered). So the payload wins only when a record actually answers to
77
+ # it, and an unresolvable payload falls back to the launched SPEXCODE_SESSION_ID. This cannot reopen the
78
+ # subagent bug above: a Task subagent's payload carries the PARENT's session_id, which DOES resolve, so the
79
+ # preference is unchanged there — the divergent branch is reached only when the two ids differ AND the
80
+ # payload names nothing. The common path costs one string compare (the ids are equal), never a store read.
71
81
  hp_session_id() {
72
82
  local pid
73
83
  case "$SPEXCODE_HARNESS" in
74
84
  codex) hp_field "$1" session_id ;;
75
- *) pid=$(hp_field "$1" session_id); printf '%s' "${pid:-$SPEXCODE_SESSION_ID}" ;;
85
+ *) pid=$(hp_field "$1" session_id)
86
+ if [ -n "$pid" ] && [ -n "$SPEXCODE_SESSION_ID" ] && [ "$pid" != "$SPEXCODE_SESSION_ID" ] \
87
+ && [ ! -e "$(hp_store_dir "$pid")/session.json" ] \
88
+ && [ -e "$(hp_store_dir "$SPEXCODE_SESSION_ID")/session.json" ]; then
89
+ printf '%s' "$SPEXCODE_SESSION_ID"
90
+ else
91
+ printf '%s' "${pid:-$SPEXCODE_SESSION_ID}"
92
+ fi ;;
76
93
  esac
77
94
  }
78
95
 
@@ -129,6 +146,9 @@ hp_store_dir() {
129
146
  local rd; rd=$(hp_runtime_dir) || return 1
130
147
  local direct="$rd/sessions/$1"
131
148
  if [ -e "$direct/session.json" ]; then printf '%s' "$direct"; return 0; fi
149
+ # same two-halves-of-absence rule as layout.ts's readAliasedRecordEntry: an id owning a store dir is already
150
+ # one of ours, so its emptiness is settled and the alias grep is the wrong question (and a whole-store scan).
151
+ if [ -d "$direct" ]; then printf '%s' "$direct"; return 0; fi
132
152
  local hit
133
153
  hit=$(grep -lF "\"harness_session_id\": \"$1\"" "$rd"/sessions/*/session.json 2>/dev/null | head -1)
134
154
  [ -n "$hit" ] && { printf '%s' "${hit%/session.json}"; return 0; }
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path'
2
2
  import { createRequire } from 'node:module'
3
- import { git, gitRequiredA, gitObjectFormat, isGitObjectId, batchRevisionOids, batchBlobTexts, combinedDiffOwnedChanges, driftPathWindow, type DiffLineRange, type DriftIndex, type DriftPathEvent } from './git.js'
3
+ import { git, gitRequiredA, gitObjectFormat, isGitObjectId, batchRevisionOids, batchBlobTexts, combinedDiffOwnedChanges, driftPathWindow, readImmutableHunkFacts, persistImmutableHunkFacts, withEventLedgerBuild, type DiffLineRange, type DriftIndex, type DriftPathEvent, type ImmutableHunkRanges } from './git.js'
4
4
 
5
5
  const RS = '\x1e'
6
6
 
@@ -484,7 +484,8 @@ const hunkMemo = new Map<string, HunkRanges>()
484
484
  // historical path. Keying on those makes the key move exactly when an input does, and the oids are already
485
485
  // resolved by this read's one `cat-file --batch-check`, so completeness costs no extra child and no new state.
486
486
  const ABSENT_IMAGE = '-'
487
- const hunkMemoKey = (images: string[]) => images.join('\0')
487
+ const HUNK_FACT_SCHEMA = 'anchor-range-histogram-v1'
488
+ const hunkMemoKey = (images: string[]) => `${HUNK_FACT_SCHEMA}\0${images.join('\0')}`
488
489
  function rememberHunks(key: string, ranges: HunkRanges): HunkRanges {
489
490
  if (hunkMemo.size >= MEMO_MAX) hunkMemo.clear()
490
491
  hunkMemo.set(key, ranges)
@@ -584,6 +585,10 @@ const anchorRevisionKey = ({ commit, path }: AnchorRevision) => `${commit}\0${pa
584
585
  // selector verdict.
585
586
  async function runAnchorQueries(root: string, queries: AnchorHitQuery[], regs: Extractor[], stopAtFirstHit: boolean): Promise<AnchorHit[][]> {
586
587
  if (!queries.length) return []
588
+ return withEventLedgerBuild(root, () => runAnchorQueriesInLedger(root, queries, regs, stopAtFirstHit))
589
+ }
590
+
591
+ async function runAnchorQueriesInLedger(root: string, queries: AnchorHitQuery[], regs: Extractor[], stopAtFirstHit: boolean): Promise<AnchorHit[][]> {
587
592
  const objectFormat = gitObjectFormat(root)
588
593
  const revisions = new Map<string, AnchorRevision>()
589
594
  const ordinaryEvents: { path: string; commit: string; event: DriftPathEvent }[] = []
@@ -604,14 +609,28 @@ async function runAnchorQueries(root: string, queries: AnchorHitQuery[], regs: E
604
609
  imageOf(event.commit, event.historicalPath),
605
610
  ...event.parents.map(({ commit, historicalPath }) => imageOf(commit, historicalPath)),
606
611
  ])
612
+ const hunkKeys = new Set<string>()
607
613
  const ordinaryByPath = new Map<string, Map<string, string>>()
608
614
  for (const { path, commit, event } of ordinaryEvents) {
609
615
  const entries = ordinaryByPath.get(path) ?? new Map<string, string>()
610
- entries.set(commit, imageIdentity(event))
616
+ const key = imageIdentity(event)
617
+ hunkKeys.add(key)
618
+ entries.set(commit, key)
611
619
  ordinaryByPath.set(path, entries)
612
620
  }
621
+ for (const { win } of queries) for (const event of win) hunkKeys.add(imageIdentity(event))
622
+ const durableHunks = readImmutableHunkFacts(root, hunkKeys)
623
+ for (const [key, ranges] of durableHunks) rememberHunks(key, ranges)
624
+ const newHunks = new Map<string, ImmutableHunkRanges>()
613
625
  const ordinaryHunks = new Map<string, Map<string, HunkRanges>>()
614
- for (const [path, entries] of ordinaryByPath) ordinaryHunks.set(path, await hunksAtMany(root, path, entries))
626
+ for (const [path, entries] of ordinaryByPath) {
627
+ const rangesByCommit = await hunksAtMany(root, path, entries)
628
+ ordinaryHunks.set(path, rangesByCommit)
629
+ for (const [commit, key] of entries) {
630
+ const ranges = rangesByCommit.get(commit)
631
+ if (ranges && !durableHunks.has(key)) newHunks.set(key, ranges)
632
+ }
633
+ }
615
634
  // The image's own memo answers before any bytes move, so the read asks git only for the revisions this
616
635
  // process has not parsed yet — the retention half of the same rule the hunk demand set follows.
617
636
  const units = new Map<string, FileRevisionUnits>()
@@ -665,8 +684,10 @@ async function runAnchorQueries(root: string, queries: AnchorHitQuery[], regs: E
665
684
  run.cursor++
666
685
  const after = units.get(anchorRevisionKey({ commit: event.commit, path: event.historicalPath }))!
667
686
  const before = event.parents.map(({ commit, historicalPath }) => units.get(anchorRevisionKey({ commit, path: historicalPath }))!)
687
+ const key = imageIdentity(event)
668
688
  const ranges = ordinaryHunks.get(event.historicalPath)?.get(event.commit)
669
- ?? await hunksAt(root, event, imageIdentity(event))
689
+ ?? await hunksAt(root, event, key)
690
+ if (!durableHunks.has(key)) newHunks.set(key, ranges)
670
691
  if (event.parents.length && ranges.before.length !== before.length)
671
692
  throw new Error(`anchor diff for ${event.commit}:${event.historicalPath} has ${ranges.before.length} parent ranges for ${before.length} parents`)
672
693
  const hit = run.hits.get(event.commit) ?? { selectors: new Set<string>() }
@@ -688,6 +709,7 @@ async function runAnchorQueries(root: string, queries: AnchorHitQuery[], regs: E
688
709
  }
689
710
  }
690
711
  }
712
+ await persistImmutableHunkFacts(root, newHunks)
691
713
  return runs.map((run) => [...run.hits].map(([commit, hit]) => ({ commit, selectors: [...hit.selectors], ...(hit.unparseable ? { unparseable: hit.unparseable } : {}) })))
692
714
  }
693
715