spexcode 0.2.8 → 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 +44 -22
  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
@@ -97,8 +97,8 @@ function walk(dir: string, parent: string | null, acc: Raw[]) {
97
97
  // single token — never a '/'-joined path, which would break every `:id` route and fetch that treats an id
98
98
  // as one path segment. So the disambiguation separator is '_': like '/' it never occurs inside a dir
99
99
  // basename (so the join stays unambiguous), but unlike '/' it is a URL/wikilink/DOM-safe unreserved char,
100
- // so a collision-qualified id (e.g. `.config_spec-scout`) stays a single token everywhere it is resolved.
101
- // Exported as the ONE mint every id producer shares: spec-yatsu mints its node ids through this same
100
+ // so a collision-qualified id (e.g. `.plugins_spec-scout`) stays a single token everywhere it is resolved.
101
+ // Exported as the ONE mint every id producer shares: spec-eval mints its node ids through this same
102
102
  // function over this same universe (every spec node), so a colliding leaf carries one canonical id
103
103
  // system-wide instead of a second, diverging bare-leaf scheme.
104
104
  export function mintIds(segs: string[][]): string[] {
@@ -135,7 +135,7 @@ function raws(): Raw[] {
135
135
  return acc
136
136
  }
137
137
 
138
- // async twin of walk/raws for the HOT board build ([[board-cache]]): reading each spec.md through
138
+ // async twin of walk/raws for the HOT board build ([[graph-cache]]): reading each spec.md through
139
139
  // fs/promises YIELDS the event loop between files, so a build never stalls a `/health` liveness probe the
140
140
  // way the sync walk (one ~450ms uninterrupted stretch) did. Same output as raws() — identical push order
141
141
  // (pre-order DFS, dir before children) and the same reId — so every caller reads the same nodes; only
@@ -170,14 +170,14 @@ function claimMatcher(file: string): (cf: string) => boolean {
170
170
  }
171
171
  }
172
172
 
173
- // spec node(s) that GOVERN a file (frontmatter `code:` — source of truth, drives drift/yatsu); reads only
173
+ // spec node(s) that GOVERN a file (frontmatter `code:` — source of truth, drives drift + eval freshness); reads only
174
174
  // frontmatter (cheap, no git) so a per-edit hook can call it.
175
175
  export function specOwners(file: string): { id: string; desc: string }[] {
176
176
  const claims = claimMatcher(file)
177
177
  return raws().filter((r) => list(r.fm.code).some(claims)).map((r) => ({ id: r.id, desc: str(r.fm.desc) }))
178
178
  }
179
179
 
180
- // spec node(s) that REFERENCE a file (frontmatter `related:` — carries coverage, never drift/yatsu):
180
+ // spec node(s) that REFERENCE a file (frontmatter `related:` — carries coverage, never drift, never eval freshness):
181
181
  // [[governed-related]]'s other half, same claim rule, same cheap frontmatter-only read.
182
182
  export function specRelated(file: string): { id: string; desc: string }[] {
183
183
  const claims = claimMatcher(file)
@@ -211,7 +211,7 @@ export function loadSpecsLite(): SpecLite[] {
211
211
  }
212
212
 
213
213
  // one node's body + parsed parts, filesystem-only (no git). The board omits both to stay lean
214
- // ([[board-lean]]); the detail view fetches them here when a node opens. null when the id isn't a node.
214
+ // ([[graph-lean]]); the detail view fetches them here when a node opens. null when the id isn't a node.
215
215
  export function specContent(id: string): { body: string; parts: ReturnType<typeof parseParts> } | null {
216
216
  const r = raws().find((x) => x.id === id)
217
217
  return r ? { body: r.body.trim(), parts: parseParts(r.body) } : null
@@ -234,7 +234,7 @@ export async function loadSpecs() {
234
234
  .filter((d) => d.behind > 0)
235
235
  const drift = driftFiles.reduce((a, d) => a + d.behind, 0)
236
236
  // related drift is the SOFT tier ([[governed-related]]): same ancestry basis, but it stays OUT of
237
- // `drift` — it never feeds status, the commit gate, or yatsu. It surfaces only as a lint warn nudge.
237
+ // `drift` — it never feeds status, the commit gate, or eval freshness. It surfaces only as a lint warn nudge.
238
238
  const relatedDriftFiles = related
239
239
  .map((f) => ({ file: f, behind: driftFor(didx, S, f) }))
240
240
  .filter((d) => d.behind > 0)
@@ -292,7 +292,7 @@ export async function specDiffAt(id: string, hash: string) {
292
292
  return latestDiff(node.relPath, hash)
293
293
  }
294
294
 
295
- // config presets - REFLEXIVE, SKILL-SHAPED preset nodes whose folder IS a skill bundle: `spec.md`'s
295
+ // plugin presets - REFLEXIVE, SKILL-SHAPED preset nodes whose folder IS a skill bundle: `spec.md`'s
296
296
  // body is the agent prompt/contract (with a {{targets}} placeholder the launcher fills with the
297
297
  // @-referenced nodes), and the SAME folder may co-locate auxiliary files — scripts, assets — that the
298
298
  // preset ships for the agent to run deterministically. So each preset reports its folder `dir`
@@ -302,19 +302,19 @@ export async function specDiffAt(id: string, hash: string) {
302
302
  // harness lifecycle events the node binds, its deterministic intra-event order, and whether it intends to
303
303
  // block (honored only on block-capable events). See loadHookConfig + the hook compiler/dispatcher.
304
304
  export type ConfigPreset = { name: string; title: string; desc: string; kind: string; dir: string; files: string[]; body: string; events: string[]; order: number; block: boolean; tools: string[] }
305
- // field-driven surface - a config plugin is a FLAT direct child of a config root (`<root>/<name>/spec.md`)
305
+ // field-driven surface - a plugin is a FLAT direct child of a plugin root (`<root>/<name>/spec.md`)
306
306
  // that carries a `surface: system|command|hook|skill|agent` frontmatter field naming where it plugs in. There are no
307
307
  // `command/`/`system/`/`hook/`/`skill/`/`agent/` bucket dirs (those were graph-invisible grouping dirs with no spec.md, so
308
308
  // the spec graph skipped them — path != graph); the surface is a FIELD on the node, so the plugin is a real
309
- // graph child of its root. BOTH config roots participate: `.config` (the instance — DIY dev-flow plugins) and
310
- // `config` (the project system spec). loadConfig gathers the `command` surface, loadSystemConfig the `system`
309
+ // graph child of its root. BOTH plugin roots participate: `.plugins` (the instance — DIY dev-flow plugins) and
310
+ // `plugin-system` (the project system spec). loadConfig gathers the `command` surface, loadSystemConfig the `system`
311
311
  // surface, loadHookConfig the `hook` surface, loadSkillConfig the `skill` surface, loadAgentConfig the `agent`
312
312
  // surface (sub-agent definitions); each scans the children under every root and filters by the field. The plugins also show on the board as ordinary spec nodes (via loadSpecs).
313
313
  // root node - the spec tree's single top-level node: the one directory directly under .spec/ that
314
314
  // holds a spec.md. The dogfood repo names it 'spexcode'; a repo scaffolded by `spex init` names it
315
315
  // 'project' (or whatever the adopter renames it to). Detected DYNAMICALLY so the config loaders resolve
316
316
  // the ACTUAL root's config dirs — never a hardcoded 'spexcode', which silently returned [] in an adopter
317
- // repo, so their .config/core contract never loaded and their launched agents got no system prompt.
317
+ // repo, so their .plugins/core contract never loaded and their launched agents got no system prompt.
318
318
  // Returns null when .spec holds no such directory. (resolveLayout's `main` is a checkout PATH, not the
319
319
  // root node NAME, so it can't serve this — a tiny filesystem probe is the right seam.)
320
320
  function rootNode(): string | null {
@@ -325,10 +325,20 @@ function rootNode(): string | null {
325
325
  return null
326
326
  }
327
327
  // resolved at call time (not module-eval) so it tracks the live tree.
328
+ // @@@ legacy-tree refusal - v0.3.0 renamed the plugin instance root `.config` → `.plugins`. A pre-0.3.0
329
+ // tree would otherwise load an EMPTY plugin surface — no contract block, no hooks, no commands — and the
330
+ // launched agents would silently run ungoverned. So refuse loudly: existence-only probe (never a dual
331
+ // read of legacy content), pointing at the one-shot migrator. Delete this check in 0.4.0.
328
332
  function configRoots(): string[] {
329
333
  const root = rootNode()
330
334
  if (!root) return []
331
- return ['.config', 'config'].map((r) => join(SPEC_DIR, root, r))
335
+ if (existsSync(join(SPEC_DIR, root, '.config')) && !existsSync(join(SPEC_DIR, root, '.plugins'))) {
336
+ throw new Error(
337
+ `.spec/${root}/.config exists but .spec/${root}/.plugins does not — this spec tree predates the v0.3.0 ` +
338
+ `plugin rename (.config → .plugins). Refusing to load an empty plugin surface (agents would launch ` +
339
+ `ungoverned). Run \`spex doctor --migrate\` to migrate the tree.`)
340
+ }
341
+ return ['.plugins', 'plugin-system'].map((r) => join(SPEC_DIR, root, r))
332
342
  }
333
343
  // co-located bundle files = everything under the node folder except its spec.md, repo-relative, recursive.
334
344
  function bundleFiles(dir: string): string[] {
@@ -343,9 +353,9 @@ function bundleFiles(dir: string): string[] {
343
353
  walk(dir)
344
354
  return out.sort()
345
355
  }
346
- // gather the preset nodes under a config root that declare `surface: <surface>`. The scan is RECURSIVE —
356
+ // gather the preset nodes under a plugin root that declare `surface: <surface>`. The scan is RECURSIVE —
347
357
  // `surface` is a FIELD, not a path (the design's core tenet), so a plugin may live at ANY depth and a
348
- // grouping parent may itself be a plugin (e.g. `.config/core` is a `surface: system` contract whose CHILDREN
358
+ // grouping parent may itself be a plugin (e.g. `.plugins/core` is a `surface: system` contract whose CHILDREN
349
359
  // are `surface: hook` nodes). The field filter keeps it safe: a node only gathers if it declares THIS
350
360
  // surface, so descending past a matched node never double-counts (children carry a different surface). For
351
361
  // `system`/`command` the result is identical to the old one-level scan on the current tree — every existing
@@ -37,7 +37,7 @@ const repoRoot = join(here, '..', '..')
37
37
  const watchRoots = [
38
38
  here, // spec-cli/src — the backend's own source
39
39
  join(repoRoot, 'spec-forge', 'src'),
40
- join(repoRoot, 'spec-yatsu', 'src'),
40
+ join(repoRoot, 'spec-eval', 'src'),
41
41
  ]
42
42
 
43
43
  type Backend = { port: number; child: ChildProcess }
@@ -128,7 +128,7 @@ const proxy = net.createServer((client) => {
128
128
  client.once('close', () => up.destroy()) // client abandoned → reap its upstream (THE leak that wedged :8787); nothing left to flush to a gone client
129
129
  // upstream gone → drop the client half, but ONLY force it when the close was ABNORMAL: on a normal FIN,
130
130
  // `up.pipe(client)` has already called `client.end()` (writableEnded), so let the client flush the last of a
131
- // large response (e.g. /api/board) rather than truncate it with a destroy; a crash/half-open close (no prior
131
+ // large response (e.g. /api/graph) rather than truncate it with a destroy; a crash/half-open close (no prior
132
132
  // end) still gets reaped.
133
133
  up.once('close', () => { if (!client.writableEnded) client.destroy() })
134
134
  client.pipe(up); up.pipe(client)
@@ -1,7 +1,7 @@
1
- // @@@ spex tree - the CLI's human-readable graph view: the same assembled board the dashboard's
1
+ // @@@ spex graph - the CLI's human-readable graph view: the same assembled board the dashboard's
2
2
  // tidy-tree renders, as an indented terminal tree. Pure presentation over buildBoard()'s nodes —
3
3
  // no read path of its own; status colours and badge semantics mirror the dashboard (drift /
4
- // stale-yatsu / open-issues counts). Governed by the spex-tree spec node.
4
+ // stale-eval / open-issues counts). Governed by the spex-tree spec node.
5
5
 
6
6
  // the subset of a board node this view consumes (buildBoard attaches more; we read only these).
7
7
  export type TreeNode = {
@@ -23,7 +23,7 @@ export type TreeOpts = { node?: string; depth?: number; color?: boolean }
23
23
  // from the warning yellow), drift=yellow (the dashboard's warning colour), pending=muted grey.
24
24
  const STATUS_ANSI: Record<string, string> = { merged: '32', active: '36', drift: '33', pending: '90' }
25
25
 
26
- // stale-yatsu count: declared scenarios whose LATEST reading exists but is no longer fresh.
26
+ // stale-eval count: declared scenarios whose LATEST reading exists but is no longer fresh.
27
27
  // board `evals` is already latest-per-scenario, so this is a straight filter — the same freshness
28
28
  // axis the dashboard's grey ✓/✗ badges read (score.jsx readingScore).
29
29
  function staleYatsu(n: TreeNode): number {
@@ -48,7 +48,7 @@ function childrenIndex(nodes: TreeNode[]): Map<string | null, TreeNode[]> {
48
48
  function roots(nodes: TreeNode[], nodeId?: string): TreeNode[] {
49
49
  if (nodeId) {
50
50
  const hit = nodes.find((n) => n.id === nodeId)
51
- if (!hit) throw new Error(`no spec node "${nodeId}" — spex tree lists every id; spex search <topic> finds one by intent`)
51
+ if (!hit) throw new Error(`no spec node "${nodeId}" — spex graph lists every id; spex spec search <topic> finds one by intent`)
52
52
  return [hit]
53
53
  }
54
54
  const ids = new Set(nodes.map((n) => n.id))
@@ -100,7 +100,7 @@ export function renderTree(nodes: TreeNode[], opts: TreeOpts = {}): string {
100
100
  }
101
101
 
102
102
  // the machine exit: the same filtered subtree as NESTED objects, badge counts precomputed — a
103
- // shaped view, not a replacement for `spex board` (which stays the full flat payload). Pruned
103
+ // shaped view, not a replacement for `spex graph --json` (which stays the full flat payload). Pruned
104
104
  // children degrade to their ids, so --depth still tells the machine what exists below the cut.
105
105
  export function treeJson(nodes: TreeNode[], opts: TreeOpts = {}): object[] {
106
106
  const byParent = childrenIndex(nodes)
@@ -13,7 +13,7 @@ import { dematerialize } from './materialize.js'
13
13
  // and the optional git hooks. EVERY removal is gated on a SpexCode IDENTITY STAMP (the managed-block
14
14
  // sentinels, the shim's own dispatch.sh command line, the trust sentinels, the generated mark / name-scoped
15
15
  // on-demand paths, the plugin name stamp), so it can only ever delete what SpexCode itself generated. The one
16
- // inviolable rule: the user's spec ASSET (.spec/.config) is NEVER touched — uninstall removes only generated
16
+ // inviolable rule: the user's spec ASSET (.spec/.plugins) is NEVER touched — uninstall removes only generated
17
17
  // WIRING, not the spec graph that wiring served.
18
18
 
19
19
  // the standard plugin-host folders a host agent scans (in addition to any named in spexcode.json's `harnesses`).
@@ -85,7 +85,7 @@ export function uninstall(targetArg: string | undefined, opts: { hooks?: boolean
85
85
  const proj = resolve(targetArg ?? process.cwd())
86
86
  console.log(`spex uninstall → ${proj}`)
87
87
 
88
- // cwd = the project so the .config loaders read THIS tree's surface nodes (the live skill/agent names tell
88
+ // cwd = the project so the .plugins loaders read THIS tree's surface nodes (the live skill/agent names tell
89
89
  // each adapter's clean() exactly which name-scoped on-demand files were its to remove).
90
90
  const prevCwd = process.cwd()
91
91
  let arts: HarnessArtifacts = { skills: [], agents: [] }
@@ -93,7 +93,7 @@ export function uninstall(targetArg: string | undefined, opts: { hooks?: boolean
93
93
  process.chdir(proj)
94
94
  arts = { skills: loadSkillConfig().map((s) => s.name), agents: loadAgentConfig().map((a) => a.name) }
95
95
  } catch {
96
- // no readable .config (already partly torn down, or never adopted) — clean still strips the harness wiring.
96
+ // no readable .plugins (already partly torn down, or never adopted) — clean still strips the harness wiring.
97
97
  } finally {
98
98
  process.chdir(prevCwd)
99
99
  }
@@ -140,6 +140,6 @@ export function uninstall(targetArg: string | undefined, opts: { hooks?: boolean
140
140
 
141
141
  console.log(`
142
142
  SpexCode wiring removed. Your spec data is untouched:
143
- • .spec/ and .config/ remain — your spec graph is YOURS, never deleted by uninstall.
143
+ • .spec/ and .plugins/ remain — your spec graph is YOURS, never deleted by uninstall.
144
144
  • To re-adopt later: \`spex init\` regenerates the shims, contract, trust, and global store.`)
145
145
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # @@@ footprint refresh (post-checkout) ([[commit-surgery]]) - a branch checkout is one of the three git
3
- # transitions that can move the materialize's inputs: .spec/.config content changes across branches, and a
3
+ # transitions that can move the materialize's inputs: .spec/.plugins content changes across branches, and a
4
4
  # contract file's TRACKEDNESS can flip (switching to a branch that tracks CLAUDE.md checks out the pristine
5
5
  # index prose — the re-materialize here writes the block back into the working file, binds the clean/smudge
6
6
  # filter, and withdraws the exclude entry; the kind-transition heals itself). Git-native anchors only —
@@ -5,7 +5,7 @@
5
5
  # command's own output — a git-native nudge that costs no harness block-cap and reaches a self-launched agent
6
6
  # too. GUARDED to the `merge node/<id>:` commit, so an ordinary `git pull`/unrelated merge never nags.
7
7
  #
8
- # The nudge TEXT and the on/off toggle both live in the CLI (`spex issues nudge <node>` prints nothing when
8
+ # The nudge TEXT and the on/off toggle both live in the CLI (`spex internal nudge <node>` prints nothing when
9
9
  # the feature is OFF in spexcode.json), so this hook is a THIN resolver + caller: resolve `spex` the same
10
10
  # three ways the pre-commit hook does, pass the merged node, echo whatever it prints. No CLI resolvable → no
11
11
  # nudge (safe degradation, same as the lint shim's advisory mode).
@@ -19,7 +19,7 @@ spex_run() {
19
19
  }
20
20
 
21
21
  # @@@ footprint refresh ([[commit-surgery]]) - a merge is one of the three git transitions that can move the
22
- # materialize's inputs (.spec/.config content, contract-file trackedness), so re-materialize here — git-native
22
+ # materialize's inputs (.spec/.plugins content, contract-file trackedness), so re-materialize here — git-native
23
23
  # anchors only, never a harness event. Quiet, best-effort: a failed refresh self-heals at the next anchor.
24
24
  spex_run internal refresh-footprint >/dev/null 2>&1 || true
25
25
 
@@ -29,5 +29,5 @@ case "$subj" in
29
29
  *) exit 0 ;;
30
30
  esac
31
31
  node=$(printf '%s' "$subj" | sed -n 's#^merge \(node/[^:]*\):.*#\1#p')
32
- spex_run issues nudge "$node"
32
+ spex_run internal nudge "$node"
33
33
  exit 0
@@ -30,7 +30,7 @@ spex_cli() {
30
30
  # @@@ tree-unchanged stamp - a commit whose tree EQUALS HEAD's tree (`git write-tree` over the commit's own
31
31
  # index — the hook's GIT_INDEX_FILE, a temp HEAD-tree index for `commit --only --allow-empty`) adds NO
32
32
  # content: it cannot smuggle code past the main-guard below and cannot introduce spec↔code drift, so the
33
- # whole hook passes it. This is `spex ack`'s empty Spec-OK stamp — the reason ack works on a trunk merge
33
+ # whole hook passes it. This is `spex spec ack`'s empty Spec-OK stamp — the reason ack works on a trunk merge
34
34
  # commit (amending it would re-author the merge after MERGE_HEAD is gone and read as direct authoring) and
35
35
  # with a DIRTY real index (the lint shim below reads the real index via git.ts's env-strip, which would
36
36
  # otherwise block the stamp on the very drift it acknowledges). Deliberately a one-line tree compare, not an
@@ -60,7 +60,7 @@ trunk=$(spex_cli internal trunk 2>/dev/null | head -1)
60
60
  if [ "$branch" = "$trunk" ] && [ ! -f "$git_dir/MERGE_HEAD" ] && [ -z "${SPEXCODE_ALLOW_MAIN:-}" ]; then
61
61
  echo "✗ SpexCode: direct commits on $trunk (the trunk) are blocked." >&2
62
62
  echo " Work in a worktree (.worktrees/<node>, branch node/<id>) and merge back." >&2
63
- echo " Merges and tree-unchanged stamps (spex ack) pass automatically; for seeding/topology: SPEXCODE_ALLOW_MAIN=1 git commit …" >&2
63
+ echo " Merges and tree-unchanged stamps (spex spec ack) pass automatically; for seeding/topology: SPEXCODE_ALLOW_MAIN=1 git commit …" >&2
64
64
  exit 1
65
65
  fi
66
66
 
@@ -74,13 +74,13 @@ if [ -n "$spex_kind" ]; then
74
74
  spex_cli internal commit-surgery || echo "• SpexCode: footprint surgery skipped (error above) — a generated artifact may be staged; CI still enforces." >&2
75
75
  fi
76
76
 
77
- # @@@ spec-lint - the hook is just a thin shim over `spex lint`. It blocks on errors only (broken
77
+ # @@@ spec-lint - the hook is just a thin shim over `spex spec lint`. It blocks on errors only (broken
78
78
  # spec↔code links); coverage/drift are warnings. Bypass with SPEXCODE_SKIP_LINT=1. It shims through the
79
79
  # shared spex_cli() resolved above (repoRoot() discovers the cwd's toplevel via git.ts, which strips the
80
80
  # hook's GIT_DIR, so lint validates THIS worktree's `.spec`).
81
81
  if [ -z "${SPEXCODE_SKIP_LINT:-}" ]; then
82
82
  if [ -n "$spex_kind" ]; then
83
- spex_cli lint >&2
83
+ spex_cli spec lint >&2
84
84
  lint_rc=$?
85
85
  # exit 75 = the launcher's mid-merge guard (spex's own source holds conflict markers — transient).
86
86
  # Degrade to advisory rather than walling every commit behind a merge someone else is resolving.
@@ -90,11 +90,11 @@ if [ -z "${SPEXCODE_SKIP_LINT:-}" ]; then
90
90
  echo "✗ SpexCode: spec-lint failed — fix the spec↔code links or bypass with SPEXCODE_SKIP_LINT=1." >&2
91
91
  exit 1
92
92
  else
93
- # @@@ yatsu backstop - reject a staged yatsu offender: a stray pixel blob copied into the tree (blobs
94
- # belong in the shared git common dir, never committed) OR a malformed yatsu.md (a scenario schema
95
- # violation). Logic lives in spec-yatsu; this is the lint-shim's twin. (this node's contract)
96
- if ! spex_cli yatsu check-staged >&2; then
97
- echo "✗ SpexCode: yatsu check-staged failed (see above) — fix it or bypass with SPEXCODE_SKIP_LINT=1." >&2
93
+ # @@@ eval backstop - reject a staged eval offender: a stray evidence blob copied into the tree (blobs
94
+ # belong in the shared git common dir, never committed) OR a malformed eval.md (a scenario schema
95
+ # violation). Logic lives in spec-eval; this is the lint-shim's twin. (this node's contract)
96
+ if ! spex_cli internal check-staged >&2; then
97
+ echo "✗ SpexCode: internal check-staged failed (see above) — fix it or bypass with SPEXCODE_SKIP_LINT=1." >&2
98
98
  exit 1
99
99
  fi
100
100
  fi
@@ -34,7 +34,7 @@ fi
34
34
 
35
35
  [ -z "$sid" ] && exit 0
36
36
  # Stamp via interpret-trailers (git's own sample-hook mechanism), NOT a raw printf append: appending after a
37
- # message that already carries a trailer block (e.g. `spex ack`'s Spec-OK) opens a new paragraph, and git then
37
+ # message that already carries a trailer block (e.g. `spex spec ack`'s Spec-OK) opens a new paragraph, and git then
38
38
  # parses ONLY the last paragraph as trailers — the earlier trailers silently become body prose. interpret-trailers
39
39
  # appends into the existing block, keeping ONE parseable block. It needs the file newline-terminated first:
40
40
  # on an unterminated message (git merge -m writes MERGE_MSG without one) it folds the trailer into the subject
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # On an idle_prompt notification, mark the session idle (the active-only guard in `session idle` keeps a
2
+ # On an idle_prompt notification, mark the session idle (the active-only guard in `internal session-idle` keeps a
3
3
  # deliberate awaiting/asking/parked/error declaration from being clobbered). GATED on `governed`: only a
4
4
  # dashboard-launched session has board state to mark — a self-launched agent's idle is none of our business.
5
5
  # State lives in the per-session GLOBAL record session.json (keyed by the harness session_id, grouped per-
@@ -12,4 +12,4 @@ sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
12
12
  sdir=$(hp_store_dir "$sid") || exit 0
13
13
  rec="$sdir/session.json"
14
14
  grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
15
- [ "$(hp_notification_type "$payload")" = idle_prompt ] && exec ${SPEX:-spex} session idle --session "$sid"
15
+ [ "$(hp_notification_type "$payload")" = idle_prompt ] && exec ${SPEX:-spex} internal session-idle --session "$sid"
@@ -15,6 +15,13 @@
15
15
  # (non-governed) session has no board to feed, so this no-ops on it. cwd = the session worktree.
16
16
  . "${SPEXCODE_HARNESS_LIB:?harness.sh not exported by dispatch.sh}"
17
17
  payload=$(cat 2>/dev/null)
18
+ # an IN-PROCESS SUBAGENT's tool call (Claude's Task tool) fires the parent's hooks with the PARENT's
19
+ # session_id — flipping here let a supervising parent's own subagents erase its declared park/ask within
20
+ # seconds and race the stop-gate into "undeclared stop" (issue #60). A subagent working is not the parent
21
+ # agent ACTING, so its calls never touch the record; the parent's own next tool call still flips. The
22
+ # discriminator is the payload's own top-level agent_id stamp (hp_is_subagent) — deterministic, never a
23
+ # timing window.
24
+ [ -n "$(hp_is_subagent "$payload")" ] && exit 0
18
25
  sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
19
26
  sdir=$(hp_store_dir "$sid") || exit 0
20
27
  rec="$sdir/session.json"
@@ -13,4 +13,6 @@ The single freshness signal for a session. Any work a session does — a new pro
13
13
 
14
14
  The state is read from ONE structured field in the hook payload, never sniffed from the terminal UI, so the signal is hard rather than guessed. Because it fires before the tool runs, a deliberate [[stop-gate]] declaration (itself made via a tool) lands after this and wins; the next real tool flips back to `active`, forcing a fresh declaration at the following stop. It is pure shell so it stays cheap firing on every tool call.
15
15
 
16
+ The one activity that does NOT count as the session acting is an IN-PROCESS SUBAGENT's tool call (the harness's Task tool — a sub-conversation inside the same process). Such a call fires the parent's hooks carrying the parent's `session_id`, so without a discriminator a supervising parent could never hold a declared state: its own subagents erased every park/ask within seconds and raced the stop-gate into "undeclared stop". The harness stamps subagent-executed calls with a top-level `agent_id` field the parent's own calls never carry; `hp_is_subagent` reads that stamp deterministically (scanning only the pre-`tool_input` payload prefix, where a tool parameter or file content can never fake an unescaped key), and this hook skips the flip entirely. A subagent working is its parent supervising, not the parent moving on — the parent's own next tool call still flips as before.
17
+
16
18
  This is the freshness half of the [[core]] discipline: it keeps the board honest about whether a session is working, waiting, or asking, so the gates and the dashboard read a true present state rather than a stale one.
@@ -9,4 +9,4 @@ sid=$(hp_session_id "$payload"); [ -n "$sid" ] || exit 0
9
9
  sdir=$(hp_store_dir "$sid") || exit 0
10
10
  rec="$sdir/session.json"
11
11
  grep -q '"governed"[[:space:]]*:[[:space:]]*true' "$rec" 2>/dev/null || exit 0
12
- exec ${SPEX:-spex} session fail --session "$sid"
12
+ exec ${SPEX:-spex} internal session-fail --session "$sid"
@@ -48,7 +48,7 @@ if [ -n "$node" ]; then
48
48
  sp=$(find .spec -path "*/$node/spec.md" 2>/dev/null | head -1)
49
49
  where="your node's spec (${sp:-.spec/.../$node/spec.md})"
50
50
  else
51
- where="the spec node that governs this area (run: spex search <topic>)"
51
+ where="the spec node that governs this area (run: spex spec search <topic>)"
52
52
  fi
53
53
  printf '{"decision":"block","reason":"Before working in this code, read %s FIRST — it is the current contract — and read its NEIGHBORS too (the parent that scopes it, the siblings it borders, the children that refine it), since its intent is only fully legible against the surrounding tree. Then act deliberately: changing the intent? edit the spec first so spec and code land together. implementing existing intent? make the code honor the spec. The one forbidden move is code that silently diverges from its spec. (Fires once per session, at your first code read or edit.)"}\n' "$where"
54
54
  exit 0
@@ -4,7 +4,7 @@
4
4
  # much and points at the split — so the contract is in view AT THE MOMENT OF THE EDIT, not just later at
5
5
  # commit (lint/drift). NON-BLOCKING (additionalContext only — never a verdict) and dedup'd PER FILE via a
6
6
  # ledger, so a 50-edit refactor annotates each file ONCE. Uses MAIN's tsx+cli ($SPEX) for the file→spec
7
- # resolve (`spex owner`); cwd = the session worktree.
7
+ # resolve (`spex spec owner`); cwd = the session worktree.
8
8
  # @@@ harness-agnostic - WHICH tool/path counts as a code MUTATION is the [[harness-adapter]]'s call, read via
9
9
  # hp_code_path … mutate (Claude Edit/Write/NotebookEdit + file_path; Codex tool_name:Bash + an apply_patch /
10
10
  # write-shape command). So this annotates edits on Claude AND Codex.
@@ -52,7 +52,7 @@ while IFS= read -r path; do
52
52
  path=$(hp_actionable_repo_path "$path") || continue
53
53
  [ -f "$led" ] && grep -qxF -- "$path" "$led" && continue
54
54
  mkdir -p "$sdir"; echo "$path" >> "$led"
55
- m=$(cd "$repo" && $S owner "$path" --actionable 2>/dev/null) # --actionable: silent on a sanely-owned file; speaks only for an OVER-owned / uncovered file
55
+ m=$(cd "$repo" && $S spec owner "$path" --actionable 2>/dev/null) # --actionable: silent on a sanely-owned file; speaks only for an OVER-owned / uncovered file
56
56
  [ -n "$m" ] || continue
57
57
  msg="${msg:+$msg
58
58
  }$m"
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: core
3
+ surface: system
4
+ status: active
5
+ hue: 200
6
+ desc: A config plugin — the minimal spec-discipline contract folded into every launched agent.
7
+ code:
8
+ ---
9
+ The CLI speaks ONE grammar: `spex <noun> <verb> [object] [flags]`. Six nouns — `spec` (the governance graph), `session` (the worktree state machine), `eval` (the measurement system), `issue` (concern threads), `remark` (resolvable pins), `evidence` (content-addressed bytes) — plus a few bare project verbs (`graph` · `init` · `materialize` · `doctor` · `serve` · `uninstall`) whose object is always this project. The verb is always the token right after its noun; a bare noun prints its drawer's help; a `--help` probe is always safe (it prints and exits before the verb runs). `SEL` = session id | unique id-prefix | node id | branch; `.` as a node argument = the node this worktree works on. When unsure of a spelling, ask the tool — `spex help`, `spex help <command>` — never guess from memory: removed spellings only report their replacement and exit.
10
+
11
+ Four disciplines, non-negotiable:
12
+
13
+ 1. SPEC FIRST. Before you touch code — and merely READING it counts, not only editing — read the governing spec's BODY: its actual prose, not the title, not the one-line desc, not your memory of it. The body is the current contract; code and its comments tell you what the code DOES, only the spec tells you what it is SUPPOSED to do. Don't know which node governs the area? `spex spec search <topic>` — not grep: grep finds code by architectural centrality, search finds intent by user-story (for a find-by-story question a keyword can't reach, dispatch the `spec-scout` agent). Read the NEIGHBORS' bodies too — the parent that scopes it, the siblings it borders, the children that refine it — a node's intent is only legible against the tree around it. If your task changes the intent, edit the spec first so spec and code land together; if it implements existing intent, make the code honor the spec. The one forbidden move is code that silently diverges from its spec.
14
+
15
+ 2. COMMIT BEFORE YOU DECLARE. Commit your spec node and the code it justifies BEFORE you declare done or propose merge — the commit comes first, never as an afterthought to a declaration. An independently-scoped feature gets its OWN sibling spec node, not a ride-along in your assigned node's commit (cosmetic polish riding along is the smell).
16
+
17
+ 3. THE BODY IS A LIVING CURRENT-STATE DOCUMENT. It states the node's PRESENT intent and is rewritten in place. Never accrete a "## vN" changelog heading, and never add current-state or verdict sections — version history is git's job, not the body's.
18
+
19
+ 4. KEEP THE LOSS SIGNAL HONEST for what you changed — eval readings are the signal the optimizer reads, so a gap is a blind spot. Changed a node that carries a `eval.md`? Re-measure it: run its scenario, compare to the expected, and file the result with `spex eval add <node>`. Made an obvious frontend change to a node with NO `eval.md`? Give it one — a scenario (description + expected) — so its loss can be measured. A frontend scenario is measured through the **actual running product** — drive a real browser, read the real DOM and capture a screenshot (or video), never reason about the code — and that real observation is filed as the reading, not left as an ad-hoc check you ran but never recorded. `spex eval lint --changed` shows the gaps in exactly the nodes you touched.
20
+
21
+ Don't reverse-engineer the file formats: `spex guide spec` and `spex guide eval` print the full spec.md and scenario schema on demand. This prompt is the clue; that manual carries the detail.
@@ -35,33 +35,33 @@ proposal=$(jget proposal)
35
35
  # BSD sed has no \| alternation.)
36
36
  cont=$(printf '%s' "$input" | sed -n 's/.*"stop_hook_active"[[:space:]]*:[[:space:]]*\([a-z]*\).*/\1/p')
37
37
 
38
- # @@@ yatsu advisory - a nudge (never a gate) emitted when a session stops CLEAN-DONE (committed work + a
39
- # done/awaiting declaration): the agent IS yatsu's evaluator, so a yatsu gap in what it just changed is a
40
- # blind spot to flag the moment work lands. SCOPED via `spex yatsu scan --changed` to the nodes THIS branch
38
+ # @@@ eval advisory - a nudge (never a gate) emitted when a session stops CLEAN-DONE (committed work + a
39
+ # done/awaiting declaration): the agent IS the measuring hand, so an eval gap in what it just changed is a
40
+ # blind spot to flag the moment work lands. SCOPED via `spex eval lint --changed` to the nodes THIS branch
41
41
  # touched — so an agent is never nagged about a score that went stale in a node it never opened (the bug
42
- # that made three workers ask "is this mine?"). Three gap classes it surfaces: yatsu-drift / yatsu-missing
43
- # (a node with a yatsu.md whose score is stale / unmeasured) and yatsu-uncovered (a FRONTEND node with no
44
- # yatsu.md — an obvious UI change carrying no loss signal). Delivered via the Stop hook's additionalContext
42
+ # that made three workers ask "is this mine?"). Three gap classes it surfaces: eval-drift / eval-missing
43
+ # (a node with an eval.md whose score is stale / unmeasured) and eval-coverage (a FRONTEND node with no
44
+ # eval.md — an obvious UI change carrying no loss signal). Delivered via the Stop hook's additionalContext
45
45
  # (NEVER a block decision: a gap is a heads-up, not a wall). FIRES ONCE: the additionalContext itself forces
46
46
  # one continuation, so the CALLER guards it on stop_hook_active — re-emitting on the forced re-stop is what
47
47
  # looped 31 turns and tripped the Stop-hook block cap. Called only on ALLOW paths, never alongside a block.
48
48
  #
49
49
  # SURFACE-NEUTRAL: a stale/unmeasured score is refreshed only by PRODUCING the measurement on the scenario's
50
50
  # OWN surface — a real run, never a desk check and never deferring to review a recording after the fact. The
51
- # nudge privileges NO surface: `scan --changed` carries each drift/missing scenario's tag on its finding line
52
- # ([[yatsu-core]]'s lint.scenarioTags — frontend-e2e / backend-api / cli / desktop / mobile), so the agent
51
+ # nudge privileges NO surface: `eval lint --changed` carries each drift/missing scenario's tag on its finding line
52
+ # ([[eval-core]]'s lint.scenarioTags — frontend-e2e / backend-api / cli / desktop / mobile), so the agent
53
53
  # reads there WHICH surface to run. One line covers all five surfaces; there is no per-surface branch.
54
- yatsu_advisory() {
54
+ eval_advisory() {
55
55
  local out ids n msg esc
56
56
  # Codex Stop hooks reject the Claude-family `hookSpecificOutput.additionalContext` shape on allow paths.
57
57
  # Keep Codex Stop stdout empty unless it is a real block decision; the dispatcher still bridges block
58
58
  # reasons to Codex stderr.
59
59
  [ "${SPEXCODE_HARNESS:-claude}" = codex ] && return 0
60
- out=$($S yatsu scan --changed 2>&1)
61
- n=$(printf '%s\n' "$out" | grep -cE 'yatsu-(drift|missing|uncovered):')
62
- [ "${n:-0}" -gt 0 ] || return 0 # no gap in what you changed (or scan unavailable) -> nothing to nudge
63
- ids=$(printf '%s\n' "$out" | sed -n "s/.*yatsu-[a-z]*: '\([^']*\)'.*/\1/p" | awk '!seen[$0]++' | head -6 | paste -sd' ' -)
64
- msg="yatsu — the loss signal the optimizer reads — flags ${n} gap(s) in nodes you changed: ${ids}. A node whose score went stale/unmeasured: re-measure it — PRODUCE the measurement YOURSELF with a real run of the scenario's actual surface (its tag on the \`spex yatsu scan --changed\` line tells you WHICH surface to run), compare to expected, and file it with \`spex yatsu eval <node>\`; don't desk-check it, and don't defer to reviewing a recording after the fact. A FRONTEND node with no yatsu.md: give it one (a scenario — description + expected), since an obvious UI change should carry a loss signal. \`spex yatsu scan --changed\` lists them. (Advisory — fires once, not a gate.)"
60
+ out=$($S eval lint --changed 2>&1)
61
+ n=$(printf '%s\n' "$out" | grep -cE 'eval-(drift|missing|coverage):')
62
+ [ "${n:-0}" -gt 0 ] || return 0 # no gap in what you changed (or eval lint unavailable) -> nothing to nudge
63
+ ids=$(printf '%s\n' "$out" | sed -n "s/.*eval-[a-z]*: '\([^']*\)'.*/\1/p" | awk '!seen[$0]++' | head -6 | paste -sd' ' -)
64
+ msg="eval — the loss signal the optimizer reads — flags ${n} gap(s) in nodes you changed: ${ids}. A node whose score went stale/unmeasured: re-measure it — PRODUCE the measurement YOURSELF with a real run of the scenario's actual surface (its tag on the \`spex eval lint --changed\` line tells you WHICH surface to run), compare to expected, and file it with \`spex eval add <node>\`; don't desk-check it, and don't defer to reviewing a recording after the fact. A FRONTEND node with no eval.md: give it one (a scenario — description + expected), since an obvious UI change should carry a loss signal. \`spex eval lint --changed\` lists them. (Advisory — fires once, not a gate.)"
65
65
  esc=$(printf '%s' "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g')
66
66
  printf '{"hookSpecificOutput":{"hookEventName":"Stop","additionalContext":"%s"}}\n' "$esc"
67
67
  }
@@ -69,17 +69,17 @@ yatsu_advisory() {
69
69
  # @@@ commit gate - a declaration of done/merge (awaiting + proposal merge|nothing) is only honest once the
70
70
  # node branch carries the work as COMMITS: the dogfood ritual commits spec+code BEFORE any proposal, yet a
71
71
  # dashboard-launched agent kept proposing merge with 0 commits / a dirty tree. So before allowing such a
72
- # declaration we run the deterministic check (`spex session commit-gate`, which goes through git.ts's git()
72
+ # declaration we run the deterministic check (`spex internal commit-gate`, which goes through git.ts's git()
73
73
  # so the hook's GIT_DIR/GIT_INDEX_FILE can't misdirect repo discovery). Clean -> allow. Dirty/0-ahead ->
74
74
  # block ONCE with the specific reason + commit instructions; on the forced continuation (the agent ignored
75
75
  # it) escape the loop by downgrading to `asking` (needs the human) with a clear note, so a FALSE "ready to
76
76
  # merge" never stands. (A propose-close declaration is exempt — it discards the worktree, so commits are moot.)
77
77
  if [ "${status:-active}" = awaiting ] && { [ "$proposal" = merge ] || [ "$proposal" = nothing ]; }; then
78
- if gatemsg=$($S session commit-gate 2>&1); then
78
+ if gatemsg=$($S internal commit-gate 2>&1); then
79
79
  # nudge ONCE: emit on the natural stop, but STAY SILENT on the forced re-stop the additionalContext
80
80
  # itself causes (stop_hook_active=true). Without this guard the advisory re-fired every clean-done stop
81
81
  # and looped — the bug a prior change DESCRIBED in a comment but never actually implemented at the call.
82
- [ "$cont" != true ] && yatsu_advisory
82
+ [ "$cont" != true ] && eval_advisory
83
83
  exit 0 # work is committed and ahead of main -> the proposal is honest, let it stop.
84
84
  fi
85
85
  if [ "$cont" = true ]; then
@@ -98,9 +98,9 @@ if [ "$cont" = true ]; then
98
98
  # the forced continuation also stopped without declaring -> escape the loop, don't block. Keep the commit
99
99
  # gate airtight: default to awaiting/nothing only when the branch is actually committed+ahead; otherwise an
100
100
  # undeclared stop with uncommitted work becomes `asking` (needs the human), never a false awaiting/done.
101
- if $S session commit-gate >/dev/null 2>&1; then
102
- $S session state awaiting --session "$sid" --propose nothing --note "auto: stopped without declaring" >/dev/null 2>&1 || true
103
- # NOTE: no yatsu nudge on the auto-declare path. It only runs on the forced continuation (cont=true),
101
+ if $S internal commit-gate >/dev/null 2>&1; then
102
+ $S internal session-state awaiting --session "$sid" --propose nothing --note "auto: stopped without declaring" >/dev/null 2>&1 || true
103
+ # NOTE: no eval nudge on the auto-declare path. It only runs on the forced continuation (cont=true),
104
104
  # where a guarded advisory could never fire anyway, and an unguarded one was a second loop vector (a
105
105
  # mark-active tool call could re-enter this branch). The clean-done path above is the single nudge site.
106
106
  else
@@ -134,5 +134,5 @@ touch "$taught" 2>/dev/null || true
134
134
  # tool calls honestly re-flips the record to active (mark-active, by design) and re-blocks the next stop;
135
135
  # this block text is the one place every undeclared stopper is guaranteed to read, so the teaching that
136
136
  # kills the park->block->re-park loop at its source lives here.
137
- printf '{"decision":"block","reason":"Your session state is a CLAIM the board, your supervisor, and other agents act on — not a box to tick to end the turn. Stopping undeclared makes your outcome a guess. Pick the ONE that is TRUE right now and run `%s session <choice>`, choosing the <choice> whose condition holds:\\n • done --propose merge — spec+code COMMITTED on the branch and genuinely ready for a human to review/merge (not just probably-done).\\n • done --propose nothing — committed, but you are NOT proposing a merge; paused for the human to look.\\n • park --note <what-you-await> — ONLY when a real BACKGROUND TASK will wake you (a spex wait you backgrounded, a running build/job). If nothing is actually running to resume you, you are NOT parked — you are waiting on the human, so use ask; never use park as a default to clear this gate.\\n • done --propose close — you propose discarding this worktree.\\n • ask --note <your-question> — you need the human: a real question, or you are simply stopped awaiting direction; you resume only when they reply.\\n\\nDECLARE LAST, THEN STOP: finish everything else in the turn first — speak, send your messages, arm your background waits — and make the declaration your FINAL call. Any tool call AFTER it flips your record back to active (mark-active, by design: activity is activity), so the next stop re-blocks and demands a fresh declaration; declaring last kills that loop at its source.\\n\\n(This full explanation shows once per session; later undeclared stops get a one-line reminder. `%s help session` re-explains the choices any time.)"}\n' "$S" "$S"
137
+ printf '{"decision":"block","reason":"Your session state is a CLAIM the graph, your supervisor, and other agents act on — not a box to tick to end the turn. Stopping undeclared makes your outcome a guess. Pick the ONE that is TRUE right now and run `%s session <choice>`, choosing the <choice> whose condition holds:\\n • done --propose merge — spec+code COMMITTED on the branch and genuinely ready for a human to review/merge (not just probably-done).\\n • done --propose nothing — committed, but you are NOT proposing a merge; paused for the human to look.\\n • park --note <what-you-await> — ONLY when a real BACKGROUND TASK will wake you (a spex session wait you backgrounded, a running build/job). If nothing is actually running to resume you, you are NOT parked — you are waiting on the human, so use ask; never use park as a default to clear this gate.\\n • done --propose close — you propose discarding this worktree.\\n • ask --note <your-question> — you need the human: a real question, or you are simply stopped awaiting direction; you resume only when they reply.\\n\\nDECLARE LAST, THEN STOP: finish everything else in the turn first — speak, send your messages, arm your background waits — and make the declaration your FINAL call. Any tool call AFTER it flips your record back to active (mark-active, by design: activity is activity), so the next stop re-blocks and demands a fresh declaration; declaring last kills that loop at its source.\\n\\n(This full explanation shows once per session; later undeclared stops get a one-line reminder. `%s help session` re-explains the choices any time.)"}\n' "$S" "$S"
138
138
  exit 0
@@ -19,14 +19,14 @@ Input: a session id — SpexCode's, a bare harness id (claude / codex thread), o
19
19
  - **SpexCode session** (first choice — the join is first-class): its record is
20
20
  `~/.spexcode/projects/*/sessions/<id>/session.json` — glob for the id, prefix ok. Take `worktree_path`,
21
21
  `branch`, `harness`, `harness_session_id`, `status`, `title`; the originating goal is
22
- `spex session prompt <id>`. For a claude-harness session the transcript id IS the SpexCode session id;
22
+ `spex session show <id>` (the record's prompt). For a claude-harness session the transcript id IS the SpexCode session id;
23
23
  for codex it is `harness_session_id`.
24
24
  - **Any other session**: treat the arg as the harness's own id. The transcript carries `cwd` (and, unless
25
25
  the worktree was detached, a branch) — the digest header surfaces them; that is your join to its desk.
26
26
 
27
27
  ## 2 · digest the transcript — mechanical first, model second
28
28
 
29
- `node .spec/<root>/.config/distill/digest.mjs <id-or-path>` locates the transcript (claude:
29
+ `node .spec/<root>/.plugins/distill/digest.mjs <id-or-path>` locates the transcript (claude:
30
30
  `$CLAUDE_CONFIG_DIR` and every `~/.claude*` config dir → `projects/*/<id>.jsonl`; codex: `$CODEX_HOME` or
31
31
  `~/.codex` → `sessions/**/rollout-*<id>.jsonl`) and prints a compact digest: the human's prompts in full,
32
32
  the agent's own text, tool calls as one-liners, error results, and a footer with the files it edited and
@@ -7,7 +7,7 @@ desc: Reverse-engineer a faithful spec tree out of existing code — responsibil
7
7
  kind: mutating
8
8
  ---
9
9
  Reverse-engineer a spec tree for the target source area(s) below — code that has no specs yet. Aim for a
10
- tree where every governed file is claimed and `spex lint` is clean, with bodies at contract altitude and in
10
+ tree where every governed file is claimed and `spex spec lint` is clean, with bodies at contract altitude and in
11
11
  the codebase's own primary language (a predominantly-Chinese repo → Chinese specs).
12
12
 
13
13
  {{targets}}
@@ -42,11 +42,11 @@ Then grow nodes under that spine:
42
42
  dignifying it with a confident spec.
43
43
 
44
44
  **Give every frontend node a loss signal.** A node that governs UI or visual code (`.tsx`/`.jsx`/`.vue`/
45
- `.svelte`/`.css`, or the dashboard) is a blind spot until it carries a `yatsu.md` — so write one as you
45
+ `.svelte`/`.css`, or the dashboard) is a blind spot until it carries a `eval.md` — so write one as you
46
46
  extract it: a **real user-path** scenario — a goal and the steps to reach it through the running app (never a
47
47
  bare render-check), covering a failure/empty/edge state — with a **description** of those steps and the
48
48
  **expected** zero-loss result. Frontend scenarios are measured by looking (YATU) — a screenshot filed with
49
- `spex yatsu eval <node> --image <png> --pass`. Backend nodes don't need one yet; run `spex yatsu scan` to
49
+ `spex eval add <node> --image <png> --pass`. Backend nodes don't need one yet; run `spex eval lint` to
50
50
  list the frontend nodes still uncovered.
51
51
 
52
52
  **Extract incrementally — don't plan the whole tree before writing.** For a large area (hundreds of files),
@@ -57,4 +57,4 @@ one subtree uncommitted, so progress survives context limits.
57
57
 
58
58
  Confirm `spexcode.json`'s `governedRoots` points at the real source dirs first — lint reads silently empty
59
59
  otherwise. Commit one node per commit (`spec: <id> — extract from <area>`) with a `Session:` trailer, and
60
- run `spex lint` after each: it must reach 0 errors, 0 coverage warnings, 0 altitude warnings.
60
+ run `spex spec lint` after each: it must reach 0 errors, 0 coverage warnings, 0 altitude warnings.
@@ -6,4 +6,4 @@ hue: 280
6
6
  desc: A config plugin — agents link an issue they open to the spec node it serves via a `Spec: <id>` body line.
7
7
  code:
8
8
  ---
9
- When you open a GitHub issue, link it to the spec node(s) it serves by adding a line to the issue **body**: `Spec: <node-id>` (comma-separate several). The id is the node's **leaf** name — the folder under `.spec/…/<id>/spec.md`, e.g. `sessions`, never the slash-path. An unrecognized id silently links nothing, so use a real node id (`spex board` lists them). A pull request needs no marker: opening it from your `node/<id>` branch links it for free.
9
+ When you open a GitHub issue, link it to the spec node(s) it serves by adding a line to the issue **body**: `Spec: <node-id>` (comma-separate several). The id is the node's **leaf** name — the folder under `.spec/…/<id>/spec.md`, e.g. `sessions`, never the slash-path. An unrecognized id silently links nothing, so use a real node id (`spex graph --json` lists them). A pull request needs no marker: opening it from your `node/<id>` branch links it for free.
@@ -18,8 +18,8 @@ Read before you move: the over-broad node's spec, every child's spec, and the `[
18
18
 
19
19
  Honor these:
20
20
 
21
- - **Reparent, never rewrite.** Move a node by `git mv`-ing its folder; its id (= folder basename), its `[[links]]`, its `code:` governance, and its `yatsu.md` all ride along untouched. If a child needs its body edited to belong in a group, it doesn't belong.
21
+ - **Reparent, never rewrite.** Move a node by `git mv`-ing its folder; its id (= folder basename), its `[[links]]`, its `code:` governance, and its `eval.md` all ride along untouched. If a child needs its body edited to belong in a group, it doesn't belong.
22
22
  - **Fewest, deepest-justified parents.** Don't trade one flat layer for six two-child wrappers — that relocates the sprawl instead of resolving it. Between two passing groupings, take the one that adds fewer nodes.
23
23
  - Parent ids name the concept (kebab-case); give the parent a `hue` near its children's family.
24
24
 
25
- Work the order: (1) read everything; (2) write each candidate seam with the one-sentence intent that justifies it — kill the unjustifiable, and mark each survivor as reparent-under-existing or new-parent; (3) make the moves, one reviewable commit per group (`spec: <parent> — regroup <children>`, with a `Session:` trailer); (4) run `spex lint` and confirm breadth is resolved with no new errors. Uncommitted `git mv`s churn the drift count transiently — committing settles it; don't chase it.
25
+ Work the order: (1) read everything; (2) write each candidate seam with the one-sentence intent that justifies it — kill the unjustifiable, and mark each survivor as reparent-under-existing or new-parent; (3) make the moves, one reviewable commit per group (`spec: <parent> — regroup <children>`, with a `Session:` trailer); (4) run `spex spec lint` and confirm breadth is resolved with no new errors. Uncommitted `git mv`s churn the drift count transiently — committing settles it; don't chase it.
@@ -10,9 +10,9 @@ code:
10
10
 
11
11
  If your task is to FIX A BUG, reproduce it *first*, as a measurement — before you touch the fix. A claim that something is broken is worth nothing until the loss signal shows it broken; a claim that you fixed it is worth nothing until the same signal shows it passing. So a bug fix is bracketed by two readings of ONE scenario:
12
12
 
13
- - **A — reproduce (fail).** Find the yatsu scenario whose expected the bug violates (if none fits, ADD one to the node's `yatsu.md` — a description + the expected correct behaviour), run it, and file the failing reading with evidence that SHOWS the bug: `spex yatsu eval <node> --scenario <s> --fail --note "<what's wrong>"` plus an `--image`/`--video` of the actual broken behaviour. This is not ceremony — reproducing is how you learn what actually breaks, and a fix aimed at an unreproduced bug aims at a guess.
14
- - **B — fix, then re-measure (pass).** Make the code honor the spec, run the SAME scenario again, and file the passing reading with evidence of the corrected behaviour: `spex yatsu eval <node> --scenario <s> --pass`.
13
+ - **A — reproduce (fail).** Find the eval scenario whose expected the bug violates (if none fits, ADD one to the node's `eval.md` — a description + the expected correct behaviour), run it, and file the failing reading with evidence that SHOWS the bug: `spex eval add <node> --scenario <s> --fail --note "<what's wrong>"` plus an `--image`/`--video` of the actual broken behaviour. This is not ceremony — reproducing is how you learn what actually breaks, and a fix aimed at an unreproduced bug aims at a guess.
14
+ - **B — fix, then re-measure (pass).** Make the code honor the spec, run the SAME scenario again, and file the passing reading with evidence of the corrected behaviour: `spex eval add <node> --scenario <s> --pass`.
15
15
 
16
- The two readings on the same scenario are the **A/B** — the error→correct transition, the fix's proof-of-work. yatsu keeps per-scenario reading history, so the pair is durable and navigable end to end.
16
+ The two readings on the same scenario are the **A/B** — the error→correct transition, the fix's proof-of-work. eval keeps per-scenario reading history, so the pair is durable and navigable end to end.
17
17
 
18
18
  Don't skip A because the fix looks obvious — an obvious fix with no reproduced failure leaves the loss signal blind to exactly the regression you just closed. This does not apply to building new intent (there is no prior failure to reproduce) — it is the discipline for **repair**: keep the loss signal honest across a bug's whole lifecycle, not just at the end.