spexcode 0.1.6 → 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 +99 -35
- package/README.zh-CN.md +135 -0
- package/package.json +5 -6
- package/spec-cli/README.md +86 -0
- package/spec-cli/bin/spex.mjs +15 -3
- package/spec-cli/hooks/dispatch.sh +20 -8
- package/spec-cli/hooks/harness.sh +18 -11
- package/spec-cli/src/board.ts +47 -18
- package/spec-cli/src/boardCache.ts +70 -0
- package/spec-cli/src/boardDelta.ts +90 -0
- package/spec-cli/src/boardStream.ts +178 -0
- package/spec-cli/src/cli.ts +184 -122
- package/spec-cli/src/client.ts +6 -4
- package/spec-cli/src/gateway.ts +64 -24
- package/spec-cli/src/git.ts +105 -92
- package/spec-cli/src/guide.ts +186 -19
- package/spec-cli/src/harness-select.ts +63 -0
- package/spec-cli/src/harness.ts +506 -100
- package/spec-cli/src/help.ts +362 -0
- package/spec-cli/src/hooks.ts +0 -14
- package/spec-cli/src/index.ts +279 -32
- package/spec-cli/src/init.ts +41 -1
- package/spec-cli/src/issues.ts +301 -0
- package/spec-cli/src/layout.ts +70 -28
- package/spec-cli/src/lint.ts +12 -10
- package/spec-cli/src/listen.ts +28 -0
- package/spec-cli/src/localIssues.ts +700 -0
- package/spec-cli/src/materialize.ts +182 -27
- package/spec-cli/src/mentions.ts +192 -0
- package/spec-cli/src/plugin-harness.ts +145 -0
- package/spec-cli/src/pty-bridge.ts +378 -81
- package/spec-cli/src/self.ts +123 -20
- package/spec-cli/src/sessions.ts +461 -298
- package/spec-cli/src/specs.ts +55 -14
- package/spec-cli/src/supervise.ts +23 -3
- package/spec-cli/src/tsx-bin.ts +14 -5
- package/spec-cli/src/uninstall.ts +146 -0
- package/spec-cli/templates/hooks/post-merge +27 -0
- package/spec-cli/templates/hooks/pre-commit +51 -31
- package/spec-cli/templates/hooks/prepare-commit-msg +31 -8
- package/spec-cli/templates/presets/careful/.config/clarify-before-code/spec.md +11 -0
- package/spec-cli/templates/spec/project/.config/core/spec-of-file/spec-of-file.sh +26 -3
- package/spec-cli/templates/spec/project/.config/core/spec.md +4 -0
- package/spec-cli/templates/spec/project/.config/core/stop-gate/stop-gate.sh +16 -4
- package/spec-cli/templates/spec/project/.config/extract/spec.md +1 -1
- package/spec-cli/templates/spec/project/.config/regroup/spec.md +1 -1
- package/spec-cli/templates/spec/project/.config/reproduce-before-fix/spec.md +18 -0
- package/spec-cli/templates/spec/project/.config/spec.md +3 -3
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +2 -2
- package/spec-cli/templates/spec/project/.config/tidy/spec.md +1 -1
- package/spec-cli/templates/spec/project/spec.md +2 -2
- package/spec-dashboard/dist/assets/index-Ct_ubwrd.css +32 -0
- package/spec-dashboard/dist/assets/index-DehTZ-h9.js +145 -0
- package/spec-dashboard/dist/index.html +17 -5
- package/spec-forge/src/cache.ts +16 -0
- package/spec-forge/src/cli.ts +4 -10
- package/spec-forge/src/drivers/github.ts +74 -4
- package/spec-forge/src/drivers.ts +13 -0
- package/spec-forge/src/port.ts +25 -0
- package/spec-forge/src/resident.ts +40 -6
- package/spec-yatsu/src/cli.ts +227 -38
- package/spec-yatsu/src/evaltab.ts +169 -19
- package/spec-yatsu/src/filing.ts +48 -0
- package/spec-yatsu/src/freshness.ts +55 -20
- package/spec-yatsu/src/proof.ts +89 -3
- package/spec-yatsu/src/scenariofresh.ts +92 -0
- package/spec-yatsu/src/sidecar.ts +75 -11
- package/spec-yatsu/src/timeline.ts +47 -0
- package/spec-yatsu/src/yatsu.ts +47 -3
- package/spec-cli/src/relay.ts +0 -28
- package/spec-cli/templates/spec/project/.config/scenario/spec.md +0 -32
- package/spec-dashboard/dist/assets/index-Bk4E1EQy.js +0 -139
- package/spec-dashboard/dist/assets/index-Cq7hwngj.css +0 -32
package/spec-cli/src/guide.ts
CHANGED
|
@@ -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 —
|
|
7
|
-
|
|
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,19 +21,21 @@ 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
|
-
|
|
21
|
-
|
|
22
|
-
The board is a viewer
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
28
32
|
\`spex lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
Look these up on demand — the formats an agent authors, and the settings it configures:
|
|
31
35
|
spex guide spec the spec.md format (frontmatter + body + the rules lint enforces)
|
|
32
|
-
spex guide yatsu the yatsu.md format (scenario schema + how loss is measured and filed)
|
|
36
|
+
spex guide yatsu the yatsu.md format (scenario schema + how loss is measured and filed)
|
|
37
|
+
spex guide config the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, lint
|
|
38
|
+
budgets, layout) — every field, and which of the two files it belongs in`
|
|
33
39
|
|
|
34
40
|
const SPEC = `spex guide spec — the spec.md file format
|
|
35
41
|
|
|
@@ -51,7 +57,7 @@ FRONTMATTER (YAML between the opening and closing --- lines; every field optiona
|
|
|
51
57
|
related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
|
|
52
58
|
(never drift, never yatsu, nothing to ack); it is the many-to-many net that claims the files
|
|
53
59
|
govern doesn't. Every listed path must exist (lint integrity error otherwise).
|
|
54
|
-
surface config/.config nodes only: system (folded into every agent's prompt) |
|
|
60
|
+
surface config/.config nodes only: system (folded into every agent's prompt) | command (a /command) |
|
|
55
61
|
hook (a lifecycle hook handler — a co-located script the dispatcher runs on the harness events
|
|
56
62
|
in events:, ordered by order:, blocking when block: true). hook nodes may nest under a grouping
|
|
57
63
|
plugin (e.g. .config/core/<id>); surface is a field, discovered recursively.
|
|
@@ -92,6 +98,11 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
|
|
|
92
98
|
name REQUIRED. Unique within the file — it keys the sidecar and \`--scenario <name>\`.
|
|
93
99
|
description REQUIRED. What to check / how to measure it through the running product.
|
|
94
100
|
expected REQUIRED. What ZERO loss looks like — the target the measurement is compared against.
|
|
101
|
+
tags REQUIRED. ≥1 classification tag (a comma list / flow list \`[a, b]\`), each drawn from the
|
|
102
|
+
configured library (\`lint.scenarioTags\` in spexcode.json; ships
|
|
103
|
+
\`frontend-e2e, backend-api, cli, desktop, mobile\`). A tag outside the library is rejected —
|
|
104
|
+
use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
|
|
105
|
+
device) so it can be filtered and, later, routed to the right driver.
|
|
95
106
|
test optional. A repo path to a co-located runnable file (a playwright.spec.ts, a script)
|
|
96
107
|
the agent MAY run by hand. Not a driver — yatsu never executes it.
|
|
97
108
|
code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
|
|
@@ -103,18 +114,29 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
|
|
|
103
114
|
Multi-line prose uses YAML block scalars: \`|\` keeps newlines, \`>\` folds wrapped lines to spaces.
|
|
104
115
|
A yatsu.md OWNS nothing — only its scenarios govern and relate (see governed-related).
|
|
105
116
|
|
|
106
|
-
THE SCHEMA IS ENFORCED (closed field set,
|
|
107
|
-
an unknown key (a typo like \`descripton:\`), a duplicate name,
|
|
108
|
-
\`spex yatsu scan\` reports it as \`yatsu-schema\`, and the
|
|
117
|
+
THE SCHEMA IS ENFORCED (closed field set, four required fields, unique names, tags within the library). A
|
|
118
|
+
missing required field, an unknown key (a typo like \`descripton:\`), a duplicate name, an out-of-library
|
|
119
|
+
tag, or no scenarios at all is rejected LOUD: \`spex yatsu scan\` reports it as \`yatsu-schema\`, and the
|
|
120
|
+
pre-commit \`yatsu check-staged\` BLOCKS the commit.
|
|
109
121
|
|
|
110
122
|
BODY (after the frontmatter): prose naming the measurement method — YATU ("You As The User"): the agent
|
|
111
123
|
looks at / calls the real product surface, not an internal helper chosen to make the proof easy.
|
|
112
124
|
|
|
113
125
|
MEASURING AND FILING: the agent runs the scenario however it likes (a browser screenshot, an API
|
|
114
126
|
transcript, a by-hand pass), compares the result to \`expected\`, and files it:
|
|
115
|
-
spex yatsu eval <node> --scenario <name> (--pass | --fail
|
|
127
|
+
spex yatsu eval <node> --scenario <name> (--pass | --fail) [--note <text>] [--image <png> | --result <txt>|-]
|
|
128
|
+
The verdict is \`--pass\` or \`--fail\` (a measurement must commit to one — an unmeasured scenario is \`missing\`,
|
|
129
|
+
not a hedged fail). \`--note <text>\` is an OPTIONAL one-line annotation on either (why it failed, how far a
|
|
130
|
+
pass sits from ideal); it does NOT replace evidence — the image/transcript is the captured actual behaviour.
|
|
116
131
|
Frontend → \`--image <png>\` (visual evidence); backend → \`--result <txt>\` (a transcript; \`-\` reads stdin).
|
|
117
132
|
|
|
133
|
+
A botched filing (a junk e2e/smoke run, a wrong verdict) is undone through the SAME surface:
|
|
134
|
+
spex yatsu retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
|
|
135
|
+
retract APPENDS a retraction event to the sidecar (never deletes a line — the trace stays, git records
|
|
136
|
+
who/when/why); the scoreboard then drops the retracted reading everywhere: the previous reading becomes
|
|
137
|
+
the latest again, or the scenario honestly returns to \`missing\`. Default target is the scenario's latest
|
|
138
|
+
reading (\`--last\` makes that explicit; repeat to peel junk back one filing at a time); \`--ts\` pins one.
|
|
139
|
+
|
|
118
140
|
THE SCOREBOARD: readings live in yatsu.evals.ndjson beside the yatsu.md — one JSON line per measurement
|
|
119
141
|
(a second git-as-database axis). Freshness is derived live from git: a reading goes STALE when a governed
|
|
120
142
|
code file, the scenario (the yatsu.md), or the evaluator moves since it was filed.
|
|
@@ -124,11 +146,156 @@ code file, the scenario (the yatsu.md), or the evaluator moves since it was file
|
|
|
124
146
|
spex yatsu show <node> the reading timeline (verdict · freshness · evidence), newest first
|
|
125
147
|
spex yatsu clean GC the content-addressed evidence cache`
|
|
126
148
|
|
|
127
|
-
const
|
|
149
|
+
const CONFIG = `spex guide config — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
|
|
150
|
+
|
|
151
|
+
SpexCode reads its runtime settings from TWO optional JSON files at the repo root. There is no imperative
|
|
152
|
+
\`spex config set\` — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
|
|
153
|
+
PORTABILITY, and picking the right one is the whole discipline:
|
|
154
|
+
|
|
155
|
+
spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
|
|
156
|
+
identity, lint budgets, launcher NAMES. "Git is the database": tracked so the
|
|
157
|
+
team shares ONE configuration.
|
|
158
|
+
spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
|
|
159
|
+
paths, private-overlay mode. Layered OVER spexcode.json (see MERGE below); an env
|
|
160
|
+
var (SPEXCODE_CLAUDE_CMD, …) still overrides both at its read site.
|
|
161
|
+
|
|
162
|
+
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
163
|
+
icon, a lint budget, a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
164
|
+
PATH of a launcher wrapper, a TLS cert path, private mode are machine facts → gitignored spexcode.local.json.
|
|
165
|
+
Both files are optional; omit any field to take its default.
|
|
166
|
+
|
|
167
|
+
MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
|
|
168
|
+
sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
|
|
169
|
+
pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
|
|
170
|
+
sit in the committed file while its host-specific DEFINITION (with the abs cmd) sits in the local file —
|
|
171
|
+
see LAUNCHERS.
|
|
172
|
+
|
|
173
|
+
── LAYOUT (spexcode.json — portable; set only for a NON-DEFAULT repo layout) ──
|
|
174
|
+
main path to the source-of-truth checkout. Default: the \`main\` worktree.
|
|
175
|
+
mainBranch the source-of-truth BRANCH worktrees fork from. Default: auto-detected.
|
|
176
|
+
branchPrefix how a node branch is named. Default "node/".
|
|
177
|
+
Example — a repo whose trunk is \`staging\`, not \`main\`:
|
|
178
|
+
{ "mainBranch": "staging" }
|
|
179
|
+
|
|
180
|
+
── DASHBOARD (spexcode.json — portable project identity) ──
|
|
181
|
+
dashboard.title browser-tab name. Default: the repo-root basename.
|
|
182
|
+
dashboard.icon browser-tab favicon: an emoji ("🔭") OR an Iconify name ("mdi:rocket-launch").
|
|
183
|
+
dashboard.apiUrl the per-project backend the board proxies to (read frontend-side). For a SHARED
|
|
184
|
+
install prefer the API_URL env var; apiUrl here is the default only when the board
|
|
185
|
+
lives inside the project.
|
|
186
|
+
Example:
|
|
187
|
+
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
|
|
188
|
+
|
|
189
|
+
── SESSIONS / WORKERS ──
|
|
190
|
+
sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8).
|
|
191
|
+
Counts compute slots, not total sessions: idle/asking/review/done do not
|
|
192
|
+
occupy one. A policy number → committed spexcode.json; omit it to use the
|
|
193
|
+
default, or tune higher/lower for the project's usual host.
|
|
194
|
+
sessions.claudeCmd the UNNAMED default worker launcher for Claude (default
|
|
195
|
+
'claude --dangerously-skip-permissions'); env SPEXCODE_CLAUDE_CMD overrides.
|
|
196
|
+
sessions.codexCmd the UNNAMED default worker launcher for Codex (default 'codex --yolo'); env
|
|
197
|
+
SPEXCODE_CODEX_CMD overrides.
|
|
198
|
+
sessions.launchers NAMED launcher profiles (see LAUNCHERS).
|
|
199
|
+
sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
|
|
200
|
+
(else the unnamed claudeCmd/codexCmd path). A portable NAME → committed.
|
|
201
|
+
A claudeCmd/codexCmd or a launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in
|
|
202
|
+
spexcode.local.json — the committed file must stay free of machine paths.
|
|
203
|
+
|
|
204
|
+
── LAUNCHERS (the profile block, split across the two files) ──
|
|
205
|
+
A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
|
|
206
|
+
by name, and the chosen name is persisted on the record so a resume reuses the same auth. Shape:
|
|
207
|
+
"launchers": { "<name>": { "harness": "claude" | "codex", "cmd": "<launch command>" } }
|
|
208
|
+
\`harness\` defaults to "claude"; \`cmd\` is required. Because \`cmd\` is a machine fact (an abs wrapper path),
|
|
209
|
+
the DEFINITION lives in the gitignored spexcode.local.json, while the portable defaultLauncher NAME sits
|
|
210
|
+
in the committed spexcode.json — the merge keeps both:
|
|
211
|
+
|
|
212
|
+
spexcode.json (committed — the portable name reference)
|
|
213
|
+
{ "sessions": { "defaultLauncher": "gpt5" } }
|
|
214
|
+
|
|
215
|
+
spexcode.local.json (gitignored — the host-specific definitions)
|
|
216
|
+
{
|
|
217
|
+
"sessions": {
|
|
218
|
+
"launchers": {
|
|
219
|
+
"gpt5": { "harness": "codex", "cmd": "/Users/me/bin/reclaude-codex --yolo" },
|
|
220
|
+
"claude-prod": { "harness": "claude", "cmd": "/Users/me/bin/reclaude --dangerously-skip-permissions" }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
── SERVE (spexcode.json — public-exposure for \`spex serve --public\`) ──
|
|
226
|
+
serve.public.enabled turn public mode on without the --public flag.
|
|
227
|
+
serve.public.http drop TLS (the --http escape hatch) — the password then travels in cleartext.
|
|
228
|
+
serve.public.tls { "cert": "<path>", "key": "<path>" } — PATHS to your own cert/key; omit for a
|
|
229
|
+
cached self-signed default. If the paths are host-specific, put them in
|
|
230
|
+
spexcode.local.json.
|
|
231
|
+
The gateway password is NEVER read from these files (flag/env only), so serve.public stays committable.
|
|
232
|
+
|
|
233
|
+
── ISSUES (spexcode.json — portable policy) ──
|
|
234
|
+
issues.enabled the issues-workflow on/off switch (default ON). OFF silences the post-merge nudge and
|
|
235
|
+
hides the dashboard view; the CLI toggle is \`spex issues on|off\`.
|
|
236
|
+
|
|
237
|
+
── LINT (spexcode.json — a top-level "lint" key; budgets are portable, so committed only) ──
|
|
238
|
+
lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
|
|
239
|
+
'.' = the whole project (only git-TRACKED files). Default
|
|
240
|
+
["spec-dashboard/src", "spec-cli/src"].
|
|
241
|
+
lint.sourceExtensions extensions coverage treats as source. Default ["ts","tsx","js","jsx"].
|
|
242
|
+
lint.testGlobs globs EXCLUDED from coverage (default ["**/*.test.*"]; [] to govern tests too).
|
|
243
|
+
lint.identifierExtensions extensions the altitude bare-filename signal recognises.
|
|
244
|
+
lint.altitude body budgets: { lineBudget, charBudget, sizeable, dense, steps }
|
|
245
|
+
(defaults 50 / 4200 / 35 / 1.3 / 3).
|
|
246
|
+
lint.maxChildren breadth budget: warn at >= this many direct children (default 8).
|
|
247
|
+
lint.driftErrorThreshold commit-local gate HARD-BLOCKS a commit touching a node >= this many commits
|
|
248
|
+
behind (default 3).
|
|
249
|
+
lint.maxOwners warn when a file is governed by > this many nodes (default 3).
|
|
250
|
+
lint.scenarioTags the closed vocabulary a yatsu scenario's tags: must draw from (default
|
|
251
|
+
["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
|
|
252
|
+
Example — govern your own source dir and loosen the altitude budget:
|
|
253
|
+
{ "lint": { "governedRoots": ["src"], "altitude": { "lineBudget": 70 } } }
|
|
254
|
+
|
|
255
|
+
── OTHER (spexcode.json unless noted) ──
|
|
256
|
+
preset the SELECTED init preset — which cumulative .config tier \`spex init\` seeds (default
|
|
257
|
+
'default'; seed-time only, read by init.ts).
|
|
258
|
+
harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
|
|
259
|
+
{ "plugin": "<folder>" } bundle. Default (omitted): all native harnesses.
|
|
260
|
+
private (spexcode.local.json ONLY) private-overlay mode — when true, \`spex materialize\` leaves ZERO
|
|
261
|
+
trace in the host's TRACKED files: managed ignore entries go to .git/info/exclude and any
|
|
262
|
+
tracked contract file is marked skip-worktree. Trades away git-derived spec version history.
|
|
263
|
+
A HOST decision → never the committed file. See PRIVATE MODE below.
|
|
264
|
+
|
|
265
|
+
── PRIVATE MODE (default ⇄ private — the two delivery modes) ──
|
|
266
|
+
DEFAULT mode (private absent/false): materialize commits .spec + spexcode.json and writes its ignore list as
|
|
267
|
+
a managed block in the TRACKED .gitignore — transparent, but every collaborator sees it. PRIVATE mode
|
|
268
|
+
(spexcode.local.json { "private": true }): the SAME contract reaches the agent, but the ignore list goes to
|
|
269
|
+
the per-clone .git/info/exclude and any host-tracked CLAUDE.md/AGENTS.md is skip-worktree'd — so \`git status\`
|
|
270
|
+
stays clean and nothing enters shared history.
|
|
271
|
+
|
|
272
|
+
Switch: edit spexcode.local.json ({ "private": true } on / false or remove = off), then \`spex materialize\`
|
|
273
|
+
(the hook gate also re-runs it on the next agent turn).
|
|
274
|
+
Reversible + idempotent: the two modes fully CANCEL OUT. default→private→default (or private→default→private)
|
|
275
|
+
converges to the SAME on-disk state as running that mode once — each mode re-asserts the inverse of
|
|
276
|
+
the other (exclude block ⇄ .gitignore block, skip-worktree set ⇄ cleared). Switch order never
|
|
277
|
+
matters; running a mode twice changes nothing.
|
|
278
|
+
|
|
279
|
+
MANUAL STEP (the only one materialize can't do for you — it also PRINTS this when needed):
|
|
280
|
+
.git/info/exclude hides UNTRACKED paths only. If you adopted DEFAULT mode first, .spec + spexcode.json are
|
|
281
|
+
already committed, so private mode can't hide them until you un-track them ONCE:
|
|
282
|
+
git rm -r --cached .spec spexcode.json # keeps the files on disk, stops tracking them
|
|
283
|
+
(then commit that on your branch). A private-from-the-start adoption never needs this.
|
|
284
|
+
|
|
285
|
+
FOOTGUN (skip-worktree): a host-tracked CLAUDE.md/AGENTS.md is skip-worktree'd in private mode, so a later
|
|
286
|
+
\`git pull\` that touches it can complain. Fix: flip back to DEFAULT mode (or \`git update-index
|
|
287
|
+
--no-skip-worktree CLAUDE.md AGENTS.md\`), pull, then re-materialize.`
|
|
288
|
+
|
|
289
|
+
const TOPICS: Record<string, string> = { spec: SPEC, yatsu: YATSU, config: CONFIG }
|
|
290
|
+
|
|
291
|
+
// every guide page ends by naming the OTHER help layer, so a reader never dead-ends here: guide is
|
|
292
|
+
// the skill layer (workflows · formats · settings); command usage lives in help.ts's two layers.
|
|
293
|
+
const FOOTER = `\n\n(This is the skill layer. Command usage: \`spex help\` for the map, \`spex help <command>\` for one command.)`
|
|
128
294
|
|
|
129
|
-
|
|
130
|
-
|
|
295
|
+
// null = unknown topic: the caller fails loud (exit non-zero) while still naming the layers to go
|
|
296
|
+
// back to — an unknown topic must never read as a successful page ([[cli-surface]]'s dead-end rule).
|
|
297
|
+
export function guideText(topic?: string): string | null {
|
|
298
|
+
if (!topic) return SETUP + FOOTER
|
|
131
299
|
const t = TOPICS[topic]
|
|
132
|
-
|
|
133
|
-
return `spex guide: no topic '${topic}'. Topics: spec, yatsu. Run \`spex guide\` (no topic) for the setup workflow.`
|
|
300
|
+
return t ? t + FOOTER : null
|
|
134
301
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HARNESSES, type Harness, type HarnessId } from './harness.js'
|
|
2
|
+
|
|
3
|
+
// @@@ harness-select - the DECLARATIVE choice of WHICH harness targets `spex materialize` delivers the
|
|
4
|
+
// SpexCode system into. The selection is persistent config (spexcode.json's `harnesses`), NOT a one-shot
|
|
5
|
+
// flag, because materialize is driven by a content-hash gate (re-run on every .config edit), so the intent
|
|
6
|
+
// must live where every re-materialize can read it. This module owns ONLY the vocabulary + validation; the
|
|
7
|
+
// per-harness write/clean mechanics live on the [[harness-adapter]], the render loop on [[harness-delivery]].
|
|
8
|
+
|
|
9
|
+
// a resolved DELIVERY TARGET. Either a NATIVE harness (claude/codex — its adapter writes shims/contract/trust
|
|
10
|
+
// directly), or a PLUGIN bundle dropped into a host-agent-scanned folder. The plugin EMITTER is a later node;
|
|
11
|
+
// here a plugin target is only validated, it produces no artifact yet.
|
|
12
|
+
export type HarnessTarget =
|
|
13
|
+
| { kind: 'native'; id: HarnessId }
|
|
14
|
+
| { kind: 'plugin'; folder: string }
|
|
15
|
+
|
|
16
|
+
// the zero-config default: deliver to EVERY native harness, no plugin.
|
|
17
|
+
export const DEFAULT_HARNESS_IDS: readonly HarnessId[] = HARNESSES.map((h) => h.id)
|
|
18
|
+
const KNOWN: readonly string[] = HARNESSES.map((h) => h.id)
|
|
19
|
+
|
|
20
|
+
// parse + validate the spexcode.json `harnesses` field into resolved targets. FAIL LOUD on an illegal set —
|
|
21
|
+
// materialize and init both gate on this so a bad config never silently delivers the wrong thing. `raw` is the
|
|
22
|
+
// JSON value as written; undefined/null → the default native set.
|
|
23
|
+
export function resolveHarnessTargets(raw: unknown): HarnessTarget[] {
|
|
24
|
+
if (raw === undefined || raw === null) return DEFAULT_HARNESS_IDS.map((id) => ({ kind: 'native', id }))
|
|
25
|
+
if (!Array.isArray(raw))
|
|
26
|
+
throw new Error(`spexcode.json "harnesses" must be an ARRAY of targets (got ${typeof raw}). Members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}; omit the field to default to [${DEFAULT_HARNESS_IDS.join(', ')}].`)
|
|
27
|
+
if (raw.length === 0)
|
|
28
|
+
throw new Error(`spexcode.json "harnesses" is EMPTY — list at least one target, or remove the field to default to [${DEFAULT_HARNESS_IDS.join(', ')}].`)
|
|
29
|
+
const targets: HarnessTarget[] = []
|
|
30
|
+
for (const m of raw) {
|
|
31
|
+
if (typeof m === 'string') {
|
|
32
|
+
if (m === 'plugin')
|
|
33
|
+
throw new Error(`spexcode.json "harnesses": a plugin target needs an EXPLICIT landing folder — write {"plugin":"<folder>"} (e.g. {"plugin":".zcode"}), not the bare string "plugin", because each host agent scans a different plugins dir.`)
|
|
34
|
+
if (!KNOWN.includes(m))
|
|
35
|
+
throw new Error(`spexcode.json "harnesses": unknown harness id "${m}" — known native ids are ${KNOWN.join(', ')}, or use {"plugin":"<folder>"}.`)
|
|
36
|
+
targets.push({ kind: 'native', id: m as HarnessId })
|
|
37
|
+
} else if (m && typeof m === 'object' && !Array.isArray(m) && 'plugin' in m) {
|
|
38
|
+
const folder = (m as { plugin?: unknown }).plugin
|
|
39
|
+
if (typeof folder !== 'string' || !folder.trim())
|
|
40
|
+
throw new Error(`spexcode.json "harnesses": a {"plugin":…} target needs a NON-EMPTY folder string (e.g. {"plugin":".zcode"}) — each host agent scans a different plugins dir, so the folder must be explicit.`)
|
|
41
|
+
targets.push({ kind: 'plugin', folder: folder.trim() })
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`spexcode.json "harnesses": each member must be a native id string (${KNOWN.join(', ')}) or a {"plugin":"<folder>"} object — got ${JSON.stringify(m)}.`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// PLUGIN EXCLUSIVITY: a plugin bundle is a SUPERSET delivery to its host agent, so pairing it with any
|
|
47
|
+
// native harness double-delivers. So a set with a plugin may carry NO native harness.
|
|
48
|
+
const natives = targets.filter((t): t is { kind: 'native'; id: HarnessId } => t.kind === 'native')
|
|
49
|
+
if (targets.some((t) => t.kind === 'plugin') && natives.length)
|
|
50
|
+
throw new Error(`spexcode.json "harnesses": a plugin target is EXCLUSIVE — it cannot coexist with native harnesses (${natives.map((t) => t.id).join(', ')}). A plugin bundle already delivers the whole system to its host agent, so pairing it with a native harness double-delivers. Choose EITHER native harnesses OR plugin target(s).`)
|
|
51
|
+
return targets
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// split the live HARNESSES adapters by whether the resolved targets SELECT them: selected get write()n,
|
|
55
|
+
// unselected get clean()ed (pruned). `plugins` carries the plugin targets (no emitter yet — a later node).
|
|
56
|
+
export function partitionHarnesses(targets: HarnessTarget[]): { selected: Harness[]; unselected: Harness[]; plugins: { folder: string }[] } {
|
|
57
|
+
const selectedIds = new Set(targets.filter((t) => t.kind === 'native').map((t) => (t as { id: HarnessId }).id))
|
|
58
|
+
return {
|
|
59
|
+
selected: HARNESSES.filter((h) => selectedIds.has(h.id)),
|
|
60
|
+
unselected: HARNESSES.filter((h) => !selectedIds.has(h.id)),
|
|
61
|
+
plugins: targets.filter((t) => t.kind === 'plugin').map((t) => ({ folder: (t as { folder: string }).folder })),
|
|
62
|
+
}
|
|
63
|
+
}
|