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.
Files changed (101) 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/anchors.ts +300 -0
  7. package/spec-cli/src/attach.ts +2 -2
  8. package/spec-cli/src/cli.ts +691 -536
  9. package/spec-cli/src/client.ts +31 -30
  10. package/spec-cli/src/contract-filter.ts +1 -1
  11. package/spec-cli/src/doctor.ts +40 -13
  12. package/spec-cli/src/gateway.ts +11 -7
  13. package/spec-cli/src/git.ts +23 -15
  14. package/spec-cli/src/{board.ts → graph.ts} +44 -14
  15. package/spec-cli/src/{boardCache.ts → graphCache.ts} +41 -21
  16. package/spec-cli/src/{boardDelta.ts → graphDelta.ts} +1 -1
  17. package/spec-cli/src/graphStream.ts +288 -0
  18. package/spec-cli/src/guide.ts +142 -98
  19. package/spec-cli/src/harness-select.ts +2 -2
  20. package/spec-cli/src/harness.ts +30 -14
  21. package/spec-cli/src/help.ts +300 -385
  22. package/spec-cli/src/hooks.ts +1 -1
  23. package/spec-cli/src/index.ts +144 -103
  24. package/spec-cli/src/init.ts +14 -13
  25. package/spec-cli/src/issues.ts +89 -33
  26. package/spec-cli/src/layout.ts +5 -5
  27. package/spec-cli/src/lint.ts +128 -30
  28. package/spec-cli/src/localIssues.ts +61 -60
  29. package/spec-cli/src/materialize.ts +1 -1
  30. package/spec-cli/src/mentions.ts +15 -15
  31. package/spec-cli/src/migrate-table.ts +398 -0
  32. package/spec-cli/src/migrate.ts +386 -0
  33. package/spec-cli/src/ranker.ts +30 -4
  34. package/spec-cli/src/reaper.ts +117 -0
  35. package/spec-cli/src/search.bench.mjs +12 -12
  36. package/spec-cli/src/search.ts +1 -1
  37. package/spec-cli/src/sessions.ts +244 -138
  38. package/spec-cli/src/specs.ts +40 -25
  39. package/spec-cli/src/supervise.ts +2 -2
  40. package/spec-cli/src/tree.ts +5 -5
  41. package/spec-cli/src/uninstall.ts +4 -4
  42. package/spec-cli/templates/hooks/post-checkout +1 -1
  43. package/spec-cli/templates/hooks/post-merge +3 -3
  44. package/spec-cli/templates/hooks/pre-commit +9 -9
  45. package/spec-cli/templates/hooks/prepare-commit-msg +1 -1
  46. package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/idle.sh +2 -2
  47. package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/mark-active.sh +7 -0
  48. package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/spec.md +2 -0
  49. package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/fail.sh +1 -1
  50. package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec-first.sh +1 -1
  51. package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec-of-file.sh +2 -2
  52. package/spec-cli/templates/spec/project/.plugins/core/spec.md +21 -0
  53. package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/stop-gate.sh +21 -21
  54. package/spec-cli/templates/spec/project/{.config → .plugins}/distill/spec.md +2 -2
  55. package/spec-cli/templates/spec/project/{.config → .plugins}/extract/spec.md +5 -5
  56. package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/forge-link/spec.md +1 -1
  57. package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/reproduce-before-fix/spec.md +3 -3
  58. package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
  59. package/spec-cli/templates/spec/project/{.config → .plugins}/regroup/spec.md +2 -2
  60. package/spec-cli/templates/spec/project/.plugins/spec.md +18 -0
  61. package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +8 -0
  62. package/spec-cli/templates/spec/project/{.config → .plugins}/tidy/spec.md +1 -1
  63. package/spec-cli/templates/spec/project/spec.md +1 -1
  64. package/spec-dashboard/dist/assets/{Dashboard-P0B9ukSG.js → Dashboard-CTcH2eW9.js} +9 -9
  65. package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +2 -0
  66. package/spec-dashboard/dist/assets/{FoldToggle-BuQ0lokE.js → FoldToggle-CVFbBpyW.js} +1 -1
  67. package/spec-dashboard/dist/assets/{IssuesPage-H-D8aHEl.js → IssuesPage-kULjonqj.js} +1 -1
  68. package/spec-dashboard/dist/assets/{MobileApp-oZXIeCPb.js → MobileApp-B0ZJju8K.js} +1 -1
  69. package/spec-dashboard/dist/assets/{SessionInterface-Blr_MEdU.js → SessionInterface-BRKJqU2U.js} +3 -3
  70. package/spec-dashboard/dist/assets/{SessionWindow-LcCzBMU7.js → SessionWindow-CDhEL7wO.js} +7 -7
  71. package/spec-dashboard/dist/assets/{Settings-_yOye-In.js → Settings-BL6FV_8S.js} +1 -1
  72. package/spec-dashboard/dist/assets/{index-uGs9v_9o.css → index-DmQsNYKK.css} +1 -1
  73. package/spec-dashboard/dist/assets/{index-BhIslAau.js → index-DulGPk6A.js} +8 -8
  74. package/spec-dashboard/dist/index.html +2 -2
  75. package/{spec-yatsu → spec-eval}/src/cache.ts +8 -5
  76. package/{spec-yatsu → spec-eval}/src/cli.ts +212 -96
  77. package/{spec-yatsu → spec-eval}/src/evaltab.ts +35 -27
  78. package/{spec-yatsu → spec-eval}/src/filing.ts +7 -5
  79. package/{spec-yatsu → spec-eval}/src/freshness.ts +44 -22
  80. package/spec-eval/src/humanok.ts +43 -0
  81. package/{spec-yatsu → spec-eval}/src/scenariofresh.ts +22 -17
  82. package/{spec-yatsu/src/yatsu.ts → spec-eval/src/scenarios.ts} +41 -26
  83. package/{spec-yatsu/src/proof.ts → spec-eval/src/sessioneval.ts} +59 -59
  84. package/{spec-yatsu → spec-eval}/src/sidecar.ts +42 -10
  85. package/{spec-yatsu → spec-eval}/src/timeline.ts +1 -1
  86. package/spec-forge/src/__fixtures__/github-forge.json +9 -9
  87. package/spec-forge/src/cli.ts +14 -13
  88. package/spec-forge/src/{needs-yatsu-eval.ts → needs-eval.ts} +6 -6
  89. package/spec-cli/src/boardStream.ts +0 -179
  90. package/spec-cli/templates/presets/careful/.config/clarify-before-code/spec.md +0 -11
  91. package/spec-cli/templates/spec/project/.config/core/spec.md +0 -17
  92. package/spec-cli/templates/spec/project/.config/spec.md +0 -15
  93. package/spec-cli/templates/spec/project/.config/supervisor/spec.md +0 -8
  94. package/spec-dashboard/dist/assets/EvalsPage-BrvAGyc4.js +0 -2
  95. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/spec.md +0 -0
  96. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/spec.md +0 -0
  97. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec.md +0 -0
  98. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec.md +0 -0
  99. /package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/spec.md +0 -0
  100. /package/spec-cli/templates/spec/project/{.config → .plugins}/distill/digest.mjs +0 -0
  101. /package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/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,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 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.
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 yatsu, nothing to ack); it is the many-to-many net that claims the files
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 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.
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]] (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
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 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.
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 — yatsu never executes it.
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 \`yatsu-owners\` (split it). Each path must exist (a ghost → \`yatsu-schema\`).
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
- A yatsu.md OWNS nothing — only its scenarios govern and relate (see governed-related).
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 yatsu scan\` reports it as \`yatsu-schema\`, and the
127
- pre-commit \`yatsu check-staged\` BLOCKS the 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 proof easy.
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 yatsu eval <node> --scenario <name> (--pass | --fail) [--note <text>]
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
- an action trace \`index\` (the set is OPEN — an unknown axis just renders as a bare number).
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 board" },
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 JSON
161
- \`--export-json\`, an API payload, a metrics dump) is recognized BY CONTENT and kept as
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: a reading's \`codeSha\` is HEAD at filing time, and a git sha names only a COMMIT — an
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 reading (its sha lacks the change it measured) and it goes stale the moment you commit.
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 yatsu retract <node> [--scenario <name>] [--last | --ts <iso>] [--note <why>]
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 reading everywhere: the previous reading becomes
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
- 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)
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
- \`spex config set\` — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
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 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
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. If the paths are host-specific, put them in
273
- spexcode.local.json.
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/rawkey/reopen/exit) refuses
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 proof of intent). Reads point anywhere.
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; the CLI toggle is \`spex issues on|off\`.
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, 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).
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
- lint.scenarioTags the closed vocabulary a yatsu scenario's tags: must draw from (default
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 .config tier \`spex init\` seeds (default
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
- readings) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
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 .config/) + spexcode.json — ALWAYS tracked. Git is the database; there is
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 blobs — never tracked;
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 blob carrying the sentinel
364
- block is cleaned IN PLACE (partial staging survives; source is the staged blob), a
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/.config edits are git-transactional — they take
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/.config or prose. Fresh
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 (board, search, lint); git-derived
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, yatsu: YATSU, config: CONFIG, footprint: FOOTPRINT }
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 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 {