spexcode 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/spec-cli/hooks/dispatch.sh +28 -40
- package/spec-cli/hooks/harness.sh +29 -19
- package/spec-cli/src/cli.ts +30 -12
- package/spec-cli/src/commit-surgery.ts +81 -0
- package/spec-cli/src/contract-filter.ts +16 -14
- package/spec-cli/src/doctor.ts +11 -5
- package/spec-cli/src/guide.ts +69 -59
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +23 -14
- package/spec-cli/src/help.ts +23 -17
- package/spec-cli/src/init.ts +5 -40
- package/spec-cli/src/layout.ts +26 -20
- package/spec-cli/src/materialize.ts +102 -126
- package/spec-cli/src/mentions.ts +5 -3
- package/spec-cli/src/plugin-harness.ts +10 -9
- package/spec-cli/src/sessions.ts +56 -28
- package/spec-cli/src/specs.ts +9 -4
- package/spec-cli/src/uninstall.ts +6 -5
- package/spec-cli/src/worktree-sources.ts +6 -4
- package/spec-cli/templates/hooks/post-checkout +22 -0
- package/spec-cli/templates/hooks/post-merge +15 -9
- package/spec-cli/templates/hooks/pre-commit +10 -0
- package/spec-cli/templates/spec/project/.config/distill/digest.mjs +136 -0
- package/spec-cli/templates/spec/project/.config/distill/spec.md +74 -0
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-CMRJGfYI.js → Dashboard-P0B9ukSG.js} +7 -7
- package/spec-dashboard/dist/assets/{EvalsPage-Dj2mxcfW.js → EvalsPage-BrvAGyc4.js} +1 -1
- package/spec-dashboard/dist/assets/{FoldToggle-BfNpeyRQ.js → FoldToggle-BuQ0lokE.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-DfY315kt.js → IssuesPage-H-D8aHEl.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-BGdC0A0P.js → MobileApp-oZXIeCPb.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-BOBCAR0t.js → SessionInterface-Blr_MEdU.js} +8 -8
- package/spec-dashboard/dist/assets/{SessionWindow-D7YmjV0i.js → SessionWindow-LcCzBMU7.js} +5 -5
- package/spec-dashboard/dist/assets/{Settings-BlSNmpH_.js → Settings-_yOye-In.js} +1 -1
- package/spec-dashboard/dist/assets/{index-BFdzpd_O.js → index-BhIslAau.js} +2 -2
- package/spec-dashboard/dist/index.html +1 -1
- package/spec-yatsu/src/cli.ts +3 -5
- package/spec-yatsu/src/evaltab.ts +3 -2
- package/spec-yatsu/src/filing.ts +3 -4
- package/spec-yatsu/src/freshness.ts +1 -3
- package/spec-yatsu/src/proof.ts +3 -2
- package/spec-yatsu/src/scenariofresh.ts +27 -5
- package/spec-yatsu/src/sidecar.ts +8 -8
- package/spec-yatsu/src/evaluator.ts +0 -24
package/spec-cli/src/guide.ts
CHANGED
|
@@ -40,7 +40,7 @@ Look these up on demand — the formats an agent authors, and the settings it co
|
|
|
40
40
|
spex guide yatsu the yatsu.md format (scenario schema + how loss is measured and filed)
|
|
41
41
|
spex guide config the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, lint
|
|
42
42
|
budgets, layout) — every field, and which of the two files it belongs in
|
|
43
|
-
spex guide footprint the
|
|
43
|
+
spex guide footprint the footprint model — what SpexCode plants in a repo, and who sees it
|
|
44
44
|
(committed | ignored | hidden), and every migration recipe`
|
|
45
45
|
|
|
46
46
|
const SPEC = `spex guide spec — the spec.md file format
|
|
@@ -176,7 +176,7 @@ reading (\`--last\` makes that explicit; repeat to peel junk back one filing at
|
|
|
176
176
|
|
|
177
177
|
THE SCOREBOARD: readings live in yatsu.evals.ndjson beside the yatsu.md — one JSON line per measurement
|
|
178
178
|
(a second git-as-database axis). Freshness is derived live from git: a reading goes STALE when a governed
|
|
179
|
-
code file
|
|
179
|
+
code file or the scenario (the yatsu.md) moves since it was filed.
|
|
180
180
|
spex yatsu scan [--changed] blind spots: yatsu-schema (malformed) · yatsu-drift (stale) ·
|
|
181
181
|
yatsu-missing (never measured) · yatsu-uncovered (governed source, no yatsu.md) ·
|
|
182
182
|
yatsu-owners (a file governed by > maxOwners scenarios — split it)
|
|
@@ -193,12 +193,12 @@ PORTABILITY, and picking the right one is the whole discipline:
|
|
|
193
193
|
identity, lint budgets, launcher NAMES. "Git is the database": tracked so the
|
|
194
194
|
team shares ONE configuration.
|
|
195
195
|
spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
|
|
196
|
-
paths
|
|
196
|
+
paths. Layered OVER spexcode.json (see MERGE
|
|
197
197
|
below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
|
|
198
198
|
|
|
199
199
|
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
200
200
|
icon, a lint budget, a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
201
|
-
PATH of a launcher wrapper
|
|
201
|
+
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
202
202
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
203
203
|
\`spex new\` or the dashboard without an explicit launcher choice.
|
|
204
204
|
|
|
@@ -324,77 +324,87 @@ Example — govern your own source dir and loosen the altitude budget:
|
|
|
324
324
|
'default'; seed-time only, read by init.ts).
|
|
325
325
|
harnesses which harness targets \`spex materialize\` delivers into — native ids ("claude"|"codex") or a
|
|
326
326
|
{ "plugin": "<folder>" } bundle. Default (omitted): all native harnesses. PERSISTENT and
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"hidden" is a HOST/person fact → spexcode.local.json. The spec DATA (.spec, spexcode.json)
|
|
333
|
-
has NO knob — always tracked. Full model + migration recipes: \`spex guide footprint\`.
|
|
334
|
-
private RETIRED (old private-overlay toggle) — read as "render": "hidden" with a loud migration
|
|
335
|
-
notice; its data-untrack semantics are gone. See \`spex guide footprint\` MIGRATIONS.`
|
|
336
|
-
|
|
337
|
-
const FOOTPRINT = `spex guide footprint — the share-axis model (what SpexCode plants in a repo, and who sees it)
|
|
327
|
+
git-transactional: the edit takes effect at the next git-native materialize anchor (the commit
|
|
328
|
+
that carries it, a checkout/merge that receives it, or a manual \`spex materialize\`) — a
|
|
329
|
+
deselected harness's artifacts are pruned by that pass.`
|
|
330
|
+
|
|
331
|
+
const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and who sees it (one fixed behavior per kind)
|
|
338
332
|
|
|
339
333
|
SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
|
|
340
334
|
measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
|
|
341
335
|
stitches the two ends into a closed loop. The footprint follows: the head+tail (.spec, spexcode.json,
|
|
342
336
|
readings) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
|
|
343
|
-
|
|
337
|
+
Materialized artifacts carry no facts, so they are NEVER tracked — there is exactly one residence
|
|
338
|
+
behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
344
339
|
|
|
345
|
-
── THE FOUR KINDS (
|
|
340
|
+
── THE FOUR KINDS (all fixed) ──
|
|
346
341
|
spec data .spec/ (incl .config/) + spexcode.json — ALWAYS tracked. Git is the database; there is
|
|
347
342
|
deliberately NO way to say "untrack the spec" in this schema.
|
|
348
343
|
machine facts spexcode.local.json, the hook shims (.claude/settings.json, .codex/hooks.json), plugin
|
|
349
|
-
bundles — NEVER tracked; always in the
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
bundles — NEVER tracked; always in the per-clone exclude.
|
|
345
|
+
artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
|
|
346
|
+
tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
|
|
352
347
|
run residue .worktrees/, the global store (~/.spexcode), .git/spexcode blobs — never tracked;
|
|
353
|
-
out-of-tree, or
|
|
354
|
-
|
|
355
|
-
──
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
348
|
+
out-of-tree, or exclude-ruled where in-tree.
|
|
349
|
+
|
|
350
|
+
── A CONTRACT FILE'S RESIDENCE IS A LIVE CONTENT FACT (re-judged at every materialize) ──
|
|
351
|
+
host-tracked → the clean/smudge content filter: the repo keeps the pristine host prose, your
|
|
352
|
+
working tree carries prose + block, status stays clean.
|
|
353
|
+
untracked, wholly ours→ one exclude entry. The exclude is the ignored-bit DECLARATION the rest of git
|
|
354
|
+
consults (checkout may overwrite, clean -fd spares, status/add -A/stash silent).
|
|
355
|
+
untracked, YOUR prose → the exclude entry is withdrawn (hiding user content would be data-loss shaped)
|
|
356
|
+
entered the file and the clean filter is pre-armed: the file shows honestly as untracked, and
|
|
357
|
+
IF you choose to \`git add\` it, the block is stripped automatically — tracking
|
|
358
|
+
is always your act; SpexCode never stages or commits anything for you.
|
|
359
|
+
|
|
360
|
+
── THE GIT-NATIVE ANCHORS (no harness event ever triggers a materialize) ──
|
|
361
|
+
spex init / spex materialize / session-worktree creation — the explicit passes;
|
|
362
|
+
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
|
|
365
|
+
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
|
|
367
|
+
effect at the commit/checkout/merge that carries them, like any other source change.
|
|
368
|
+
An environment with no spex-planted hooks (CI, a cloud agent's fresh clone, a teammate who hasn't
|
|
369
|
+
installed) simply runs \`spex materialize\` in its setup step — there is no committed-artifact mode.
|
|
370
|
+
TRACK ≠ PUSH: none of this ever touches remotes; where commits GO is branch/remote policy.
|
|
372
371
|
|
|
373
372
|
── GUARANTEES (the forgetting law) ──
|
|
374
|
-
materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every
|
|
375
|
-
SpexCode's own identity stamps, then re-asserts
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
── THE CONTENT FILTER (
|
|
382
|
-
Per-clone only — git config filter.spexcode.* + .git/info/attributes + a shim under .git/spexcode/ —
|
|
383
|
-
planted
|
|
384
|
-
sees it); smudge re-injects it on checkout. A missing shim degrades to
|
|
385
|
-
own edits to the
|
|
373
|
+
materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every materialize first ERASES all landing points by
|
|
374
|
+
SpexCode's own identity stamps, then re-asserts — legacy states (a .gitignore managed block, a committed
|
|
375
|
+
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
|
|
377
|
+
clones and session worktrees are self-sufficient: data by checkout, materialized artifacts by
|
|
378
|
+
re-materialize, the machine snapshot (spexcode.local.json) by copy.
|
|
379
|
+
|
|
380
|
+
── THE CONTENT FILTER (mixed-content contract files) ──
|
|
381
|
+
Per-clone only — git config filter.spexcode.* + .git/info/attributes + a shim under .git/spexcode/ —
|
|
382
|
+
planted where mixed content exists or is imminent (tracked, or untracked with your prose). clean strips
|
|
383
|
+
the sentinel block (history never sees it); smudge re-injects it on checkout. A missing shim degrades to
|
|
384
|
+
identity (never a git fatal). Your own edits to the prose still show as real modifications; only the
|
|
385
|
+
block is invisible to git.
|
|
386
|
+
|
|
387
|
+
── PRIVATE LOCAL NODES (manual posture — works today) ──
|
|
388
|
+
Spec data is always tracked, but tracked WHERE is yours to choose: to keep a node off the shared
|
|
389
|
+
remote, give it a different git HOME instead of untracking it. The manual recipe:
|
|
390
|
+
1. create the node dir under .spec/ as usual (spec.md and friends);
|
|
391
|
+
2. add the dir's path to .git/info/exclude — per-clone, so the shared repo never sees it;
|
|
392
|
+
3. inside the dir, \`git init --separate-git-dir\` pointing somewhere under the main repo's .git/
|
|
393
|
+
(e.g. .git/spexcode/<name>.git) — the dir then holds only a one-line .git pointer file, so the
|
|
394
|
+
spec loader never walks an object store;
|
|
395
|
+
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
|
|
397
|
+
views are blind to it (version count, the history tab, drift), and a dispatched worker's worktree
|
|
398
|
+
checkout does not contain it. Those gaps are what the pending spec-local design (a first-class
|
|
399
|
+
private overlay root) closes — not built yet. Cautions: \`git clean -fdx\` in the outer repo deletes
|
|
400
|
+
the inner repo along with the dir, so off-machine backup means giving the INNER repo its own private
|
|
401
|
+
remote; and taking the node public later is a migration (move it into the shared tree and commit),
|
|
402
|
+
not a flag flip.
|
|
386
403
|
|
|
387
404
|
── MIGRATIONS ──
|
|
388
|
-
|
|
389
|
-
reads the old flag as hidden and prints this recipe until you migrate.
|
|
390
|
-
legacy untracked spec the retired mode also untracked .spec + spexcode.json. Track them once:
|
|
391
|
-
(untrack-private) git add .spec spexcode.json # then commit on your branch
|
|
405
|
+
legacy untracked spec track the sources once: git add .spec spexcode.json (commit on your branch)
|
|
392
406
|
WARNING: tracking is not retroactive secrecy — history already pushed
|
|
393
407
|
elsewhere cannot be recalled.
|
|
394
|
-
hidden → committed/ignored just edit \`render\` — the hook gate re-renders on the very next harness
|
|
395
|
-
event (the gate key covers spexcode.json/.local), or run \`spex materialize\`
|
|
396
|
-
to apply immediately; the erase phase unplants the filter/exclude block in
|
|
397
|
-
the same pass. Narrowing \`harnesses\` self-heals the same way.
|
|
398
408
|
back out entirely \`spex uninstall\` (add --hooks to also remove the spexcode git hooks).`
|
|
399
409
|
|
|
400
410
|
const TOPICS: Record<string, string> = { spec: SPEC, yatsu: YATSU, config: CONFIG, footprint: FOOTPRINT }
|
|
@@ -2,9 +2,9 @@ import { HARNESSES, type Harness, type HarnessId } from './harness.js'
|
|
|
2
2
|
|
|
3
3
|
// @@@ harness-select - the DECLARATIVE choice of WHICH harness targets `spex materialize` delivers the
|
|
4
4
|
// SpexCode system into. The selection is persistent config (spexcode.json's `harnesses`), NOT a one-shot
|
|
5
|
-
// flag, because materialize
|
|
5
|
+
// flag, because materialize re-runs at every git-native anchor ([[commit-surgery]]), so the intent
|
|
6
6
|
// must live where every re-materialize can read it. This module owns ONLY the vocabulary + validation; the
|
|
7
|
-
// per-harness write/clean mechanics live on the [[harness-adapter]], the
|
|
7
|
+
// per-harness write/clean mechanics live on the [[harness-adapter]], the materialize loop on [[harness-delivery]].
|
|
8
8
|
|
|
9
9
|
// a resolved DELIVERY TARGET. Either a NATIVE harness (claude/codex — its adapter writes shims/contract/trust
|
|
10
10
|
// directly), or a PLUGIN bundle dropped into a host-agent-scanned folder. The plugin EMITTER is a later node;
|
package/spec-cli/src/harness.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { git } from './git.js'
|
|
|
20
20
|
// dispatch.sh's first argument (`bash <dispatch> <id> <Event>`). dispatch.sh exports SPEXCODE_HARNESS, so a
|
|
21
21
|
// hook subprocess learns its harness deterministically from the shim that wired it — never from guessing the
|
|
22
22
|
// payload shape. On the TS side the harness is derived from the selected launcher or ALL adapters at once
|
|
23
|
-
// (materialize
|
|
23
|
+
// (materialize writes every harness's artifacts).
|
|
24
24
|
|
|
25
25
|
export type HarnessId = 'claude' | 'codex'
|
|
26
26
|
export type HarnessLivenessRecord = { session: string; harnessSessionId?: string | null }
|
|
@@ -159,7 +159,7 @@ export interface Harness {
|
|
|
159
159
|
// its existing importers.
|
|
160
160
|
export type DispatchResult = { ok: boolean; error?: string }
|
|
161
161
|
export type HarnessDeliveryRecord = { session: string; worktreePath?: string; harnessSessionId?: string | null; runtimeDir?: string }
|
|
162
|
-
// the on-demand surface artifacts a materialize
|
|
162
|
+
// the on-demand surface artifacts a materialize pass wrote, by node NAME — so clean() knows EXACTLY which
|
|
163
163
|
// skill subdirs / agent files are SpexCode's to remove (name-scoped, never a blind wipe of a dir the user may
|
|
164
164
|
// also populate). materialize passes the live skill/agent node names; clean reconstructs the same paths.
|
|
165
165
|
export type HarnessArtifacts = { skills: readonly string[]; agents: readonly string[] }
|
|
@@ -179,24 +179,33 @@ export const rvSock = (id: string) => join(tmpdir(), `spexcode-rv-${id}.sock`)
|
|
|
179
179
|
// A crashed/killed claude can leave its rvSock FILE on disk (a unix-domain socket path is NOT auto-unlinked on
|
|
180
180
|
// an unclean exit), so the old `existsSync(rvSock)` read a DEAD pane as `online` for as long as the stale file
|
|
181
181
|
// lingered — the incident's "dead pane stuck `working` for 30+ min". The honest signal is a live LISTENER:
|
|
182
|
-
// connect() to the socket.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
182
|
+
// connect() to the socket. The verdict is TRI-STATE, because only two probe results actually PROVE anything:
|
|
183
|
+
// 'live' — the connect completed: a real claude is accepting.
|
|
184
|
+
// 'dead' — ECONNREFUSED (a stale file nothing listens on) / ENOENT (no file): death PROVEN, instantly.
|
|
185
|
+
// 'unproven' — the probe itself failed to conclude: a TIMEOUT (under load the prober's event loop fires the
|
|
186
|
+
// expired timer before the pending connect event — the thrashed-backend incident where every live worker
|
|
187
|
+
// read offline in one board answer), or EAGAIN (the listen backlog is FULL, which proves a listener is
|
|
188
|
+
// alive-but-busy, the opposite of dead). Collapsing these into 'dead' is how a load spike masqueraded as
|
|
189
|
+
// a graveyard (issue #40); the caller must render unproven death as `unknown`, never `offline`.
|
|
190
|
+
// The common cases cost no waiting (connect/refuse/absent are instant); the short timeout only bounds the
|
|
191
|
+
// wedged/thrashed path. Never throws.
|
|
192
|
+
export type ListenerProbe = 'live' | 'dead' | 'unproven'
|
|
193
|
+
const PROVEN_DEAD = new Set(['ECONNREFUSED', 'ENOENT'])
|
|
194
|
+
export function rendezvousListening(id: string, timeoutMs = 800): Promise<ListenerProbe> {
|
|
186
195
|
return new Promise((resolve) => {
|
|
187
196
|
let settled = false
|
|
188
197
|
let c: ReturnType<typeof createConnection> | undefined
|
|
189
|
-
const done = (v:
|
|
198
|
+
const done = (v: ListenerProbe) => {
|
|
190
199
|
if (settled) return
|
|
191
200
|
settled = true
|
|
192
201
|
clearTimeout(timer)
|
|
193
202
|
try { c?.destroy() } catch { /* */ }
|
|
194
203
|
resolve(v)
|
|
195
204
|
}
|
|
196
|
-
const timer = setTimeout(() => done(
|
|
197
|
-
try { c = createConnection({ path: rvSock(id) }) } catch { return done(
|
|
198
|
-
c.on('connect', () => done(
|
|
199
|
-
c.on('error', () => done(
|
|
205
|
+
const timer = setTimeout(() => done('unproven'), timeoutMs)
|
|
206
|
+
try { c = createConnection({ path: rvSock(id) }) } catch { return done('unproven') }
|
|
207
|
+
c.on('connect', () => done('live'))
|
|
208
|
+
c.on('error', (e) => done(PROVEN_DEAD.has((e as NodeJS.ErrnoException).code ?? '') ? 'dead' : 'unproven'))
|
|
200
209
|
})
|
|
201
210
|
}
|
|
202
211
|
// The app-server Unix socket MUST live on a SHORT, sun_path-safe path — NOT nested under the project runtime
|
|
@@ -771,7 +780,7 @@ export function removeManagedBlock(file: string, comment: readonly [string, stri
|
|
|
771
780
|
// remove ONLY our block plus the blank lines writeManagedBlock inserted around it; do NOT normalize the
|
|
772
781
|
// user's OWN whitespace elsewhere — this must leave every other byte intact so it is a faithful INVERSE of
|
|
773
782
|
// writeManagedBlock's append. A global `\n{3,}→\n\n` collapse used to sit here and mutated pre-existing
|
|
774
|
-
// blank-line runs in the user's file, which broke the policy round-trip ([[
|
|
783
|
+
// blank-line runs in the user's file, which broke the policy round-trip ([[residence]]): a mode flip
|
|
775
784
|
// and back left a spurious one-line diff on a .gitignore that had internal blank lines. The leading-newline
|
|
776
785
|
// strip is GUARDED the same way: it exists only for a block sitting at the TOP of the file (whose '\n'
|
|
777
786
|
// replacement would otherwise become a leading blank) — a host file that BEGINS with its own blank lines
|
|
@@ -785,7 +794,7 @@ export function removeManagedBlock(file: string, comment: readonly [string, stri
|
|
|
785
794
|
|
|
786
795
|
// the shim for one harness: every event → `SPEX='…' bash <dispatch> <harnessId> <Event>`. The harness id is
|
|
787
796
|
// baked in so dispatch.sh can export SPEXCODE_HARNESS (the detector for the shell side). SPEX is inherited by
|
|
788
|
-
// the cli-needing handlers
|
|
797
|
+
// the cli-needing handlers.
|
|
789
798
|
function buildShim(id: HarnessId, events: readonly string[], dispatch: string, spex: string): { json: string; cmd: (e: string) => string } {
|
|
790
799
|
const cmd = (e: string) => `SPEX='${spex}' bash ${dispatch} ${id} ${e}`
|
|
791
800
|
const hooks: Record<string, unknown> = {}
|
|
@@ -1045,7 +1054,7 @@ export const codexHarness: Harness = {
|
|
|
1045
1054
|
resumeArg: (rec) => (rec.harnessSessionId ? `--resume ${rec.harnessSessionId}` : ''),
|
|
1046
1055
|
}
|
|
1047
1056
|
|
|
1048
|
-
// every adapter — materialize iterates this to
|
|
1057
|
+
// every adapter — materialize iterates this to write each harness's artifacts in one pass.
|
|
1049
1058
|
export const HARNESSES: readonly Harness[] = [claudeHarness, codexHarness]
|
|
1050
1059
|
|
|
1051
1060
|
// the legacy/default adapter for old records and config defaults. New launches derive harness from a launcher.
|
package/spec-cli/src/help.ts
CHANGED
|
@@ -77,7 +77,7 @@ session list. Identical to GET /api/board; needs the backend (spex serve) reacha
|
|
|
77
77
|
spex guide spec the spec.md file format + every lint rule
|
|
78
78
|
spex guide yatsu the yatsu.md scenario format + how loss is measured and filed
|
|
79
79
|
spex guide config every spexcode.json / spexcode.local.json field, and which file it belongs in
|
|
80
|
-
spex guide footprint the
|
|
80
|
+
spex guide footprint the footprint model: never-tracked artifacts, exclude + content filter, anchors
|
|
81
81
|
|
|
82
82
|
guide is the SKILL layer — workflows and formats. Command usage lives here in help
|
|
83
83
|
(\`spex help <cmd>\`); guide carries what the commands assume you know.`,
|
|
@@ -187,6 +187,9 @@ a node/<id> branch links its PR for free). Read-only — git/.spec stays the sin
|
|
|
187
187
|
|
|
188
188
|
Creates a session: node branch + worktree + a launched agent carrying your prompt (= session new).
|
|
189
189
|
Give it ONLY its task — the dev-flow contract reaches it through the materialized system prompt.
|
|
190
|
+
--node <id> (or the prompt's first [[id]] mention, same effect) binds the session to that node:
|
|
191
|
+
branch name node/<id>-<shortid>, board attribution, and one appended line with the node's spec.md
|
|
192
|
+
path when the node exists. With neither, the session is bound to no node.
|
|
190
193
|
--prompt-file <path> reads the prompt from a file (- = stdin), so a long multi-paragraph prompt never
|
|
191
194
|
fights shell quoting; it is exclusive with the inline prompt (both given = error), and an unreadable
|
|
192
195
|
or empty file refuses the launch.
|
|
@@ -313,15 +316,14 @@ ${ROUTING_NOTE}`,
|
|
|
313
316
|
|
|
314
317
|
// ── install & serve (operator) ────────────────────────────────────────────
|
|
315
318
|
init: {
|
|
316
|
-
line: 'init [dir] adopt SpexCode on a repo: seed .spec + hooks + materialize [--preset name]
|
|
317
|
-
body: `Usage: spex init [dir=cwd] [--preset default|careful]
|
|
319
|
+
line: 'init [dir] adopt SpexCode on a repo: seed .spec + hooks + materialize [--preset name]',
|
|
320
|
+
body: `Usage: spex init [dir=cwd] [--preset default|careful]
|
|
318
321
|
|
|
319
322
|
Scaffolds adoption in one shot: seeds a starter .spec tree (project root + .config plugins), plants
|
|
320
323
|
spexcode.json, installs the git hooks, and materializes the harness artifacts (contract block +
|
|
321
324
|
shims). Additive — never overwrites your files. --preset picks the .config plugin tier (cumulative).
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
prints a one-time decision hint instead (see spex guide footprint).`,
|
|
325
|
+
Footprint is fixed: materialized artifacts are never tracked — hidden via the per-clone .git/info/exclude, with
|
|
326
|
+
a tracked/mixed CLAUDE.md/AGENTS.md covered by the clean/smudge filter (see spex guide footprint).`,
|
|
325
327
|
see: 'spex guide (the full setup workflow) · spex uninstall (the inverse) · spex lint (adoption TODO)',
|
|
326
328
|
},
|
|
327
329
|
uninstall: {
|
|
@@ -329,30 +331,32 @@ prints a one-time decision hint instead (see spex guide footprint).`,
|
|
|
329
331
|
body: `Usage: spex uninstall [dir=cwd] [--hooks]
|
|
330
332
|
|
|
331
333
|
Removes every SpexCode-GENERATED artifact (harness shims · contract blocks · trust entries ·
|
|
332
|
-
|
|
334
|
+
exclude/ignore blocks · global store · plugin bundle) and never your .spec/.config data or your own
|
|
333
335
|
prose. Git hooks are preserved unless --hooks.`,
|
|
334
336
|
see: 'spex init (re-adopt later — your .spec survives)',
|
|
335
337
|
},
|
|
336
338
|
materialize: {
|
|
337
|
-
line: 'materialize re-
|
|
339
|
+
line: 'materialize re-materialize the harness artifacts (contract block · shims) for cwd’s project',
|
|
338
340
|
body: `Usage: spex materialize
|
|
339
341
|
|
|
340
|
-
|
|
341
|
-
CLAUDE.md/AGENTS.md plus the .claude/.codex shims, and prints the content hash.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
Materializes the surface:system config nodes into the managed <!-- spexcode --> block of
|
|
343
|
+
CLAUDE.md/AGENTS.md plus the .claude/.codex shims, and prints the content hash. The materialize anchors on
|
|
344
|
+
git-native events only (init · this verb · session-worktree creation · the pre-commit/post-checkout/
|
|
345
|
+
post-merge hooks) — run it by hand after a toolchain update, or in the setup step of any clone that
|
|
346
|
+
has no spex-planted hooks yet (CI, a cloud agent): the artifacts are generated and excluded, so they
|
|
347
|
+
never arrive via git.`,
|
|
348
|
+
see: 'spex doctor (verify the materialized artifacts actually reach an agent)',
|
|
345
349
|
},
|
|
346
350
|
doctor: {
|
|
347
351
|
line: 'doctor diagnose whether the workflow actually reaches this agent — per-layer, per-harness',
|
|
348
352
|
body: `Usage: spex doctor per-layer coverage report: preconditions · git-hook floor · contract ·
|
|
349
|
-
hooks + handler existence · backend — for every harness materialize
|
|
353
|
+
hooks + handler existence · backend — for every harness materialize delivers to
|
|
350
354
|
spex doctor contract print the composed surface:system text any agent here reads
|
|
351
355
|
spex doctor conflicts detect double-delivery (loose artifacts beside the managed ones)
|
|
352
356
|
|
|
353
357
|
Run it when a worker seems to be missing its contract or hooks — it names the broken layer and the
|
|
354
358
|
repair, instead of you diffing materialized files by hand.`,
|
|
355
|
-
see: 'spex materialize (re-
|
|
359
|
+
see: 'spex materialize (re-materialize the artifacts doctor checks)',
|
|
356
360
|
},
|
|
357
361
|
serve: {
|
|
358
362
|
line: 'serve run the API backend (default :8787) [--port N] [--public --password pw]',
|
|
@@ -381,10 +385,12 @@ so bind wide only on a network you trust (for the internet, use \`spex serve --p
|
|
|
381
385
|
// ── plumbing ──────────────────────────────────────────────────────────────
|
|
382
386
|
internal: {
|
|
383
387
|
line: '', // deliberately not on the map
|
|
384
|
-
body: `Usage: spex internal <trunk | codex-launch | codex-turn>
|
|
388
|
+
body: `Usage: spex internal <trunk | commit-surgery | refresh-footprint | codex-launch | codex-turn>
|
|
385
389
|
|
|
386
390
|
Machine plumbing — called by generated hooks and launch scripts, never typed by a human or agent:
|
|
387
|
-
trunk
|
|
391
|
+
trunk print the resolved source-of-truth branch (the pre-commit main-guard captures it)
|
|
392
|
+
commit-surgery pre-commit footprint anchor: unconditional materialize + staged-index repair
|
|
393
|
+
refresh-footprint quiet materialize — the post-checkout/post-merge freshness anchor
|
|
388
394
|
codex-launch <sock> <cwd> [prompt…] backend-owned codex thread/start + first turn (launch script)
|
|
389
395
|
codex-turn <sock> <threadId> <text…> fire a follow-up turn on an owned thread (tests/scripts)
|
|
390
396
|
|
package/spec-cli/src/init.ts
CHANGED
|
@@ -53,27 +53,9 @@ function resolveHooksDir(dir: string): string | null {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export async function specInit(targetArg: string | undefined, presetArg?: string
|
|
56
|
+
export async function specInit(targetArg: string | undefined, presetArg?: string): Promise<void> {
|
|
57
57
|
const targetDir = resolve(targetArg ?? process.cwd())
|
|
58
58
|
|
|
59
|
-
// the one-step render vote (`--render <committed|ignored|hidden>`, see [[render-policy]]): validated up
|
|
60
|
-
// front against the same vocabulary materialize enforces (resolveRenderPolicy — an unknown word fails loud
|
|
61
|
-
// BEFORE anything is written), applied to the config files below, and honored by this run's materialize.
|
|
62
|
-
const render = renderArg?.trim()
|
|
63
|
-
if (renderArg !== undefined) {
|
|
64
|
-
if (!render) {
|
|
65
|
-
console.error('spex init: --render needs a value — the render axis has three words: committed | ignored | hidden (see `spex guide footprint`)')
|
|
66
|
-
process.exit(1)
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const { resolveRenderPolicy } = await import('./materialize.js')
|
|
70
|
-
resolveRenderPolicy({ render })
|
|
71
|
-
} catch (e) {
|
|
72
|
-
console.error(`spex init: ${(e as Error).message}`)
|
|
73
|
-
process.exit(1)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
59
|
// the preset the NEW adopter gets — `--preset <name>` wins, else an existing target spexcode.json's
|
|
78
60
|
// `preset` field, else the lean `default`. Validated loudly against the chain (an unknown name would
|
|
79
61
|
// otherwise seed silently). A non-default tier stacks its template package on top of the default set below.
|
|
@@ -129,19 +111,6 @@ export async function specInit(targetArg: string | undefined, presetArg?: string
|
|
|
129
111
|
console.log(`✓ planted spexcode.json — lint.governedRoots starts as ${roots} (the whole git-tracked tree, tests excluded); curate explicit roots later if you want a narrower graph`)
|
|
130
112
|
}
|
|
131
113
|
|
|
132
|
-
// 1c. apply the --render vote where the axis says each word lives ([[render-policy]]): committed/ignored
|
|
133
|
-
// are project facts → the committed spexcode.json; hidden is a host/person fact → the gitignored
|
|
134
|
-
// spexcode.local.json. An explicit flag is an explicit instruction, so it sets the field even in a
|
|
135
|
-
// pre-existing config file (the ONE deliberate exception to "existing files are left untouched").
|
|
136
|
-
if (render) {
|
|
137
|
-
const home = render === 'hidden' ? 'spexcode.local.json' : 'spexcode.json'
|
|
138
|
-
const p = join(targetDir, home)
|
|
139
|
-
const cur = readJsonConfig(p) // fails loud on a malformed existing file, {} when absent
|
|
140
|
-
cur.render = render
|
|
141
|
-
writeFileSync(p, JSON.stringify(cur, null, 2) + '\n')
|
|
142
|
-
console.log(`✓ render policy voted: "${render}" → ${home}`)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
114
|
// validate the harness DELIVERY TARGET set ([[harness-select]]) up front: a bad `harnesses` set (plugin +
|
|
146
115
|
// native, or a plugin with no folder) must fail LOUD here, not be silently swallowed by the materialize
|
|
147
116
|
// try/catch below. A fresh starter spexcode.json omits the field (defaults to all natives), so this only
|
|
@@ -175,22 +144,18 @@ export async function specInit(targetArg: string | undefined, presetArg?: string
|
|
|
175
144
|
if (installed.length) console.log(`✓ installed git hooks (${installed.join(', ')}) → ${hooksDir}`)
|
|
176
145
|
}
|
|
177
146
|
|
|
178
|
-
// 2c.
|
|
147
|
+
// 2c. MATERIALIZE the harness-discovered artifacts so a USER-self-launched claude/codex works with zero further
|
|
179
148
|
// steps: the hook manifest (in the GLOBAL per-project store, not the worktree), the AGENTS.md/CLAUDE.md
|
|
180
149
|
// <spexcode> contract block (user content preserved), the .claude/.codex shims, and the Codex trust (global,
|
|
181
150
|
// scoped) so codex self-launch is prompt-free. Runs with cwd = the target so the loaders read the just-seeded
|
|
182
|
-
// .config. Idempotent — the
|
|
151
|
+
// .config. Idempotent — the planted git hooks (pre-commit/post-checkout/post-merge) keep it fresh
|
|
152
|
+
// thereafter on the git-native anchors ([[commit-surgery]]); no harness event ever triggers a materialize.
|
|
183
153
|
const prevCwd = process.cwd()
|
|
184
154
|
try {
|
|
185
155
|
process.chdir(targetDir)
|
|
186
|
-
const { materialize
|
|
156
|
+
const { materialize } = await import('./materialize.js')
|
|
187
157
|
materialize(targetDir)
|
|
188
158
|
console.log('✓ materialized harness artifacts (global hook manifest, AGENTS.md/CLAUDE.md block, harness shims, Codex trust)')
|
|
189
|
-
// the adoption vote hint ([[render-policy]]): a host-TRACKED contract file now carries the generated
|
|
190
|
-
// block and shows honestly dirty under the default policy — print the one-time decision guidance while
|
|
191
|
-
// the vote is open (an explicit `render`, including --render on this very run, retires it).
|
|
192
|
-
const hint = renderVoteHint(targetDir)
|
|
193
|
-
if (hint) console.log(`\n${hint}`)
|
|
194
159
|
} catch (e) {
|
|
195
160
|
console.warn(`• materialize skipped (${(e as Error).message}) — run \`spex materialize\` once the packages are installed.`)
|
|
196
161
|
} finally {
|
package/spec-cli/src/layout.ts
CHANGED
|
@@ -10,21 +10,15 @@ type Config = {
|
|
|
10
10
|
mainBranch?: string // source-of-truth BRANCH worktrees fork from (default: auto-detected — see mainBranch())
|
|
11
11
|
branchPrefix?: string // how a branch names its node (default: "node/")
|
|
12
12
|
preset?: string // the SELECTED init preset — which cumulative .config tier `spex init` seeds (default 'default'; seed-time only, no launcher gate; read by init.ts; see [[init-preset]])
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// `committed` is a project fact → spexcode.json; `hidden` is a host/person fact → spexcode.local.json.
|
|
23
|
-
// The schema deliberately has NO knob for the spec DATA: `.spec` + spexcode.json are ALWAYS tracked
|
|
24
|
-
// ("git is the database") — the vocabulary itself makes "untrack the spec" unsayable.
|
|
25
|
-
render?: 'committed' | 'ignored' | 'hidden'
|
|
26
|
-
// DEPRECATED ([[render-policy]] compat): the retired private-overlay toggle. Read as render:'hidden' (with a
|
|
27
|
-
// loud, non-fatal migration notice); its old data-untrack semantics are gone — see `spex guide footprint`.
|
|
13
|
+
// RETIRED ([[residence]]) — the old three-word footprint vote. Materialized artifacts carry no facts and are never
|
|
14
|
+
// tracked now (one residence behavior: the per-clone exclude, plus the content filter for a mixed
|
|
15
|
+
// contract file), so the field is IGNORED with a loud non-fatal notice (materialize's retiredAxisNotice);
|
|
16
|
+
// it stays in the type only so the notice can read it. The schema deliberately has NO knob for the spec
|
|
17
|
+
// DATA: `.spec` + spexcode.json are ALWAYS tracked ("git is the database") — the vocabulary itself makes
|
|
18
|
+
// "untrack the spec" unsayable.
|
|
19
|
+
render?: string
|
|
20
|
+
// RETIRED (residence compat): the old private-overlay toggle — ignored with the same loud notice;
|
|
21
|
+
// its data-untrack semantics are long gone. See `spex guide footprint` MIGRATIONS.
|
|
28
22
|
private?: boolean
|
|
29
23
|
// which harness targets `spex materialize` delivers into — native ids ('claude'|'codex') or a {plugin:"<folder>"}
|
|
30
24
|
// bundle; resolved + validated by [[harness-select]] (harness-select.ts). Default (omitted): all native harnesses.
|
|
@@ -145,17 +139,29 @@ export function spexcodeHome(): string {
|
|
|
145
139
|
export function encodeProject(root: string): string {
|
|
146
140
|
return root.replace(/[/.]/g, '-')
|
|
147
141
|
}
|
|
148
|
-
// this project's per-PROJECT runtime tier — the
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
142
|
+
// this project's per-PROJECT runtime tier — the sessions/ records AND the per-TREE materialize slots (below) —
|
|
143
|
+
// living under the SAME global per-project dir, so NOTHING SpexCode materializes stays in the worktree (the
|
|
144
|
+
// worktree holds only the harness-discovered CLAUDE.md/AGENTS.md + shims, which must sit in-tree).
|
|
145
|
+
// proj-aware for `spex init <dir>` / materialize(proj); cwd-based default for the hooks/board. The shell
|
|
146
|
+
// hooks mirror this as hp_runtime_dir.
|
|
153
147
|
export function runtimeRoot(proj?: string): string {
|
|
154
148
|
const gcd = proj
|
|
155
149
|
? git(['-C', proj, 'rev-parse', '--path-format=absolute', '--git-common-dir']).trim()
|
|
156
150
|
: gitCommonDir()
|
|
157
151
|
return join(spexcodeHome(), 'projects', encodeProject(dirname(gcd)))
|
|
158
152
|
}
|
|
153
|
+
// the per-WORKTREE materialize slot — <runtime>/trees/<enc(worktree-toplevel)> — holding the materialize
|
|
154
|
+
// products that are a pure function of ONE tree's .config (hooks-manifest, content-hash, plugin-folders).
|
|
155
|
+
// Slotted per tree exactly like sessions/<id> is slotted per session: the old single global file made the
|
|
156
|
+
// last-materialized tree win, so dispatch ran tree A's compiled hook set inside tree B's sessions
|
|
157
|
+
// ([[hook-dispatch]]). Key = the sessions encodeProject transform over `rev-parse --show-toplevel`, the
|
|
158
|
+
// SAME derivation dispatch.sh's shell mirror (hp_tree_dir) runs from its own cwd — so writer and reader
|
|
159
|
+
// land on the same slot from the same tree, and only from the same tree. Throws when `wt` is not a live
|
|
160
|
+
// git tree (fail loud); a best-effort caller (the close-time GC) wraps it.
|
|
161
|
+
export function treeSlotDir(wt: string): string {
|
|
162
|
+
const top = git(['-C', wt, 'rev-parse', '--show-toplevel']).trim()
|
|
163
|
+
return join(runtimeRoot(wt), 'trees', encodeProject(top || wt))
|
|
164
|
+
}
|
|
159
165
|
// this project's per-session records dir, one session's dir, its structured record, and a sibling artifact —
|
|
160
166
|
// all keyed by session_id under <home>/projects/<enc>/sessions/.
|
|
161
167
|
export function sessionsRoot(): string { return join(runtimeRoot(), 'sessions') }
|