spexcode 0.2.7 → 0.3.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.
Files changed (97) hide show
  1. package/README.md +56 -44
  2. package/package.json +3 -3
  3. package/spec-cli/bin/spex.mjs +2 -2
  4. package/spec-cli/hooks/dispatch.sh +1 -1
  5. package/spec-cli/hooks/harness.sh +26 -6
  6. package/spec-cli/src/attach.ts +2 -2
  7. package/spec-cli/src/cli.ts +695 -536
  8. package/spec-cli/src/client.ts +31 -30
  9. package/spec-cli/src/contract-filter.ts +1 -1
  10. package/spec-cli/src/doctor.ts +40 -13
  11. package/spec-cli/src/gateway.ts +11 -7
  12. package/spec-cli/src/git.ts +2 -2
  13. package/spec-cli/src/{board.ts → graph.ts} +44 -14
  14. package/spec-cli/src/{boardCache.ts → graphCache.ts} +41 -21
  15. package/spec-cli/src/{boardDelta.ts → graphDelta.ts} +1 -1
  16. package/spec-cli/src/graphStream.ts +288 -0
  17. package/spec-cli/src/guide.ts +123 -96
  18. package/spec-cli/src/harness-select.ts +2 -2
  19. package/spec-cli/src/harness.ts +30 -14
  20. package/spec-cli/src/help.ts +289 -384
  21. package/spec-cli/src/hooks.ts +1 -1
  22. package/spec-cli/src/index.ts +130 -103
  23. package/spec-cli/src/init.ts +9 -9
  24. package/spec-cli/src/issues.ts +89 -33
  25. package/spec-cli/src/layout.ts +5 -5
  26. package/spec-cli/src/lint.ts +73 -8
  27. package/spec-cli/src/localIssues.ts +42 -60
  28. package/spec-cli/src/materialize.ts +1 -1
  29. package/spec-cli/src/mentions.ts +15 -15
  30. package/spec-cli/src/migrate-table.ts +397 -0
  31. package/spec-cli/src/migrate.ts +386 -0
  32. package/spec-cli/src/ranker.ts +30 -4
  33. package/spec-cli/src/reaper.ts +117 -0
  34. package/spec-cli/src/search.bench.mjs +10 -10
  35. package/spec-cli/src/search.ts +1 -1
  36. package/spec-cli/src/sessions.ts +244 -138
  37. package/spec-cli/src/specs.ts +25 -15
  38. package/spec-cli/src/supervise.ts +2 -2
  39. package/spec-cli/src/tree.ts +5 -5
  40. package/spec-cli/src/uninstall.ts +4 -4
  41. package/spec-cli/templates/hooks/post-checkout +1 -1
  42. package/spec-cli/templates/hooks/post-merge +3 -3
  43. package/spec-cli/templates/hooks/pre-commit +9 -9
  44. package/spec-cli/templates/hooks/prepare-commit-msg +1 -1
  45. package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/idle.sh +2 -2
  46. package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/mark-active.sh +7 -0
  47. package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/spec.md +2 -0
  48. package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/fail.sh +1 -1
  49. package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec-first.sh +1 -1
  50. package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec-of-file.sh +2 -2
  51. package/spec-cli/templates/spec/project/.plugins/core/spec.md +21 -0
  52. package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/stop-gate.sh +21 -21
  53. package/spec-cli/templates/spec/project/{.config → .plugins}/distill/spec.md +2 -2
  54. package/spec-cli/templates/spec/project/{.config → .plugins}/extract/spec.md +4 -4
  55. package/spec-cli/templates/spec/project/{.config → .plugins}/forge-link/spec.md +1 -1
  56. package/spec-cli/templates/spec/project/{.config → .plugins}/regroup/spec.md +2 -2
  57. package/spec-cli/templates/spec/project/{.config → .plugins}/reproduce-before-fix/spec.md +3 -3
  58. package/spec-cli/templates/spec/project/{.config → .plugins}/spec.md +4 -4
  59. package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +8 -0
  60. package/spec-cli/templates/spec/project/{.config → .plugins}/tidy/spec.md +1 -1
  61. package/spec-cli/templates/spec/project/spec.md +1 -1
  62. package/spec-dashboard/dist/assets/{Dashboard-P0B9ukSG.js → Dashboard-C7Bzsv86.js} +9 -9
  63. package/spec-dashboard/dist/assets/EvalsPage-DKZZIdHq.js +2 -0
  64. package/spec-dashboard/dist/assets/{FoldToggle-BuQ0lokE.js → FoldToggle-D5iB4Ac2.js} +1 -1
  65. package/spec-dashboard/dist/assets/{IssuesPage-H-D8aHEl.js → IssuesPage-CMFTsQhg.js} +1 -1
  66. package/spec-dashboard/dist/assets/{MobileApp-oZXIeCPb.js → MobileApp-DwuTKgdP.js} +1 -1
  67. package/spec-dashboard/dist/assets/{SessionInterface-Blr_MEdU.js → SessionInterface-CBS5_cmK.js} +3 -3
  68. package/spec-dashboard/dist/assets/{SessionWindow-LcCzBMU7.js → SessionWindow-CqAnjWfI.js} +2 -2
  69. package/spec-dashboard/dist/assets/{Settings-_yOye-In.js → Settings-BW5f0OaW.js} +1 -1
  70. package/spec-dashboard/dist/assets/{index-uGs9v_9o.css → index-Cc26X4ce.css} +1 -1
  71. package/spec-dashboard/dist/assets/{index-BhIslAau.js → index-Ce0wDyQS.js} +8 -8
  72. package/spec-dashboard/dist/index.html +2 -2
  73. package/{spec-yatsu → spec-eval}/src/cache.ts +8 -5
  74. package/{spec-yatsu → spec-eval}/src/cli.ts +164 -95
  75. package/{spec-yatsu → spec-eval}/src/evaltab.ts +24 -24
  76. package/{spec-yatsu → spec-eval}/src/filing.ts +7 -5
  77. package/{spec-yatsu → spec-eval}/src/freshness.ts +50 -24
  78. package/{spec-yatsu → spec-eval}/src/scenariofresh.ts +22 -17
  79. package/{spec-yatsu/src/yatsu.ts → spec-eval/src/scenarios.ts} +41 -26
  80. package/{spec-yatsu/src/proof.ts → spec-eval/src/sessioneval.ts} +59 -59
  81. package/{spec-yatsu → spec-eval}/src/sidecar.ts +7 -1
  82. package/{spec-yatsu → spec-eval}/src/timeline.ts +1 -1
  83. package/spec-forge/src/__fixtures__/github-forge.json +9 -9
  84. package/spec-forge/src/cli.ts +14 -13
  85. package/spec-forge/src/{needs-yatsu-eval.ts → needs-eval.ts} +6 -6
  86. package/spec-cli/src/boardStream.ts +0 -179
  87. package/spec-cli/templates/spec/project/.config/core/spec.md +0 -17
  88. package/spec-cli/templates/spec/project/.config/supervisor/spec.md +0 -8
  89. package/spec-dashboard/dist/assets/EvalsPage-BrvAGyc4.js +0 -2
  90. /package/spec-cli/templates/presets/careful/{.config → .plugins}/clarify-before-code/spec.md +0 -0
  91. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/spec.md +0 -0
  92. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/spec.md +0 -0
  93. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec.md +0 -0
  94. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec.md +0 -0
  95. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/spec.md +0 -0
  96. /package/spec-cli/templates/spec/project/{.config → .plugins}/distill/digest.mjs +0 -0
  97. /package/spec-cli/templates/spec/project/{.config → .plugins}/memory-hygiene/spec.md +0 -0
@@ -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 dashboard\` needs a manual dashboard build, or use the dev server.)
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 (PORT=<n> for another endpoint)
22
- Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two PORTs.
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 board for every project, pointed per project
25
- spex dashboard # serves the bundled board on :5173, proxying /api
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 board is a viewer — which backend it proxies is the only "which project" knob.
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 yatsu the yatsu.md format (scenario schema + how loss is measured and filed)
41
- spex guide config the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, lint
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,30 @@ 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 board\`, \`ack\`, and a node/<id> branch use. A spec states a node's PRESENT
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 board.
57
- hue board colour, 0–360. Default 210.
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: files this node GOVERNS (is source of truth for) — ideally ONE, a YAML list of repo-relative
60
- paths/dirs/*-globs. Drives drift + yatsu. Many nodes MAY govern the same file (ordinary
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.
63
65
  related: files this node REFERENCES but does not own — a YAML list, same path forms. Carries coverage
64
- (never drift, never yatsu, nothing to ack); it is the many-to-many net that claims the files
66
+ (never drift, never eval freshness, nothing to ack); it is the many-to-many net that claims the files
65
67
  govern doesn't. Every listed path must exist (lint integrity error otherwise).
66
- surface config/.config nodes only: system (folded into every agent's prompt) | command (a /command) |
67
- hook (a lifecycle hook handler a co-located script the dispatcher runs on the harness events
68
- in events:, ordered by order:, blocking when block: true). hook nodes may nest under a grouping
69
- plugin (e.g. .config/core/<id>); surface is a field, discovered recursively.
68
+ surface plugin-system/.plugins nodes only: one or MORE of system (folded into every agent's prompt) |
69
+ command (a /command) | skill (an on-demand SKILL.md the harness loads when a task matches the
70
+ node's desc) | agent (a spawnable sub-agent definition; its \`tools:\` list is the spawned
71
+ agent's tool allowlist) | hook (a lifecycle hook handler — a co-located script the dispatcher
72
+ runs on the harness events in events:, ordered by order:, blocking when block: true).
73
+ Comma-list several to plug into each. hook nodes may nest under a grouping
74
+ plugin (e.g. .plugins/core/<id>); surface is a field, discovered recursively.
70
75
  events hook surface only: harness lifecycle events this node binds (YAML list — PreToolUse, Stop, …).
71
76
  order hook surface only: integer; the dispatcher runs same-event hooks low to high.
72
77
  block hook surface only: true if the hook may block its event (honored only on block-capable events).
@@ -75,31 +80,41 @@ BODY (Markdown after the frontmatter): the contract — intent, invariants, outw
75
80
  code does it. Two optional level-2 headings split ground truth from detail:
76
81
  ## raw source human-authored, rarely-changed intent — the loss function's target.
77
82
  ## 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]] (a dangling link is
79
- fine it marks a node worth writing).
80
-
81
- WHAT lint CHECKS (spex lint; the pre-commit hook gates on errors):
82
- integrity (error) every code: path exists.
83
- living (error) no "## vN" changelog headings — the body is current-state.
84
- altitude (warn) the body stays high-altitude: line/char budgets (~50 lines / 4200 chars), low
85
- code-identifier density, no step-by-step phrasing. Over budget = rewrite higher.
86
- coverage (warn) every source file is claimed by ≥1 node via code: OR related: (related is the net).
87
- drift (warn) a governed file has commits newer than the node's spec version it may be stale.
88
- Remedy: edit the spec to the new intent (re-versions the node), OR \`spex ack <node>
89
- --reason "…"\` when only mechanics changed and the contract still holds.
90
- owners (warn) a file governed by > maxOwners nodes (default 3) does too much — SPLIT it so each
91
- governor owns its own module (or merge the nodes, or give it one foundation owner).
92
-
93
- LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex lint\` must reach 0 errors
94
- before merge. \`spex init\` seeds the first tree; \`spex guide yatsu\` covers the sibling loss-signal file.`
95
-
96
- const YATSU = `spex guide yatsu the yatsu.md file format
97
-
98
- A yatsu.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss the gap between live
83
+ Bodies without those headings are read whole. Link sibling nodes with [[node-id]] every link must name
84
+ a REAL node (lint's mention rule; backtick a placeholder like \`[[node]]\` so it reads as sample text).
85
+
86
+ WHAT lint CHECKS (spex spec lint; the pre-commit hook gates on errors):
87
+ integrity (error) every code:/related: path exists.
88
+ one-govern (error) a node governs (code:) at most ONE file keep the true subject, move the rest
89
+ to related:.
90
+ living (error) no "## vN" changelog headings the body is current-state.
91
+ id-format (error) each id char is ascii [a-z0-9-] or a non-ascii unicode letter/number (CJK ok; one
92
+ optional leading dot; no space / '/' / '_' / uppercase Latin), and its leaf dir name
93
+ is unique tree-wide.
94
+ mention (error) every [[node-id]] in prose names a real node (fenced/backticked samples exempt).
95
+ altitude (warn) the body stays high-altitude: line/char budgets (~50 lines / 4200 chars), low
96
+ code-identifier density, no step-by-step phrasing. Over budget = rewrite higher.
97
+ breadth (warn) a node with >= maxChildren direct children (default 8) — altitude's structural
98
+ twin; is an intermediate grouping layer missing?
99
+ coverage (warn) every source file is claimed by ≥1 node via code: OR related: (related is the net).
100
+ drift (warn) a governed file has commits newer than the node's spec version — it may be stale.
101
+ Remedy: edit the spec to the new intent (re-versions the node), OR \`spex spec ack <node>
102
+ --reason "…"\` when only mechanics changed and the contract still holds.
103
+ related-drift (warn) a related: file moved ahead of the node a soft nudge, one summary line, never blocks.
104
+ owners (warn) a file governed by > maxOwners nodes (default 3) does too much — SPLIT it so each
105
+ governor owns its own module (or merge the nodes, or give it one foundation owner).
106
+ confusable-id (warn) two leaf ids one edit apart read as the same word — rename one to read apart.
107
+
108
+ LIFECYCLE: author each node on a node/<id> branch, one node per commit; \`spex spec lint\` must reach 0 errors
109
+ before merge. \`spex init\` seeds the first tree; \`spex guide eval\` covers the sibling eval.md, the measurement file.`
110
+
111
+ const EVAL = `spex guide eval — the eval.md file format
112
+
113
+ An eval.md sits BESIDE a node's spec.md and says how to MEASURE the node's loss — the gap between live
99
114
  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 yatsu.md is
101
- a blind spot: \`spex yatsu scan\` flags it \`yatsu-uncovered\`. yatsu defines no DSL and RUNS NOTHING — the
102
- agent measures; yatsu keeps score.
115
+ \`lint.sourceExtensions\` — default .ts/.tsx/.js/.jsx, set it for a Rust/Go/Python tree) with no eval.md is
116
+ a blind spot: \`spex eval lint\` flags it \`eval-coverage\`. The eval system defines no DSL and RUNS
117
+ NOTHING — the agent measures; eval keeps score.
103
118
 
104
119
  FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each scenario:
105
120
  name REQUIRED. Unique within the file — it keys the sidecar and \`--scenario <name>\`.
@@ -111,28 +126,29 @@ FRONTMATTER: a \`scenarios:\` list (a YAML block sequence of mappings). Each sce
111
126
  use an existing one, or add it to the library to mint it. Tags classify a scenario (surface,
112
127
  device) so it can be filtered and, later, routed to the right driver.
113
128
  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 — yatsu never executes it.
129
+ the agent MAY run by hand. Not a driver — eval never executes it.
115
130
  code optional. The file THIS scenario GOVERNS, ideally one (a comma list / flow list \`[a, b]\` is
116
131
  allowed) — its own slice of the code freshness axis, so scenarios on one node go stale
117
132
  independently. Absent → it inherits the node's \`code:\` list. A file governed by > maxOwners
118
- scenarios warns \`yatsu-owners\` (split it). Each path must exist (a ghost → \`yatsu-schema\`).
133
+ scenarios warns \`eval-owners\` (split it). Each path must exist (a ghost → \`eval-schema\`).
119
134
  related optional. Files this scenario REFERENCES but does not govern — same path forms. They do NOT
120
135
  stale it (the freshness mirror of a spec node's govern/related). Each path must exist.
121
136
  Multi-line prose uses YAML block scalars: \`|\` keeps newlines, \`>\` folds wrapped lines to spaces.
122
- A yatsu.md OWNS nothing — only its scenarios govern and relate (see governed-related).
137
+ An eval.md OWNS nothing — only its scenarios govern and relate (see governed-related).
123
138
 
124
139
  THE SCHEMA IS ENFORCED (closed field set, four required fields, unique names, tags within the library). A
125
140
  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 yatsu scan\` reports it as \`yatsu-schema\`, and the
127
- pre-commit \`yatsu check-staged\` BLOCKS the commit.
141
+ tag, or no scenarios at all is rejected LOUD: \`spex eval lint\` reports it as \`eval-schema\`, and the
142
+ pre-commit \`internal check-staged\` BLOCKS the commit.
128
143
 
129
144
  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 proof easy.
145
+ looks at / calls the real product surface, not an internal helper chosen to make the evidence easy.
131
146
 
132
147
  MEASURING AND FILING: the agent runs the scenario however it likes (a browser run, an API
133
148
  transcript, a by-hand pass), compares the result to \`expected\`, and files it:
134
- spex yatsu eval <node> --scenario <name> (--pass | --fail) [--note <text>]
135
- [--image <png> …repeatable] [--result <txt>|-] [--video <webm|mp4> [--timeline <json>]]
149
+ spex eval add <node> [--scenario <name>] (--pass | --fail) [--note <text>]
150
+ [--image <png> …repeatable] [--result <txt>|-] [--video <webm|mp4>] [--timeline <json>]
151
+ (--scenario may be omitted only when the node declares exactly one scenario.)
136
152
  The verdict is \`--pass\` or \`--fail\` (a measurement must commit to one — an unmeasured scenario is \`missing\`,
137
153
  not a hedged fail). \`--note <text>\` is an OPTIONAL one-line annotation on either (why it failed, how far a
138
154
  pass sits from ideal); it does NOT replace evidence — the image/video/transcript is the captured actual behaviour.
@@ -145,10 +161,11 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
145
161
  — never a value the agent eyeballs off the finished artefact afterwards (that's
146
162
  misaligned and dishonest, worse than none). \`--timeline <json>\` carries one; its \`axis\`
147
163
  is the evidence's: a video is \`time\` (ms), a transcript \`line\`, a still SEQUENCE \`frame\`,
148
- an action trace \`index\` (the set is OPEN — an unknown axis just renders as a bare number).
164
+ a structured data export \`index\` (record ordinals; the format's axis set is open — an
165
+ unknown axis just renders as a bare number).
149
166
  \`at\` = the position on that axis, \`step\` = a short name for that moment; copy this shape:
150
167
  { "v": 2, "axis": "time",
151
- "events": [ { "at": 0, "step": "open board" },
168
+ "events": [ { "at": 0, "step": "open graph" },
152
169
  { "at": 1200, "step": "type query" } ] }
153
170
  The run exports it: in whatever drives the evidence — Playwright, a computer-use hand, a
154
171
  CLI harness stamping line numbers — take a baseline and at EACH real step push
@@ -157,36 +174,40 @@ PICK THE EVIDENCE KIND BY WHAT THE BEHAVIOUR DOES OVER TIME:
157
174
  skip it for a short single-step artefact. (Legacy \`{ "v": 1, "events": [{ "tMs" }] }\` — the
158
175
  time axis with \`tMs\` — is still accepted, read as \`axis: "time"\`.)
159
176
  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 JSON
161
- \`--export-json\`, an API payload, a metrics dump) is recognized BY CONTENT and kept as
177
+ backend / CLI → \`--result <txt>\` (a transcript; \`-\` reads stdin). A STRUCTURED export (a tool's
178
+ \`--export-json\` dump, an API payload, a metrics dump) is recognized BY CONTENT and kept as
162
179
  \`data\` — rendered as a validatable data block, not flattened into scrolling transcript
163
180
  text; free-form output stays a transcript. You pick the flag; the KIND follows the bytes.
164
181
  The flags combine in ONE filing — several stills can ride beside the clip of the same run.
165
- ANCHOR DISCIPLINE: a reading's \`codeSha\` is HEAD at filing time, and a git sha names only a COMMIT — an
182
+ ANCHOR DISCIPLINE: an eval's \`codeSha\` is HEAD at filing time, and a git sha names only a COMMIT — an
166
183
  uncommitted change has none. So measure the tree you are about to commit, COMMIT it, then file; confidence
167
184
  is earned on the working tree, but the anchor can only land after the commit. Filing from a dirty tree
168
- mis-anchors the reading (its sha lacks the change it measured) and it goes stale the moment you commit.
185
+ mis-anchors the eval (its sha lacks the change it measured) and it goes stale the moment you commit.
169
186
 
170
187
  A botched filing (a junk e2e/smoke run, a wrong verdict) is undone through the SAME surface:
171
- spex yatsu retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
188
+ spex eval retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
172
189
  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 reading everywhere: the previous reading becomes
190
+ who/when/why); the scoreboard then drops the retracted eval everywhere: the previous eval becomes
174
191
  the latest again, or the scenario honestly returns to \`missing\`. Default target is the scenario's latest
175
- reading (\`--last\` makes that explicit; repeat to peel junk back one filing at a time); \`--ts\` pins one.
176
-
177
- THE SCOREBOARD: readings live in yatsu.evals.ndjson beside the yatsu.md — one JSON line per measurement
178
- (a second git-as-database axis). Freshness is derived live from git: a reading goes STALE when a governed
179
- code file or the scenario (the yatsu.md) moves since it was filed.
180
- spex yatsu scan [--changed] blind spots: yatsu-schema (malformed) · yatsu-drift (stale) ·
181
- yatsu-missing (never measured) · yatsu-uncovered (governed source, no yatsu.md) ·
182
- yatsu-owners (a file governed by > maxOwners scenarios — split it)
183
- spex yatsu show <node> the reading timeline (verdict · freshness · evidence), newest first
184
- spex yatsu clean GC the content-addressed evidence cache`
185
-
186
- const CONFIG = `spex guide config SpexCode's runtime settings (spexcode.json / spexcode.local.json)
192
+ eval (\`--last\` makes that explicit; repeat to peel junk back one filing at a time); \`--ts\` pins one.
193
+
194
+ THE SCOREBOARD: evals live in evals.ndjson beside the eval.md — one JSON line per measurement
195
+ (a second git-as-database axis). Freshness is derived live from git: an eval goes STALE when a governed
196
+ code file or the scenario (the eval.md) moves since it was filed.
197
+ spex eval lint [--changed] the measurement layer's findings PURE ADVISORY, always exit 0 (spec
198
+ lint's errors block commits; a measurement gap never blocks anyone):
199
+ eval-schema (malformed) · eval-drift (stale) · eval-missing (never
200
+ measured) · eval-dangling (orphaned remark track) · eval-coverage
201
+ (governed source, no eval.md spec lint's coverage, one rule per layer) ·
202
+ eval-owners (a file governed by > maxOwners scenarios — split it)
203
+ spex eval ls <node> the eval timeline (verdict · freshness · evidence), newest first
204
+ spex eval scenario ls [<node>] the declared contracts; --unmeasured = the blind-spot worklist
205
+ spex eval clean GC the content-addressed evidence cache`
206
+
207
+ const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
187
208
 
188
209
  SpexCode reads its runtime settings from TWO optional JSON files at the repo root. There is no imperative
189
- \`spex config set\` — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
210
+ settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
190
211
  PORTABILITY, and picking the right one is the whole discipline:
191
212
 
192
213
  spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy, dashboard
@@ -200,7 +221,7 @@ Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A
200
221
  icon, a lint budget, a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
201
222
  PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
202
223
  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.
224
+ \`spex session new\` or the dashboard without an explicit launcher choice.
204
225
 
205
226
  MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
206
227
  sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
@@ -218,14 +239,16 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
218
239
  ── DASHBOARD (spexcode.json — portable project identity) ──
219
240
  dashboard.title browser-tab name. Default: the repo-root basename.
220
241
  dashboard.icon browser-tab favicon: an emoji ("🔭") OR an Iconify name ("mdi:rocket-launch").
221
- dashboard.apiUrl the per-project backend the board proxies to (read frontend-side). For a SHARED
222
- install prefer the API_URL env var; apiUrl here is the default only when the board
242
+ dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
243
+ install prefer the API_URL env var; apiUrl here is the default only when the dashboard
223
244
  lives inside the project.
224
245
  Example:
225
246
  { "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
226
247
 
227
248
  ── SESSIONS / WORKERS ──
228
- sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8).
249
+ sessions.maxActive concurrency cap — max agents AUTONOMOUSLY PROGRESSING at once (default 8;
250
+ precedence: spexcode.json → SPEXCODE_MAX_ACTIVE env → default; read live, so
251
+ an edit applies without a restart).
229
252
  Counts compute slots, not total sessions: idle/asking/review/done do not
230
253
  occupy one. A policy number → committed spexcode.json; omit it to use the
231
254
  default, or tune higher/lower for the project's usual host.
@@ -265,12 +288,13 @@ in the committed spexcode.json — the merge keeps both:
265
288
  }
266
289
  }
267
290
 
268
- ── SERVE (spexcode.json — public-exposure for \`spex serve --public\`) ──
291
+ ── SERVE (spexcode.json ONLY — public-exposure for \`spex serve --public\`; this section is read straight
292
+ from spexcode.json, the local overlay is NOT consulted here) ──
269
293
  serve.public.enabled turn public mode on without the --public flag.
270
294
  serve.public.http drop TLS (the --http escape hatch) — the password then travels in cleartext.
271
295
  serve.public.tls { "cert": "<path>", "key": "<path>" } — PATHS to your own cert/key; omit for a
272
- cached self-signed default. If the paths are host-specific, put them in
273
- spexcode.local.json.
296
+ cached self-signed default. Host-specific paths can instead ride the
297
+ --tls-cert/--tls-key flags or SPEXCODE_TLS_CERT/SPEXCODE_TLS_KEY env (which win).
274
298
  The gateway password is NEVER read from these files (flag/env only), so serve.public stays committable.
275
299
 
276
300
  ── BACKEND ROUTING (not a config field — how a \`spex\` command picks its backend) ──
@@ -286,20 +310,23 @@ resolves its backend per this ladder, flag first:
286
310
  health-probes before trusting (a dead record is ignored).
287
311
  3. the other side as fallback (human with no live record → env; worker with no env → record).
288
312
  4. default http://127.0.0.1:$PORT||8787.
289
- WRITES are project-bound: every mutating verb (new/merge/send/close/rename/rawkey/reopen/exit) refuses
313
+ WRITES are project-bound: every mutating verb (new/merge/send/close/rename/resume/stop) refuses
290
314
  loudly when the resolved backend serves a DIFFERENT same-host project — an explicit --api/--port skips
291
- the guard (the flag is the proof of intent). Reads point anywhere.
315
+ the guard (the flag is the declaration of intent). Reads point anywhere.
292
316
 
293
317
  ── ISSUES (spexcode.json — portable policy) ──
294
318
  issues.enabled the issues-workflow on/off switch (default ON). OFF silences the post-merge nudge and
295
- hides the dashboard view; the CLI toggle is \`spex issues on|off\`.
319
+ hides the dashboard view. Flip it by editing the JSON there is no CLI toggle verb;
320
+ \`spex doctor\` reports the current state (and flags a legacy \`proposals.enabled\` key,
321
+ which is no longer read).
296
322
 
297
323
  ── FORGE (spexcode.json — which forge this repo's remote is; a project fact, so committed) ──
298
324
  forge.host explicit forge host id ('github' | 'gitlab' | …) overriding the automatic derivation.
299
325
  Normally OMIT it: spec-forge resolves the host from the origin remote's hostname —
300
- github.com → github, a gitlab/self-hosted remote → gitlab and only an ambiguous
301
- self-hosted domain the heuristic misreads needs the override. A resolved host with no
302
- registered driver degrades to an EMPTY forge slice (local issues still work, no error).
326
+ github.com → github, bitbucket bitbucket, any other remote → gitlab (the common
327
+ self-hosted shape) — and only a domain the heuristic misreads needs the override.
328
+ A resolved host with no registered driver degrades to an EMPTY forge slice (local
329
+ issues still work, no error).
303
330
 
304
331
  ── LINT (spexcode.json — a top-level "lint" key; budgets are portable, so committed only) ──
305
332
  lint.governedRoots dirs whose source files must each be governed by a spec (coverage).
@@ -314,13 +341,13 @@ the guard (the flag is the proof of intent). Reads point anywhere.
314
341
  lint.driftErrorThreshold commit-local gate HARD-BLOCKS a commit touching a node >= this many commits
315
342
  behind (default 3).
316
343
  lint.maxOwners warn when a file is governed by > this many nodes (default 3).
317
- lint.scenarioTags the closed vocabulary a yatsu scenario's tags: must draw from (default
344
+ lint.scenarioTags the closed vocabulary an eval scenario's tags: must draw from (default
318
345
  ["frontend-e2e","backend-api","cli","desktop","mobile"]); extend to mint a tag.
319
346
  Example — govern your own source dir and loosen the altitude budget:
320
347
  { "lint": { "governedRoots": ["src"], "altitude": { "lineBudget": 70 } } }
321
348
 
322
349
  ── OTHER (spexcode.json unless noted) ──
323
- preset the SELECTED init preset — which cumulative .config tier \`spex init\` seeds (default
350
+ preset the SELECTED init preset — which cumulative .plugins tier \`spex init\` seeds (default
324
351
  'default'; seed-time only, read by init.ts).
325
352
  harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
326
353
  { "plugin": "<folder>" } bundle. Default (omitted): all native harnesses. PERSISTENT and
@@ -333,18 +360,18 @@ const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and
333
360
  SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
334
361
  measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
335
362
  stitches the two ends into a closed loop. The footprint follows: the head+tail (.spec, spexcode.json,
336
- readings) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
363
+ evals) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
337
364
  Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
338
365
  behavior, decided per KIND (and, for a contract file, by its live CONTENT).
339
366
 
340
367
  ── THE FOUR KINDS (all fixed) ──
341
- spec data .spec/ (incl .config/) + spexcode.json — ALWAYS tracked. Git is the database; there is
368
+ spec data .spec/ (incl .plugins/) + spexcode.json — ALWAYS tracked. Git is the database; there is
342
369
  deliberately NO way to say "untrack the spec" in this schema.
343
370
  machine facts spexcode.local.json, the hook shims (.claude/settings.json, .codex/hooks.json), plugin
344
371
  bundles — NEVER tracked; always in the per-clone exclude.
345
372
  artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
346
373
  tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
347
- run residue .worktrees/, the global store (~/.spexcode), .git/spexcode blobs — never tracked;
374
+ run residue .worktrees/, the global store (~/.spexcode), .git/spexcode evidence — never tracked;
348
375
  out-of-tree, or exclude-ruled where in-tree.
349
376
 
350
377
  ── A CONTRACT FILE'S RESIDENCE IS A LIVE CONTENT FACT (re-judged at every materialize) ──
@@ -360,10 +387,10 @@ behavior, decided per KIND (and, for a contract file, by its live CONTENT).
360
387
  ── THE GIT-NATIVE ANCHORS (no harness event ever triggers a materialize) ──
361
388
  spex init / spex materialize / session-worktree creation — the explicit passes;
362
389
  pre-commit the correctness anchor: an UNCONDITIONAL materialize (masks provably fresh at the only
363
- moment history is written) + staged-index surgery — a staged blob carrying the sentinel
364
- block is cleaned IN PLACE (partial staging survives; source is the staged blob), a
390
+ moment history is written) + staged-index surgery — a staged file carrying the sentinel
391
+ block is cleaned IN PLACE (partial staging survives; source is the staged content), a
365
392
  HEAD-untracked generated artifact is unstaged. Repairs and proceeds, never rejects.
366
- post-checkout/post-merge freshness anchors: .spec/.config edits are git-transactional — they take
393
+ post-checkout/post-merge freshness anchors: .spec/.plugins edits are git-transactional — they take
367
394
  effect at the commit/checkout/merge that carries them, like any other source change.
368
395
  An environment with no spex-planted hooks (CI, a cloud agent's fresh clone, a teammate who hasn't
369
396
  installed) simply runs \`spex materialize\` in its setup step — there is no committed-artifact mode.
@@ -373,7 +400,7 @@ TRACK ≠ PUSH: none of this ever touches remotes; where commits GO is branch/re
373
400
  materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every materialize first ERASES all landing points by
374
401
  SpexCode's own identity stamps, then re-asserts — legacy states (a .gitignore managed block, a committed
375
402
  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/.config or prose. Fresh
403
+ materialize plus the global store: a total backout that never touches your .spec/.plugins or prose. Fresh
377
404
  clones and session worktrees are self-sufficient: data by checkout, materialized artifacts by
378
405
  re-materialize, the machine snapshot (spexcode.local.json) by copy.
379
406
 
@@ -393,7 +420,7 @@ remote, give it a different git HOME instead of untracking it. The manual recipe
393
420
  (e.g. .git/spexcode/<name>.git) — the dir then holds only a one-line .git pointer file, so the
394
421
  spec loader never walks an object store;
395
422
  4. commit the node's changes through that inner repo.
396
- The effect, honestly: filesystem-derived surfaces see the node (board, search, lint); git-derived
423
+ The effect, honestly: filesystem-derived surfaces see the node (graph, search, lint); git-derived
397
424
  views are blind to it (version count, the history tab, drift), and a dispatched worker's worktree
398
425
  checkout does not contain it. Those gaps are what the pending spec-local design (a first-class
399
426
  private overlay root) closes — not built yet. Cautions: \`git clean -fdx\` in the outer repo deletes
@@ -407,7 +434,7 @@ not a flag flip.
407
434
  elsewhere cannot be recalled.
408
435
  back out entirely \`spex uninstall\` (add --hooks to also remove the spexcode git hooks).`
409
436
 
410
- const TOPICS: Record<string, string> = { spec: SPEC, yatsu: YATSU, config: CONFIG, footprint: FOOTPRINT }
437
+ const TOPICS: Record<string, string> = { spec: SPEC, eval: EVAL, settings: SETTINGS, footprint: FOOTPRINT }
411
438
 
412
439
  // every guide page ends by naming the OTHER help layer, so a reader never dead-ends here: guide is
413
440
  // 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 scans a different plugins dir.`)
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 scans a different plugins dir, so the folder must be explicit.`)
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)}.`)
@@ -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}`) plus one whole-box pid→(ppid, comm) table (a single `ps` spawn).
29
- // The adapter that cares (codex) walks the pane pid's descendants in that table; claude ignores it.
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 a codex-ish process (`codex` by any
120
- // name, or the `node` its CLI runs under) is live in the pane pid's DESCENDANT tree — NOT the pane's
121
- // foreground command name (that is `bash`, the launch wrapper, even while the TUI renders field-confirmed),
122
- // and NOT the SHARED per-project app-server socket (it stays bound after a failed `--remote resume` dropped
123
- // the pane back to the shell). A missing probe (tmux/ps couldn't report) is not-live. The 'starting' boot
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 a codex-ish process is live in the pane pid's DESCENDANT tree — see
1046
- // paneTreeRunsCodex. NOT the pane's foreground command (that is `bash`, the launch wrapper, even while the
1047
- // TUI renders the field-confirmed false-OFFLINE) and NOT the app-server socket (SHARED per-project, it
1048
- // survives a failed `--remote resume` the earlier false-ONLINE).
1049
- liveness: (_rec, tmuxAlive, _runtimeDir, pane) => (tmuxAlive && paneTreeRunsCodex(pane) ? 'online' : 'offline'),
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 {