spexcode 0.4.0 → 0.4.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 +37 -17
- package/package.json +9 -5
- package/spec-cli/README.md +23 -14
- package/spec-cli/hooks/dispatch.sh +7 -5
- package/spec-cli/hooks/harness.sh +19 -8
- package/spec-cli/src/anchors.ts +163 -30
- package/spec-cli/src/cli.ts +97 -42
- package/spec-cli/src/client.ts +34 -11
- package/spec-cli/src/doctor.ts +174 -36
- package/spec-cli/src/gateway-auth.ts +194 -0
- package/spec-cli/src/gateway-hub.ts +360 -0
- package/spec-cli/src/gateway.ts +31 -16
- package/spec-cli/src/git.ts +12 -5
- package/spec-cli/src/graph.ts +53 -36
- package/spec-cli/src/graphStream.ts +265 -34
- package/spec-cli/src/guide.ts +157 -76
- package/spec-cli/src/harness-select.ts +20 -6
- package/spec-cli/src/harness.ts +266 -67
- package/spec-cli/src/help.ts +84 -41
- package/spec-cli/src/host.ts +610 -0
- package/spec-cli/src/identity-presets.d.ts +13 -0
- package/spec-cli/src/identity-presets.js +128 -0
- package/spec-cli/src/index.ts +163 -86
- package/spec-cli/src/init.ts +53 -25
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +115 -169
- package/spec-cli/src/login-page.ts +12 -5
- package/spec-cli/src/materialize.ts +39 -13
- package/spec-cli/src/mentions.ts +16 -11
- package/spec-cli/src/opencode.ts +145 -0
- package/spec-cli/src/pi-harness.ts +124 -0
- package/spec-cli/src/project-identity.ts +73 -0
- package/spec-cli/src/pty-bridge.ts +321 -521
- package/spec-cli/src/pty-helper.mjs +86 -0
- package/spec-cli/src/reaper.ts +16 -8
- package/spec-cli/src/reviewSnapshot.ts +23 -0
- package/spec-cli/src/reviews.ts +347 -0
- package/spec-cli/src/session-timeline.ts +162 -0
- package/spec-cli/src/sessions.ts +210 -63
- package/spec-cli/src/shim-runtime.ts +169 -0
- package/spec-cli/src/slash-commands.ts +76 -0
- package/spec-cli/src/source-files.ts +79 -0
- package/spec-cli/src/specs.ts +47 -24
- package/spec-cli/src/supervise.ts +37 -15
- package/spec-cli/src/tree.ts +8 -8
- package/spec-cli/src/uninstall.ts +56 -23
- package/spec-cli/templates/hooks/prepare-commit-msg +32 -13
- package/spec-cli/templates/spec/project/.plugins/{extract → commands/extract}/spec.md +24 -18
- package/spec-cli/templates/spec/project/.plugins/{regroup → commands/regroup}/spec.md +3 -3
- package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
- package/spec-cli/templates/spec/project/.plugins/commands/spec.md +30 -0
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
- package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
- package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
- package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
- package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +62 -0
- package/spec-cli/templates/spec/project/.plugins/skills/spec.md +19 -0
- package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
- package/spec-cli/templates/spec/project/spec.md +6 -5
- package/spec-cli/templates/spexcode.json +4 -2
- package/spec-dashboard/dist/assets/Dashboard-CiHh-gLD.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-DmiX3rdU.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-CIbVGRUJ.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-D-N9_eh0.js +2 -0
- package/spec-dashboard/dist/assets/Modal-DHMzSFJ4.js +1 -0
- package/spec-dashboard/dist/assets/PageScroll-C15adEYI.js +1 -0
- package/spec-dashboard/dist/assets/ProjectsPage-sQpzglp5.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-B8pGU7Rg.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-CuDO_67z.js +13 -0
- package/spec-dashboard/dist/assets/Settings-C_N1wX1f.js +1 -0
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
- package/spec-dashboard/dist/assets/index-DmWbmvCq.js +41 -0
- package/spec-dashboard/dist/assets/index-GGIVdKwH.css +1 -0
- package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
- package/spec-dashboard/dist/index.html +10 -10
- package/spec-dashboard/src/reviewFilters.js +279 -0
- package/spec-dashboard/src/reviewQuery.js +174 -0
- package/spec-dashboard/src/session.js +167 -0
- package/spec-eval/src/cli.ts +80 -45
- package/spec-eval/src/evaltab.ts +4 -3
- package/spec-eval/src/humanok.ts +1 -1
- package/spec-eval/src/matrix.ts +693 -0
- package/spec-eval/src/scenarios.ts +116 -4
- package/spec-eval/src/sessioneval.ts +726 -66
- package/spec-cli/src/migrate-table.ts +0 -398
- package/spec-cli/src/migrate.ts +0 -386
- package/spec-cli/templates/spec/project/.plugins/distill/spec.md +0 -74
- package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +0 -8
- package/spec-cli/templates/spec/project/.plugins/tidy/spec.md +0 -25
- package/spec-dashboard/dist/assets/Dashboard-CTcH2eW9.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +0 -2
- package/spec-dashboard/dist/assets/FoldToggle-CVFbBpyW.js +0 -1
- package/spec-dashboard/dist/assets/IssuesPage-kULjonqj.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-B0ZJju8K.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-BRKJqU2U.js +0 -71
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
- package/spec-dashboard/dist/assets/SessionWindow-CDhEL7wO.js +0 -9
- package/spec-dashboard/dist/assets/Settings-BL6FV_8S.js +0 -1
- package/spec-dashboard/dist/assets/index-DmQsNYKK.css +0 -1
- package/spec-dashboard/dist/assets/index-DulGPk6A.js +0 -41
- /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/digest.mjs +0 -0
package/spec-cli/src/guide.ts
CHANGED
|
@@ -5,35 +5,35 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
5
5
|
|
|
6
6
|
1. Install the CLI (one-time, global — ONE install serves every project)
|
|
7
7
|
npm i -g spexcode # ONE command lands on PATH: \`spex\` (Node ≥ 22)
|
|
8
|
-
|
|
8
|
+
Project-scoped commands operate on the repo of your current directory; the dashboard is host-scoped.
|
|
9
9
|
(Dogfooding an unpublished HEAD from a source checkout? \`npm link\` at the repo ROOT — that links
|
|
10
10
|
the \`spexcode\` package itself, never the internal @spexcode/spec-cli. Both paths own the same
|
|
11
11
|
\`spex\` bin, so uninstall one before switching (\`npm rm -g spexcode\`; a legacy link of the
|
|
12
|
-
inner package uninstalls as \`@spexcode/spec-cli\`).
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
inner package uninstalls as \`@spexcode/spec-cli\`). Contributors run \`npm run api\` and
|
|
13
|
+
\`npm run web\` from the source root for backend reloads and Vite/HMR; those are not installed-user
|
|
14
|
+
startup commands.)
|
|
15
15
|
|
|
16
16
|
2. Adopt a repo
|
|
17
|
-
cd <your-repo> && spex init
|
|
17
|
+
cd <your-repo> && spex init --harness claude,codex,opencode,pi # seeds .spec/ + git hooks (additive, never overwrites)
|
|
18
|
+
--harness is required and has no default — the explicit choice of which harness(es) materialize
|
|
19
|
+
delivers into. The example lists every built-in; drop the ones you don't use (any one id or
|
|
20
|
+
comma-separated subset is valid).
|
|
18
21
|
Works on any git repo. Edit .spec/project/spec.md to describe it, then grow child nodes
|
|
19
22
|
(each a dir with a spec.md + a \`code:\` list of the files it governs).
|
|
20
23
|
|
|
21
|
-
3. Run
|
|
24
|
+
3. Run this repo's backend — it reads .spec + git from cwd and registers in this user's host registry
|
|
22
25
|
spex serve # http://localhost:8787 (--port <n> for another endpoint)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
the internet, use \`spex serve --public\` instead).
|
|
32
|
-
(Dogfood/source alternative: API_URL=http://localhost:<port> npm run dev in spec-dashboard —
|
|
33
|
-
the dev server; "dashboard": { "apiUrl": "..." } in spexcode.json applies only to that layout.)
|
|
26
|
+
Run one \`spex serve\` from every project you want online. Give each additional backend a free
|
|
27
|
+
--port; the port only binds that backend and is never paired with a dashboard process.
|
|
28
|
+
|
|
29
|
+
4. Open the shared host dashboard — run this ONCE for the current user, from any directory
|
|
30
|
+
spex dashboard # host gateway + UI on http://localhost:5173
|
|
31
|
+
It discovers backends already running and any that start later. \`/projects\` is the global project
|
|
32
|
+
switcher and management surface; each project's dashboard lives under \`/p/:id/\`. The gateway is
|
|
33
|
+
loopback-only by default; \`--host\` widens its bind as described by \`spex dashboard --help\`.
|
|
34
34
|
|
|
35
35
|
5. Govern your layout (optional)
|
|
36
|
-
spexcode.json sets lint's governedRoots/
|
|
36
|
+
spexcode.json sets lint's governedRoots/include/exclude/test source policy and any non-default layout.
|
|
37
37
|
\`spex spec lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
|
|
38
38
|
|
|
39
39
|
Look these up on demand — the formats an agent authors, and the settings it configures:
|
|
@@ -62,13 +62,27 @@ FRONTMATTER (YAML between the opening and closing --- lines; every field optiona
|
|
|
62
62
|
Drives drift + eval freshness. Many nodes MAY govern the same file (ordinary
|
|
63
63
|
composition); a file governed by > maxOwners nodes warns (the \`owners\` rule — split it). Omit
|
|
64
64
|
for a pure-prose node: a cross-cutting contract no file owns.
|
|
65
|
-
The entry may pin
|
|
66
|
-
method; top-level functions, arrow/const declarations,
|
|
67
|
-
type/interface anchor warns).
|
|
68
|
-
|
|
65
|
+
The entry may pin named units — ANCHORS: one or more \`path#symbol\` rows, ALL on the same
|
|
66
|
+
base file (\`#Class.method\` for a class method; top-level functions, arrow/const declarations,
|
|
67
|
+
classes, enums anchor cleanly; a type/interface anchor warns). One-govern counts DISTINCT
|
|
68
|
+
base paths, so selectors never widen govern past one file. Several selectors are OR: a
|
|
69
|
+
commit hitting ANY upgrades drift to the blocking \`anchor-drift\` error (one error per
|
|
70
|
+
entry, naming the hit selectors — a commit counts once). Without an anchor, drift stays
|
|
71
|
+
advisory forever. A base path is either whole-file (bare) or selector-scoped, never both;
|
|
72
|
+
duplicates, globs/directories with a selector, and dead/ambiguous units all error loud. A
|
|
73
|
+
selector-scoped governor claims units, not the file, so it stays out of the \`owners\` bound
|
|
74
|
+
(spex spec owner still displays it, marked "(scoped)"). Anchors are optional.
|
|
75
|
+
Python \`.py\`/\`.pyi\` anchors recognize structural \`def\`, \`async def\`, and \`class\`
|
|
76
|
+
declarations. Methods and nested declarations use lexical qualified names such as
|
|
77
|
+
\`Class.method\`, \`outer.inner\`, or \`Outer.Inner.method\`; attached decorators belong to
|
|
78
|
+
the declaration's range. Runtime-created/assigned callables, imported aliases, and generated
|
|
79
|
+
names are outside this declaration extractor and therefore resolve as dead anchors.
|
|
69
80
|
related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
|
|
70
81
|
(never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
|
|
71
|
-
govern doesn't. Every listed path must exist (lint integrity error otherwise).
|
|
82
|
+
govern doesn't. Every listed path must exist (lint integrity error otherwise). A related row
|
|
83
|
+
may also pin \`path#symbol\`: the node then hears about a commit ONLY when it moves that
|
|
84
|
+
unit — a hit is a soft \`related-drift\` warn naming the selector, a miss is SILENT (a scoped
|
|
85
|
+
related file's ordinary file-level nudge is off). Still never blocks, no ack, no eval freshness.
|
|
72
86
|
surface plugin-system/.plugins nodes only: one or MORE of system (folded into every agent's prompt) |
|
|
73
87
|
command (a /command) | skill (an on-demand SKILL.md the harness loads when a task matches the
|
|
74
88
|
node's desc) | agent (a spawnable sub-agent definition; its \`tools:\` list is the spawned
|
|
@@ -92,11 +106,14 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
|
|
|
92
106
|
deleted/renamed), an ambiguous one (two same-named units in one file), a file that
|
|
93
107
|
no longer parses, a language with no designated extractor, or an extractor that
|
|
94
108
|
can't run here (e.g. no host typescript — 'npm i -D typescript' or drop the anchor)
|
|
95
|
-
all error, never silently pass.
|
|
96
|
-
|
|
109
|
+
all error, never silently pass. So do a relation's STRUCTURAL defects: a duplicate
|
|
110
|
+
entry, a base path both bare and selector-scoped, or a selector on a
|
|
111
|
+
glob/directory.
|
|
112
|
+
anchor-drift (error) a commit since the node's version intersected an ANCHORED unit's lines (judged
|
|
97
113
|
from the file as it existed AT each commit) and no Spec-OK ack covers it — the
|
|
98
|
-
blocking tier of drift.
|
|
99
|
-
|
|
114
|
+
blocking tier of drift. Selectors on one file are OR'd: one error per entry, the
|
|
115
|
+
hit selectors named, each commit counted once. Remedy: update the spec, or
|
|
116
|
+
\`spex spec ack\` with a real reason (recorded in the ack commit body).
|
|
100
117
|
one-govern (error) a node governs (code:) at most ONE file — keep the true subject, move the rest
|
|
101
118
|
to related:.
|
|
102
119
|
living (error) no "## vN" changelog headings — the body is current-state.
|
|
@@ -104,31 +121,36 @@ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
|
|
|
104
121
|
optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
|
|
105
122
|
is unique tree-wide.
|
|
106
123
|
mention (error) every [[node-id]] in prose names a real node (fenced/backticked samples exempt).
|
|
107
|
-
altitude (warn) the body stays high-altitude: line/char budgets (~50 lines / 4200 chars), low
|
|
108
|
-
code-identifier density, no step-by-step phrasing. Over budget = rewrite higher.
|
|
109
|
-
breadth (warn) a node with >= maxChildren direct children (default 8) — altitude's structural
|
|
110
|
-
twin; is an intermediate grouping layer missing?
|
|
111
124
|
coverage (warn) every source file is claimed by ≥1 node — via code: OR related: (related is the net).
|
|
112
125
|
drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
|
|
113
126
|
ALWAYS advisory: unanchored drift never blocks a commit (the blocking tier is
|
|
114
|
-
anchor-drift above).
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
anchor-drift above). On a selector-SCOPED code file whose window has NO hit (a
|
|
128
|
+
miss), this advisory stays by default; the committed \`lint.scopedCodeMiss:
|
|
129
|
+
"ignore"\` silences ONLY it (hit blocks, bare drift, integrity, acks, related,
|
|
130
|
+
eval freshness all untouched). Remedy: edit the spec to the new intent
|
|
131
|
+
(re-versions the node), OR \`spex spec ack <node> --reason "…"\` when only
|
|
132
|
+
mechanics changed and the contract still holds.
|
|
117
133
|
anchor (warn) an anchor pins a type/interface — types reshape with every refactor; anchor the
|
|
118
134
|
behaviour-bearing unit instead.
|
|
119
|
-
related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never
|
|
136
|
+
blocks. A selector-scoped related row instead warns per HIT (selector named);
|
|
137
|
+
its file-level misses are silent.
|
|
138
|
+
owners (warn) a file governed WHOLE-FILE by > maxOwners nodes (default 3) does too much — SPLIT
|
|
139
|
+
it so each governor owns its own module (or merge the nodes, or give it one
|
|
140
|
+
foundation owner). Selector-scoped governors don't count toward the bound.
|
|
122
141
|
confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
|
|
123
142
|
|
|
143
|
+
Heuristic spec health is deliberately outside this production gate. Bare \`spex doctor\` reports opt-in
|
|
144
|
+
altitude and breadth findings; the tidy workflow consumes that report and adds semantic judgment.
|
|
145
|
+
|
|
124
146
|
LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
|
|
125
147
|
before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.`
|
|
126
148
|
|
|
127
149
|
const EVAL = `spex guide eval — the eval.md file format
|
|
128
150
|
|
|
129
151
|
An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
|
|
130
|
-
behaviour and the spec. It is optional, but a node that governs
|
|
131
|
-
|
|
152
|
+
behaviour and the spec. It is optional, but a node that governs a file admitted by lint's shared tracked-text
|
|
153
|
+
include-minus-exclude/test policy with no eval.md is
|
|
132
154
|
a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
|
|
133
155
|
NOTHING — the agent measures; eval keeps score.
|
|
134
156
|
|
|
@@ -141,8 +163,13 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
|
|
|
141
163
|
\`frontend-e2e, backend-api, cli, desktop, mobile\`). A tag outside the library is rejected —
|
|
142
164
|
use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
|
|
143
165
|
device) so it can be filtered and, later, routed to the right driver.
|
|
144
|
-
test optional.
|
|
145
|
-
|
|
166
|
+
test optional. Either a repo-path scalar (the backward-compatible shorthand) or a strict object:
|
|
167
|
+
test:
|
|
168
|
+
path: tests/auth.spec.ts
|
|
169
|
+
name: rejects an expired session
|
|
170
|
+
Both forms normalize in JSON to \`{ "path": "..." }\` with optional \`"name"\`. The object
|
|
171
|
+
requires exactly \`path\` + \`name\`; its case name is opaque text preserved for the measuring
|
|
172
|
+
hand. The path must exist. SpexCode does not parse WDIO/Playwright or execute anything.
|
|
146
173
|
code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
|
|
147
174
|
allowed) — its own slice of the code freshness axis, so scenarios on one node go stale
|
|
148
175
|
independently. Absent → it inherits the node's \`code:\` list. A file governed by > maxOwners
|
|
@@ -187,7 +214,12 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
|
|
|
187
214
|
CLI harness stamping line numbers — take a baseline and at EACH real step push
|
|
188
215
|
\`{ at: <position>, step: "…" }\`; dump that array as \`--timeline\`. Its \`axis\` MUST match the
|
|
189
216
|
evidence it rides (a \`line\` map needs a \`--result\` transcript, a \`time\` map a \`--video\`);
|
|
190
|
-
skip it for a short single-step artefact.
|
|
217
|
+
skip it for a short single-step artefact. A step name is a SHORT human label for its
|
|
218
|
+
moment ("open graph") — never a metadata channel: the run's identity, its verdict, and
|
|
219
|
+
the evidence's extent already have canonical homes (the scenario's \`test:\` field, the
|
|
220
|
+
measurement's --pass/--fail, the artefact itself), so a step like
|
|
221
|
+
"runner start: path/to/spec.ts :: <full case title>" smuggles data the measurement already
|
|
222
|
+
carries and turns the step ruler into noise. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
|
|
191
223
|
time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
|
|
192
224
|
STATIC end state → \`--image <png>\` (repeatable — N stills). Layout, an icon, copy, one rendered frame.
|
|
193
225
|
backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
|
|
@@ -222,23 +254,26 @@ code file or the scenario (the eval.md) moves since it was filed.
|
|
|
222
254
|
|
|
223
255
|
const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
|
|
224
256
|
|
|
225
|
-
SpexCode reads
|
|
257
|
+
SpexCode reads PROJECT runtime settings from TWO optional JSON files at the repo root. There is no imperative
|
|
226
258
|
settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
|
|
227
259
|
PORTABILITY, and picking the right one is the whole discipline:
|
|
228
260
|
|
|
229
261
|
spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
|
|
230
|
-
identity, lint budgets, launcher NAMES. "Git is the database": tracked so the
|
|
262
|
+
identity, lint policy, doctor health budgets, launcher NAMES. "Git is the database": tracked so the
|
|
231
263
|
team shares ONE configuration.
|
|
232
264
|
spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
|
|
233
265
|
paths. Layered OVER spexcode.json (see MERGE
|
|
234
266
|
below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
|
|
235
267
|
|
|
236
268
|
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
237
|
-
icon,
|
|
269
|
+
icon, lint policy, doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
238
270
|
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
239
271
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
240
272
|
\`spex session new\` or the dashboard without an explicit launcher choice.
|
|
241
273
|
|
|
274
|
+
The host-wide gateway has one separate per-user setting, \`gateway.icon\` in
|
|
275
|
+
\`$SPEXCODE_HOME/config.json\`. It is documented below and never belongs to either project file.
|
|
276
|
+
|
|
242
277
|
MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
|
|
243
278
|
sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
|
|
244
279
|
pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
|
|
@@ -254,13 +289,23 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
|
|
|
254
289
|
|
|
255
290
|
── DASHBOARD (spexcode.json — portable project identity) ──
|
|
256
291
|
dashboard.title browser-tab name. Default: the repo-root basename.
|
|
257
|
-
dashboard.icon
|
|
292
|
+
dashboard.icon the project's icon identity. The Projects UI writes one shared preset id:
|
|
293
|
+
spexcode | gateway | mdi:rocket-launch | compass | terminal | package | database | spark.
|
|
294
|
+
Existing emoji ("🔭"), arbitrary Iconify names ("lucide:radar"), and full URLs remain
|
|
295
|
+
supported; picking a preset migrates that one existing field, never creates another.
|
|
258
296
|
dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
|
|
259
297
|
install prefer the API_URL env var; apiUrl here is the default only when the dashboard
|
|
260
298
|
lives inside the project.
|
|
261
299
|
Example:
|
|
262
300
|
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
|
|
263
301
|
|
|
302
|
+
── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
|
|
303
|
+
gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
|
|
304
|
+
The admin-only Projects picker writes this field atomically. It is the gateway's ONE
|
|
305
|
+
authored icon setting and is never copied into spexcode.json or spexcode.local.json.
|
|
306
|
+
Example:
|
|
307
|
+
{ "gateway": { "icon": "database" } }
|
|
308
|
+
|
|
264
309
|
── SESSIONS / WORKERS ──
|
|
265
310
|
sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
|
|
266
311
|
precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
|
|
@@ -268,8 +313,8 @@ Example:
|
|
|
268
313
|
Counts compute slots, not total sessions: idle/asking/review/done do not
|
|
269
314
|
occupy one. A policy number → committed spexcode.json; omit it to use the
|
|
270
315
|
default, or tune higher/lower for the project's usual host.
|
|
271
|
-
sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds
|
|
272
|
-
|
|
316
|
+
sessions.launchers the NAMED launcher profiles (see LAUNCHERS). \`spex init\` seeds one entry per
|
|
317
|
+
SELECTED harness (--harness); edit/add more like any other.
|
|
273
318
|
sessions.defaultLauncher the launcher name a create with no explicit --launcher/dropdown pick uses
|
|
274
319
|
(required for no-choice creates). A portable NAME → committed.
|
|
275
320
|
A launcher \`cmd\` that is a HOST-SPECIFIC ABSOLUTE PATH belongs in spexcode.local.json — the committed file
|
|
@@ -278,18 +323,24 @@ must stay free of machine paths.
|
|
|
278
323
|
── LAUNCHERS (the profile block, split across the two files) ──
|
|
279
324
|
A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
|
|
280
325
|
by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
|
|
281
|
-
reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS
|
|
282
|
-
|
|
283
|
-
"claude"
|
|
284
|
-
"codex"
|
|
285
|
-
|
|
326
|
+
reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
|
|
327
|
+
harness the adopter SELECTED (--harness), from the template pool
|
|
328
|
+
"claude" → { "harness": "claude", "cmd": "claude" }
|
|
329
|
+
"codex" → { "harness": "codex", "cmd": "codex" }
|
|
330
|
+
"opencode" → { "harness": "opencode", "cmd": "opencode" }
|
|
331
|
+
"pi" → { "harness": "pi", "cmd": "pi" }
|
|
332
|
+
These plain commands preserve each harness's normal permission model. Automatic-permission commands such as
|
|
333
|
+
\`claude --dangerously-skip-permissions\`, \`codex --yolo\`, or \`opencode --auto\` are NEVER clean-init
|
|
334
|
+
defaults: define and select one explicitly only when that access is intended. To run workers under an auth
|
|
286
335
|
wrapper (e.g. reclaude), point a launcher's \`cmd\` at it in spexcode.local.json — there is no environment
|
|
287
|
-
override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir
|
|
288
|
-
variants. Shape:
|
|
289
|
-
"launchers": { "<name>": { "harness": "claude" | "codex"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
in
|
|
336
|
+
override that rewrites a launcher's command. Add more profiles when a project needs named auth/config-dir or
|
|
337
|
+
permission variants. Shape:
|
|
338
|
+
"launchers": { "<name>": { "harness": "claude" | "codex" | "opencode" | "pi",
|
|
339
|
+
"cmd": "<launch command>" } }
|
|
340
|
+
\`harness\` defaults to "claude"; \`cmd\` is required and embedded whole. A portable plain command may live
|
|
341
|
+
in committed spexcode.json (as the init seeds do). A host-specific command — an absolute wrapper path,
|
|
342
|
+
credential route, or personal permission choice — belongs in gitignored spexcode.local.json, while its
|
|
343
|
+
portable defaultLauncher NAME may stay in committed spexcode.json; the merge keeps both:
|
|
293
344
|
|
|
294
345
|
spexcode.json (committed — the portable name reference)
|
|
295
346
|
{ "sessions": { "defaultLauncher": "gpt5" } }
|
|
@@ -321,9 +372,11 @@ resolves its backend per this ladder, flag first:
|
|
|
321
372
|
sugar for --api http://127.0.0.1:<n>).
|
|
322
373
|
2a. worker (SPEXCODE_SESSION_ID set): env SPEXCODE_API_URL — the backend-injected lifeline; cwd
|
|
323
374
|
discovery never steals it.
|
|
324
|
-
2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\`
|
|
325
|
-
|
|
326
|
-
health-probes before trusting (a dead record is
|
|
375
|
+
2b. human (no session id): the cwd project's RECORDED live backend — \`spex serve\` publishes
|
|
376
|
+
{url,pid,instanceId,root} atomically to ~/.spexcode/projects/<enc>/backend.json
|
|
377
|
+
at bind time; the reader health-probes before trusting (a dead record is
|
|
378
|
+
ignored). The same record is what \`spex dashboard\` reconciles its
|
|
379
|
+
host-wide project list from.
|
|
327
380
|
3. the other side as fallback (human with no live record → env; worker with no env → record).
|
|
328
381
|
4. default http://127.0.0.1:$PORT||8787.
|
|
329
382
|
WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
|
|
@@ -348,26 +401,52 @@ the guard (the flag is the declaration of intent). Reads point anywhere.
|
|
|
348
401
|
lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
|
|
349
402
|
'.' = the whole project (only git-TRACKED files). Default
|
|
350
403
|
["spec-dashboard/src", "spec-cli/src"].
|
|
351
|
-
lint.
|
|
352
|
-
|
|
353
|
-
lint.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
lint.
|
|
404
|
+
lint.sourceIncludeGlobs optional repo-relative includes. Omit = ALL tracked current regular text under
|
|
405
|
+
governedRoots; [] = intentionally none. Slash-less globs match at any depth.
|
|
406
|
+
lint.sourceExcludeGlobs repo-relative paths subtracted from the includes (default []). Nothing about
|
|
407
|
+
docs/vendor/build/config is guessed: exclude it here when that is project policy.
|
|
408
|
+
lint.sourceExtensions compatibility shorthand compiled into include globs (".py" → "**/*.py") and
|
|
409
|
+
unioned with sourceIncludeGlobs; it has no separate matching path.
|
|
410
|
+
lint.testGlobs globs EXCLUDED from coverage. Defaults cover .test/.spec names, test/tests/
|
|
411
|
+
directories, and test_* / *_test conventions; [] governs tests too.
|
|
412
|
+
lint.maxOwners warn when a file is governed WHOLE-FILE by > this many nodes (default 3).
|
|
413
|
+
Selector-scoped governors (code: path#symbol) don't count toward the bound.
|
|
358
414
|
(lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
|
|
359
415
|
by code anchors — \`code: path#symbol\` — whose hits error unconditionally; a
|
|
360
416
|
leftover key is ignored.)
|
|
417
|
+
lint.scopedCodeMiss "warn" (default) | "ignore" — the file-level drift ADVISORY on a selector-
|
|
418
|
+
scoped code: file whose window commits hit no selector (a miss). "ignore"
|
|
419
|
+
silences ONLY that advisory; it never touches hit blocks (anchor-drift),
|
|
420
|
+
bare-path drift, integrity, Spec-OK acks, related semantics, or eval
|
|
421
|
+
freshness. A project policy → committed spexcode.json.
|
|
361
422
|
lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
|
|
362
423
|
["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
|
|
363
|
-
Example — govern your own source dir
|
|
364
|
-
{ "lint": { "governedRoots": ["src"]
|
|
424
|
+
Example — govern your own source dir:
|
|
425
|
+
{ "lint": { "governedRoots": ["src"] } }
|
|
426
|
+
Example — declare project-specific exclusions (nothing is guessed from these names):
|
|
427
|
+
{ "lint": { "governedRoots": ["."], "sourceExcludeGlobs": ["vendor/**", "dist/**", "docs/**"] } }
|
|
428
|
+
Migration: \`lint.maxChildren\` is RETIRED and no longer read; move its value to
|
|
429
|
+
\`doctor.breadth.maxChildren\`. Bare \`spex doctor\` flags a stale key.
|
|
430
|
+
|
|
431
|
+
── DOCTOR HEALTH (spexcode.json — portable advisory-diagnosis budgets) ──
|
|
432
|
+
doctor.altitude the one altitude proxy config consumed by bare \`spex doctor\`:
|
|
433
|
+
{ lineBudget, charBudget, sizeable, dense, steps, identifierExtensions }
|
|
434
|
+
Defaults: 50 / 4200 / 35 / 1.3 / 3 / []. Exact filename signals derive from
|
|
435
|
+
lint's tracked source candidates; identifierExtensions adds compatibility wildcard
|
|
436
|
+
rows (".legacy" → "*.legacy") to that same matcher.
|
|
437
|
+
doctor.breadth the one tree-breadth hypothesis consumed by bare \`spex doctor\`:
|
|
438
|
+
{ maxChildren }. Default: 8. Reports each affected node's direct child count and
|
|
439
|
+
points to the regroup workflow; genuine flat peers may remain flat.
|
|
440
|
+
Example — tune opt-in health diagnosis without changing the lint gate:
|
|
441
|
+
{ "doctor": { "altitude": { "lineBudget": 70 }, "breadth": { "maxChildren": 10 } } }
|
|
365
442
|
|
|
366
443
|
── OTHER (spexcode.json unless noted) ──
|
|
367
444
|
preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
|
|
368
445
|
'default'; seed-time only, read by init.ts).
|
|
369
|
-
harnesses which harness targets \`spex materialize\` delivers into — native ids
|
|
370
|
-
|
|
446
|
+
harnesses which harness targets \`spex materialize\` delivers into — native ids
|
|
447
|
+
("claude"|"codex"|"opencode"|"pi") or a
|
|
448
|
+
{ "plugin": "<folder>" } bundle. REQUIRED — there is no default set: \`spex init --harness\`
|
|
449
|
+
stamps the explicit choice, and a missing field fails materialize loud. PERSISTENT and
|
|
371
450
|
git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
|
|
372
451
|
that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
|
|
373
452
|
deselected harness's artifacts are pruned by that pass.`
|
|
@@ -376,9 +455,11 @@ const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and
|
|
|
376
455
|
|
|
377
456
|
SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
|
|
378
457
|
measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
|
|
379
|
-
stitches the two ends into a closed loop.
|
|
380
|
-
|
|
381
|
-
|
|
458
|
+
stitches the two ends into a closed loop. Materialize is the base operation of harness ADAPTATION:
|
|
459
|
+
one pass renders the spec tree into whatever artifacts the selected harness auto-discovers, so that
|
|
460
|
+
is how SpexCode reaches an agent — never a launch-time flag. The footprint follows: the head+tail
|
|
461
|
+
(.spec, spexcode.json, evals) is the ASSET and lives in git like source; everything else is derived
|
|
462
|
+
wiring or a machine fact. Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
|
|
382
463
|
behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
383
464
|
|
|
384
465
|
── THE FOUR KINDS (all fixed) ──
|
|
@@ -13,19 +13,33 @@ export type HarnessTarget =
|
|
|
13
13
|
| { kind: 'native'; id: HarnessId }
|
|
14
14
|
| { kind: 'plugin'; folder: string }
|
|
15
15
|
|
|
16
|
-
// the
|
|
17
|
-
|
|
16
|
+
// the native vocabulary (for messages and init's flag parsing). There is NO default set: the delivery
|
|
17
|
+
// targets are an explicit adopter choice, stamped by `spex init --harness` and read back from config forever.
|
|
18
|
+
export const NATIVE_HARNESS_IDS: readonly HarnessId[] = HARNESSES.map((h) => h.id)
|
|
18
19
|
const KNOWN: readonly string[] = HARNESSES.map((h) => h.id)
|
|
19
20
|
|
|
21
|
+
// the one-line repair every missing-selection error carries — the field is REQUIRED, never defaulted,
|
|
22
|
+
// because with many harnesses a silent "deliver everywhere" would litter the adopter's tree with artifacts
|
|
23
|
+
// for tools they never installed.
|
|
24
|
+
const MISSING = `spexcode.json has no "harnesses" field — the delivery targets are an EXPLICIT choice, never a default. Declare it, e.g. "harnesses": ["claude"] — members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}. Fresh adoption: \`spex init --harness <ids>\` stamps it.`
|
|
25
|
+
|
|
26
|
+
// parse `spex init --harness <spec>` into the raw JSON members the `harnesses` field carries: a comma-
|
|
27
|
+
// separated list where `plugin:<folder>` means a plugin target and anything else is a native id. Pure
|
|
28
|
+
// spelling translation — legality (unknown ids, plugin exclusivity) stays with resolveHarnessTargets.
|
|
29
|
+
export function parseHarnessFlag(spec: string): unknown[] {
|
|
30
|
+
return spec.split(',').map((s) => s.trim()).filter(Boolean)
|
|
31
|
+
.map((s) => (s.startsWith('plugin:') ? { plugin: s.slice('plugin:'.length) } : s))
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// parse + validate the spexcode.json `harnesses` field into resolved targets. FAIL LOUD on an illegal set —
|
|
21
35
|
// 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;
|
|
36
|
+
// JSON value as written; a MISSING field is itself illegal (the choice must exist, stamped by init).
|
|
23
37
|
export function resolveHarnessTargets(raw: unknown): HarnessTarget[] {
|
|
24
|
-
if (raw === undefined || raw === null)
|
|
38
|
+
if (raw === undefined || raw === null) throw new Error(MISSING)
|
|
25
39
|
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>"}
|
|
40
|
+
throw new Error(`spexcode.json "harnesses" must be an ARRAY of targets (got ${typeof raw}). Members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}.`)
|
|
27
41
|
if (raw.length === 0)
|
|
28
|
-
throw new Error(`spexcode.json "harnesses" is EMPTY — list at least one target
|
|
42
|
+
throw new Error(`spexcode.json "harnesses" is EMPTY — list at least one target: native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}.`)
|
|
29
43
|
const targets: HarnessTarget[] = []
|
|
30
44
|
for (const m of raw) {
|
|
31
45
|
if (typeof m === 'string') {
|