spexcode 0.3.0 → 0.4.1

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 (59) hide show
  1. package/package.json +1 -1
  2. package/spec-cli/src/anchors.ts +341 -0
  3. package/spec-cli/src/cli.ts +20 -20
  4. package/spec-cli/src/gateway.ts +8 -1
  5. package/spec-cli/src/git.ts +21 -13
  6. package/spec-cli/src/graph.ts +13 -7
  7. package/spec-cli/src/guide.ts +63 -15
  8. package/spec-cli/src/harness.ts +85 -34
  9. package/spec-cli/src/help.ts +33 -17
  10. package/spec-cli/src/index.ts +39 -7
  11. package/spec-cli/src/init.ts +5 -4
  12. package/spec-cli/src/lint.ts +103 -28
  13. package/spec-cli/src/localIssues.ts +19 -0
  14. package/spec-cli/src/migrate-table.ts +27 -16
  15. package/spec-cli/src/search.bench.mjs +2 -2
  16. package/spec-cli/src/session-timeline.ts +148 -0
  17. package/spec-cli/src/sessions.ts +29 -4
  18. package/spec-cli/src/specs.ts +44 -21
  19. package/spec-cli/templates/hooks/prepare-commit-msg +6 -1
  20. package/spec-cli/templates/spec/project/.plugins/{extract → commands/extract}/spec.md +1 -1
  21. package/spec-cli/templates/spec/project/.plugins/commands/spec.md +16 -0
  22. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  23. package/spec-cli/templates/spec/project/.plugins/core/spec.md +1 -1
  24. package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
  25. package/spec-cli/templates/spec/project/.plugins/skills/spec.md +17 -0
  26. package/spec-cli/templates/spec/project/.plugins/spec.md +5 -2
  27. package/spec-cli/templates/spec/project/spec.md +1 -1
  28. package/spec-dashboard/dist/assets/{Dashboard-C7Bzsv86.js → Dashboard-C_fGmOKK.js} +3 -3
  29. package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +2 -0
  30. package/spec-dashboard/dist/assets/{FoldToggle-D5iB4Ac2.js → FoldToggle-x9gtO1OQ.js} +1 -1
  31. package/spec-dashboard/dist/assets/{IssuesPage-CMFTsQhg.js → IssuesPage-5f_vL-JV.js} +1 -1
  32. package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +1 -0
  33. package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +66 -0
  34. package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +13 -0
  35. package/spec-dashboard/dist/assets/{Settings-BW5f0OaW.js → Settings-DKb5Ji_X.js} +1 -1
  36. package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +41 -0
  37. package/spec-dashboard/dist/assets/index-BbMkwuix.css +1 -0
  38. package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +6 -0
  39. package/spec-dashboard/dist/index.html +2 -2
  40. package/spec-eval/src/cli.ts +82 -21
  41. package/spec-eval/src/evaltab.ts +15 -6
  42. package/spec-eval/src/humanok.ts +43 -0
  43. package/spec-eval/src/scenarios.ts +116 -4
  44. package/spec-eval/src/sidecar.ts +35 -9
  45. package/spec-cli/templates/presets/careful/.plugins/clarify-before-code/spec.md +0 -11
  46. package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +0 -8
  47. package/spec-dashboard/dist/assets/EvalsPage-DKZZIdHq.js +0 -2
  48. package/spec-dashboard/dist/assets/MobileApp-DwuTKgdP.js +0 -1
  49. package/spec-dashboard/dist/assets/SessionInterface-CBS5_cmK.js +0 -71
  50. package/spec-dashboard/dist/assets/SessionWindow-CqAnjWfI.js +0 -9
  51. package/spec-dashboard/dist/assets/index-Cc26X4ce.css +0 -1
  52. package/spec-dashboard/dist/assets/index-Ce0wDyQS.js +0 -41
  53. /package/spec-cli/templates/spec/project/.plugins/{regroup → commands/regroup}/spec.md +0 -0
  54. /package/spec-cli/templates/spec/project/.plugins/{tidy → commands/tidy}/spec.md +0 -0
  55. /package/spec-cli/templates/spec/project/.plugins/{forge-link → prompts/forge-link}/spec.md +0 -0
  56. /package/spec-cli/templates/spec/project/.plugins/{memory-hygiene → prompts/memory-hygiene}/spec.md +0 -0
  57. /package/spec-cli/templates/spec/project/.plugins/{reproduce-before-fix → prompts/reproduce-before-fix}/spec.md +0 -0
  58. /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/digest.mjs +0 -0
  59. /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/spec.md +0 -0
@@ -2,6 +2,7 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs'
2
2
  import { readFile, readdir } from 'node:fs/promises'
3
3
  import { join, relative, basename } from 'node:path'
4
4
  import { repoRoot, historyIndex, rowsFor, statsFor, pathsStats, driftIndex, driftFor, fileDiffAt } from './git.js'
5
+ import { parseCodeEntry, parseRelation } from './anchors.js'
5
6
 
6
7
  // a node is any directory under .spec holding a spec.md; its parent is the nearest ancestor that also holds one.
7
8
  const ROOT = repoRoot()
@@ -97,7 +98,7 @@ function walk(dir: string, parent: string | null, acc: Raw[]) {
97
98
  // single token — never a '/'-joined path, which would break every `:id` route and fetch that treats an id
98
99
  // as one path segment. So the disambiguation separator is '_': like '/' it never occurs inside a dir
99
100
  // 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. `.plugins_spec-scout`) stays a single token everywhere it is resolved.
101
+ // so a collision-qualified id (e.g. `.plugins_<id>`) stays a single token everywhere it is resolved.
101
102
  // Exported as the ONE mint every id producer shares: spec-eval mints its node ids through this same
102
103
  // function over this same universe (every spec node), so a colliding leaf carries one canonical id
103
104
  // system-wide instead of a second, diverging bare-leaf scheme.
@@ -171,17 +172,21 @@ function claimMatcher(file: string): (cf: string) => boolean {
171
172
  }
172
173
 
173
174
  // spec node(s) that GOVERN a file (frontmatter `code:` — source of truth, drives drift + eval freshness); reads only
174
- // frontmatter (cheap, no git) so a per-edit hook can call it.
175
- export function specOwners(file: string): { id: string; desc: string }[] {
175
+ // frontmatter (cheap, no git) so a per-edit hook can call it. `scoped` = every claiming entry carries a
176
+ // `#selector` such a governor still displays, but does not count toward the owners bound ([[code-anchor]]).
177
+ export function specOwners(file: string): { id: string; desc: string; scoped: boolean }[] {
176
178
  const claims = claimMatcher(file)
177
- return raws().filter((r) => list(r.fm.code).some(claims)).map((r) => ({ id: r.id, desc: str(r.fm.desc) }))
179
+ return raws().flatMap((r) => {
180
+ const entries = list(r.fm.code).map(parseCodeEntry).filter((e) => claims(e.path))
181
+ return entries.length ? [{ id: r.id, desc: str(r.fm.desc), scoped: entries.every((e) => e.anchor !== null) }] : []
182
+ })
178
183
  }
179
184
 
180
185
  // spec node(s) that REFERENCE a file (frontmatter `related:` — carries coverage, never drift, never eval freshness):
181
186
  // [[governed-related]]'s other half, same claim rule, same cheap frontmatter-only read.
182
187
  export function specRelated(file: string): { id: string; desc: string }[] {
183
188
  const claims = claimMatcher(file)
184
- return raws().filter((r) => list(r.fm.related).some(claims)).map((r) => ({ id: r.id, desc: str(r.fm.desc) }))
189
+ return raws().filter((r) => list(r.fm.related).some((e) => claims(parseCodeEntry(e).path))).map((r) => ({ id: r.id, desc: str(r.fm.desc) }))
185
190
  }
186
191
 
187
192
  // memo fileDiffAt by (version sha + spec.md path) — a commit's patch is immutable. Keyed by path too: one
@@ -226,8 +231,18 @@ export async function loadSpecs() {
226
231
  // session = the Session: trailer of the node's latest version; frontmatter `session:` is the fallback.
227
232
  const fmSession = str(r.fm.session)
228
233
  const session = h[0]?.session || (fmSession && fmSession !== 'null' ? fmSession : null)
229
- const code = list(r.fm.code)
230
- const related = list(r.fm.related)
234
+ // a code:/related: row may pin symbols (`path#fn` — [[code-anchor]]): parseRelation groups each
235
+ // relation per BASE path, so `code`/`related` carry the distinct PATHS (what every path consumer —
236
+ // drift, claims, eval attribution — expects, file-level as before), the scoped entries (path +
237
+ // selectors) ride separately for lint's anchor engine, and structural problems (duplicates,
238
+ // bare/scoped mixing, glob selectors, the code cap) surface as lint integrity errors.
239
+ const codeRel = parseRelation(list(r.fm.code), 'code')
240
+ const relatedRel = parseRelation(list(r.fm.related), 'related')
241
+ const code = codeRel.entries.map((e) => e.path)
242
+ const codeScoped = codeRel.entries.filter((e) => e.selectors.length > 0)
243
+ const related = relatedRel.entries.map((e) => e.path)
244
+ const relatedScoped = relatedRel.entries.filter((e) => e.selectors.length > 0)
245
+ const relationProblems = [...codeRel.problems, ...relatedRel.problems]
231
246
  const S = h[0]?.hash || ''
232
247
  const driftFiles = code
233
248
  .map((f) => ({ file: f, behind: driftFor(didx, S, f) }))
@@ -235,8 +250,10 @@ export async function loadSpecs() {
235
250
  const drift = driftFiles.reduce((a, d) => a + d.behind, 0)
236
251
  // related drift is the SOFT tier ([[governed-related]]): same ancestry basis, but it stays OUT of
237
252
  // `drift` — it never feeds status, the commit gate, or eval freshness. It surfaces only as a lint warn nudge.
238
- const relatedDriftFiles = related
239
- .map((f) => ({ file: f, behind: driftFor(didx, S, f) }))
253
+ // A SCOPED related entry is excluded here: its file-level movement is silent by design — only a
254
+ // selector HIT warns, and that verdict needs the anchor engine, so lint derives it, not the loader.
255
+ const relatedDriftFiles = relatedRel.entries.filter((e) => !e.selectors.length)
256
+ .map((e) => ({ file: e.path, behind: driftFor(didx, S, e.path) }))
240
257
  .filter((d) => d.behind > 0)
241
258
  const fmStatus = str(r.fm.status, '') || null
242
259
  return {
@@ -250,7 +267,10 @@ export async function loadSpecs() {
250
267
  hue: Number(str(r.fm.hue, '210')),
251
268
  desc: str(r.fm.desc),
252
269
  code,
270
+ codeScoped,
253
271
  related,
272
+ relatedScoped,
273
+ relationProblems,
254
274
  version: h.length,
255
275
  reason: h[0]?.reason || '',
256
276
  // ISO date of the node's latest version commit (h is newest-first), or null if unversioned.
@@ -271,7 +291,7 @@ export async function loadSpecs() {
271
291
  export async function specHistory(id: string) {
272
292
  const node = raws().find((r) => r.id === id)
273
293
  if (!node) return []
274
- const codePaths = list(node.fm.code)
294
+ const codePaths = [...new Set(list(node.fm.code).map((e) => parseCodeEntry(e).path))]
275
295
  // index (cached) and the code-path walk are independent — run them in parallel, both async git.
276
296
  const [idx, cStats] = await Promise.all([historyIndex(ROOT), pathsStats(ROOT, codePaths)])
277
297
  const sStats = statsFor(idx, node.relPath)
@@ -302,11 +322,11 @@ export async function specDiffAt(id: string, hash: string) {
302
322
  // harness lifecycle events the node binds, its deterministic intra-event order, and whether it intends to
303
323
  // block (honored only on block-capable events). See loadHookConfig + the hook compiler/dispatcher.
304
324
  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 plugin is a FLAT direct child of a plugin root (`<root>/<name>/spec.md`)
306
- // that carries a `surface: system|command|hook|skill|agent` frontmatter field naming where it plugs in. There are no
325
+ // field-driven surface - a plugin is a spec node at ANY depth under a plugin root that carries a
326
+ // `surface: system|command|hook|skill|agent|review` frontmatter field naming where it plugs in. There are no
307
327
  // `command/`/`system/`/`hook/`/`skill/`/`agent/` bucket dirs (those were graph-invisible grouping dirs with no spec.md, so
308
328
  // 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 plugin roots participate: `.plugins` (the instance — DIY dev-flow plugins) and
329
+ // graph child (a grouping parent like `.plugins/prompts` is itself a spec node, never a bare dir). BOTH plugin roots participate: `.plugins` (the instance — DIY dev-flow plugins) and
310
330
  // `plugin-system` (the project system spec). loadConfig gathers the `command` surface, loadSystemConfig the `system`
311
331
  // surface, loadHookConfig the `hook` surface, loadSkillConfig the `skill` surface, loadAgentConfig the `agent`
312
332
  // 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).
@@ -354,14 +374,13 @@ function bundleFiles(dir: string): string[] {
354
374
  return out.sort()
355
375
  }
356
376
  // gather the preset nodes under a plugin root that declare `surface: <surface>`. The scan is RECURSIVE —
357
- // `surface` is a FIELD, not a path (the design's core tenet), so a plugin may live at ANY depth and a
358
- // grouping parent may itself be a plugin (e.g. `.plugins/core` is a `surface: system` contract whose CHILDREN
359
- // are `surface: hook` nodes). The field filter keeps it safe: a node only gathers if it declares THIS
360
- // surface, so descending past a matched node never double-counts (children carry a different surface). For
361
- // `system`/`command` the result is identical to the old one-level scan on the current tree — every existing
362
- // such node is a flat direct child and no nested node declares those surfaces — so the gather set (hence
363
- // the appended system prompt and the command dropdown) is byte-for-byte unchanged.
364
- function loadSurface(surface: 'command' | 'system' | 'hook' | 'skill' | 'agent'): ConfigPreset[] {
377
+ // `surface` is a FIELD, not a path (the design's core tenet), so a plugin may live at ANY depth: under a
378
+ // surface-less grouping shelf (the auxiliary `surface: system` contracts live under `.plugins/prompts/`),
379
+ // or under a plugin that is itself a grouping parent (`.plugins/core` is a `surface: system` contract whose
380
+ // CHILDREN are `surface: hook` nodes). The field filter keeps it safe: a node only gathers if it declares THIS
381
+ // surface, so descending past a matched node never double-counts (children carry a different surface),
382
+ // and the gather set is path-independent regrouping a plugin never changes what materializes.
383
+ function loadSurface(surface: 'command' | 'system' | 'hook' | 'skill' | 'agent' | 'review'): ConfigPreset[] {
365
384
  const out: ConfigPreset[] = []
366
385
  const visit = (nodeDir: string, name: string) => {
367
386
  if (existsSync(join(nodeDir, 'spec.md'))) {
@@ -412,3 +431,7 @@ export function loadSkillConfig(): ConfigPreset[] { return loadSurface('skill')
412
431
  // .claude/agents/<name>.md). Like a skill, the node's `desc` is the on-demand load-trigger and its `body` is the
413
432
  // agent's system prompt; additionally its `tools` field is the harness tool allowlist for the spawned agent.
414
433
  export function loadAgentConfig(): ConfigPreset[] { return loadSurface('agent') }
434
+ // the review-track prose presets ([[review-commands]]): offered in the eval detail's remark-composer `/`
435
+ // dropdown; picking one PREFILLS the composer with the node's `body` ({node}/{scenario}/{expected}
436
+ // placeholders filled at insert time). Display+prefill only — the send stays the ordinary remark write.
437
+ export function loadReviewConfig(): ConfigPreset[] { return loadSurface('review') }
@@ -26,7 +26,12 @@ if [ -z "$sid" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then # codex: alias the threa
26
26
  if [ -e "$rd/sessions/$CODEX_THREAD_ID/session.json" ]; then
27
27
  sid="$CODEX_THREAD_ID" # a governed record keyed by the thread id (rare/direct)
28
28
  else # else the record that captured it as harness_session_id
29
- hit=$(grep -lF "\"harness_session_id\": \"$CODEX_THREAD_ID\"" "$rd"/sessions/*/session.json 2>/dev/null | head -1)
29
+ # @@@ unmatched-is-noop - grep's no-match (exit 1) and an absent sessions glob (exit 2) are the
30
+ # NOT-GOVERNED case, not an error: any repo inherits a foreign CODEX_THREAD_ID from a codex session's
31
+ # shell, and under `set -euo pipefail` a bare grep|head assignment would abort the hook — and the
32
+ # commit — before the intended `[ -z "$sid" ] && exit 0` no-op. Genuine errors past this lookup
33
+ # (interpret-trailers etc.) still fail loud via set -e.
34
+ hit=$(grep -lF "\"harness_session_id\": \"$CODEX_THREAD_ID\"" "$rd"/sessions/*/session.json 2>/dev/null | head -1) || hit=""
30
35
  [ -n "$hit" ] && sid=$(basename "$(dirname "$hit")")
31
36
  fi
32
37
  fi
@@ -45,7 +45,7 @@ Then grow nodes under that spine:
45
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
- **expected** zero-loss result. Frontend scenarios are measured by looking (YATU) — a screenshot filed with
48
+ **expected** zero-loss result. Frontend scenarios are measured by looking through the running product, as a real user would — a screenshot filed with
49
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
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: commands
3
+ status: active
4
+ hue: 40
5
+ desc: Grouping shelf for the `surface: command` plugins — the `/`-dropdown launch presets a new session can pick. A shelf, not a surface — routing stays field-driven.
6
+ ---
7
+ # commands
8
+
9
+ The invocable **command** plugins live here: leaf plugins whose body is a launch preset a new session
10
+ picks from the `/` dropdown, each carrying `surface: command`. Grouping them keeps `.plugins/` legible at
11
+ a glance — the command presets on this shelf, the skill plugins on `skills/`, the auxiliary system
12
+ contracts on `prompts/`, with `core` a flat child beside them.
13
+
14
+ This node is a **shelf, not a surface**: it declares no `surface` field and gathers nothing itself.
15
+ Discovery is recursive and field-driven, so a resident plugs in exactly as it would at the root — a
16
+ plugin that serves both surfaces shelves once by its primary identity, never duplicated.
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: supervisor
3
+ surface: command
4
+ status: active
5
+ hue: 280
6
+ desc: Launch a supervisor agent that manages other agents from the main checkout to drive a goal to completion.
7
+ ---
8
+ You are a SpexCode supervisor — a **manager**, not a feature worker. Your work base is the main checkout (the repository root), NOT your own worktree: do all git via `git -C <root>`, everything else via the `spex` CLI, and never write feature code. **FIRST, read the project's agent notes (`<root>/CLAUDE.md` / `AGENTS.md`, including the materialized `<!-- spexcode -->` contract block)** for project-specific rules; the loop below (dispatch → monitor → review → merge → close, parallelizing independent tasks) is your playbook. Then drive the goal: decompose it into worker-sized tasks and dispatch one worker per independent task (`spex session new "<task>"` — give each ONLY its task; a task about one specific node mentions it as `[[<id>]]`, which only sets the branch name and board attribution; the session's real node links come from what it edits), monitor with `spex session watch`, review proposals with `spex session review <id>`, merge good ones with `git -C <root> merge --no-ff <branch>`, then close. Never let a worker self-merge; keep `spex spec lint` at 0 errors. To WAIT on a worker, POLL one-shot (`spex session review <id>` or `spex session ls` — both return immediately); never block on `spex session watch`, which STREAMS forever and will freeze your turn. One footgun that bites a fresh supervisor: before `spex session close <id>`, confirm the merge landed (`git -C <root> log -1` shows HEAD at the new merge commit) — closing an unmerged branch discards the work. Report progress as you go and when the goal is complete. Your goal follows:
@@ -10,7 +10,7 @@ The CLI speaks ONE grammar: `spex <noun> <verb> [object] [flags]`. Six nouns —
10
10
 
11
11
  Four disciplines, non-negotiable:
12
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.
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. 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
14
 
15
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
16
 
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: prompts
3
+ status: active
4
+ hue: 110
5
+ desc: Grouping shelf for the auxiliary `surface: system` prompt contracts — single-body always-on prose. `core` is not a resident — the core contract subsystem is a direct `.plugins` child. A shelf, not a surface — routing stays field-driven.
6
+ ---
7
+ # prompts
8
+
9
+ The **auxiliary** `surface: system` prompt contracts live here: leaf plugins whose whole substance is one
10
+ prose body an agent must always carry — materialized (in name order, together with every other system
11
+ body) into the `<!-- spexcode -->` contract block — rather than a verb it invokes. Grouping them keeps
12
+ `.plugins/` legible at a glance: peripheral prompt contracts on this shelf; the invocable surfaces
13
+ (command/skill/agent) and `core` as flat children beside it.
14
+
15
+ The shelf boundary is **leafness, not surface**: a single-body prose contract shelves here, while a
16
+ contract that anchors its own subtree outranks the shelf — `core`, the contract subsystem whose children
17
+ are the `surface: hook` gates, is a *peer* of this shelf, never a resident.
18
+
19
+ This node is a **shelf, not a surface**: it declares no `surface` field and gathers nothing itself.
20
+ Discovery is recursive and field-driven, so residents plug in exactly as they would at the root.
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: skills
3
+ status: active
4
+ hue: 280
5
+ desc: Grouping shelf for the `surface: skill` plugins — the harness skills an agent invokes on demand. A shelf, not a surface — routing stays field-driven.
6
+ ---
7
+ # skills
8
+
9
+ The invocable **skill** plugins live here: leaf plugins that materialize into the harness's skill dir,
10
+ where an agent invokes them on demand, each carrying `surface: skill`. Grouping them keeps `.plugins/`
11
+ legible at a glance — the skill plugins on this shelf, the command presets on `commands/`, the auxiliary
12
+ system contracts on `prompts/`, with `core` a flat child beside them.
13
+
14
+ This node is a **shelf, not a surface**: it declares no `surface` field and gathers nothing itself.
15
+ Discovery is recursive and field-driven, so a resident plugs in exactly as it would at the root. A plugin
16
+ that serves both surfaces shelves here by its primary (skill) identity and still gathers as a command
17
+ through its field.
@@ -6,8 +6,11 @@ desc: The instance home — this repo's DIY dev-flow plugins live here as skill-
6
6
  ---
7
7
  `.plugins/` is the **instance** of the plugin system: the concrete dev-flow plugins this repo ships for
8
8
  working in it. Each plugin is a skill-shaped node — its folder *is* the unit (a `spec.md` plus any
9
- co-located scripts) — living as a flat child of `.plugins/` and carrying a `surface: command|system` field
10
- that names where it plugs in.
9
+ co-located scripts) — carrying a `surface: command|system|…` field that names where it plugs in.
10
+ Discovery is recursive and field-driven, so plugins may sit under a grouping shelf: the auxiliary
11
+ `surface: system` prompt contracts live under `prompts/`, the `surface: command` presets under
12
+ `commands/`, the `surface: skill` plugins under `skills/`, while `core` — the contract subsystem whose
13
+ children are the `surface: hook` gates — sits as a flat child here.
11
14
 
12
15
  The launcher's system gather and the new-session dropdown read from here. Only **active** plugins
13
16
  gather: a `pending` node is declared intent, not yet an active plugin. The seed ships `core`
@@ -12,7 +12,7 @@ made it. This node is the founding spec everything else hangs from — **rewrite
12
12
  your own project**, then grow child package/feature nodes beneath it (each its own directory with a
13
13
  `spec.md`).
14
14
 
15
- `.plugins/` holds the dev-flow plugins this instance ships — skill-shaped flat child nodes, each tagged
15
+ `.plugins/` holds the dev-flow plugins this instance ships — skill-shaped child nodes, each tagged
16
16
  with a `surface` field: a `surface: system` node folds into every launched agent's system prompt as
17
17
  always-on contract (the seed ships `core`), and a `surface: command` node is a prompt preset the
18
18
  new-session dropdown composes over target nodes (the seed ships `tidy`). Add, edit, or