spexcode 0.2.1 → 0.2.3
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 +158 -103
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +24 -1
- package/spec-cli/src/attach.ts +50 -0
- package/spec-cli/src/cli.ts +217 -64
- package/spec-cli/src/client.ts +47 -9
- package/spec-cli/src/{self.ts → doctor.ts} +26 -25
- package/spec-cli/src/guide.ts +79 -21
- package/spec-cli/src/harness.ts +53 -29
- package/spec-cli/src/help.ts +137 -49
- package/spec-cli/src/index.ts +31 -11
- package/spec-cli/src/issues.ts +48 -21
- package/spec-cli/src/layout.ts +3 -5
- package/spec-cli/src/lint.ts +34 -5
- package/spec-cli/src/localIssues.ts +44 -60
- package/spec-cli/src/materialize.ts +4 -2
- package/spec-cli/src/mentions.ts +22 -1
- package/spec-cli/src/pty-bridge.ts +39 -4
- package/spec-cli/src/ranker.ts +31 -12
- package/spec-cli/src/search.bench.mjs +30 -7
- package/spec-cli/src/search.ts +39 -0
- package/spec-cli/src/sessions.ts +160 -69
- package/spec-cli/src/specs.ts +16 -4
- package/spec-cli/src/supervise.ts +30 -6
- package/spec-cli/src/tree.ts +118 -0
- package/spec-cli/templates/hooks/post-merge +2 -2
- package/spec-cli/templates/hooks/pre-commit +34 -15
- package/spec-cli/templates/hooks/prepare-commit-msg +8 -1
- package/spec-cli/templates/spexcode.json +7 -0
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +1 -0
- package/spec-dashboard/dist/assets/Dashboard-Dlg78cbC.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-CDxc1-in.js +3 -0
- package/spec-dashboard/dist/assets/FoldToggle-B5leylLf.js +1 -0
- package/spec-dashboard/dist/assets/IssuesPage-C2yFXiO-.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-RHNECU6x.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-YLD6IOmC.js +71 -0
- package/spec-dashboard/dist/assets/SessionWindow-CmKtpNUX.js +9 -0
- package/spec-dashboard/dist/assets/Settings-ZnOwskMZ.js +1 -0
- package/spec-dashboard/dist/assets/index-BdRQfrkR.js +41 -0
- package/spec-dashboard/dist/assets/index-DEc5Ru3l.css +1 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-yatsu/src/cli.ts +128 -26
- package/spec-yatsu/src/evaltab.ts +7 -6
- package/spec-yatsu/src/filing.ts +6 -3
- package/spec-yatsu/src/proof.ts +10 -0
- package/spec-yatsu/src/scenariofresh.ts +100 -30
- package/spec-yatsu/src/sidecar.ts +25 -3
- package/spec-yatsu/src/timeline.ts +53 -23
- package/README.zh-CN.md +0 -135
- package/spec-dashboard/dist/assets/index-Ct_ubwrd.css +0 -32
- package/spec-dashboard/dist/assets/index-DehTZ-h9.js +0 -145
package/README.md
CHANGED
|
@@ -1,149 +1,204 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="docs/banner.png" alt="SpexCode" width="720">
|
|
4
4
|
|
|
5
|
-
>
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://www.npmjs.com/package/spexcode"><img alt="npm" src="https://img.shields.io/npm/v/spexcode?logo=npm&logoColor=white&color=cb3837"></a>
|
|
7
|
+
<img alt="license: MIT" src="https://img.shields.io/badge/license-MIT-2f81f7">
|
|
8
|
+
<img alt="node ≥ 22" src="https://img.shields.io/badge/node-%E2%89%A5%2022-3fb950?logo=nodedotjs&logoColor=white">
|
|
9
|
+
<a href="https://spexcode.net"><img alt="docs" src="https://img.shields.io/badge/docs-spexcode.net-8957e5"></a>
|
|
10
|
+
</p>
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
*spec node*: a `.spec/**/spec.md` whose body states the part's present intent. Git is the database:
|
|
11
|
-
a node's version is its count of content commits, and "drift" is governed code that moved ahead of
|
|
12
|
-
its spec. The `spex` CLI and the live dashboard read everything straight from git; there is no
|
|
13
|
-
separate store.
|
|
12
|
+
</div>
|
|
14
13
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Spec-driven development with AI agents in the loop. SpexCode keeps a versioned tree of specs inside
|
|
15
|
+
your git repo, links every spec to the code it governs, and runs a session manager that dispatches
|
|
16
|
+
coding agents into isolated worktrees. You review and merge; the tool keeps intent and
|
|
17
|
+
implementation from drifting apart. (All screenshots below are this very repo on its own board.)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
[](https://spexcode.net/assets/spec-tree-growth.mp4)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
<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>
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
way to use it is by talking to your coding agent. You describe what you want in plain language
|
|
25
|
-
(*"add a spec node for the auth flow", "extract specs for this package", "dispatch a worker to
|
|
26
|
-
implement Y"*) and the agent runs the `spex` CLI for you while you supervise on the board. The
|
|
27
|
-
manual CLI below is the substrate; your agent is the daily interface.
|
|
23
|
+
English | [中文](./docs/README.zh-CN.md) · Docs: [spexcode.net](https://spexcode.net) · License: MIT
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`<!-- spexcode -->` managed block in your repo's `CLAUDE.md`/`AGENTS.md`, which
|
|
32
|
-
**[Claude Code](https://www.anthropic.com/claude-code)** and **Codex** auto-discover as always-on
|
|
33
|
-
context. From there the agent self-serves detail on demand from the built-in manual: `spex guide`
|
|
34
|
-
(the workflow), `spex guide spec` / `spex guide yatsu` (the file formats), and `spex guide config`
|
|
35
|
-
(every `spexcode.json` setting). You can tell it *"run `spex guide config` and set me up a
|
|
36
|
-
launcher"*.
|
|
25
|
+
Quick links: [the model](#the-model) · [quick start](#quick-start) ·
|
|
26
|
+
[agents](#working-with-agents) · [yatsu](#measuring-behavior-yatsu) · [config](#configuration)
|
|
37
27
|
|
|
38
|
-
The
|
|
39
|
-
for the agent-driven workflow, **[getting started](https://spexcode.net/getting-started/)** for the
|
|
40
|
-
setup end to end.
|
|
28
|
+
## The model
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
> spec files versioned by git, checked by `spex lint` and shown on a read-only dashboard. No AI,
|
|
44
|
-
> nothing to run but Node and git. The vibe-coding path sits on top of that; it doesn't replace it.
|
|
30
|
+
<div align="center"><img src="docs/sdd-tuxedo-pooh.png" alt="spec-driven development meme" width="260"></div>
|
|
45
31
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
A spec node is a directory under `.spec/` containing a `spec.md`: frontmatter (title, status, a
|
|
33
|
+
`code:` list of the files it governs) plus a prose body stating what that part of the system is
|
|
34
|
+
supposed to do, right now. Nodes nest, so the tree mirrors how you think about the project rather
|
|
35
|
+
than the file layout. The body has two parts. The short **raw source** states the intent; changing it takes explicit
|
|
36
|
+
human approval (an agent can draft it, a human signs off). The **expanded spec** is the agent's
|
|
37
|
+
detailed reading of that intent; it iterates freely but must always match the raw source.
|
|
50
38
|
|
|
51
|
-
|
|
39
|
+
<img src="docs/readme-node.png" alt="spec node popup">
|
|
52
40
|
|
|
53
|
-
|
|
41
|
+
Two rules make this workable:
|
|
54
42
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
1. **Git is the database.** There is no separate store. A node's version count is the number of
|
|
44
|
+
commits that touched its `spec.md`, its history view is `git log` on that file, and each version
|
|
45
|
+
is attributed to an agent session through a `Session:` commit trailer. This is also why a spec
|
|
46
|
+
body always describes present intent and gets rewritten in place: changelog headings inside the
|
|
47
|
+
body are banned (the linter enforces it), because git already keeps the history.
|
|
48
|
+
2. **Spec and code land together.** A change is one commit that updates both the `spec.md` and the
|
|
49
|
+
code it justifies. When code moves without its spec, the linter flags it,
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
drift: spec-cli/src/board.ts is 1 commit(s) ahead of spec 'board-lean' (v8) — may be stale
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
and keeps flagging until the spec catches up.
|
|
56
|
+
|
|
57
|
+
## The optimization loop
|
|
58
|
+
|
|
59
|
+
Specs, commits, and yatsu readings compose into one loop. The spec is the loss function: it states what you want, and
|
|
60
|
+
it's the half a human signs off on. Commits are the optimizer. **yatsu**, the measurement
|
|
61
|
+
subsystem, is the eval: it scores how far live behavior currently sits from the spec, and the
|
|
62
|
+
score's history lives in git like everything else.
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
`.config` plugins that define the dev flow), a starter **`spexcode.json`**, and the per-clone
|
|
63
|
-
**git hooks** (a `pre-commit` hook that runs **spec-lint**, blocking on broken spec↔code links, and
|
|
64
|
-
**main-guard**, which blocks direct commits to `main`, plus a `prepare-commit-msg` hook that stamps
|
|
65
|
-
each commit's session attribution). It also **materializes** the harness artifacts that make the
|
|
66
|
-
agent path work: the `<!-- spexcode -->` contract block in `CLAUDE.md`/`AGENTS.md`, and the
|
|
67
|
-
`.claude/` / `.codex/` shims (the `settings.json` hooks) a self-launched agent discovers. Those
|
|
68
|
-
artifacts are generated and gitignored; they are regenerated on each machine, never committed.
|
|
64
|
+
<img src="docs/readme-loop.png" alt="the spec/code optimization loop">
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
It also settles where the human stands day to day: nobody reads a neural net by staring at its
|
|
67
|
+
weights, and between merge gates you don't have to stare at agent diffs either. Attention goes to
|
|
68
|
+
the spec and the eval readings; the diff gets read once, at merge time.
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
Requires Node ≥ 22 and git. This part is plain tooling — no AI involved yet.
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
|
-
|
|
75
|
+
npm i -g spexcode # installs the `spex` command
|
|
76
|
+
cd your-repo
|
|
77
|
+
spex init # seeds .spec/, installs git hooks, renders the agent contract
|
|
78
|
+
spex serve # API backend on :8787
|
|
79
|
+
spex dashboard # board UI on :5173, proxying to the backend
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
`spex init` is additive. It works on any existing git repo and never overwrites your files: it
|
|
83
|
+
creates a root `.spec/project/spec.md` and a starter `spexcode.json`, installs the pre-commit
|
|
84
|
+
hooks, and writes a managed block into `CLAUDE.md`/`AGENTS.md` so any agent working in the repo
|
|
85
|
+
discovers the workflow on its own.
|
|
86
|
+
|
|
87
|
+
Then grow the tree:
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
1. Edit `.spec/project/spec.md` to describe the project.
|
|
90
|
+
2. Add child nodes for the parts you want governed, each with a `code:` list pointing at existing
|
|
91
|
+
files.
|
|
92
|
+
3. Run `spex lint`. Coverage warnings list the source files no spec claims yet; that list is your
|
|
93
|
+
adoption TODO.
|
|
81
94
|
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
leave zero trace in the tracked tree — ignores go to the local git exclude, not the committed
|
|
87
|
-
`.gitignore` — so you can run SpexCode on a repo you share but don't own). Facts that are true for
|
|
88
|
-
one machine.
|
|
95
|
+
You are not expected to hand-author all of this. The intended workflow is to have an agent do most
|
|
96
|
+
of the spec writing; `spex guide spec` prints the exact file format it needs.
|
|
97
|
+
[Getting started](https://spexcode.net/getting-started/) on the docs site walks the setup end to
|
|
98
|
+
end.
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
is the authoritative manual for every field and which of the two files it belongs in.
|
|
100
|
+
<img src="docs/readme-board.png" alt="dashboard screenshot">
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
*SpexCode's own repo on its own board; the sessions top-left are agents building the tool.*
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
## Working with agents
|
|
105
|
+
|
|
106
|
+
This part needs tmux and a logged-in [Claude Code](https://www.anthropic.com/claude-code) or Codex
|
|
107
|
+
on the machine.
|
|
96
108
|
|
|
97
109
|
```sh
|
|
98
|
-
spex
|
|
99
|
-
spex dashboard # the board UI on :5173, proxying /api to the backend
|
|
110
|
+
spex new "make the settings page remember the last tab" --node settings
|
|
100
111
|
```
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
launches a worker session in its own worktree on branch `node/settings`. The worker reads the
|
|
114
|
+
governing spec before touching code, makes the change, rewrites the spec body to match, commits
|
|
115
|
+
both (a hook stamps the `Session:` trailer), then proposes a merge and stops. Workers never merge
|
|
116
|
+
themselves. The merge stays with the manager: when you fire it, the session's own agent runs the
|
|
117
|
+
actual `git merge`, so conflicts land on the one who knows the work. The same dispatch is a
|
|
118
|
+
button on the dashboard (the new-session box on the board); the command form is what agents
|
|
119
|
+
themselves use when they delegate.
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
you can run several projects' boards side by side; the working directory picks which project each
|
|
106
|
-
serves. Give each tab its own identity in that project's `spexcode.json`: `dashboard.title` names it
|
|
107
|
-
and `dashboard.icon` sets the favicon — an emoji (`"🔭"`), an Iconify name (`"mdi:rocket-launch"`),
|
|
108
|
-
or a URL.
|
|
121
|
+
You supervise from outside — on the board, or with the same commands your agent uses:
|
|
109
122
|
|
|
110
|
-
|
|
123
|
+
```sh
|
|
124
|
+
spex watch # stream session transitions: launched / review / done / needs-input ...
|
|
125
|
+
spex review settings # commits ahead of trunk, merge-base diff, typecheck/lint gates
|
|
126
|
+
spex merge settings # gated merge into the trunk
|
|
127
|
+
spex session close settings
|
|
128
|
+
```
|
|
111
129
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
| `spex watch` | stream session / board transitions as they happen |
|
|
116
|
-
| `spex guide` | print the full workflow, plus the `spec.md` / `yatsu.md` / `config` manuals |
|
|
117
|
-
| `spex board` | dump the current board state as JSON |
|
|
130
|
+
Independent tasks run in parallel. Each worker is isolated in its own worktree, git serializes the
|
|
131
|
+
merges, and a pre-commit guard blocks direct commits on the trunk, so everything flows through
|
|
132
|
+
reviewable node branches.
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
The process is enforced by mechanism, not prompt engineering: the backend creates the branch and a
|
|
135
|
+
hook stamps the attribution; the materialized contract block carries the rest, so your dispatch
|
|
136
|
+
prompt stays task-only. More on this mode of working:
|
|
137
|
+
[working with agents](https://spexcode.net/working-with-agents/).
|
|
120
138
|
|
|
121
|
-
##
|
|
139
|
+
## Measuring behavior: yatsu
|
|
122
140
|
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
yatsu — short for **You As The Stupid User** — is the measuring half of
|
|
142
|
+
[the loop](#the-optimization-loop): you measure behavior from the product's real surface, the way a
|
|
143
|
+
clueless real end user would touch it, not through an internal helper or shortcut that makes the
|
|
144
|
+
proof easy. A spec says what a part should do; a
|
|
145
|
+
`yatsu.md` beside it says how to check. Each scenario is a plain description plus an expected
|
|
146
|
+
result. yatsu itself runs nothing (no DSL, no runner). An agent runs the scenario however it can:
|
|
147
|
+
a test file, a real browser, or just clicking through by hand and screenshotting. It compares
|
|
148
|
+
actual to expected and files the reading with evidence:
|
|
125
149
|
|
|
126
150
|
```sh
|
|
127
|
-
|
|
128
|
-
npm --prefix spec-cli install
|
|
129
|
-
npm --prefix spec-dashboard install
|
|
130
|
-
npm run hooks # install the per-clone git hooks (main-guard + the session-stamp hook)
|
|
151
|
+
spex yatsu eval settings --scenario remembers-tab --pass --image proof.png
|
|
131
152
|
```
|
|
132
153
|
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
Readings live in a git-tracked ndjson next to the spec, so measurements get the same attribution
|
|
155
|
+
and history as spec versions. Bug fixes are expected to bracket: file a failing reading that
|
|
156
|
+
reproduces the bug, fix, then file a passing reading on the same scenario.
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
<img src="docs/readme-eval.png" alt="eval view screenshot">
|
|
159
|
+
|
|
160
|
+
*The eval view: scenario readings on the left; the selected reading's expected result, staleness,
|
|
161
|
+
and recorded video evidence in the middle.*
|
|
162
|
+
|
|
163
|
+
## What's in the repo
|
|
164
|
+
|
|
165
|
+
| Package | Role |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `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. |
|
|
168
|
+
| `spec-dashboard` | React board: the node graph, per-node spec/history/issues panes, and a real terminal onto each live agent session. |
|
|
169
|
+
| `spec-yatsu` | Scenario definitions, readings, evidence blobs. |
|
|
170
|
+
| `spec-forge` | Read-only tracer that resolves a forge's open issues and PRs to the spec nodes they serve (GitHub today). An issue links itself with a `Spec: <node-id>` line in its body; a PR from a `node/<id>` branch links for free. |
|
|
171
|
+
|
|
172
|
+
## The linter
|
|
173
|
+
|
|
174
|
+
`spex lint` checks the spec↔code graph and is the real gate (the git hook is fast local feedback):
|
|
175
|
+
|
|
176
|
+
- **integrity** (error): a `code:` path that doesn't exist
|
|
177
|
+
- **living** (error): a changelog heading in a spec body
|
|
178
|
+
- **altitude** (warn): a body that slid from contract prose into an implementation dump. The usual
|
|
179
|
+
smell is a numbered step list or a wall of function names; this rule is why spec bodies stay
|
|
180
|
+
short enough to actually read
|
|
181
|
+
- **coverage** (warn): unclaimed source files
|
|
182
|
+
- **drift** (warn): governed code changed after its spec's last version, derived live from git
|
|
183
|
+
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
`spexcode.json` (committed, portable: layout, lint budgets, dashboard identity, launcher names) and
|
|
187
|
+
`spexcode.local.json` (gitignored, host-specific: absolute launcher paths, plus a `private: true`
|
|
188
|
+
overlay for repos you use but don't own) cover every setting. No `spex config set` yet: you edit the two files by hand (or ask your agent
|
|
189
|
+
to), and `spex guide config` documents every field. The other
|
|
190
|
+
manuals are `spex guide` (the workflow), `spex guide spec`, and `spex guide yatsu`; `spex help`
|
|
191
|
+
maps the commands.
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
140
194
|
|
|
141
|
-
|
|
195
|
+
[`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) gets you from a clone to a first merged change.
|
|
196
|
+
[`docs/AGENT_GUIDE.md`](docs/AGENT_GUIDE.md) has the full mechanics of the node model and the
|
|
197
|
+
reflexive config system.
|
|
142
198
|
|
|
143
|
-
##
|
|
199
|
+
## Credit
|
|
144
200
|
|
|
145
|
-
|
|
146
|
-
there for the first round of discussion and feedback.
|
|
201
|
+
First introduced on the [LINUX DO](https://linux.do) community — thanks to everyone there for the first round of discussion.
|
|
147
202
|
|
|
148
203
|
## License
|
|
149
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
package/spec-cli/bin/spex.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @@@ spex launcher - this repo has no build step, so the installed `spex` bin shells to tsx to
|
|
3
3
|
// run the TypeScript CLI directly. After `npm link` (or a global install) `spex lint` works anywhere.
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
|
-
import { existsSync } from 'node:fs'
|
|
5
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { dirname, join } from 'node:path'
|
|
@@ -12,6 +12,29 @@ import { dirname, join } from 'node:path'
|
|
|
12
12
|
// `spex` work from any cwd (agents, git hooks) against this package's code, operating on the cwd.
|
|
13
13
|
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..') // spec-cli/
|
|
14
14
|
const cli = join(pkg, 'src', 'cli.ts')
|
|
15
|
+
|
|
16
|
+
// @@@ mid-merge guard - no build step means every spex call parses this package's live TypeScript, so
|
|
17
|
+
// while a merge conflict is being resolved in the checkout that hosts it, the source holds conflict
|
|
18
|
+
// markers and tsx dies with a raw esbuild stacktrace — on EVERY call, including the Stop hook and an
|
|
19
|
+
// agent's `spex session done`. Catch that one transient state up front: scan the source trees the CLI
|
|
20
|
+
// imports (spec-cli ←→ spec-yatsu ←→ spec-forge), and if any file carries a marker, print one actionable
|
|
21
|
+
// line and exit 75 (EX_TEMPFAIL: transient, retry) instead of spawning tsx into the stacktrace.
|
|
22
|
+
const srcRoots = [join(pkg, 'src'), join(pkg, '..', 'spec-yatsu', 'src'), join(pkg, '..', 'spec-forge', 'src')]
|
|
23
|
+
const conflicted = srcRoots.flatMap((root) => {
|
|
24
|
+
if (!existsSync(root)) return []
|
|
25
|
+
return readdirSync(root, { recursive: true })
|
|
26
|
+
.filter((f) => /\.(ts|tsx|js|mjs)$/.test(String(f)))
|
|
27
|
+
.map((f) => join(root, String(f)))
|
|
28
|
+
.filter((path) => {
|
|
29
|
+
try { return /^<{7} /m.test(readFileSync(path, 'utf8')) } catch { return false }
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
if (conflicted.length) {
|
|
33
|
+
console.error('spex: paused mid-merge — unresolved conflict markers in the source spex runs:')
|
|
34
|
+
for (const f of conflicted) console.error(` ${f}`)
|
|
35
|
+
console.error('spex executes this TypeScript directly (no build step); resolve the merge, then retry. (exit 75)')
|
|
36
|
+
process.exit(75)
|
|
37
|
+
}
|
|
15
38
|
// tsx lives in spec-cli/node_modules in the dev monorepo, but npm may hoist it above the installed
|
|
16
39
|
// `spexcode` package in a real consumer project. Try local candidates first, then let Node resolve upward
|
|
17
40
|
// from spec-cli so project-local and global installs work the same way.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @@@ session attach - the HUMAN escape hatch into a worker: every session is just a tmux session on the
|
|
2
|
+
// backend's private socket, and the most direct way to see or rescue one is to sit in it. This verb is the
|
|
3
|
+
// sanctioned foreground `tmux attach` — no programmatic exception-handling ambition, the user fixes it by
|
|
4
|
+
// being there. It is deliberately the ONE session verb that does NOT route through the backend
|
|
5
|
+
// ([[remote-client]]'s exception): a terminal cannot be brokered over HTTP, and attaching a tmux CLIENT to
|
|
6
|
+
// the same server is tmux's native multi-client support, not a second actor on the socket. That makes it
|
|
7
|
+
// LOCAL-only by nature — the guards below fail loud (never degrade) when the premise doesn't hold.
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { networkInterfaces } from 'node:os'
|
|
10
|
+
import { alive, apiBase, TMUX_SOCK } from './sessions.js'
|
|
11
|
+
|
|
12
|
+
const AGENT_ALTERNATIVES = 'read the pane with `spex session capture`, drive it with `session send` / `session rawkey`'
|
|
13
|
+
|
|
14
|
+
// attach only makes sense on the machine that runs the tmux server — the backend's. The board the selector
|
|
15
|
+
// resolved against IS that backend, so the test is: does the RESOLVED backend (see [[remote-client]]'s
|
|
16
|
+
// ladder — flag / worker env / cwd record / fallback) point at this machine? Loopback and any address this
|
|
17
|
+
// host owns count as local; anything else (a tailnet/LAN IP of another box, a hostname we can't claim)
|
|
18
|
+
// fails loud with the reason and the remote-capable alternatives, never a silent local fallback onto a
|
|
19
|
+
// tmux socket that holds no sessions.
|
|
20
|
+
export async function assertLocalBackend(): Promise<void> {
|
|
21
|
+
const base = await apiBase()
|
|
22
|
+
let host: string
|
|
23
|
+
try { host = new URL(base).hostname } catch { host = '' }
|
|
24
|
+
const mine = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
|
|
25
|
+
for (const addrs of Object.values(networkInterfaces())) for (const a of addrs ?? []) mine.add(a.address)
|
|
26
|
+
if (mine.has(host)) return
|
|
27
|
+
console.error(`spex session attach: attach is LOCAL-only, and the resolved backend is another machine (${base}).
|
|
28
|
+
The tmux session lives on THAT machine — a terminal can't be attached over HTTP. Either run attach there
|
|
29
|
+
(e.g. over ssh), or ${AGENT_ALTERNATIVES} — those work remotely.`)
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// foreground takeover of the session's real tmux window; returns only via detach (C-b d) or the session
|
|
34
|
+
// ending. Interactive and blocking by design — a caller without a terminal (an agent inside its turn, a
|
|
35
|
+
// pipe) is refused up front and pointed at the remote-capable verbs instead of tmux's bare "not a terminal".
|
|
36
|
+
export async function attachSession(id: string): Promise<never> {
|
|
37
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
38
|
+
console.error(`spex session attach: attach is INTERACTIVE and needs a terminal — it blocks until you detach.
|
|
39
|
+
An agent must not run it inside a turn (it freezes you); ${AGENT_ALTERNATIVES}.`)
|
|
40
|
+
process.exit(2)
|
|
41
|
+
}
|
|
42
|
+
if (!(await alive(id))) {
|
|
43
|
+
console.error(`spex session attach: ${id} is offline — no live tmux session to attach.
|
|
44
|
+
Bring it back with \`spex session reopen ${id}\`, or read its record with \`spex ls ${id}\`.`)
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
console.log(`attaching to ${id} — detach with C-b d (the session keeps running)`)
|
|
48
|
+
const r = spawnSync('tmux', ['-u', '-L', TMUX_SOCK, 'attach-session', '-t', id], { stdio: 'inherit' })
|
|
49
|
+
process.exit(r.status ?? 1)
|
|
50
|
+
}
|