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.
- package/README.md +97 -149
- package/package.json +1 -1
- package/spec-cli/hooks/harness.sh +26 -6
- package/spec-cli/src/anchors.ts +27 -5
- package/spec-cli/src/cli.ts +171 -28
- package/spec-cli/src/client.ts +112 -13
- package/spec-cli/src/delivery-queue.ts +62 -4
- package/spec-cli/src/execution-trace.ts +444 -0
- package/spec-cli/src/gateway-hub.ts +10 -8
- package/spec-cli/src/gateway.ts +99 -6
- package/spec-cli/src/git.ts +477 -101
- package/spec-cli/src/graph.ts +56 -6
- package/spec-cli/src/graphCache.ts +86 -17
- package/spec-cli/src/graphStream.ts +1 -1
- package/spec-cli/src/guide.ts +93 -4
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +344 -66
- package/spec-cli/src/help.ts +22 -10
- package/spec-cli/src/host-resources.ts +62 -7
- package/spec-cli/src/identity-presets.js +16 -6
- package/spec-cli/src/index.ts +101 -13
- package/spec-cli/src/layout.ts +116 -33
- package/spec-cli/src/lint.ts +37 -7
- package/spec-cli/src/materialize.ts +10 -5
- package/spec-cli/src/plugin-harness.ts +10 -10
- package/spec-cli/src/reviews.ts +57 -28
- package/spec-cli/src/session-execution.ts +68 -0
- package/spec-cli/src/session-files.ts +121 -0
- package/spec-cli/src/session-follow.ts +4 -4
- package/spec-cli/src/session-reparent.ts +33 -0
- package/spec-cli/src/session-timeline.ts +163 -14
- package/spec-cli/src/session-web.ts +135 -0
- package/spec-cli/src/sessions.ts +584 -104
- package/spec-cli/src/uninstall.ts +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +2 -21
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -13
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +5 -22
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +4 -6
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +3 -9
- package/spec-dashboard/dist/assets/{App-F9uaAVcH.js → App-B6IbFbGu.js} +2 -2
- package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +2 -0
- package/spec-dashboard/dist/assets/{IssuesPage-CuKLFhH3.js → IssuesPage-BG9rNOgV.js} +1 -1
- package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +2 -0
- package/spec-dashboard/dist/assets/{Modal-CQgYymmr.js → Modal-B3EcgeA3.js} +1 -1
- package/spec-dashboard/dist/assets/{PageScroll-hT7UTLvD.js → PageScroll-CNZOugWV.js} +1 -1
- package/spec-dashboard/dist/assets/{ProjectsPage-CtXxakF9.js → ProjectsPage-C2CRHmvs.js} +1 -1
- package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +40 -0
- package/spec-dashboard/dist/assets/{SessionWindow-CixDi4PI.js → SessionWindow-1nYgZ4CL.js} +1 -1
- package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +1 -0
- package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +13 -0
- package/spec-dashboard/dist/assets/data-SlvxNA2r.js +1 -0
- package/spec-dashboard/dist/assets/{index-DrVao0Ep.js → index-DLVeOHL-.js} +10 -10
- package/spec-dashboard/dist/assets/index-DYLGfPjx.css +1 -0
- package/spec-dashboard/dist/assets/{launch-BBH02b1v.js → launch-yxCYr64x.js} +29 -29
- package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +1 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-eval/src/cli.ts +6 -1
- package/spec-eval/src/evaltab.ts +7 -5
- package/spec-eval/src/freshness.ts +383 -104
- package/spec-eval/src/scenariofresh.ts +2 -2
- package/spec-eval/src/sessioneval.ts +126 -132
- package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
- package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
- package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
- package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
- package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
- package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
- package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
- 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
|
|
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
|
-
[](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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
body
|
|
42
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
## Software as heuristic learning
|
|
64
60
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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
|
|
89
|
-
use
|
|
90
|
-
`spex init` is additive: it works on any existing git repo and
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
the
|
|
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
|
|
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
|
|
99
|
-
spex dashboard #
|
|
97
|
+
spex serve # this project's backend — prints its URL
|
|
98
|
+
spex dashboard # the machine's one gateway — every project behind one URL
|
|
100
99
|
```
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
Three stacked layers, and each is worth having without the ones above it:
|
|
126
108
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
on the machine.
|
|
118
|
+
## Working with agents (L1)
|
|
133
119
|
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
and recorded video evidence in the middle.*
|
|
148
|
+
## The dashboard (L2)
|
|
190
149
|
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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 (
|
|
11
|
-
# `pi`'s generated extension ([[pi-harness]])
|
|
12
|
-
# `case "$SPEXCODE_HARNESS"` below routes all
|
|
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)
|
|
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; }
|
package/spec-cli/src/anchors.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
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,
|
|
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
|
|