spexcode 0.2.5 → 0.2.6
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 +1 -1
- 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 +21 -11
- 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 +61 -58
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +5 -5
- package/spec-cli/src/help.ts +20 -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 +22 -13
- 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-dashboard/dist/assets/{Dashboard-CMRJGfYI.js → Dashboard-BlRRsxE7.js} +7 -7
- package/spec-dashboard/dist/assets/{EvalsPage-Dj2mxcfW.js → EvalsPage-BzVE38-Z.js} +1 -1
- package/spec-dashboard/dist/assets/{FoldToggle-BfNpeyRQ.js → FoldToggle-DFuLVOeu.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-DfY315kt.js → IssuesPage-CzDaazhe.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-BGdC0A0P.js → MobileApp-CXQrQCNp.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-BOBCAR0t.js → SessionInterface-D1pUBl6q.js} +8 -8
- package/spec-dashboard/dist/assets/{SessionWindow-D7YmjV0i.js → SessionWindow-Y25Bwg1e.js} +5 -5
- package/spec-dashboard/dist/assets/{Settings-BlSNmpH_.js → Settings-R610Vbzd.js} +1 -1
- package/spec-dashboard/dist/assets/{index-BFdzpd_O.js → index-BO0Zuweu.js} +2 -2
- package/spec-dashboard/dist/index.html +1 -1
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
|
|
@@ -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,80 @@ 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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
render RETIRED (the old three-word footprint vote) — ignored with a loud notice. Materialized
|
|
331
|
+
artifacts are never tracked; there is exactly one residence behavior. Remove the field.
|
|
332
|
+
\`spex guide footprint\`.
|
|
333
|
+
private RETIRED (old private-overlay toggle) — ignored with a loud notice, same as \`render\`; its
|
|
334
|
+
data-untrack semantics are gone. See \`spex guide footprint\` MIGRATIONS.`
|
|
335
|
+
|
|
336
|
+
const FOOTPRINT = `spex guide footprint — what SpexCode plants in a repo, and who sees it (no vote — one behavior)
|
|
338
337
|
|
|
339
338
|
SpexCode claims software engineering's HEAD (the recording of intent) and TAIL (the storage of
|
|
340
339
|
measurement) and leaves the MIDDLE — construction — to the harness/agent/test framework; freshness
|
|
341
340
|
stitches the two ends into a closed loop. The footprint follows: the head+tail (.spec, spexcode.json,
|
|
342
341
|
readings) is the ASSET and lives in git like source; everything else is derived wiring or a machine fact.
|
|
343
|
-
|
|
342
|
+
Materialized artifacts carry no facts, so they are NEVER tracked — the old render vote is retired; there
|
|
343
|
+
is exactly one residence behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
344
344
|
|
|
345
|
-
── THE FOUR KINDS (
|
|
345
|
+
── THE FOUR KINDS (all fixed) ──
|
|
346
346
|
spec data .spec/ (incl .config/) + spexcode.json — ALWAYS tracked. Git is the database; there is
|
|
347
347
|
deliberately NO way to say "untrack the spec" in this schema.
|
|
348
348
|
machine facts spexcode.local.json, the hook shims (.claude/settings.json, .codex/hooks.json), plugin
|
|
349
|
-
bundles — NEVER tracked; always in the
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
bundles — NEVER tracked; always in the per-clone exclude.
|
|
350
|
+
artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
|
|
351
|
+
tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
|
|
352
352
|
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
|
-
|
|
353
|
+
out-of-tree, or exclude-ruled where in-tree.
|
|
354
|
+
|
|
355
|
+
── A CONTRACT FILE'S RESIDENCE IS A LIVE CONTENT FACT (re-judged at every materialize) ──
|
|
356
|
+
host-tracked → the clean/smudge content filter: the repo keeps the pristine host prose, your
|
|
357
|
+
working tree carries prose + block, status stays clean.
|
|
358
|
+
untracked, wholly ours→ one exclude entry. The exclude is the ignored-bit DECLARATION the rest of git
|
|
359
|
+
consults (checkout may overwrite, clean -fd spares, status/add -A/stash silent).
|
|
360
|
+
untracked, YOUR prose → the exclude entry is withdrawn (hiding user content would be data-loss shaped)
|
|
361
|
+
entered the file and the clean filter is pre-armed: the file shows honestly as untracked, and
|
|
362
|
+
IF you choose to \`git add\` it, the block is stripped automatically — tracking
|
|
363
|
+
is always your act; SpexCode never stages or commits anything for you.
|
|
364
|
+
|
|
365
|
+
── THE GIT-NATIVE ANCHORS (no harness event ever triggers a materialize) ──
|
|
366
|
+
spex init / spex materialize / session-worktree creation — the explicit passes;
|
|
367
|
+
pre-commit the correctness anchor: an UNCONDITIONAL materialize (masks provably fresh at the only
|
|
368
|
+
moment history is written) + staged-index surgery — a staged blob carrying the sentinel
|
|
369
|
+
block is cleaned IN PLACE (partial staging survives; source is the staged blob), a
|
|
370
|
+
HEAD-untracked generated artifact is unstaged. Repairs and proceeds, never rejects.
|
|
371
|
+
post-checkout/post-merge freshness anchors: .spec/.config edits are git-transactional — they take
|
|
372
|
+
effect at the commit/checkout/merge that carries them, like any other source change.
|
|
373
|
+
An environment with no spex-planted hooks (CI, a cloud agent's fresh clone, a teammate who hasn't
|
|
374
|
+
installed) simply runs \`spex materialize\` in its setup step — there is no committed-artifact mode.
|
|
375
|
+
TRACK ≠ PUSH: none of this ever touches remotes; where commits GO is branch/remote policy.
|
|
372
376
|
|
|
373
377
|
── 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
|
|
378
|
+
materialize(P₂) ∘ materialize(P₁) = materialize(P₂): every materialize first ERASES all landing points by
|
|
379
|
+
SpexCode's own identity stamps, then re-asserts — legacy states (a .gitignore managed block, a committed
|
|
380
|
+
artifact, the retired vote's homes) are forgotten by the same pass. \`spex uninstall\` is the empty
|
|
381
|
+
materialize plus the global store: a total backout that never touches your .spec/.config or prose. Fresh
|
|
382
|
+
clones and session worktrees are self-sufficient: data by checkout, materialized artifacts by
|
|
383
|
+
re-materialize, the machine snapshot (spexcode.local.json) by copy.
|
|
384
|
+
|
|
385
|
+
── THE CONTENT FILTER (mixed-content contract files) ──
|
|
386
|
+
Per-clone only — git config filter.spexcode.* + .git/info/attributes + a shim under .git/spexcode/ —
|
|
387
|
+
planted where mixed content exists or is imminent (tracked, or untracked with your prose). clean strips
|
|
388
|
+
the sentinel block (history never sees it); smudge re-injects it on checkout. A missing shim degrades to
|
|
389
|
+
identity (never a git fatal). Your own edits to the prose still show as real modifications; only the
|
|
390
|
+
block is invisible to git.
|
|
386
391
|
|
|
387
392
|
── MIGRATIONS ──
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
render: "..." (retired) remove the field from spexcode.json / spexcode.local.json — it is ignored
|
|
394
|
+
with a loud notice. A previously COMMITTED artifact heals at your next commit
|
|
395
|
+
touching it (pre-commit strips the block from the staged blob); or run
|
|
396
|
+
\`spex materialize\` + commit the block's removal once.
|
|
397
|
+
private: true (retired) same — remove it; its data-untrack semantics are long gone.
|
|
398
|
+
legacy untracked spec track the sources once: git add .spec spexcode.json (commit on your branch)
|
|
392
399
|
WARNING: tracking is not retroactive secrecy — history already pushed
|
|
393
400
|
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
401
|
back out entirely \`spex uninstall\` (add --hooks to also remove the spexcode git hooks).`
|
|
399
402
|
|
|
400
403
|
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[] }
|
|
@@ -771,7 +771,7 @@ export function removeManagedBlock(file: string, comment: readonly [string, stri
|
|
|
771
771
|
// remove ONLY our block plus the blank lines writeManagedBlock inserted around it; do NOT normalize the
|
|
772
772
|
// user's OWN whitespace elsewhere — this must leave every other byte intact so it is a faithful INVERSE of
|
|
773
773
|
// 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 ([[
|
|
774
|
+
// blank-line runs in the user's file, which broke the policy round-trip ([[residence]]): a mode flip
|
|
775
775
|
// and back left a spurious one-line diff on a .gitignore that had internal blank lines. The leading-newline
|
|
776
776
|
// strip is GUARDED the same way: it exists only for a block sitting at the TOP of the file (whose '\n'
|
|
777
777
|
// replacement would otherwise become a leading blank) — a host file that BEGINS with its own blank lines
|
|
@@ -785,7 +785,7 @@ export function removeManagedBlock(file: string, comment: readonly [string, stri
|
|
|
785
785
|
|
|
786
786
|
// the shim for one harness: every event → `SPEX='…' bash <dispatch> <harnessId> <Event>`. The harness id is
|
|
787
787
|
// baked in so dispatch.sh can export SPEXCODE_HARNESS (the detector for the shell side). SPEX is inherited by
|
|
788
|
-
// the cli-needing handlers
|
|
788
|
+
// the cli-needing handlers.
|
|
789
789
|
function buildShim(id: HarnessId, events: readonly string[], dispatch: string, spex: string): { json: string; cmd: (e: string) => string } {
|
|
790
790
|
const cmd = (e: string) => `SPEX='${spex}' bash ${dispatch} ${id} ${e}`
|
|
791
791
|
const hooks: Record<string, unknown> = {}
|
|
@@ -1045,7 +1045,7 @@ export const codexHarness: Harness = {
|
|
|
1045
1045
|
resumeArg: (rec) => (rec.harnessSessionId ? `--resume ${rec.harnessSessionId}` : ''),
|
|
1046
1046
|
}
|
|
1047
1047
|
|
|
1048
|
-
// every adapter — materialize iterates this to
|
|
1048
|
+
// every adapter — materialize iterates this to write each harness's artifacts in one pass.
|
|
1049
1049
|
export const HARNESSES: readonly Harness[] = [claudeHarness, codexHarness]
|
|
1050
1050
|
|
|
1051
1051
|
// 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.`,
|
|
@@ -313,15 +313,14 @@ ${ROUTING_NOTE}`,
|
|
|
313
313
|
|
|
314
314
|
// ── install & serve (operator) ────────────────────────────────────────────
|
|
315
315
|
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]
|
|
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]
|
|
318
318
|
|
|
319
319
|
Scaffolds adoption in one shot: seeds a starter .spec tree (project root + .config plugins), plants
|
|
320
320
|
spexcode.json, installs the git hooks, and materializes the harness artifacts (contract block +
|
|
321
321
|
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).`,
|
|
322
|
+
Footprint needs no vote: materialized artifacts are never tracked — hidden via the per-clone .git/info/exclude, with
|
|
323
|
+
a tracked/mixed CLAUDE.md/AGENTS.md covered by the clean/smudge filter (see spex guide footprint).`,
|
|
325
324
|
see: 'spex guide (the full setup workflow) · spex uninstall (the inverse) · spex lint (adoption TODO)',
|
|
326
325
|
},
|
|
327
326
|
uninstall: {
|
|
@@ -329,30 +328,32 @@ prints a one-time decision hint instead (see spex guide footprint).`,
|
|
|
329
328
|
body: `Usage: spex uninstall [dir=cwd] [--hooks]
|
|
330
329
|
|
|
331
330
|
Removes every SpexCode-GENERATED artifact (harness shims · contract blocks · trust entries ·
|
|
332
|
-
|
|
331
|
+
exclude/ignore blocks · global store · plugin bundle) and never your .spec/.config data or your own
|
|
333
332
|
prose. Git hooks are preserved unless --hooks.`,
|
|
334
333
|
see: 'spex init (re-adopt later — your .spec survives)',
|
|
335
334
|
},
|
|
336
335
|
materialize: {
|
|
337
|
-
line: 'materialize re-
|
|
336
|
+
line: 'materialize re-materialize the harness artifacts (contract block · shims) for cwd’s project',
|
|
338
337
|
body: `Usage: spex materialize
|
|
339
338
|
|
|
340
|
-
|
|
341
|
-
CLAUDE.md/AGENTS.md plus the .claude/.codex shims, and prints the content hash.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
339
|
+
Materializes the surface:system config nodes into the managed <!-- spexcode --> block of
|
|
340
|
+
CLAUDE.md/AGENTS.md plus the .claude/.codex shims, and prints the content hash. The materialize anchors on
|
|
341
|
+
git-native events only (init · this verb · session-worktree creation · the pre-commit/post-checkout/
|
|
342
|
+
post-merge hooks) — run it by hand after a toolchain update, or in the setup step of any clone that
|
|
343
|
+
has no spex-planted hooks yet (CI, a cloud agent): the artifacts are generated and excluded, so they
|
|
344
|
+
never arrive via git.`,
|
|
345
|
+
see: 'spex doctor (verify the materialized artifacts actually reach an agent)',
|
|
345
346
|
},
|
|
346
347
|
doctor: {
|
|
347
348
|
line: 'doctor diagnose whether the workflow actually reaches this agent — per-layer, per-harness',
|
|
348
349
|
body: `Usage: spex doctor per-layer coverage report: preconditions · git-hook floor · contract ·
|
|
349
|
-
hooks + handler existence · backend — for every harness materialize
|
|
350
|
+
hooks + handler existence · backend — for every harness materialize delivers to
|
|
350
351
|
spex doctor contract print the composed surface:system text any agent here reads
|
|
351
352
|
spex doctor conflicts detect double-delivery (loose artifacts beside the managed ones)
|
|
352
353
|
|
|
353
354
|
Run it when a worker seems to be missing its contract or hooks — it names the broken layer and the
|
|
354
355
|
repair, instead of you diffing materialized files by hand.`,
|
|
355
|
-
see: 'spex materialize (re-
|
|
356
|
+
see: 'spex materialize (re-materialize the artifacts doctor checks)',
|
|
356
357
|
},
|
|
357
358
|
serve: {
|
|
358
359
|
line: 'serve run the API backend (default :8787) [--port N] [--public --password pw]',
|
|
@@ -381,10 +382,12 @@ so bind wide only on a network you trust (for the internet, use \`spex serve --p
|
|
|
381
382
|
// ── plumbing ──────────────────────────────────────────────────────────────
|
|
382
383
|
internal: {
|
|
383
384
|
line: '', // deliberately not on the map
|
|
384
|
-
body: `Usage: spex internal <trunk | codex-launch | codex-turn>
|
|
385
|
+
body: `Usage: spex internal <trunk | commit-surgery | refresh-footprint | codex-launch | codex-turn>
|
|
385
386
|
|
|
386
387
|
Machine plumbing — called by generated hooks and launch scripts, never typed by a human or agent:
|
|
387
|
-
trunk
|
|
388
|
+
trunk print the resolved source-of-truth branch (the pre-commit main-guard captures it)
|
|
389
|
+
commit-surgery pre-commit footprint anchor: unconditional materialize + staged-index repair
|
|
390
|
+
refresh-footprint quiet materialize — the post-checkout/post-merge freshness anchor
|
|
388
391
|
codex-launch <sock> <cwd> [prompt…] backend-owned codex thread/start + first turn (launch script)
|
|
389
392
|
codex-turn <sock> <threadId> <text…> fire a follow-up turn on an owned thread (tests/scripts)
|
|
390
393
|
|
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') }
|