spexcode 0.2.0 → 0.2.1

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 CHANGED
@@ -1,52 +1,52 @@
1
1
  <img src="docs/sdd-tuxedo-pooh.png" alt="Writing code vs. authoring a living, executable specification artifact" width="420">
2
2
 
3
+ English | [中文](./README.zh-CN.md)
4
+
3
5
  > Spec-driven development fails two ways: the spec drifts out of sync with the code, or it
4
- > bloats into stale ceremony. SpexCode keeps each spec short and current rewritten in place,
5
- > versioned by git, never an accumulating changelog.
6
+ > bloats into stale ceremony. SpexCode keeps each spec short and current: rewritten in place
7
+ > and versioned by git, not an accumulating changelog.
6
8
 
7
9
  **SpexCode** is a spec-driven, self-developing dev tool. Every part of a project becomes a versioned
8
- *spec node* a `.spec/**/spec.md` whose body states the part's *present* intent and **git is the
9
- database**: a node's version is its count of content commits, and "drift" is governed code that moved
10
- ahead of its spec. A `spex` CLI plus a live dashboard read all of it straight from git; there is no
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
11
13
  separate store.
12
14
 
13
15
  - **[Using SpexCode](#using-spexcode)** — adopt the `spex` CLI and drive it through your coding agent to govern *your own* project.
14
16
  - **[Contributing to SpexCode](#contributing-to-spexcode)** — hack on the tool itself, in this repo.
15
- - **[Working with agents →](https://spexcode.net/working-with-agents/)** — the deeper narrative on the docs site, behind this landing.
17
+ - **[Working with agents →](https://spexcode.net/working-with-agents/)** — the longer write-up on the docs site.
16
18
 
17
19
  ---
18
20
 
19
21
  ## Using SpexCode
20
22
 
21
- You set SpexCode up once `npm i -g spexcode` then `spex init` in your repo and after that the primary
22
- way you use it is **by talking to your coding agent**. You describe what you want in plain language
23
- *"add a spec node for the auth flow", "extract specs for this package", "dispatch a worker to implement
24
- Y"* and the agent runs the `spex` CLI for you while you supervise on the board. The manual CLI below is
25
- the substrate; your agent is the daily interface.
23
+ You set SpexCode up once (`npm i -g spexcode`, then `spex init` in your repo). After that, the main
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.
26
28
 
27
- That works because a freshly-launched agent **already knows SpexCode**. `spex init` materializes the whole
28
- contract the spec-node ritual, the commit-before-declare gate, the merge style into a
29
+ That works because a freshly launched agent already knows SpexCode. `spex init` materializes the
30
+ contract (the spec-node ritual, the commit-before-declare gate, the merge style) into a
29
31
  `<!-- spexcode -->` managed block in your repo's `CLAUDE.md`/`AGENTS.md`, which
30
- **[Claude Code](https://www.anthropic.com/claude-code)** and **Codex** **auto-discover** as always-on
31
- context (no `--append-system-prompt`, nothing to wire). From there the agent self-serves detail on demand
32
- from the built-in manual — `spex guide` (the workflow), `spex guide spec` / `spex guide yatsu` (the file
33
- formats), and `spex guide config` (every `spexcode.json` setting). You can literally say *"run `spex guide
34
- config` and set me up a launcher"* and it will.
35
-
36
- For the full narrative the three pillars of driving SpexCode by talking to your agent (agent
37
- self-learning via the materialized block + `spex guide`, config, and the init artifacts) see
38
- **[working with agents](https://spexcode.net/working-with-agents/)** on the docs site;
39
- **[getting started](https://spexcode.net/getting-started/)** walks the setup end to end. This README is
40
- the concise landing; the site goes deep.
41
-
42
- > **It's also just plain tooling.** Strip the agent away and the core is still useful on its own: **spec
43
- > files versioned by git**, checked by `spex lint` and shown on a read-only dashboard — no AI, and nothing
44
- > to run but Node and git. The vibe-coding path sits *on top* of that; it doesn't replace it.
45
-
46
- > **Requirements.** Core: **Node 22** and **git**. Driving SpexCode through an agent (or dispatching
47
- > workers onto your nodes) also needs **tmux** and an authenticated **Claude Code or Codex** on your PATH
48
- > — and those agents run commands on your machine, so read [`SECURITY.md`](./docs/SECURITY.md) before
49
- > exposing the backend.
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"*.
37
+
38
+ The docs site covers this in full: **[working with agents](https://spexcode.net/working-with-agents/)**
39
+ for the agent-driven workflow, **[getting started](https://spexcode.net/getting-started/)** for the
40
+ setup end to end.
41
+
42
+ > **It's also just plain tooling.** Strip the agent away and the core is still useful on its own:
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.
45
+
46
+ > **Requirements.** Core: **Node ≥ 22** and **git**. Driving SpexCode through an agent (or
47
+ > dispatching workers onto your nodes) also needs **tmux** and an authenticated **Claude Code or
48
+ > Codex** on your PATH. Those agents run commands on your machine, so read
49
+ > [`SECURITY.md`](./docs/SECURITY.md) before exposing the backend.
50
50
 
51
51
  ### Set it up
52
52
 
@@ -58,17 +58,18 @@ cd ~/my-app
58
58
  spex init # additive — never restructures your code
59
59
  ```
60
60
 
61
- `spex init` is additive: it seeds a starter **`.spec/`** tree (a root `project` node plus the `.config`
62
- plugins that define the dev flow), a starter **`spexcode.json`**, and the per-clone **git hooks** (a
63
- `pre-commit` hook that runs **spec-lint** blocking on broken spec↔code links and **main-guard**,
64
- which blocks direct commits to `main`, plus a `prepare-commit-msg` hook that stamps each commit's
65
- session attribution). It also **materializes** the harness artifacts that make the agent path
66
- work: the `<!-- spexcode -->` contract block in `CLAUDE.md`/`AGENTS.md`, and the `.claude/` / `.codex/`
67
- shims (the `settings.json` hooks) a self-launched agent discovers. Those artifacts are generated and
68
- gitignored regenerated on each machine, never committed.
61
+ `spex init` is additive: it seeds a starter **`.spec/`** tree (a root `project` node plus the
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.
69
69
 
70
- Then make it yours — either ask your agent to, or do it by hand: edit `.spec/project/spec.md` to describe
71
- the project, point `spexcode.json`'s `lint.governedRoots` at your real source dir(s), and check the graph:
70
+ Then make it yours — either ask your agent to, or do it by hand: edit `.spec/project/spec.md` to
71
+ describe the project, point `spexcode.json`'s `lint.governedRoots` at your real source dir(s), and
72
+ check the graph:
72
73
 
73
74
  ```sh
74
75
  spex lint # the "coverage" warnings are your adoption TODO list
@@ -76,18 +77,18 @@ spex lint # the "coverage" warnings are your adoption TODO list
76
77
 
77
78
  ### Configure it
78
79
 
79
- Two optional JSON files at the repo root hold every setting, split by portability — pick the right one and
80
- that's the whole discipline:
80
+ Two optional JSON files at the repo root hold every setting, split by portability:
81
81
 
82
- - **`spexcode.json`** — *committed, portable*: layout, dashboard identity (`title` + `icon`), lint budgets,
83
- and launcher **names**. Facts that are true for the project.
84
- - **`spexcode.local.json`** — *gitignored, host-specific*: absolute launcher command paths, cert/secret
85
- paths, and the private-overlay switch (`private: true`, which makes `spex materialize` leave zero trace
86
- in the tracked tree — ignores go to the local git exclude, not the committed `.gitignore` — so you can
87
- run SpexCode on a repo you share but don't own). Facts that are true for one machine.
82
+ - **`spexcode.json`** — *committed, portable*: layout, dashboard identity (`title` + `icon`), lint
83
+ budgets, and launcher **names**. Facts that are true for the project.
84
+ - **`spexcode.local.json`** — *gitignored, host-specific*: absolute launcher command paths,
85
+ cert/secret paths, and the private-overlay switch (`private: true`, which makes `spex materialize`
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.
88
89
 
89
- There is no `spex config set` — you (or your agent) edit the files directly. **`spex guide config`** is the
90
- authoritative manual for every field and which of the two files it belongs in.
90
+ There is no `spex config set` — you (or your agent) edit the files directly. **`spex guide config`**
91
+ is the authoritative manual for every field and which of the two files it belongs in.
91
92
 
92
93
  ### Run it
93
94
 
@@ -100,11 +101,11 @@ spex dashboard # the board UI on :5173, proxying /api to the backend
100
101
 
101
102
  Open <http://localhost:5173>.
102
103
 
103
- Both ports are flags (`spex serve --port 8788`, `spex dashboard --port 5174 --api-port 8788`), so you can
104
- run several projects' boards side by side the working directory picks which project each serves. Give
105
- each tab its own identity in that project's `spexcode.json`: `dashboard.title` names it and
106
- `dashboard.icon` sets the favicon — an emoji (`"🔭"`), an Iconify name (`"mdi:rocket-launch"`), or a URL,
107
- nothing to download.
104
+ Both ports are flags (`spex serve --port 8788`, `spex dashboard --port 5174 --api-port 8788`), so
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.
108
109
 
109
110
  Day to day (the commands your agent runs for you — and that you can run yourself):
110
111
 
@@ -115,15 +116,12 @@ Day to day (the commands your agent runs for you — and that you can run yourse
115
116
  | `spex guide` | print the full workflow, plus the `spec.md` / `yatsu.md` / `config` manuals |
116
117
  | `spex board` | dump the current board state as JSON |
117
118
 
118
- The spec tree is ground truth and git is its database: every change is a `spec.md` node, **rewritten in
119
- place** (never a `## vN` changelog) and versioned by its commits.
120
-
121
119
  ---
122
120
 
123
121
  ## Contributing to SpexCode
124
122
 
125
- This repository *is* the SpexCode source, and it **dogfoods itself**: every change to the tool lands as a
126
- spec node merged into `main`. Set up a checkout:
123
+ This repository *is* the SpexCode source, and it dogfoods itself: every change to the tool lands as
124
+ a spec node merged into `main`. Set up a checkout:
127
125
 
128
126
  ```sh
129
127
  git clone https://github.com/shuxueshuxue/spexcode && cd spexcode
@@ -132,8 +130,8 @@ npm --prefix spec-dashboard install
132
130
  npm run hooks # install the per-clone git hooks (main-guard + the session-stamp hook)
133
131
  ```
134
132
 
135
- The development loop runs from source, with hot-reload — this is what `npm run web` is for, as opposed
136
- to an installed user's `spex dashboard`:
133
+ The development loop runs from source, with hot reload (`npm run web`, as opposed to an installed
134
+ user's `spex dashboard`):
137
135
 
138
136
  ```sh
139
137
  npm run api # backend on :8787, hot-reloads on spec-cli/src changes
@@ -142,6 +140,11 @@ npm run web # the dashboard via Vite (HMR), proxying /api → :8787
142
140
 
143
141
  ---
144
142
 
143
+ ## Credits
144
+
145
+ SpexCode was first introduced publicly on [LINUX DO](https://linux.do) — thanks to the community
146
+ there for the first round of discussion and feedback.
147
+
145
148
  ## License
146
149
 
147
150
  [MIT](./LICENSE).
@@ -0,0 +1,135 @@
1
+ <img src="docs/sdd-tuxedo-pooh.png" alt="写代码 vs. 维护一份活的、可执行的规格文档" width="420">
2
+
3
+ [English](./README.md) | 中文
4
+
5
+ > Spec 驱动开发通常死于两种方式:spec 和代码脱节,或者膨胀成一堆过时的仪式文档。SpexCode 让每个
6
+ > spec 保持简短、保持当前态:原地重写,由 git 记版本,不做累积式的 changelog。
7
+
8
+ **SpexCode** 是一个 spec 驱动、用自己开发自己的开发工具。项目的每个部分都是一个带版本的
9
+ *spec 节点*,即一个 `.spec/**/spec.md`,正文描述这个部分当前的意图。git 就是数据库:节点的版本号
10
+ 是它的内容 commit 数,"drift" 指被管辖的代码走到了 spec 前面。`spex` CLI 和实时看板直接从 git
11
+ 读取这一切,没有第二份存储。
12
+
13
+ - **[使用 SpexCode](#使用-spexcode)**:安装 `spex` CLI,通过你的 coding agent 治理*你自己的*项目。
14
+ - **[参与开发](#参与开发)**:在本仓库里改进工具本身。
15
+ - **[Working with agents →](https://spexcode.net/working-with-agents/)**:文档站上更完整的介绍。
16
+
17
+ ---
18
+
19
+ ## 使用 SpexCode
20
+
21
+ SpexCode 只需要设置一次(`npm i -g spexcode`,然后在你的仓库里 `spex init`)。之后的主要用法是和
22
+ 你的 coding agent 对话:用自然语言说你要什么(*"给鉴权流程加一个 spec 节点""把这个包的 spec
23
+ 提取出来""派一个 worker 去实现 Y"*),agent 替你执行 `spex` CLI,你在看板上盯进度。下面的手动
24
+ CLI 是底座,agent 才是日常界面。
25
+
26
+ 这套之所以成立,是因为新启动的 agent 已经认识 SpexCode。`spex init` 会把整套约定(spec 节点流程、
27
+ 先 commit 再声明完成、merge 方式)生成到仓库 `CLAUDE.md`/`AGENTS.md` 里的 `<!-- spexcode -->`
28
+ 托管块中,**[Claude Code](https://www.anthropic.com/claude-code)** 和 **Codex** 启动时自动读到。
29
+ 更细的内容 agent 自己按需查内置手册:`spex guide`(工作流)、`spex guide spec` /
30
+ `spex guide yatsu`(文件格式)、`spex guide config`(`spexcode.json` 的全部配置项)。你可以直接
31
+ 对它说*"跑一下 `spex guide config`,帮我配好 launcher"*。
32
+
33
+ 完整的说明在文档站:**[working with agents](https://spexcode.net/working-with-agents/)** 讲
34
+ agent 驱动的用法,**[getting started](https://spexcode.net/getting-started/)** 从头到尾走一遍安装。
35
+
36
+ > **抛开 agent,它也是一套普通工具。** 核心部分单独用也成立:git 记版本的 spec 文件,`spex lint`
37
+ > 检查,只读看板展示。不含 AI,除了 Node 和 git 什么都不用跑。vibe coding 的玩法建立在这层之上,
38
+ > 不是替代它。
39
+
40
+ > **环境要求。** 核心:**Node ≥ 22** 和 **git**。要通过 agent 驱动(或者往节点上派 worker),
41
+ > 还需要 **tmux** 和 PATH 里已登录的 **Claude Code 或 Codex**。这些 agent 会在你的机器上执行
42
+ > 命令,对外暴露后端之前请先读 [`SECURITY.md`](./docs/SECURITY.md)。
43
+
44
+ ### 安装
45
+
46
+ 装一次发布版 CLI,然后在任何项目里接入:
47
+
48
+ ```sh
49
+ npm i -g spexcode # 安装 spex 命令(需要 Node ≥ 22)
50
+ cd ~/my-app
51
+ spex init # 纯增量,不会动你的代码结构
52
+ ```
53
+
54
+ `spex init` 是增量式的:生成一棵起始 **`.spec/`** 树(根 `project` 节点,加上定义开发流程的
55
+ `.config` 插件)、一份起始 **`spexcode.json`**、每个 clone 各自的 **git hooks**(`pre-commit`
56
+ 跑 **spec-lint**,spec↔code 链接坏了会拦下;**main-guard** 拦截直接向 `main` 的提交;
57
+ `prepare-commit-msg` 给每个 commit 盖 session 归属戳)。同时生成 agent 路径需要的产物:
58
+ `CLAUDE.md`/`AGENTS.md` 里的 `<!-- spexcode -->` 契约块,以及 `.claude/`、`.codex/` 的
59
+ settings hooks。这些产物是生成物,已被 gitignore,每台机器各自重新生成,不进版本库。
60
+
61
+ 然后把它变成你的项目,让 agent 改或者自己动手都行:编辑 `.spec/project/spec.md` 描述项目,把
62
+ `spexcode.json` 的 `lint.governedRoots` 指向真实源码目录,再检查一遍:
63
+
64
+ ```sh
65
+ spex lint # coverage 警告就是你的接入 TODO 清单
66
+ ```
67
+
68
+ ### 配置
69
+
70
+ 仓库根目录两个可选的 JSON 文件承载全部设置,按可移植性分开:
71
+
72
+ - **`spexcode.json`**:提交进仓库,可移植。布局、看板标识(`title` + `icon`)、lint 预算、
73
+ launcher 的**名字**。对整个项目成立的事实。
74
+ - **`spexcode.local.json`**:gitignore,单机。launcher 命令的绝对路径、证书和密钥路径、私有覆盖
75
+ 开关(`private: true`,让 `spex materialize` 在被跟踪的文件里不留任何痕迹,ignore 写进本地
76
+ git exclude 而不是提交的 `.gitignore`,适合你参与但不拥有的仓库)。只对这台机器成立的事实。
77
+
78
+ 没有 `spex config set`,你(或你的 agent)直接编辑文件。每个字段的含义、该放哪个文件,
79
+ **`spex guide config`** 是权威手册。
80
+
81
+ ### 运行
82
+
83
+ 启动后端和看板,打开页面:
84
+
85
+ ```sh
86
+ spex serve # 后端(API + sessions),:8787
87
+ spex dashboard # 看板 UI,:5173,/api 代理到后端
88
+ ```
89
+
90
+ 打开 <http://localhost:5173>。
91
+
92
+ 两个端口都是参数(`spex serve --port 8788`、`spex dashboard --port 5174 --api-port 8788`),
93
+ 多个项目的看板可以并排跑,工作目录决定各自服务哪个项目。每个标签页的身份在各自项目的
94
+ `spexcode.json` 里配:`dashboard.title` 定名字,`dashboard.icon` 定 favicon,emoji(`"🔭"`)、
95
+ Iconify 名字(`"mdi:rocket-launch"`)或 URL 都行。
96
+
97
+ 日常命令(agent 替你跑,你也可以自己跑):
98
+
99
+ | 命令 | 作用 |
100
+ | --- | --- |
101
+ | `spex lint` | 检查 spec↔code 图:coverage、drift、living 规则 |
102
+ | `spex watch` | 实时输出 session / 看板的状态变化 |
103
+ | `spex guide` | 打印完整工作流,以及 `spec.md` / `yatsu.md` / `config` 手册 |
104
+ | `spex board` | 以 JSON 输出当前看板状态 |
105
+
106
+ ---
107
+
108
+ ## 参与开发
109
+
110
+ 这个仓库就是 SpexCode 的源码,而且它 dogfood 自己:工具的每个改动都以 spec 节点的形式合入
111
+ `main`。搭一个开发环境:
112
+
113
+ ```sh
114
+ git clone https://github.com/shuxueshuxue/spexcode && cd spexcode
115
+ npm --prefix spec-cli install
116
+ npm --prefix spec-dashboard install
117
+ npm run hooks # 安装每个 clone 各自的 git hooks(main-guard + session 戳)
118
+ ```
119
+
120
+ 开发循环直接跑源码,带热重载(开发用 `npm run web`,区别于安装用户的 `spex dashboard`):
121
+
122
+ ```sh
123
+ npm run api # 后端 :8787,spec-cli/src 改动即热重载
124
+ npm run web # Vite 起看板(HMR),/api 代理到 :8787
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 致谢
130
+
131
+ SpexCode 的首次公开介绍发在 [LINUX DO](https://linux.do),感谢佬友们的第一轮讨论和反馈。
132
+
133
+ ## License
134
+
135
+ [MIT](./LICENSE)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -37,7 +37,7 @@ function flushExit(code = 0): Promise<never> {
37
37
  }
38
38
  const has = (name: string) => process.argv.includes(`--${name}`)
39
39
  // bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
40
- const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api-port', '--preset'])
40
+ const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api-port', '--host', '--preset'])
41
41
  function positionals(from: number): string[] {
42
42
  const out: string[] = []
43
43
  for (let i = from; i < process.argv.length; i++) {
@@ -137,13 +137,15 @@ if (cmd === 'serve') {
137
137
  }
138
138
  await import('./supervise.js')
139
139
  } else if (cmd === 'dashboard') {
140
- // the natural post-install UI: serve the bundled dashboard on its OWN loopback port, proxying /api +
141
- // the terminal socket to a separately-run `spex serve`. Replaces the dogfood-only `npm run web` (vite).
140
+ // the natural post-install UI: serve the bundled dashboard on its OWN port (loopback by default;
141
+ // --host widens the bind for LAN/tailnet viewing), proxying /api + the terminal socket to a
142
+ // separately-run `spex serve`. Replaces the dogfood-only `npm run web` (vite).
142
143
  const { serveDashboardLocal } = await import('./gateway.js')
143
144
  const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173)
144
145
  const apiPort = Number(flag('api-port') ?? process.env.PORT ?? 8787)
146
+ const host = flag('host') ?? '127.0.0.1'
145
147
  if (!Number.isInteger(port) || !Number.isInteger(apiPort)) { console.error('spex dashboard: --port and --api-port must be integers'); process.exit(2) }
146
- serveDashboardLocal({ port, apiPort })
148
+ serveDashboardLocal({ port, apiPort, host })
147
149
  } else if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
148
150
  // `spex help <cmd>` drills into one command; bare help is the map. Both name the next layer down.
149
151
  const { commandHelp, overviewHelp } = await import('./help.js')
@@ -421,7 +423,14 @@ if (cmd === 'serve') {
421
423
  } else if (sub === 'done') {
422
424
  // sugar for awaiting; --propose merge|nothing|close, optional --note
423
425
  const p = (flag('propose') as any) || 'nothing'
424
- const closeNote = p === 'close' ? CLOSE_CLEANUP : ''
426
+ let closeNote = p === 'close' ? CLOSE_CLEANUP : ''
427
+ if (p === 'close') {
428
+ // the DATA half of the close nudge ([[local-issues]] closeoutNudge): the still-open local issues this
429
+ // session touched, listed by id — empty/OFF/no-identity prints nothing. Loud on failure but never
430
+ // gating: the declaration must land whatever the issue store is doing.
431
+ try { closeNote += (await import('./localIssues.js')).closeoutNudge(sess ?? s.ownSessionId()) }
432
+ catch (e) { console.error(`issue closeout check failed (declaration unaffected): ${e instanceof Error ? e.message : e}`) }
433
+ }
425
434
  console.log(s.markDone(p, sess) ? `done (${p})${DECLARED}${closeNote}` : 'no session record')
426
435
  } else if (sub === 'park') {
427
436
  // sugar: the agent is waiting on a background task; it will self-resume (NOT idle/awaiting)
@@ -173,15 +173,17 @@ export function startGateway(opts: GatewayOpts): void {
173
173
  socket.on('error', bail); up.on('error', bail)
174
174
  })
175
175
 
176
- // `spex dashboard` passes an explicit loopback host; `--public` passes none → bind ALL interfaces (the
177
- // original behaviour, IPv4+IPv6), so adding the local path never narrows the public gateway's reach.
178
- const isLocal = !!opts.host
176
+ // `spex dashboard` passes an explicit host (loopback by default, --host to widen); `--public` passes
177
+ // none → bind ALL interfaces (the original behaviour, IPv4+IPv6), so adding the local path never
178
+ // narrows the public gateway's reach. The gate note keys on LOOPBACK, not on host-being-explicit:
179
+ // an ungated loopback bind is normal, an ungated wide bind is announced — never silent.
180
+ const isLoopback = opts.host === '127.0.0.1' || opts.host === 'localhost' || opts.host === '::1'
179
181
  const onListen = () => {
180
182
  const scheme = secure ? 'https' : 'http'
181
183
  const label = opts.label ?? 'public mode'
182
- const gate = isLocal ? '' : ` — ${gated ? 'password-gated' : 'OPEN (no password)'}` // ungated loopback is normal, not a warning
183
- console.log(`[gateway] ${label} on ${scheme}://${isLocal ? 'localhost' : '0.0.0.0'}:${opts.publicPort}${gate}, proxying /api to :${opts.upstreamPort}`)
184
- if (!secure && !isLocal) console.log('[gateway] (TLS off — --http)')
184
+ const gate = isLoopback ? '' : ` — ${gated ? 'password-gated' : 'OPEN (no password)'}`
185
+ console.log(`[gateway] ${label} on ${scheme}://${isLoopback ? 'localhost' : (opts.host ?? '0.0.0.0')}:${opts.publicPort}${gate}, proxying /api to :${opts.upstreamPort}`)
186
+ if (!secure && !isLoopback && !opts.host) console.log('[gateway] (TLS off — --http)')
185
187
  }
186
188
  // a busy public port is a hard, loud, non-zero exit — the SAME contract as the supervisor's proxy
187
189
  // (see [[spec-cli]] / listen.ts), so `spex serve` and `spex dashboard` fail a port clash identically.
@@ -270,13 +272,15 @@ export function ensureDashboardBuilt(repoRoot: string, distDir: string): void {
270
272
  }
271
273
 
272
274
  // @@@ serveDashboardLocal - the engine behind `spex dashboard`: the SAME gateway as public mode, bound to
273
- // loopback with no TLS and no password. It serves the bundled dist and proxies /api + the terminal socket
274
- // to a separately-run `spex serve`. This is the post-install replacement for the dogfood-only `npm run web`
275
- // (a vite dev server an installed user has no source tree for). See [[packaging]].
276
- export function serveDashboardLocal(opts: { port: number; apiPort: number }): void {
275
+ // loopback by default with no TLS and no password `--host` widens the bind to a chosen interface
276
+ // (LAN/tailnet viewing) while staying plain HTTP; the internet face remains `spex serve --public`.
277
+ // It serves the bundled dist and proxies /api + the terminal socket to a separately-run `spex serve`.
278
+ // This is the post-install replacement for the dogfood-only `npm run web` (a vite dev server an
279
+ // installed user has no source tree for). See [[packaging]].
280
+ export function serveDashboardLocal(opts: { port: number; apiPort: number; host?: string }): void {
277
281
  const pkgRoot = fileURLToPath(new URL('..', import.meta.url))
278
282
  const distDir = resolveDistDir()
279
283
  ensureDashboardBuilt(join(pkgRoot, '..'), distDir) // bundled dist already has index.html → returns at once
280
284
  console.log(`[dashboard] serving ${distDir.endsWith('dashboard-dist') ? 'bundled' : 'monorepo'} build, /api → backend :${opts.apiPort}`)
281
- startGateway({ host: '127.0.0.1', publicPort: opts.port, upstreamPort: opts.apiPort, password: '', tls: null, distDir, label: 'dashboard' })
285
+ startGateway({ host: opts.host ?? '127.0.0.1', publicPort: opts.port, upstreamPort: opts.apiPort, password: '', tls: null, distDir, label: 'dashboard' })
282
286
  }
@@ -3,9 +3,13 @@ const SETUP = `spex guide — run SpexCode on your own repo
3
3
  The product model: install SpexCode ONCE, then use it across all your projects — an agent drives
4
4
  the rest, you don't hand-author the spec tree or wire the dashboard yourself.
5
5
 
6
- 1. Install the CLI (one-time, global — this ONE checkout serves every project)
7
- cd spec-cli && npm install && npm link # \`spex\` now runs from ANY directory
6
+ 1. Install the CLI (one-time, global — ONE install serves every project)
7
+ npm i -g spexcode # ONE command lands on PATH: \`spex\` (Node 22)
8
8
  It always operates on the repo of your current directory — that cwd is the only "which repo" knob.
9
+ (Dogfooding an unpublished HEAD from a source checkout? \`npm link\` at the repo ROOT — that links
10
+ the \`spexcode\` package itself, never the internal @spexcode/spec-cli. Both paths own the same
11
+ \`spex\` bin, so uninstall one before switching (\`npm rm -g spexcode\`). A source link ships no
12
+ prebuilt dashboard dist — \`spex dashboard\` needs a manual dashboard build, or use the dev server.)
9
13
 
10
14
  2. Adopt a repo
11
15
  cd <your-repo> && spex init # seeds .spec/ + git hooks (additive, never overwrites)
@@ -17,11 +21,11 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
17
21
  Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two PORTs.
18
22
 
19
23
  4. Open the dashboard — the SAME board for every project, pointed per project
20
- cd spec-dashboard && npm install # once
21
- API_URL=http://localhost:<port> npm run dev # point this board at step 3's backend
22
- The board is a viewer: API_URL is how the shared install points at each project (one dev-server
23
- per project). "dashboard": { "apiUrl": "..." } in spexcode.json is the default ONLY when the board
24
- lives inside the project (the dogfood layout) for a shared install, use API_URL.
24
+ spex dashboard # serves the bundled board on :5173, proxying /api
25
+ Point it at another backend with --api-port (pairs with \`spex serve --port\`); one dashboard per
26
+ project. The board is a viewer which backend it proxies is the only "which project" knob.
27
+ (Dogfood/source alternative: API_URL=http://localhost:<port> npm run dev in spec-dashboard
28
+ the dev server; "dashboard": { "apiUrl": "..." } in spexcode.json applies only to that layout.)
25
29
 
26
30
  5. Govern your layout (optional)
27
31
  spexcode.json sets lint's governedRoots/sourceExtensions and any non-default worktree layout.
@@ -276,10 +276,12 @@ TLS (own cert via --tls-cert/--tls-key; --http drops TLS).`,
276
276
  },
277
277
  dashboard: {
278
278
  line: 'dashboard serve the dashboard UI (default :5173), proxying /api to a running serve',
279
- body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787]
279
+ body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787] [--host H=127.0.0.1]
280
280
 
281
281
  Serves the bundled dashboard on its own port and proxies /api + the terminal socket to a running
282
- \`spex serve\`. The installed replacement for the dev-only \`npm run web\`.`,
282
+ \`spex serve\`. The installed replacement for the dev-only \`npm run web\`. Loopback-only by default;
283
+ --host 0.0.0.0 (or a specific interface) opens it to a LAN/tailnet — still plain HTTP with no gate,
284
+ so bind wide only on a network you trust (for the internet, use \`spex serve --public\`).`,
283
285
  see: 'spex serve (must be running first)',
284
286
  },
285
287
 
@@ -4,8 +4,8 @@ import { cors } from 'hono/cors'
4
4
  import { etag } from 'hono/etag'
5
5
  import { createNodeWebSocket } from '@hono/node-ws'
6
6
  import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig } from './specs.js'
7
- import { issuesEnabled, postLocalIssue, remarkOnHost, resolveRemark, retractRemark } from './localIssues.js'
8
- import { closeIssue, mergedIssues, replyIssue } from './issues.js'
7
+ import { issuesEnabled, remarkOnHost, resolveRemark, retractRemark } from './localIssues.js'
8
+ import { closeIssue, createIssue, issueStores, mergedIssues, replyIssue } from './issues.js'
9
9
  import { residentForgeState, refreshForgeNow } from '../../spec-forge/src/resident.js'
10
10
  import { summarize } from './mentions.js'
11
11
  import { resolveLayout, mainBranch } from './layout.js'
@@ -159,6 +159,7 @@ app.get('/api/launchers', (c) => c.json({
159
159
  app.get('/api/issues', etag(), (c) =>
160
160
  c.json({
161
161
  enabled: issuesEnabled(),
162
+ stores: issueStores(),
162
163
  issues: mergedIssues({ host: 'github', state: residentForgeState() }, loadSpecsLite().map((s) => s.id)),
163
164
  }))
164
165
  // the WRITE surface ([[local-issues]] / [[issues-view]]) — the human reply path, STORE-ROUTED through the one
@@ -211,10 +212,16 @@ app.post('/api/issues', async (c) => {
211
212
  if (!concern) return c.json({ error: 'empty concern' }, 400)
212
213
  const nodes = Array.isArray(body?.nodes) ? (body.nodes as unknown[]).filter((n): n is string => typeof n === 'string') : []
213
214
  const postBody = typeof body?.body === 'string' ? body.body : undefined
215
+ const store = typeof body?.store === 'string' && body.store.trim() ? body.store.trim() : 'local'
214
216
  // typed evidence[] — yatsu content-addressed hashes (the annotator's clip reference rides here, not prose)
215
217
  const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
216
- const { thread, outcomes } = await postLocalIssue(concern, { nodes, body: postBody, evidence, author: 'human' })
217
- return c.json({ ok: true, id: thread.id, outcomes: summarize(outcomes) }, 201)
218
+ try {
219
+ const r = await createIssue(concern, { store, nodes, body: postBody, evidence, author: 'human' })
220
+ if (r.store !== 'local') await refreshForgeNow()
221
+ return c.json({ ok: true, id: r.id, store: r.store, url: r.url, outcomes: summarize(r.outcomes) }, 201)
222
+ } catch (e) {
223
+ return c.json({ error: String((e as Error).message || e) }, store === 'local' ? 500 : 502)
224
+ }
218
225
  })
219
226
 
220
227
  // the REMARK write surface ([[remark-substrate]]) — server PARITY with the CLI: the dashboard can author /