spexcode 0.2.8 → 0.4.0
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 +56 -44
- package/package.json +3 -3
- package/spec-cli/bin/spex.mjs +2 -2
- package/spec-cli/hooks/dispatch.sh +1 -1
- package/spec-cli/hooks/harness.sh +26 -6
- package/spec-cli/src/anchors.ts +300 -0
- package/spec-cli/src/attach.ts +2 -2
- package/spec-cli/src/cli.ts +691 -536
- package/spec-cli/src/client.ts +31 -30
- package/spec-cli/src/contract-filter.ts +1 -1
- package/spec-cli/src/doctor.ts +40 -13
- package/spec-cli/src/gateway.ts +11 -7
- package/spec-cli/src/git.ts +23 -15
- package/spec-cli/src/{board.ts → graph.ts} +44 -14
- package/spec-cli/src/{boardCache.ts → graphCache.ts} +41 -21
- package/spec-cli/src/{boardDelta.ts → graphDelta.ts} +1 -1
- package/spec-cli/src/graphStream.ts +288 -0
- package/spec-cli/src/guide.ts +142 -98
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +30 -14
- package/spec-cli/src/help.ts +300 -385
- package/spec-cli/src/hooks.ts +1 -1
- package/spec-cli/src/index.ts +144 -103
- package/spec-cli/src/init.ts +14 -13
- package/spec-cli/src/issues.ts +89 -33
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +128 -30
- package/spec-cli/src/localIssues.ts +61 -60
- package/spec-cli/src/materialize.ts +1 -1
- package/spec-cli/src/mentions.ts +15 -15
- package/spec-cli/src/migrate-table.ts +398 -0
- package/spec-cli/src/migrate.ts +386 -0
- package/spec-cli/src/ranker.ts +30 -4
- package/spec-cli/src/reaper.ts +117 -0
- package/spec-cli/src/search.bench.mjs +12 -12
- package/spec-cli/src/search.ts +1 -1
- package/spec-cli/src/sessions.ts +244 -138
- package/spec-cli/src/specs.ts +40 -25
- package/spec-cli/src/supervise.ts +2 -2
- package/spec-cli/src/tree.ts +5 -5
- package/spec-cli/src/uninstall.ts +4 -4
- package/spec-cli/templates/hooks/post-checkout +1 -1
- package/spec-cli/templates/hooks/post-merge +3 -3
- package/spec-cli/templates/hooks/pre-commit +9 -9
- package/spec-cli/templates/hooks/prepare-commit-msg +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/idle.sh +2 -2
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/mark-active.sh +7 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/spec.md +2 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/fail.sh +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec-first.sh +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec-of-file.sh +2 -2
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +21 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/stop-gate.sh +21 -21
- package/spec-cli/templates/spec/project/{.config → .plugins}/distill/spec.md +2 -2
- package/spec-cli/templates/spec/project/{.config → .plugins}/extract/spec.md +5 -5
- package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/forge-link/spec.md +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/reproduce-before-fix/spec.md +3 -3
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/regroup/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/spec.md +18 -0
- package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +8 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/tidy/spec.md +1 -1
- package/spec-cli/templates/spec/project/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-P0B9ukSG.js → Dashboard-CTcH2eW9.js} +9 -9
- package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +2 -0
- package/spec-dashboard/dist/assets/{FoldToggle-BuQ0lokE.js → FoldToggle-CVFbBpyW.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-H-D8aHEl.js → IssuesPage-kULjonqj.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-oZXIeCPb.js → MobileApp-B0ZJju8K.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-Blr_MEdU.js → SessionInterface-BRKJqU2U.js} +3 -3
- package/spec-dashboard/dist/assets/{SessionWindow-LcCzBMU7.js → SessionWindow-CDhEL7wO.js} +7 -7
- package/spec-dashboard/dist/assets/{Settings-_yOye-In.js → Settings-BL6FV_8S.js} +1 -1
- package/spec-dashboard/dist/assets/{index-uGs9v_9o.css → index-DmQsNYKK.css} +1 -1
- package/spec-dashboard/dist/assets/{index-BhIslAau.js → index-DulGPk6A.js} +8 -8
- package/spec-dashboard/dist/index.html +2 -2
- package/{spec-yatsu → spec-eval}/src/cache.ts +8 -5
- package/{spec-yatsu → spec-eval}/src/cli.ts +212 -96
- package/{spec-yatsu → spec-eval}/src/evaltab.ts +35 -27
- package/{spec-yatsu → spec-eval}/src/filing.ts +7 -5
- package/{spec-yatsu → spec-eval}/src/freshness.ts +44 -22
- package/spec-eval/src/humanok.ts +43 -0
- package/{spec-yatsu → spec-eval}/src/scenariofresh.ts +22 -17
- package/{spec-yatsu/src/yatsu.ts → spec-eval/src/scenarios.ts} +41 -26
- package/{spec-yatsu/src/proof.ts → spec-eval/src/sessioneval.ts} +59 -59
- package/{spec-yatsu → spec-eval}/src/sidecar.ts +42 -10
- package/{spec-yatsu → spec-eval}/src/timeline.ts +1 -1
- package/spec-forge/src/__fixtures__/github-forge.json +9 -9
- package/spec-forge/src/cli.ts +14 -13
- package/spec-forge/src/{needs-yatsu-eval.ts → needs-eval.ts} +6 -6
- package/spec-cli/src/boardStream.ts +0 -179
- package/spec-cli/templates/presets/careful/.config/clarify-before-code/spec.md +0 -11
- package/spec-cli/templates/spec/project/.config/core/spec.md +0 -17
- package/spec-cli/templates/spec/project/.config/spec.md +0 -15
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +0 -8
- package/spec-dashboard/dist/assets/EvalsPage-BrvAGyc4.js +0 -2
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/distill/digest.mjs +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/memory-hygiene/spec.md +0 -0
package/spec-cli/src/guide.ts
CHANGED
|
@@ -10,7 +10,8 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
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
12
|
inner package uninstalls as \`@spexcode/spec-cli\`). A source link ships no
|
|
13
|
-
prebuilt dashboard dist — \`spex
|
|
13
|
+
prebuilt dashboard dist — \`spex serve ui\` builds it once on first run (needs spec-dashboard's
|
|
14
|
+
npm deps installed), or use the dev server.)
|
|
14
15
|
|
|
15
16
|
2. Adopt a repo
|
|
16
17
|
cd <your-repo> && spex init # seeds .spec/ + git hooks (additive, never overwrites)
|
|
@@ -18,13 +19,13 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
18
19
|
(each a dir with a spec.md + a \`code:\` list of the files it governs).
|
|
19
20
|
|
|
20
21
|
3. Run the backend — it reads .spec + git from the cwd repo
|
|
21
|
-
spex serve # http://localhost:8787 (
|
|
22
|
-
Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two
|
|
22
|
+
spex serve # http://localhost:8787 (--port <n> for another endpoint)
|
|
23
|
+
Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two ports.
|
|
23
24
|
|
|
24
|
-
4. Open the dashboard — the SAME
|
|
25
|
-
spex
|
|
25
|
+
4. Open the dashboard — the SAME dashboard for every project, pointed per project
|
|
26
|
+
spex serve ui # serves the bundled dashboard on :5173, proxying /api
|
|
26
27
|
Point it at another backend with --api-port (pairs with \`spex serve --port\`); one dashboard per
|
|
27
|
-
project. The
|
|
28
|
+
project. The dashboard is a viewer — which backend it proxies is the only "which project" knob.
|
|
28
29
|
Loopback-only by default: viewing from another machine needs \`--host 0.0.0.0\` (or a specific
|
|
29
30
|
interface) — still plain HTTP with no gate, so bind wide only on a LAN/tailnet you trust (for
|
|
30
31
|
the internet, use \`spex serve --public\` instead).
|
|
@@ -33,12 +34,12 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
33
34
|
|
|
34
35
|
5. Govern your layout (optional)
|
|
35
36
|
spexcode.json sets lint's governedRoots/sourceExtensions and any non-default worktree layout.
|
|
36
|
-
\`spex lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
|
|
37
|
+
\`spex spec lint\` must report 0 errors; coverage warnings are your adoption TODO (files no node claims yet).
|
|
37
38
|
|
|
38
39
|
Look these up on demand — the formats an agent authors, and the settings it configures:
|
|
39
40
|
spex guide spec the spec.md format (frontmatter + body + the rules lint enforces)
|
|
40
|
-
spex guide
|
|
41
|
-
spex guide
|
|
41
|
+
spex guide eval the eval.md format (scenario schema + how loss is measured and filed)
|
|
42
|
+
spex guide settings the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, lint
|
|
42
43
|
budgets, layout) — every field, and which of the two files it belongs in
|
|
43
44
|
spex guide footprint the footprint model — what SpexCode plants in a repo, and who sees it
|
|
44
45
|
(committed | ignored | hidden), and every migration recipe`
|
|
@@ -47,26 +48,34 @@ const SPEC = `spex guide spec — the spec.md file format
|
|
|
47
48
|
|
|
48
49
|
A spec node is a DIRECTORY under .spec/<project>/…/<id>/ holding a spec.md. The node's id is its leaf dir
|
|
49
50
|
name when that is globally unique, else the shortest parent-qualified path-suffix that disambiguates (so ids
|
|
50
|
-
are unique by construction) — the same id \`spex
|
|
51
|
+
are unique by construction) — the same id \`spex graph\`, \`spec ack\`, and a node/<id> branch use. A spec states a node's PRESENT
|
|
51
52
|
intent at CONTRACT altitude — what it guarantees and why — and is rewritten in place as intent changes;
|
|
52
53
|
version history is git's job, never a changelog in the body.
|
|
53
54
|
|
|
54
55
|
FRONTMATTER (YAML between the opening and closing --- lines; every field optional, sensible defaults):
|
|
55
56
|
title display name. Defaults to the dir id.
|
|
56
|
-
desc one-line summary shown on the
|
|
57
|
-
hue
|
|
57
|
+
desc one-line summary shown on the graph.
|
|
58
|
+
hue node colour on the graph, 0–360. Default 210.
|
|
58
59
|
status pending | active | merged | drift. Usually DERIVED from git state — rarely hand-set.
|
|
59
|
-
code:
|
|
60
|
-
|
|
60
|
+
code: the file this node GOVERNS (is source of truth for) — a YAML list, but AT MOST ONE entry
|
|
61
|
+
(the \`one-govern\` lint error otherwise: keep the true subject, move the rest to related:).
|
|
62
|
+
Drives drift + eval freshness. Many nodes MAY govern the same file (ordinary
|
|
61
63
|
composition); a file governed by > maxOwners nodes warns (the \`owners\` rule — split it). Omit
|
|
62
64
|
for a pure-prose node: a cross-cutting contract no file owns.
|
|
65
|
+
The entry may pin ONE named unit — an ANCHOR: \`path#symbol\` (\`#Class.method\` for a class
|
|
66
|
+
method; top-level functions, arrow/const declarations, classes, enums anchor cleanly; a
|
|
67
|
+
type/interface anchor warns). An anchor upgrades drift on THAT unit to a blocking error
|
|
68
|
+
(\`anchor-drift\`); without one, drift stays advisory forever. Anchors are optional.
|
|
63
69
|
related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
|
|
64
|
-
(never drift, never
|
|
70
|
+
(never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
|
|
65
71
|
govern doesn't. Every listed path must exist (lint integrity error otherwise).
|
|
66
|
-
surface
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
surface plugin-system/.plugins nodes only: one or MORE of system (folded into every agent's prompt) |
|
|
73
|
+
command (a /command) | skill (an on-demand SKILL.md the harness loads when a task matches the
|
|
74
|
+
node's desc) | agent (a spawnable sub-agent definition; its \`tools:\` list is the spawned
|
|
75
|
+
agent's tool allowlist) | hook (a lifecycle hook handler — a co-located script the dispatcher
|
|
76
|
+
runs on the harness events in events:, ordered by order:, blocking when block: true).
|
|
77
|
+
Comma-list several to plug into each. hook nodes may nest under a grouping
|
|
78
|
+
plugin (e.g. .plugins/core/<id>); surface is a field, discovered recursively.
|
|
70
79
|
events hook surface only: harness lifecycle events this node binds (YAML list — PreToolUse, Stop, …).
|
|
71
80
|
order hook surface only: integer; the dispatcher runs same-event hooks low to high.
|
|
72
81
|
block hook surface only: true if the hook may block its event (honored only on block-capable events).
|
|
@@ -75,31 +84,53 @@ BODY (Markdown after the frontmatter): the contract — intent, invariants, outw
|
|
|
75
84
|
code does it. Two optional level-2 headings split ground truth from detail:
|
|
76
85
|
## raw source human-authored, rarely-changed intent — the loss function's target.
|
|
77
86
|
## expanded spec agent-authored detail that must keep serving the raw source.
|
|
78
|
-
Bodies without those headings are read whole. Link sibling nodes with [[node-id]]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
WHAT lint CHECKS (spex lint; the pre-commit hook gates on errors):
|
|
82
|
-
integrity
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
drift
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
Bodies without those headings are read whole. Link sibling nodes with [[node-id]] — every link must name
|
|
88
|
+
a REAL node (lint's mention rule; backtick a placeholder like \`[[node]]\` so it reads as sample text).
|
|
89
|
+
|
|
90
|
+
WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
|
|
91
|
+
integrity (error) every code:/related: path exists — and every anchor RESOLVES: a dead anchor (unit
|
|
92
|
+
deleted/renamed), an ambiguous one (two same-named units in one file), a file that
|
|
93
|
+
no longer parses, a language with no designated extractor, or an extractor that
|
|
94
|
+
can't run here (e.g. no host typescript — 'npm i -D typescript' or drop the anchor)
|
|
95
|
+
all error, never silently pass.
|
|
96
|
+
anchor-drift (error) a commit since the node's version intersected the ANCHORED unit's lines (judged
|
|
97
|
+
from the file as it existed AT each commit) and no Spec-OK ack covers it — the
|
|
98
|
+
blocking tier of drift. Remedy: update the spec, or \`spex spec ack\` with a real
|
|
99
|
+
reason (recorded in the ack commit body).
|
|
100
|
+
one-govern (error) a node governs (code:) at most ONE file — keep the true subject, move the rest
|
|
101
|
+
to related:.
|
|
102
|
+
living (error) no "## vN" changelog headings — the body is current-state.
|
|
103
|
+
id-format (error) each id char is ascii [a-z0-9-] or a non-ascii unicode letter/number (CJK ok; one
|
|
104
|
+
optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
|
|
105
|
+
is unique tree-wide.
|
|
106
|
+
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
|
+
coverage (warn) every source file is claimed by ≥1 node — via code: OR related: (related is the net).
|
|
112
|
+
drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
|
|
113
|
+
ALWAYS advisory: unanchored drift never blocks a commit (the blocking tier is
|
|
114
|
+
anchor-drift above). Remedy: edit the spec to the new intent (re-versions the
|
|
115
|
+
node), OR \`spex spec ack <node> --reason "…"\` when only mechanics changed and the
|
|
116
|
+
contract still holds.
|
|
117
|
+
anchor (warn) an anchor pins a type/interface — types reshape with every refactor; anchor the
|
|
118
|
+
behaviour-bearing unit instead.
|
|
119
|
+
related-drift (warn) a related: file moved ahead of the node — a soft nudge, one summary line, never blocks.
|
|
120
|
+
owners (warn) a file governed by > maxOwners nodes (default 3) does too much — SPLIT it so each
|
|
121
|
+
governor owns its own module (or merge the nodes, or give it one foundation owner).
|
|
122
|
+
confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
|
|
123
|
+
|
|
124
|
+
LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
|
|
125
|
+
before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.`
|
|
126
|
+
|
|
127
|
+
const EVAL = `spex guide eval — the eval.md file format
|
|
128
|
+
|
|
129
|
+
An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
|
|
99
130
|
behaviour and the spec. It is optional, but a node that governs SOURCE code (its code: includes a file whose extension is in
|
|
100
|
-
\`lint.sourceExtensions\` — default .ts/.tsx/.js/.jsx, set it for a Rust/Go/Python tree) with no
|
|
101
|
-
a blind spot: \`spex
|
|
102
|
-
agent measures;
|
|
131
|
+
\`lint.sourceExtensions\` — default .ts/.tsx/.js/.jsx, set it for a Rust/Go/Python tree) with no eval.md is
|
|
132
|
+
a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
|
|
133
|
+
NOTHING — the agent measures; eval keeps score.
|
|
103
134
|
|
|
104
135
|
FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each scenario:
|
|
105
136
|
name REQUIRED. Unique within the file — it keys the sidecar and \`--scenario <name>\`.
|
|
@@ -111,28 +142,29 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
|
|
|
111
142
|
use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
|
|
112
143
|
device) so it can be filtered and, later, routed to the right driver.
|
|
113
144
|
test optional. A repo path to a co-located runnable file (a playwright.spec.ts, a script)
|
|
114
|
-
the agent MAY run by hand. Not a driver —
|
|
145
|
+
the agent MAY run by hand. Not a driver — eval never executes it.
|
|
115
146
|
code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
|
|
116
147
|
allowed) — its own slice of the code freshness axis, so scenarios on one node go stale
|
|
117
148
|
independently. Absent → it inherits the node's \`code:\` list. A file governed by > maxOwners
|
|
118
|
-
scenarios warns \`
|
|
149
|
+
scenarios warns \`eval-owners\` (split it). Each path must exist (a ghost → \`eval-schema\`).
|
|
119
150
|
related optional. Files this scenario REFERENCES but does not govern — same path forms. They do NOT
|
|
120
151
|
stale it (the freshness mirror of a spec node's govern/related). Each path must exist.
|
|
121
152
|
Multi-line prose uses YAML block scalars: \`|\` keeps newlines, \`>\` folds wrapped lines to spaces.
|
|
122
|
-
|
|
153
|
+
An eval.md OWNS nothing — only its scenarios govern and relate (see governed-related).
|
|
123
154
|
|
|
124
155
|
THE SCHEMA IS ENFORCED (closed field set, four required fields, unique names, tags within the library). A
|
|
125
156
|
missing required field, an unknown key (a typo like \`descripton:\`), a duplicate name, an out-of-library
|
|
126
|
-
tag, or no scenarios at all is rejected LOUD: \`spex
|
|
127
|
-
pre-commit \`
|
|
157
|
+
tag, or no scenarios at all is rejected LOUD: \`spex eval lint\` reports it as \`eval-schema\`, and the
|
|
158
|
+
pre-commit \`internal check-staged\` BLOCKS the commit.
|
|
128
159
|
|
|
129
160
|
BODY (after the frontmatter): prose naming the measurement method — YATU ("You As The User"): the agent
|
|
130
|
-
looks at / calls the real product surface, not an internal helper chosen to make the
|
|
161
|
+
looks at / calls the real product surface, not an internal helper chosen to make the evidence easy.
|
|
131
162
|
|
|
132
163
|
MEASURING AND FILING: the agent runs the scenario however it likes (a browser run, an API
|
|
133
164
|
transcript, a by-hand pass), compares the result to \`expected\`, and files it:
|
|
134
|
-
spex
|
|
135
|
-
[--image <png> …repeatable] [--result <txt>|-] [--video <webm|mp4> [--timeline <json>]
|
|
165
|
+
spex eval add <node> [--scenario <name>] (--pass | --fail) [--note <text>]
|
|
166
|
+
[--image <png> …repeatable] [--result <txt>|-] [--video <webm|mp4>] [--timeline <json>]
|
|
167
|
+
(--scenario may be omitted only when the node declares exactly one scenario.)
|
|
136
168
|
The verdict is \`--pass\` or \`--fail\` (a measurement must commit to one — an unmeasured scenario is \`missing\`,
|
|
137
169
|
not a hedged fail). \`--note <text>\` is an OPTIONAL one-line annotation on either (why it failed, how far a
|
|
138
170
|
pass sits from ideal); it does NOT replace evidence — the image/video/transcript is the captured actual behaviour.
|
|
@@ -145,10 +177,11 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
|
|
|
145
177
|
— never a value the agent eyeballs off the finished artefact afterwards (that's
|
|
146
178
|
misaligned and dishonest, worse than none). \`--timeline <json>\` carries one; its \`axis\`
|
|
147
179
|
is the evidence's: a video is \`time\` (ms), a transcript \`line\`, a still SEQUENCE \`frame\`,
|
|
148
|
-
|
|
180
|
+
a structured data export \`index\` (record ordinals; the format's axis set is open — an
|
|
181
|
+
unknown axis just renders as a bare number).
|
|
149
182
|
\`at\` = the position on that axis, \`step\` = a short name for that moment; copy this shape:
|
|
150
183
|
{ "v": 2, "axis": "time",
|
|
151
|
-
"events": [ { "at": 0, "step": "open
|
|
184
|
+
"events": [ { "at": 0, "step": "open graph" },
|
|
152
185
|
{ "at": 1200, "step": "type query" } ] }
|
|
153
186
|
The run exports it: in whatever drives the evidence — Playwright, a computer-use hand, a
|
|
154
187
|
CLI harness stamping line numbers — take a baseline and at EACH real step push
|
|
@@ -157,36 +190,40 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
|
|
|
157
190
|
skip it for a short single-step artefact. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
|
|
158
191
|
time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
|
|
159
192
|
STATIC end state → \`--image <png>\` (repeatable — N stills). Layout, an icon, copy, one rendered frame.
|
|
160
|
-
backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a
|
|
161
|
-
\`--export-json
|
|
193
|
+
backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
|
|
194
|
+
\`--export-json\` dump, an API payload, a metrics dump) is recognized BY CONTENT and kept as
|
|
162
195
|
\`data\` — rendered as a validatable data block, not flattened into scrolling transcript
|
|
163
196
|
text; free-form output stays a transcript. You pick the flag; the KIND follows the bytes.
|
|
164
197
|
The flags combine in ONE filing — several stills can ride beside the clip of the same run.
|
|
165
|
-
ANCHOR DISCIPLINE:
|
|
198
|
+
ANCHOR DISCIPLINE: an eval's \`codeSha\` is HEAD at filing time, and a git sha names only a COMMIT — an
|
|
166
199
|
uncommitted change has none. So measure the tree you are about to commit, COMMIT it, then file; confidence
|
|
167
200
|
is earned on the working tree, but the anchor can only land after the commit. Filing from a dirty tree
|
|
168
|
-
mis-anchors the
|
|
201
|
+
mis-anchors the eval (its sha lacks the change it measured) and it goes stale the moment you commit.
|
|
169
202
|
|
|
170
203
|
A botched filing (a junk e2e/smoke run, a wrong verdict) is undone through the SAME surface:
|
|
171
|
-
spex
|
|
204
|
+
spex eval retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
|
|
172
205
|
retract APPENDS a retraction event to the sidecar (never deletes a line — the trace stays, git records
|
|
173
|
-
who/when/why); the scoreboard then drops the retracted
|
|
206
|
+
who/when/why); the scoreboard then drops the retracted eval everywhere: the previous eval becomes
|
|
174
207
|
the latest again, or the scenario honestly returns to \`missing\`. Default target is the scenario's latest
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
THE SCOREBOARD:
|
|
178
|
-
(a second git-as-database axis). Freshness is derived live from git:
|
|
179
|
-
code file or the scenario (the
|
|
180
|
-
spex
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
eval (\`--last\` makes that explicit; repeat to peel junk back one filing at a time); \`--ts\` pins one.
|
|
209
|
+
|
|
210
|
+
THE SCOREBOARD: evals live in evals.ndjson beside the eval.md — one JSON line per measurement
|
|
211
|
+
(a second git-as-database axis). Freshness is derived live from git: an eval goes STALE when a governed
|
|
212
|
+
code file or the scenario (the eval.md) moves since it was filed.
|
|
213
|
+
spex eval lint [--changed] the measurement layer's findings — PURE ADVISORY, always exit 0 (spec
|
|
214
|
+
lint's errors block commits; a measurement gap never blocks anyone):
|
|
215
|
+
eval-schema (malformed) · eval-drift (stale) · eval-missing (never
|
|
216
|
+
measured) · eval-dangling (orphaned remark track) · eval-coverage
|
|
217
|
+
(governed source, no eval.md — spec lint's coverage, one rule per layer) ·
|
|
218
|
+
eval-owners (a file governed by > maxOwners scenarios — split it)
|
|
219
|
+
spex eval ls <node> the eval timeline (verdict · freshness · evidence), newest first
|
|
220
|
+
spex eval scenario ls [<node>] the declared contracts; --unmeasured = the blind-spot worklist
|
|
221
|
+
spex eval clean GC the content-addressed evidence cache`
|
|
222
|
+
|
|
223
|
+
const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
|
|
187
224
|
|
|
188
225
|
SpexCode reads its runtime settings from TWO optional JSON files at the repo root. There is no imperative
|
|
189
|
-
|
|
226
|
+
settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
|
|
190
227
|
PORTABILITY, and picking the right one is the whole discipline:
|
|
191
228
|
|
|
192
229
|
spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
|
|
@@ -200,7 +237,7 @@ Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A
|
|
|
200
237
|
icon, a lint budget, a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
201
238
|
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
202
239
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
203
|
-
\`spex new\` or the dashboard without an explicit launcher choice.
|
|
240
|
+
\`spex session new\` or the dashboard without an explicit launcher choice.
|
|
204
241
|
|
|
205
242
|
MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
|
|
206
243
|
sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
|
|
@@ -218,14 +255,16 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
|
|
|
218
255
|
── DASHBOARD (spexcode.json — portable project identity) ──
|
|
219
256
|
dashboard.title browser-tab name. Default: the repo-root basename.
|
|
220
257
|
dashboard.icon browser-tab favicon: an emoji ("🔭") OR an Iconify name ("mdi:rocket-launch").
|
|
221
|
-
dashboard.apiUrl the per-project backend the
|
|
222
|
-
install prefer the API_URL env var; apiUrl here is the default only when the
|
|
258
|
+
dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
|
|
259
|
+
install prefer the API_URL env var; apiUrl here is the default only when the dashboard
|
|
223
260
|
lives inside the project.
|
|
224
261
|
Example:
|
|
225
262
|
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
|
|
226
263
|
|
|
227
264
|
── SESSIONS / WORKERS ──
|
|
228
|
-
sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8
|
|
265
|
+
sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
|
|
266
|
+
precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
|
|
267
|
+
an edit applies without a restart).
|
|
229
268
|
Counts compute slots, not total sessions: idle/asking/review/done do not
|
|
230
269
|
occupy one. A policy number → committed spexcode.json; omit it to use the
|
|
231
270
|
default, or tune higher/lower for the project's usual host.
|
|
@@ -265,12 +304,13 @@ in the committed spexcode.json — the merge keeps both:
|
|
|
265
304
|
}
|
|
266
305
|
}
|
|
267
306
|
|
|
268
|
-
── SERVE (spexcode.json — public-exposure for \`spex serve --public
|
|
307
|
+
── SERVE (spexcode.json ONLY — public-exposure for \`spex serve --public\`; this section is read straight
|
|
308
|
+
from spexcode.json, the local overlay is NOT consulted here) ──
|
|
269
309
|
serve.public.enabled turn public mode on without the --public flag.
|
|
270
310
|
serve.public.http drop TLS (the --http escape hatch) — the password then travels in cleartext.
|
|
271
311
|
serve.public.tls { "cert": "<path>", "key": "<path>" } — PATHS to your own cert/key; omit for a
|
|
272
|
-
cached self-signed default.
|
|
273
|
-
|
|
312
|
+
cached self-signed default. Host-specific paths can instead ride the
|
|
313
|
+
--tls-cert/--tls-key flags or SPEXCODE_TLS_CERT/SPEXCODE_TLS_KEY env (which win).
|
|
274
314
|
The gateway password is NEVER read from these files (flag/env only), so serve.public stays committable.
|
|
275
315
|
|
|
276
316
|
── BACKEND ROUTING (not a config field — how a \`spex\` command picks its backend) ──
|
|
@@ -286,20 +326,23 @@ resolves its backend per this ladder, flag first:
|
|
|
286
326
|
health-probes before trusting (a dead record is ignored).
|
|
287
327
|
3. the other side as fallback (human with no live record → env; worker with no env → record).
|
|
288
328
|
4. default http://127.0.0.1:$PORT||8787.
|
|
289
|
-
WRITES are project-bound: every mutating verb (new/merge/send/close/rename/
|
|
329
|
+
WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
|
|
290
330
|
loudly when the resolved backend serves a DIFFERENT same-host project — an explicit --api/--port skips
|
|
291
|
-
the guard (the flag is the
|
|
331
|
+
the guard (the flag is the declaration of intent). Reads point anywhere.
|
|
292
332
|
|
|
293
333
|
── ISSUES (spexcode.json — portable policy) ──
|
|
294
334
|
issues.enabled the issues-workflow on/off switch (default ON). OFF silences the post-merge nudge and
|
|
295
|
-
hides the dashboard view
|
|
335
|
+
hides the dashboard view. Flip it by editing the JSON — there is no CLI toggle verb;
|
|
336
|
+
\`spex doctor\` reports the current state (and flags a legacy \`proposals.enabled\` key,
|
|
337
|
+
which is no longer read).
|
|
296
338
|
|
|
297
339
|
── FORGE (spexcode.json — which forge this repo's remote is; a project fact, so committed) ──
|
|
298
340
|
forge.host explicit forge host id ('github' | 'gitlab' | …) overriding the automatic derivation.
|
|
299
341
|
Normally OMIT it: spec-forge resolves the host from the origin remote's hostname —
|
|
300
|
-
github.com → github,
|
|
301
|
-
self-hosted domain the heuristic misreads needs the override.
|
|
302
|
-
registered driver degrades to an EMPTY forge slice (local
|
|
342
|
+
github.com → github, bitbucket → bitbucket, any other remote → gitlab (the common
|
|
343
|
+
self-hosted shape) — and only a domain the heuristic misreads needs the override.
|
|
344
|
+
A resolved host with no registered driver degrades to an EMPTY forge slice (local
|
|
345
|
+
issues still work, no error).
|
|
303
346
|
|
|
304
347
|
── LINT (spexcode.json — a top-level "lint" key; budgets are portable, so committed only) ──
|
|
305
348
|
lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
|
|
@@ -311,16 +354,17 @@ the guard (the flag is the proof of intent). Reads point anywhere.
|
|
|
311
354
|
lint.altitude body budgets: { lineBudget, charBudget, sizeable, dense, steps }
|
|
312
355
|
(defaults 50 / 4200 / 35 / 1.3 / 3).
|
|
313
356
|
lint.maxChildren breadth budget: warn at >= this many direct children (default 8).
|
|
314
|
-
lint.driftErrorThreshold commit-local gate HARD-BLOCKS a commit touching a node >= this many commits
|
|
315
|
-
behind (default 3).
|
|
316
357
|
lint.maxOwners warn when a file is governed by > this many nodes (default 3).
|
|
317
|
-
|
|
358
|
+
(lint.driftErrorThreshold is RETIRED: the count-based commit gate is replaced
|
|
359
|
+
by code anchors — \`code: path#symbol\` — whose hits error unconditionally; a
|
|
360
|
+
leftover key is ignored.)
|
|
361
|
+
lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
|
|
318
362
|
["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
|
|
319
363
|
Example — govern your own source dir and loosen the altitude budget:
|
|
320
364
|
{ "lint": { "governedRoots": ["src"], "altitude": { "lineBudget": 70 } } }
|
|
321
365
|
|
|
322
366
|
── OTHER (spexcode.json unless noted) ──
|
|
323
|
-
preset the SELECTED init preset — which cumulative .
|
|
367
|
+
preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
|
|
324
368
|
'default'; seed-time only, read by init.ts).
|
|
325
369
|
harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
|
|
326
370
|
{ "plugin": "<folder>" } bundle. Default (omitted): all native harnesses. PERSISTENT and
|
|
@@ -333,18 +377,18 @@ const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and
|
|
|
333
377
|
SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
|
|
334
378
|
measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
|
|
335
379
|
stitches the two ends into a closed loop. The footprint follows: the head+tail (.spec, spexcode.json,
|
|
336
|
-
|
|
380
|
+
evals) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
|
|
337
381
|
Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
|
|
338
382
|
behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
339
383
|
|
|
340
384
|
── THE FOUR KINDS (all fixed) ──
|
|
341
|
-
spec data .spec/ (incl .
|
|
385
|
+
spec data .spec/ (incl .plugins/) + spexcode.json — ALWAYS tracked. Git is the database; there is
|
|
342
386
|
deliberately NO way to say "untrack the spec" in this schema.
|
|
343
387
|
machine facts spexcode.local.json, the hook shims (.claude/settings.json, .codex/hooks.json), plugin
|
|
344
388
|
bundles — NEVER tracked; always in the per-clone exclude.
|
|
345
389
|
artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
|
|
346
390
|
tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
|
|
347
|
-
run residue .worktrees/, the global store (~/.spexcode), .git/spexcode
|
|
391
|
+
run residue .worktrees/, the global store (~/.spexcode), .git/spexcode evidence — never tracked;
|
|
348
392
|
out-of-tree, or exclude-ruled where in-tree.
|
|
349
393
|
|
|
350
394
|
── A CONTRACT FILE'S RESIDENCE IS A LIVE CONTENT FACT (re-judged at every materialize) ──
|
|
@@ -360,10 +404,10 @@ behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
|
360
404
|
── THE GIT-NATIVE ANCHORS (no harness event ever triggers a materialize) ──
|
|
361
405
|
spex init / spex materialize / session-worktree creation — the explicit passes;
|
|
362
406
|
pre-commit the correctness anchor: an UNCONDITIONAL materialize (masks provably fresh at the only
|
|
363
|
-
moment history is written) + staged-index surgery — a staged
|
|
364
|
-
block is cleaned IN PLACE (partial staging survives; source is the staged
|
|
407
|
+
moment history is written) + staged-index surgery — a staged file carrying the sentinel
|
|
408
|
+
block is cleaned IN PLACE (partial staging survives; source is the staged content), a
|
|
365
409
|
HEAD-untracked generated artifact is unstaged. Repairs and proceeds, never rejects.
|
|
366
|
-
post-checkout/post-merge freshness anchors: .spec/.
|
|
410
|
+
post-checkout/post-merge freshness anchors: .spec/.plugins edits are git-transactional — they take
|
|
367
411
|
effect at the commit/checkout/merge that carries them, like any other source change.
|
|
368
412
|
An environment with no spex-planted hooks (CI, a cloud agent's fresh clone, a teammate who hasn't
|
|
369
413
|
installed) simply runs \`spex materialize\` in its setup step — there is no committed-artifact mode.
|
|
@@ -373,7 +417,7 @@ TRACK ≠ PUSH: none of this ever touches remotes; where commits GO is branch/re
|
|
|
373
417
|
materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every materialize first ERASES all landing points by
|
|
374
418
|
SpexCode's own identity stamps, then re-asserts — legacy states (a .gitignore managed block, a committed
|
|
375
419
|
artifact) are forgotten by the same pass. \`spex uninstall\` is the empty
|
|
376
|
-
materialize plus the global store: a total backout that never touches your .spec/.
|
|
420
|
+
materialize plus the global store: a total backout that never touches your .spec/.plugins or prose. Fresh
|
|
377
421
|
clones and session worktrees are self-sufficient: data by checkout, materialized artifacts by
|
|
378
422
|
re-materialize, the machine snapshot (spexcode.local.json) by copy.
|
|
379
423
|
|
|
@@ -393,7 +437,7 @@ remote, give it a different git HOME instead of untracking it. The manual recipe
|
|
|
393
437
|
(e.g. .git/spexcode/<name>.git) — the dir then holds only a one-line .git pointer file, so the
|
|
394
438
|
spec loader never walks an object store;
|
|
395
439
|
4. commit the node's changes through that inner repo.
|
|
396
|
-
The effect, honestly: filesystem-derived surfaces see the node (
|
|
440
|
+
The effect, honestly: filesystem-derived surfaces see the node (graph, search, lint); git-derived
|
|
397
441
|
views are blind to it (version count, the history tab, drift), and a dispatched worker's worktree
|
|
398
442
|
checkout does not contain it. Those gaps are what the pending spec-local design (a first-class
|
|
399
443
|
private overlay root) closes — not built yet. Cautions: \`git clean -fdx\` in the outer repo deletes
|
|
@@ -407,7 +451,7 @@ not a flag flip.
|
|
|
407
451
|
elsewhere cannot be recalled.
|
|
408
452
|
back out entirely \`spex uninstall\` (add --hooks to also remove the spexcode git hooks).`
|
|
409
453
|
|
|
410
|
-
const TOPICS: Record<string, string> = { spec: SPEC,
|
|
454
|
+
const TOPICS: Record<string, string> = { spec: SPEC, eval: EVAL, settings: SETTINGS, footprint: FOOTPRINT }
|
|
411
455
|
|
|
412
456
|
// every guide page ends by naming the OTHER help layer, so a reader never dead-ends here: guide is
|
|
413
457
|
// the skill layer (workflows · formats · settings); command usage lives in help.ts's two layers.
|
|
@@ -30,14 +30,14 @@ export function resolveHarnessTargets(raw: unknown): HarnessTarget[] {
|
|
|
30
30
|
for (const m of raw) {
|
|
31
31
|
if (typeof m === 'string') {
|
|
32
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
|
|
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 reads a different plugins dir.`)
|
|
34
34
|
if (!KNOWN.includes(m))
|
|
35
35
|
throw new Error(`spexcode.json "harnesses": unknown harness id "${m}" — known native ids are ${KNOWN.join(', ')}, or use {"plugin":"<folder>"}.`)
|
|
36
36
|
targets.push({ kind: 'native', id: m as HarnessId })
|
|
37
37
|
} else if (m && typeof m === 'object' && !Array.isArray(m) && 'plugin' in m) {
|
|
38
38
|
const folder = (m as { plugin?: unknown }).plugin
|
|
39
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
|
|
40
|
+
throw new Error(`spexcode.json "harnesses": a {"plugin":…} target needs a NON-EMPTY folder string (e.g. {"plugin":".zcode"}) — each host agent reads a different plugins dir, so the folder must be explicit.`)
|
|
41
41
|
targets.push({ kind: 'plugin', folder: folder.trim() })
|
|
42
42
|
} else {
|
|
43
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)}.`)
|
package/spec-cli/src/harness.ts
CHANGED
|
@@ -25,10 +25,17 @@ import { git } from './git.js'
|
|
|
25
25
|
export type HarnessId = 'claude' | 'codex'
|
|
26
26
|
export type HarnessLivenessRecord = { session: string; harnessSessionId?: string | null }
|
|
27
27
|
// the per-pane runtime probe the caller snapshots ONCE for the whole session list and hands liveness():
|
|
28
|
-
// the pane's root pid (tmux `#{pane_pid}`)
|
|
29
|
-
//
|
|
28
|
+
// the pane's root pid (tmux `#{pane_pid}`), the hot-tier `pidAlive` verdict, and — ONLY on the legacy path —
|
|
29
|
+
// one whole-box pid→(ppid, comm) table (a single `ps` spawn).
|
|
30
|
+
// `pidAlive` = the hot registry's verdict for THIS session's launch-registered `agent.pid`: true = the pid
|
|
31
|
+
// answers kill-0 (alive), false = proven dead (ESRCH, permanently latched per pid-reuse guard), undefined =
|
|
32
|
+
// NO agent.pid file (a pre-registration/old session). codex reads this as its liveness truth when present
|
|
33
|
+
// and falls back to `procs` (the whole-box tree walk) only when it is undefined; claude ignores it (its
|
|
34
|
+
// truth is the rendezvous socket).
|
|
35
|
+
// `procs` is gathered (the single `ps` spawn) ONLY when a pid-less codex session still needs the legacy
|
|
36
|
+
// tree-walk, so a box with no codex — or all pid-registered launches — never pays for it.
|
|
30
37
|
export type ProcTable = Map<number, { ppid: number; comm: string }>
|
|
31
|
-
export type PaneProbe = { panePid?: number; procs?: ProcTable }
|
|
38
|
+
export type PaneProbe = { panePid?: number; procs?: ProcTable; pidAlive?: boolean }
|
|
32
39
|
|
|
33
40
|
export interface Harness {
|
|
34
41
|
readonly id: HarnessId
|
|
@@ -116,11 +123,13 @@ export interface Harness {
|
|
|
116
123
|
// live listener (the caller probes all windowed sessions once per snapshot). The adapter adds only its own
|
|
117
124
|
// channel check. claude: online iff the window is up AND its reclaude rendezvous socket has a live LISTENER
|
|
118
125
|
// (`socketLive` — a connect that a live claude accepts and a stale socket FILE refuses; claude IGNORES the
|
|
119
|
-
// pane probe). codex: online iff the window is up AND
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
126
|
+
// pane probe). codex: online iff the window is up AND the launch-registered `agent.pid` is alive
|
|
127
|
+
// (`pane.pidAlive`, the hot-tier kill-0 verdict — zero ps scan); a pre-registration session with no agent.pid
|
|
128
|
+
// (`pidAlive` undefined) falls back to the LEGACY whole-box tree walk — a codex-ish process (`codex` by any
|
|
129
|
+
// name, or the `node` its CLI runs under) live in the pane pid's DESCENDANT tree, NOT the pane's foreground
|
|
130
|
+
// command name (that is `bash`, the launch wrapper, even while the TUI renders — field-confirmed), and NOT the
|
|
131
|
+
// SHARED per-project app-server socket (it stays bound after a failed `--remote resume` dropped the pane back
|
|
132
|
+
// to the shell). A missing probe (tmux/ps couldn't report) is not-live. The 'starting' boot
|
|
124
133
|
// grace lives in the caller (sessions.ts liveness), so a still-booting pane reads starting, not offline.
|
|
125
134
|
liveness(rec: HarnessLivenessRecord, tmuxAlive: boolean, runtimeDir?: string, pane?: PaneProbe, socketLive?: boolean): 'online' | 'offline'
|
|
126
135
|
// deliver a follow-up prompt to a LIVE session and report whether it landed. claude: through the rendezvous
|
|
@@ -1042,11 +1051,18 @@ export const codexHarness: Harness = {
|
|
|
1042
1051
|
removeTrust: (proj) => removeCodexTrust(mainCheckout(proj)),
|
|
1043
1052
|
clean(proj, arts) { cleanHarness(this, proj, arts) },
|
|
1044
1053
|
slashCommands: codexSlashCommands,
|
|
1045
|
-
// online iff the tmux window is up AND
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1054
|
+
// online iff the tmux window is up AND the agent is live. PRIMARY: the launch-registered `agent.pid` hot-tier
|
|
1055
|
+
// verdict (`pidAlive`) — a 100ms syscall (kill-0), no ps scan. LEGACY: a pre-registration session has no
|
|
1056
|
+
// agent.pid (`pidAlive` undefined) → fall back to the whole-box ps DESCENDANT-tree walk (paneTreeRunsCodex):
|
|
1057
|
+
// a codex-ish process live below the pane pid, NOT the pane's foreground command (that is `bash`, the launch
|
|
1058
|
+
// wrapper, even while the TUI renders — the field-confirmed false-OFFLINE) and NOT the app-server socket
|
|
1059
|
+
// (SHARED per-project, it survives a failed `--remote resume` — the earlier false-ONLINE). The legacy path
|
|
1060
|
+
// self-extinguishes as pre-registration sessions close.
|
|
1061
|
+
liveness: (_rec, tmuxAlive, _runtimeDir, pane) => {
|
|
1062
|
+
if (!tmuxAlive) return 'offline'
|
|
1063
|
+
if (pane?.pidAlive !== undefined) return pane.pidAlive ? 'online' : 'offline'
|
|
1064
|
+
return paneTreeRunsCodex(pane) ? 'online' : 'offline'
|
|
1065
|
+
},
|
|
1050
1066
|
deliver: (rec, text) => deliverViaCodexAppServer(rec, text),
|
|
1051
1067
|
// owned thread id → `--resume <id>` MARKER the codex launch script reads to resume that thread DIRECTLY (NOT
|
|
1052
1068
|
// a tail handed to a bare `codex` — the script's final `codex … resume "$tid"` performs codex's own resume on
|
|
@@ -1088,7 +1104,7 @@ export function launcherList(root = mainCheckout()): Launcher[] {
|
|
|
1088
1104
|
export const MISSING_DEFAULT_LAUNCHER_ERROR =
|
|
1089
1105
|
'sessions.defaultLauncher is required for a launch without --launcher; set it in spexcode.json or spexcode.local.json (for example {"sessions":{"defaultLauncher":"claude"}})'
|
|
1090
1106
|
|
|
1091
|
-
// the configured default launcher NAME ([[launcher-select]]) — the profile `spex new`/a dropdown pick with no
|
|
1107
|
+
// the configured default launcher NAME ([[launcher-select]]) — the profile `spex session new`/a dropdown pick with no
|
|
1092
1108
|
// explicit choice resolves. Missing config is a fail-loud setup error, never an implicit fallthrough to a
|
|
1093
1109
|
// `claude` launcher (which `spex init` seeds by name, so a default can point at it explicitly).
|
|
1094
1110
|
export function defaultLauncher(root = mainCheckout()): string {
|