spexcode 0.4.1 → 0.4.3
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 +37 -17
- package/package.json +9 -5
- package/spec-cli/README.md +23 -14
- package/spec-cli/hooks/dispatch.sh +7 -5
- package/spec-cli/hooks/harness.sh +19 -8
- package/spec-cli/src/anchors.ts +110 -18
- package/spec-cli/src/cli.ts +88 -37
- package/spec-cli/src/client.ts +34 -11
- package/spec-cli/src/doctor.ts +174 -36
- package/spec-cli/src/gateway-auth.ts +194 -0
- package/spec-cli/src/gateway-hub.ts +360 -0
- package/spec-cli/src/gateway.ts +27 -19
- package/spec-cli/src/git.ts +12 -5
- package/spec-cli/src/graph.ts +54 -43
- package/spec-cli/src/graphStream.ts +265 -34
- package/spec-cli/src/guide.ts +105 -55
- package/spec-cli/src/harness-select.ts +20 -6
- package/spec-cli/src/harness.ts +181 -33
- package/spec-cli/src/help.ts +64 -27
- package/spec-cli/src/host.ts +610 -0
- package/spec-cli/src/identity-presets.d.ts +13 -0
- package/spec-cli/src/identity-presets.js +128 -0
- package/spec-cli/src/index.ts +145 -86
- package/spec-cli/src/init.ts +53 -25
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +25 -121
- package/spec-cli/src/login-page.ts +12 -5
- package/spec-cli/src/materialize.ts +39 -13
- package/spec-cli/src/mentions.ts +16 -11
- package/spec-cli/src/opencode.ts +145 -0
- package/spec-cli/src/pi-harness.ts +124 -0
- package/spec-cli/src/project-identity.ts +73 -0
- package/spec-cli/src/pty-bridge.ts +321 -521
- package/spec-cli/src/pty-helper.mjs +86 -0
- package/spec-cli/src/reaper.ts +16 -8
- package/spec-cli/src/reviewSnapshot.ts +23 -0
- package/spec-cli/src/reviews.ts +347 -0
- package/spec-cli/src/session-timeline.ts +14 -0
- package/spec-cli/src/sessions.ts +188 -66
- package/spec-cli/src/shim-runtime.ts +169 -0
- package/spec-cli/src/slash-commands.ts +76 -0
- package/spec-cli/src/source-files.ts +79 -0
- package/spec-cli/src/specs.ts +12 -7
- package/spec-cli/src/supervise.ts +37 -15
- package/spec-cli/src/tree.ts +8 -8
- package/spec-cli/src/uninstall.ts +56 -23
- package/spec-cli/templates/hooks/prepare-commit-msg +32 -18
- package/spec-cli/templates/spec/project/.plugins/commands/extract/spec.md +24 -18
- package/spec-cli/templates/spec/project/.plugins/commands/regroup/spec.md +3 -3
- package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
- package/spec-cli/templates/spec/project/.plugins/commands/spec.md +22 -8
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +29 -11
- package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
- package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
- package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
- package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
- package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +38 -50
- package/spec-cli/templates/spec/project/.plugins/skills/spec.md +9 -7
- package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
- package/spec-cli/templates/spec/project/spec.md +6 -5
- package/spec-cli/templates/spexcode.json +4 -2
- package/spec-dashboard/dist/assets/Dashboard-CiHh-gLD.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-DmiX3rdU.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-CIbVGRUJ.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-D-N9_eh0.js +2 -0
- package/spec-dashboard/dist/assets/Modal-DHMzSFJ4.js +1 -0
- package/spec-dashboard/dist/assets/PageScroll-C15adEYI.js +1 -0
- package/spec-dashboard/dist/assets/ProjectsPage-sQpzglp5.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-B8pGU7Rg.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-CuDO_67z.js +13 -0
- package/spec-dashboard/dist/assets/Settings-C_N1wX1f.js +1 -0
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
- package/spec-dashboard/dist/assets/index-DmWbmvCq.js +41 -0
- package/spec-dashboard/dist/assets/index-GGIVdKwH.css +1 -0
- package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
- package/spec-dashboard/dist/index.html +10 -10
- package/spec-dashboard/src/reviewFilters.js +279 -0
- package/spec-dashboard/src/reviewQuery.js +174 -0
- package/spec-dashboard/src/session.js +167 -0
- package/spec-eval/src/cli.ts +49 -28
- package/spec-eval/src/humanok.ts +1 -1
- package/spec-eval/src/matrix.ts +693 -0
- package/spec-eval/src/sessioneval.ts +726 -66
- package/spec-cli/src/migrate-table.ts +0 -408
- package/spec-cli/src/migrate.ts +0 -386
- package/spec-dashboard/dist/assets/Dashboard-C_fGmOKK.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +0 -2
- package/spec-dashboard/dist/assets/FoldToggle-x9gtO1OQ.js +0 -1
- package/spec-dashboard/dist/assets/IssuesPage-5f_vL-JV.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +0 -66
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
- package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +0 -13
- package/spec-dashboard/dist/assets/Settings-DKb5Ji_X.js +0 -1
- package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +0 -41
- package/spec-dashboard/dist/assets/index-BbMkwuix.css +0 -1
- package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +0 -6
package/spec-cli/src/harness.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { createConnection, type Socket } from 'node:net'
|
|
|
6
6
|
import { execFile, execFileSync } from 'node:child_process'
|
|
7
7
|
import { promisify } from 'node:util'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import { claudeSlashCommands, codexSlashCommands, type SlashCommand } from './slash-commands.js'
|
|
9
|
+
import { claudeSlashCommands, codexSlashCommands, opencodeSlashCommands, piSlashCommands, type SlashCommand } from './slash-commands.js'
|
|
10
|
+
import { OPENCODE_EVENTS, opencodePluginSource } from './opencode.js'
|
|
11
|
+
import { piExtensionSource, writePiTrust, removePiTrust } from './pi-harness.js'
|
|
10
12
|
import { runtimeRoot, mainCheckout, readConfig } from './layout.js'
|
|
11
13
|
import { git } from './git.js'
|
|
12
14
|
|
|
@@ -22,7 +24,7 @@ import { git } from './git.js'
|
|
|
22
24
|
// payload shape. On the TS side the harness is derived from the selected launcher or ALL adapters at once
|
|
23
25
|
// (materialize writes every harness's artifacts).
|
|
24
26
|
|
|
25
|
-
export type HarnessId = 'claude' | 'codex'
|
|
27
|
+
export type HarnessId = 'claude' | 'codex' | 'opencode' | 'pi'
|
|
26
28
|
export type HarnessLivenessRecord = { session: string; harnessSessionId?: string | null }
|
|
27
29
|
// the per-pane runtime probe the caller snapshots ONCE for the whole session list and hands liveness():
|
|
28
30
|
// the pane's root pid (tmux `#{pane_pid}`), the hot-tier `pidAlive` verdict, and — ONLY on the legacy path —
|
|
@@ -54,7 +56,6 @@ export interface Harness {
|
|
|
54
56
|
// instead of showing the folder name. This is the ONLY harness branch in the headline path: the capability
|
|
55
57
|
// is data on the adapter, not an `if (codex)` in sessions.ts.
|
|
56
58
|
readonly paneTitleIsSelfSummary: boolean
|
|
57
|
-
|
|
58
59
|
// --- launch / sessionId ---
|
|
59
60
|
// the base agent command. Claude: `claude …`; Codex starts a project-scoped app-server and launches the
|
|
60
61
|
// visible TUI with `--remote` pointed at it. `cmd` is the SESSION's persisted launcher command
|
|
@@ -101,16 +102,18 @@ export interface Harness {
|
|
|
101
102
|
// ONLY place agent-surface divergence lives (the skillDir analog). Claude reads .claude/agents/<name>.md;
|
|
102
103
|
// Codex has no file-discovered agent-definition primitive, so it returns null and materialize skips it.
|
|
103
104
|
agentDir(proj: string): string | null
|
|
104
|
-
// the shim payload:
|
|
105
|
-
//
|
|
106
|
-
|
|
105
|
+
// the shim payload: `content` is whatever artifact THIS harness auto-discovers to wire every event to the
|
|
106
|
+
// dispatcher (harness id baked in) — a settings/hooks JSON for claude/codex, a generated event-bus PLUGIN
|
|
107
|
+
// for opencode, a generated TypeScript EXTENSION for pi — plus the per-event command string (shared with
|
|
108
|
+
// the trust writer so they hash identically).
|
|
109
|
+
shim(dispatch: string, spex: string): { content: string; cmd: (e: string) => string }
|
|
107
110
|
// make a dispatched/self-launched agent run the hooks with zero prompts. Codex writes PROJECT trust — and, on
|
|
108
111
|
// a binary without `--dangerously-bypass-hook-trust`, per-hook trusted_hash blocks — into the GLOBAL
|
|
109
112
|
// ~/.codex/config.toml (codex's security model: trust is global-only). PROJECT trust is UNCONDITIONAL: it
|
|
110
113
|
// ENABLES the project config layer so codex discovers our hooks at all, a tier bypass_hook_trust does NOT
|
|
111
114
|
// cover. Claude is a no-op (it relies on folder-trust). `cmdFor` MUST be the same per-event command the shim
|
|
112
115
|
// emitted.
|
|
113
|
-
writeTrust(proj: string, cmdFor: (e: string) => string):
|
|
116
|
+
writeTrust(proj: string, cmdFor: (e: string) => string): readonly string[]
|
|
114
117
|
|
|
115
118
|
// --- the `/` menu ---
|
|
116
119
|
// the slash-command list, computed the way THIS harness computes its own `/` menu.
|
|
@@ -421,7 +424,7 @@ export function codexSupportsBypassHookTrust(binary: string): boolean {
|
|
|
421
424
|
bypassProbe.set(binary, ok)
|
|
422
425
|
return ok
|
|
423
426
|
}
|
|
424
|
-
export function codexLaunchCommand(_id: string, codexCmd = 'codex
|
|
427
|
+
export function codexLaunchCommand(_id: string, codexCmd = 'codex', serverCmd?: string, dir = runtimeRoot()): string {
|
|
425
428
|
const server = process.env.SPEXCODE_CODEX_SERVER_CMD || serverCmd || codexBinary(codexCmd)
|
|
426
429
|
// The bypass flag ONLY reaches a thread's hook trust as a per-request `config` override, NOT as a CLI flag on
|
|
427
430
|
// the shared `app-server` process (the app-server never reads its own `--dangerously-bypass-hook-trust` for a
|
|
@@ -756,13 +759,18 @@ export function codexTurn(sock: string, threadId: string, text: string, cwd?: st
|
|
|
756
759
|
// rollout just lands a few seconds LATE (not lost). Handing the id to `resume` before then is the "no rollout
|
|
757
760
|
// found for thread id" failure, so codex-launch WAITS for the rollout to land before it trusts the id.
|
|
758
761
|
const codexSessionsDir = () => join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'sessions')
|
|
759
|
-
// does a rollout file for this thread id exist yet? Rollouts are grouped by date
|
|
760
|
-
//
|
|
762
|
+
// does a rollout file for this thread id exist yet? Rollouts are grouped by date; walk day-dirs newest-first
|
|
763
|
+
// (lexical order = chronological on zero-padded YYYY/MM/DD) and return on first hit — the fresh rollout lives in
|
|
764
|
+
// the newest real dir, so the common case reads one dir. The walk is exhaustive, never capped at "the newest few
|
|
765
|
+
// dirs": future-dated junk under sessions/ (a test once planted 2099/12/* in the real CODEX_HOME) sorts above
|
|
766
|
+
// every real day-dir, and a cap let three such dirs mask ALL real rollouts — every codex launch then failed
|
|
767
|
+
// "persisted no rollout" with the rollout sitting on disk. A full walk is a readdir per day-dir — still cheap.
|
|
761
768
|
export function codexRolloutExists(threadId: string, root = codexSessionsDir()): boolean {
|
|
762
769
|
const kids = (d: string) => { try { return readdirSync(d).sort().reverse() } catch { return [] as string[] } }
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
770
|
+
for (const y of kids(root)) for (const m of kids(join(root, y))) for (const d of kids(join(root, y, m))) {
|
|
771
|
+
if (kids(join(root, y, m, d)).some((f) => f.includes(threadId))) return true
|
|
772
|
+
}
|
|
773
|
+
return false
|
|
766
774
|
}
|
|
767
775
|
// poll until the thread's rollout lands (resume-ready) or the budget runs out. Returns false on timeout so the
|
|
768
776
|
// caller can FAIL LOUD instead of handing `resume` / the stored record a non-resumable id. The budget must
|
|
@@ -846,11 +854,11 @@ export function removeManagedBlock(file: string, comment: readonly [string, stri
|
|
|
846
854
|
// the shim for one harness: every event → `SPEX='…' bash <dispatch> <harnessId> <Event>`. The harness id is
|
|
847
855
|
// baked in so dispatch.sh can export SPEXCODE_HARNESS (the detector for the shell side). SPEX is inherited by
|
|
848
856
|
// the cli-needing handlers.
|
|
849
|
-
function buildShim(id: HarnessId, events: readonly string[], dispatch: string, spex: string): {
|
|
857
|
+
function buildShim(id: HarnessId, events: readonly string[], dispatch: string, spex: string): { content: string; cmd: (e: string) => string } {
|
|
850
858
|
const cmd = (e: string) => `SPEX='${spex}' bash ${dispatch} ${id} ${e}`
|
|
851
859
|
const hooks: Record<string, unknown> = {}
|
|
852
860
|
for (const e of events) hooks[e] = [{ hooks: [{ type: 'command', command: cmd(e) }] }]
|
|
853
|
-
return {
|
|
861
|
+
return { content: JSON.stringify({ hooks }, null, 2), cmd }
|
|
854
862
|
}
|
|
855
863
|
|
|
856
864
|
// ---------------------------------------------------------------------------------------------------------
|
|
@@ -909,7 +917,7 @@ function stripCodexTrustFor(cur: string, proj: string, hooksJson: string): strin
|
|
|
909
917
|
// leave a DUPLICATE key (which breaks codex config loading) and self-heals a config that already carried one.
|
|
910
918
|
// Scoped to THIS project path; never touches the user's other config. CODEX_HOME respected for testability.
|
|
911
919
|
// (`events` may be empty for a trust-only stamp in tests.)
|
|
912
|
-
export function writeCodexTrust(proj: string, events: readonly string[], cmdFor: (e: string) => string):
|
|
920
|
+
export function writeCodexTrust(proj: string, events: readonly string[], cmdFor: (e: string) => string): string {
|
|
913
921
|
const home = process.env.CODEX_HOME || join(homedir(), '.codex')
|
|
914
922
|
const file = join(home, 'config.toml')
|
|
915
923
|
const hooksJson = join(proj, '.codex', 'hooks.json')
|
|
@@ -922,6 +930,7 @@ export function writeCodexTrust(proj: string, events: readonly string[], cmdFor:
|
|
|
922
930
|
const cleaned = stripCodexTrustFor(existsSync(file) ? readFileSync(file, 'utf8') : '', proj, hooksJson)
|
|
923
931
|
if (!existsSync(home)) mkdirSync(home, { recursive: true })
|
|
924
932
|
writeFileSync(file, cleaned ? `${cleaned}\n\n${blk}\n` : `${blk}\n`)
|
|
933
|
+
return file
|
|
925
934
|
}
|
|
926
935
|
|
|
927
936
|
// the inverse of writeCodexTrust: strip THIS project's codex trust from the GLOBAL config.toml — the SAME
|
|
@@ -986,7 +995,9 @@ function cleanHarness(h: Harness, proj: string, arts: HarnessArtifacts): void {
|
|
|
986
995
|
// (tmux/ps couldn't report) is not-live, and the caller's boot grace still shows a fresh launch — whose tree
|
|
987
996
|
// may not yet contain codex — as 'starting', not 'offline'.
|
|
988
997
|
const CODEXISH = /^(codex|node)/i // the vendored binary ('codex', 'codex-x86_64…') or the CLI's node runtime
|
|
989
|
-
|
|
998
|
+
// the shared descendant-tree walk: does a process matching `re` live BELOW the pane pid? (The pane pid itself
|
|
999
|
+
// is the shell, so descendants only.) Pure over the caller's one ps snapshot.
|
|
1000
|
+
function paneTreeRuns(pane: PaneProbe | undefined, re: RegExp): boolean {
|
|
990
1001
|
if (!pane?.panePid || !pane.procs?.size) return false
|
|
991
1002
|
const kids = new Map<number, number[]>()
|
|
992
1003
|
for (const [pid, p] of pane.procs) {
|
|
@@ -996,24 +1007,75 @@ export function paneTreeRunsCodex(pane?: PaneProbe): boolean {
|
|
|
996
1007
|
while (stack.length) {
|
|
997
1008
|
const pid = stack.pop()!
|
|
998
1009
|
const comm = pane.procs.get(pid)?.comm ?? ''
|
|
999
|
-
if (
|
|
1010
|
+
if (re.test(comm.slice(comm.lastIndexOf('/') + 1))) return true // basename — macOS ps comm is a full path
|
|
1000
1011
|
const c = kids.get(pid); if (c) stack.push(...c)
|
|
1001
1012
|
}
|
|
1002
1013
|
return false
|
|
1003
1014
|
}
|
|
1015
|
+
export function paneTreeRunsCodex(pane?: PaneProbe): boolean { return paneTreeRuns(pane, CODEXISH) }
|
|
1016
|
+
|
|
1017
|
+
// ONE whole-box pid→(ppid, comm) snapshot (a single `ps` spawn) — the table paneTreeRuns walks. Owned here
|
|
1018
|
+
// (beside its consumers) and shared with sessions.ts's liveSnapshot, so the two probe layers can never parse
|
|
1019
|
+
// ps differently. A failed/timed-out ps returns an empty table: the callers read that as not-provably-running.
|
|
1020
|
+
export async function procSnapshot(timeoutMs = 4000): Promise<ProcTable> {
|
|
1021
|
+
const t: ProcTable = new Map()
|
|
1022
|
+
let out = ''
|
|
1023
|
+
try { ({ stdout: out } = await pexec('ps', ['-eo', 'pid=,ppid=,comm='], { timeout: timeoutMs, killSignal: 'SIGKILL' })) } catch { return t }
|
|
1024
|
+
for (const line of out.split('\n')) {
|
|
1025
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line)
|
|
1026
|
+
if (m) t.set(Number(m[1]), { ppid: Number(m[2]), comm: m[3].trim() })
|
|
1027
|
+
}
|
|
1028
|
+
return t
|
|
1029
|
+
}
|
|
1004
1030
|
|
|
1005
1031
|
// ---------------------------------------------------------------------------------------------------------
|
|
1006
1032
|
// the two implementations.
|
|
1007
1033
|
|
|
1008
1034
|
const CLAUDE_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'StopFailure', 'Notification'] as const
|
|
1009
1035
|
const CODEX_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'] as const
|
|
1036
|
+
// the five claude-shaped events pi's generated extension SYNTHESIZES from its own lifecycle (session_start →
|
|
1037
|
+
// SessionStart, input → UserPromptSubmit, tool_call → PreToolUse, tool_result → PostToolUse, agent_end +
|
|
1038
|
+
// agent_settled → Stop). pi has no idle/attention or failed-stop event → no Notification/StopFailure, same
|
|
1039
|
+
// real gap as codex.
|
|
1040
|
+
const PI_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'] as const
|
|
1010
1041
|
|
|
1011
1042
|
// the resolved base launcher command per harness (the wrapper that sets the config-dir env), shared by
|
|
1012
|
-
// launchCmd and baseCmd so the two never diverge: the launcher's pinned `cmd` wins. The
|
|
1013
|
-
// the fallback for a truly-old record with NO pinned cmd and NO launcher name —
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1043
|
+
// launchCmd and baseCmd so the two never diverge: the launcher's pinned `cmd` wins. The plain command is only
|
|
1044
|
+
// the fallback for a truly-old record with NO pinned cmd and NO launcher name — compatibility must preserve
|
|
1045
|
+
// the harness's normal permission model, never silently introduce an automatic-permission flag. There is no
|
|
1046
|
+
// env/config-field resolution because launchers are ordinary named config entries ([[launcher-select]]).
|
|
1047
|
+
const claudeBaseCmd = (cmd?: string) => cmd || 'claude'
|
|
1048
|
+
const codexBaseCmd = (cmd?: string) => cmd || 'codex'
|
|
1049
|
+
const piBaseCmd = (cmd?: string) => cmd || 'pi' // pi runs tools without permission prompts — no yolo flag exists or is needed
|
|
1050
|
+
const opencodeBaseCmd = (cmd?: string) => cmd || 'opencode'
|
|
1051
|
+
|
|
1052
|
+
// @@@ opencodeLaunchCommand - the tail-branching launch script (the codex marker pattern, minus any server:
|
|
1053
|
+
// opencode is a per-session process like claude). The caller-appended tail ("$@") is EITHER one single-quoted
|
|
1054
|
+
// prompt arg (a NEW launch → `--prompt`), or a resume marker from opencodeHarness.resumeArg: `--resume <id>`
|
|
1055
|
+
// re-attaches the owned opencode session (`--session <id>`, the SAME conversation), `--continue` re-attaches
|
|
1056
|
+
// the worktree's last session when no id was ever captured (the plugin failed before its first event). A new
|
|
1057
|
+
// launch's tail can never BE a literal marker (it's one quoted prompt), so the branch is unambiguous.
|
|
1058
|
+
export function opencodeLaunchCommand(opencodeCmd = 'opencode'): string {
|
|
1059
|
+
const script = [
|
|
1060
|
+
`if [ "\${1:-}" = "--resume" ]; then`,
|
|
1061
|
+
// the marker carries the owned session id — export it so the plugin can seed rootSession at load: a
|
|
1062
|
+
// resumed session re-fires NO bus event until poked, so without this the rendezvous daemon rejects
|
|
1063
|
+
// every delivery (resume-continuity A-side: continuity ✓, steerability ✗).
|
|
1064
|
+
` export SPEXCODE_OPENCODE_RESUME_ID="$2"`,
|
|
1065
|
+
` exec ${opencodeCmd} --session "$2"`,
|
|
1066
|
+
`elif [ "\${1:-}" = "--continue" ]; then`,
|
|
1067
|
+
// no owned id to seed — mark the continue-resume so the plugin knows to ask the SDK for the
|
|
1068
|
+
// reattached session (scoped to this marker so a FRESH launch can never adopt a stale session).
|
|
1069
|
+
` export SPEXCODE_OPENCODE_CONTINUE=1`,
|
|
1070
|
+
` exec ${opencodeCmd} --continue`,
|
|
1071
|
+
`elif [ -n "\${1:-}" ]; then`,
|
|
1072
|
+
` exec ${opencodeCmd} --prompt "$1"`,
|
|
1073
|
+
`else`,
|
|
1074
|
+
` exec ${opencodeCmd}`,
|
|
1075
|
+
`fi`,
|
|
1076
|
+
].join('\n')
|
|
1077
|
+
return `bash -lc ${shQuote(script)} spexcode-opencode`
|
|
1078
|
+
}
|
|
1017
1079
|
|
|
1018
1080
|
export const claudeHarness: Harness = {
|
|
1019
1081
|
id: 'claude',
|
|
@@ -1030,7 +1092,7 @@ export const claudeHarness: Harness = {
|
|
|
1030
1092
|
skillDir: (proj) => join(proj, '.claude', 'skills'),
|
|
1031
1093
|
agentDir: (proj) => join(proj, '.claude', 'agents'),
|
|
1032
1094
|
shim: (dispatch, spex) => buildShim('claude', CLAUDE_EVENTS, dispatch, spex),
|
|
1033
|
-
writeTrust: () =>
|
|
1095
|
+
writeTrust: () => [], // Claude relies on folder-trust — no artifact to report
|
|
1034
1096
|
removeTrust: () => { /* Claude wrote no trust — nothing to strip */ },
|
|
1035
1097
|
clean(proj, arts) { cleanHarness(this, proj, arts) },
|
|
1036
1098
|
slashCommands: claudeSlashCommands,
|
|
@@ -1097,7 +1159,7 @@ export const codexHarness: Harness = {
|
|
|
1097
1159
|
// the prompt and the worker runs unattended. bypass_hook_trust stays on `thread/start` + the resume flag
|
|
1098
1160
|
// as DEFENCE for the non-resume paths (and if a version bump makes a hash mismatch, the app-server
|
|
1099
1161
|
// thread still runs the hooks); it does not REPLACE the hashes here.
|
|
1100
|
-
writeTrust: (proj, cmdFor) => writeCodexTrust(mainCheckout(proj), CODEX_EVENTS, cmdFor),
|
|
1162
|
+
writeTrust: (proj, cmdFor) => [writeCodexTrust(mainCheckout(proj), CODEX_EVENTS, cmdFor)],
|
|
1101
1163
|
// trust is keyed by the MAIN checkout (where the codex shim materializes) — strip it at the same key.
|
|
1102
1164
|
removeTrust: (proj) => removeCodexTrust(mainCheckout(proj)),
|
|
1103
1165
|
clean(proj, arts) { cleanHarness(this, proj, arts) },
|
|
@@ -1121,8 +1183,93 @@ export const codexHarness: Harness = {
|
|
|
1121
1183
|
resumeArg: (rec) => (rec.harnessSessionId ? `--resume ${rec.harnessSessionId}` : ''),
|
|
1122
1184
|
}
|
|
1123
1185
|
|
|
1186
|
+
// @@@ piHarness - the pi adapter (@earendil-works/pi-coding-agent). pi is the CLOSEST to claude of the four:
|
|
1187
|
+
// the caller pins the session id at launch (`--session-id <id>`, creating the session if missing), the shim
|
|
1188
|
+
// lives IN the worktree, and the rendezvous prompt/liveness channel is REUSED wholesale — pi has no external
|
|
1189
|
+
// hook binding (its lifecycle surface is the in-process extension API), so the shim is a GENERATED TypeScript
|
|
1190
|
+
// extension (.pi/extensions/spexcode.ts, run natively by pi) that forwards five claude-shaped events to
|
|
1191
|
+
// dispatch.sh AND binds this session's rendezvous socket itself (sessions.ts already exports
|
|
1192
|
+
// CLAUDE_BG_RENDEZVOUS_SOCK to every ownsRendezvous launch) speaking the reclaude line protocol — so
|
|
1193
|
+
// deliverViaRendezvous and the socket-listener liveness work UNCHANGED. Trust: pi gates project-local
|
|
1194
|
+
// extensions behind saved per-directory trust (~/.pi/agent/trust.json), so writeTrust stamps the main
|
|
1195
|
+
// checkout there (the nearest-parent lookup covers nested worktrees) and the launch carries `--approve` as
|
|
1196
|
+
// one-run defence. See pi-harness.ts for the extension source + trust mechanics.
|
|
1197
|
+
export const piHarness: Harness = {
|
|
1198
|
+
id: 'pi',
|
|
1199
|
+
events: PI_EVENTS,
|
|
1200
|
+
ownsRendezvous: true, // the generated extension binds rvSock(id) and speaks the reclaude protocol
|
|
1201
|
+
paneTitleIsSelfSummary: false, // pi's pane title is not an agent-written task summary → headline uses the prompt preview
|
|
1202
|
+
launchCmd: (_id, _rt, cmd) => `${piBaseCmd(cmd)} --approve`, // --approve = one-run project trust (belt to writeTrust's braces)
|
|
1203
|
+
baseCmd: piBaseCmd,
|
|
1204
|
+
sessionIdArg: (id) => `--session-id ${id}`, // caller pins the exact session id, claude-style (created if missing)
|
|
1205
|
+
sessionEnvVar: 'PI_SESSION_ID', // exported by the generated extension at session_start; tool subprocesses inherit it
|
|
1206
|
+
shimFile: (proj) => join(proj, '.pi', 'extensions', 'spexcode.ts'),
|
|
1207
|
+
worktreeHookAnchor: () => null, // the extension lives in the worktree and self-anchors, like claude
|
|
1208
|
+
contractFiles: (proj) => [join(proj, 'AGENTS.md')], // pi auto-loads AGENTS.md context files (shared with codex — writeManagedBlock is idempotent)
|
|
1209
|
+
skillDir: (proj) => join(proj, '.pi', 'skills'), // Agent Skills standard dirs, discovered after project trust
|
|
1210
|
+
agentDir: () => null, // pi has no file-discovered sub-agent primitive — materialize skips it
|
|
1211
|
+
shim: (dispatch, spex) => ({
|
|
1212
|
+
content: piExtensionSource(dispatch, spex),
|
|
1213
|
+
cmd: (e: string) => `SPEX='${spex}' bash ${dispatch} pi ${e}`, // what the extension actually spawns, for parity with buildShim
|
|
1214
|
+
}),
|
|
1215
|
+
writeTrust: (proj) => [writePiTrust(mainCheckout(proj))], // trust keys on the MAIN checkout; nearest-parent lookup covers worktrees
|
|
1216
|
+
removeTrust: (proj) => removePiTrust(mainCheckout(proj)),
|
|
1217
|
+
clean(proj, arts) { cleanHarness(this, proj, arts) },
|
|
1218
|
+
slashCommands: piSlashCommands,
|
|
1219
|
+
// claude's exact liveness: the window is up AND a live LISTENER answers on the rendezvous socket — the
|
|
1220
|
+
// socket the generated extension binds. socketLive is already probed for every windowed session.
|
|
1221
|
+
liveness: (_rec, tmuxAlive, _runtimeDir, _pane, socketLive) => (tmuxAlive && !!socketLive ? 'online' : 'offline'),
|
|
1222
|
+
deliver: (rec, text) => deliverViaRendezvous(rec.session, text),
|
|
1223
|
+
// reopen the SAME conversation: `--session <id>` resumes the exact session we pinned at launch and FAILS
|
|
1224
|
+
// LOUD when its file is gone (unlike `--session-id`, which would silently mint a fresh empty session).
|
|
1225
|
+
resumeArg: (rec) => `--session ${rec.session}`,
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
export const opencodeHarness: Harness = {
|
|
1229
|
+
id: 'opencode',
|
|
1230
|
+
events: OPENCODE_EVENTS,
|
|
1231
|
+
// LITERALLY true: the generated plugin ([[opencode-harness]], opencode.ts) BINDS the per-session rendezvous
|
|
1232
|
+
// socket the launch env hands it and speaks the reply/repaint mini-protocol, so claude's deliver (atomic
|
|
1233
|
+
// parse-confirmed write) and socket-listener liveness are reused verbatim — no opencode transport code.
|
|
1234
|
+
ownsRendezvous: true,
|
|
1235
|
+
paneTitleIsSelfSummary: false, // opencode's TUI title is not the agent's live task self-summary → headline uses the prompt
|
|
1236
|
+
launchCmd: (_id, _rt, cmd) => opencodeLaunchCommand(opencodeBaseCmd(cmd)), // the tail-branching script (prompt vs --resume/--continue marker)
|
|
1237
|
+
baseCmd: opencodeBaseCmd,
|
|
1238
|
+
sessionIdArg: () => '', // opencode mints its own session id; the plugin's first event reports it back (opencode-capture)
|
|
1239
|
+
// opencode exports NO per-session env var to its tool subprocesses (probed, 1.18.3). Identity flows through
|
|
1240
|
+
// the launch-injected SPEXCODE_SESSION_ID — honest here because each opencode TUI is a per-session process
|
|
1241
|
+
// (no codex-style shared-server contamination). This var is therefore never set; envSessionId's
|
|
1242
|
+
// SPEXCODE_SESSION_ID tier resolves the record.
|
|
1243
|
+
sessionEnvVar: 'OPENCODE_SESSION_ID',
|
|
1244
|
+
// the "shim" is a generated opencode PLUGIN in the worktree's own tree — opencode auto-loads project plugins
|
|
1245
|
+
// by walking the cwd, so like claude it self-anchors and needs no root-checkout rewrite or worktree anchor.
|
|
1246
|
+
shimFile: (proj) => join(proj, '.opencode', 'plugins', 'spexcode.ts'),
|
|
1247
|
+
worktreeHookAnchor: () => null,
|
|
1248
|
+
contractFiles: (proj) => [join(proj, 'AGENTS.md')], // opencode reads AGENTS.md natively (same file codex owns; the managed block is idempotent across writers)
|
|
1249
|
+
skillDir: (proj) => join(proj, '.opencode', 'skills'),
|
|
1250
|
+
agentDir: (proj) => join(proj, '.opencode', 'agents'),
|
|
1251
|
+
// content = the plugin source; cmd = the SAME per-event command the plugin bakes into dispatch calls, so
|
|
1252
|
+
// any consumer that hashes/inspects commands sees one truth (trust is a no-op here regardless).
|
|
1253
|
+
shim: (dispatch, spex) => ({ content: opencodePluginSource(dispatch, spex), cmd: (e) => `SPEX='${spex}' bash ${dispatch} opencode ${e}` }),
|
|
1254
|
+
writeTrust: () => [], // permission policy stays with the launcher command; no trust artifact to report
|
|
1255
|
+
removeTrust: () => { /* nothing was written */ },
|
|
1256
|
+
clean(proj, arts) { cleanHarness(this, proj, arts) },
|
|
1257
|
+
slashCommands: opencodeSlashCommands,
|
|
1258
|
+
// online iff the window is up AND the agent answers on a channel: PREFER the rendezvous socket listener
|
|
1259
|
+
// (the plugin is alive), FALL BACK to the launch-registered agent.pid (kill-0) so a plugin that failed to
|
|
1260
|
+
// load still reads honestly from the process signal instead of a false offline.
|
|
1261
|
+
liveness: (_rec, tmuxAlive, _runtimeDir, pane, socketLive) =>
|
|
1262
|
+
(tmuxAlive && (!!socketLive || pane?.pidAlive === true) ? 'online' : 'offline'),
|
|
1263
|
+
deliver: (rec, text) => deliverViaRendezvous(rec.session, text),
|
|
1264
|
+
// owned opencode session id → `--resume <id>` marker (the launch script re-attaches `--session <id>`, the
|
|
1265
|
+
// SAME conversation); never captured → `--continue` marker (opencode's own "last session in this directory",
|
|
1266
|
+
// which in a dedicated worktree is this worker's). The discriminator is sound for the same reason codex's
|
|
1267
|
+
// is: a NEW launch's tail is always ONE single-quoted prompt arg, never a literal marker.
|
|
1268
|
+
resumeArg: (rec) => (rec.harnessSessionId ? `--resume ${rec.harnessSessionId}` : '--continue'),
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1124
1271
|
// every adapter — materialize iterates this to write each harness's artifacts in one pass.
|
|
1125
|
-
export const HARNESSES: readonly Harness[] = [claudeHarness, codexHarness]
|
|
1272
|
+
export const HARNESSES: readonly Harness[] = [claudeHarness, codexHarness, opencodeHarness, piHarness]
|
|
1126
1273
|
|
|
1127
1274
|
// the legacy/default adapter for old records and config defaults. New launches derive harness from a launcher.
|
|
1128
1275
|
export const defaultHarness: Harness = claudeHarness
|
|
@@ -1135,16 +1282,17 @@ export function harnessById(id: string): Harness {
|
|
|
1135
1282
|
}
|
|
1136
1283
|
|
|
1137
1284
|
// --- named launcher profiles ([[launcher-select]]) ----------------------------------------------------------
|
|
1138
|
-
// a launcher = a `{ harness, cmd }`
|
|
1139
|
-
// `claude` and `codex` are NOT special built-ins — `spex init` SEEDS them as ordinary named
|
|
1140
|
-
// regular command path), so they are edited like any other. harness defaults to claude.
|
|
1141
|
-
// fail-loud on an unknown name (a session must never silently launch under the wrong
|
|
1142
|
-
// harness id. There is NO env-derived built-in fallback: the dropdown lists exactly
|
|
1285
|
+
// a launcher = a `{ harness, cmd }` entry in spexcode.json's `sessions.launchers`, keyed by a
|
|
1286
|
+
// human-chosen name. `claude` and `codex` are NOT special built-ins — `spex init` SEEDS them as ordinary named
|
|
1287
|
+
// launchers (with the regular command path), so they are edited like any other. harness defaults to claude.
|
|
1288
|
+
// resolveLauncher throws fail-loud on an unknown name (a session must never silently launch under the wrong
|
|
1289
|
+
// auth) and validates the harness id. There is NO env-derived built-in fallback: the dropdown lists exactly
|
|
1290
|
+
// the config's real launchers.
|
|
1143
1291
|
export type Launcher = { name: string; harness: string; cmd: string }
|
|
1144
1292
|
export type LauncherDefault = { default: string | null; error: string | null }
|
|
1145
1293
|
|
|
1146
|
-
// the configured named launchers from spexcode.json, as a stable name-sorted list (for the dashboard dropdown
|
|
1147
|
-
// the CLI). Picking a launcher is the ONLY launch choice; the old separate harness pick is gone.
|
|
1294
|
+
// the configured named launchers from spexcode.json, as a stable name-sorted list (for the dashboard dropdown
|
|
1295
|
+
// + the CLI). Picking a launcher is the ONLY launch choice; the old separate harness pick is gone.
|
|
1148
1296
|
export function launcherList(root = mainCheckout()): Launcher[] {
|
|
1149
1297
|
const m = readConfig(root).sessions?.launchers || {}
|
|
1150
1298
|
return Object.keys(m)
|
package/spec-cli/src/help.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
type Entry = { line: string; body: string; see?: string }
|
|
15
15
|
|
|
16
16
|
const SEL_NOTE = `SEL = session id (or unique id-prefix) | node id | branch — every session read/control verb
|
|
17
|
-
accepts any of the
|
|
17
|
+
accepts any of the four; inside a session worktree, . means that worktree's session. On the list verbs (ls/watch),
|
|
18
|
+
none (or @all) means every session.`
|
|
18
19
|
|
|
19
20
|
const ROUTING_NOTE = `Backend routing: every backend-touching verb accepts --api <url> (--port <n> = localhost sugar) to name
|
|
20
21
|
its backend explicitly — the flag always wins. Bare, it resolves: worker env / the cwd project's live
|
|
@@ -24,9 +25,9 @@ const DOT_NOTE = `\`.\` as a node argument means the node THIS worktree works on
|
|
|
24
25
|
node/<id> branch). One-shot payload reads (graph · spec search · session ls/show/review · eval ls ·
|
|
25
26
|
scenario ls · issue ls/show/links) take --json.`
|
|
26
27
|
|
|
27
|
-
const MENTION_NOTE = `Mentions: @session · [[node]] · @new work in ANY prompt, issue, or remark body —
|
|
28
|
-
arg included. [[node]] names the topic node; @session hands the text to that live agent;
|
|
29
|
-
fresh worker on the thread's node.`
|
|
28
|
+
const MENTION_NOTE = `Mentions: @session · [[node]] · @new / @new:<launcher> work in ANY prompt, issue, or remark body —
|
|
29
|
+
text passed as a CLI arg included. [[node]] names the topic node; @session hands the text to that live agent;
|
|
30
|
+
@new spawns a fresh worker on the thread's node (bare = configured default; :<launcher> = that named profile).`
|
|
30
31
|
|
|
31
32
|
const ENTRIES: Record<string, Entry> = {
|
|
32
33
|
// ── project verbs (implicit object = this project) ────────────────────────
|
|
@@ -44,12 +45,14 @@ derived status, title, and attention badges (drift:N · stale:N · issues:N · g
|
|
|
44
45
|
see: 'spex spec search (find one node by intent) · spex session ls (just the sessions, as a table)',
|
|
45
46
|
},
|
|
46
47
|
init: {
|
|
47
|
-
line: 'init [dir] adopt SpexCode on a repo: seed .spec + hooks + materialize [--preset name]',
|
|
48
|
-
body: `Usage: spex init [dir=cwd] [--preset default]
|
|
48
|
+
line: 'init [dir] adopt SpexCode on a repo: seed .spec + hooks + materialize --harness <ids> [--preset name]',
|
|
49
|
+
body: `Usage: spex init [dir=cwd] --harness <id[,id]|plugin:<folder>> [--preset default]
|
|
49
50
|
|
|
50
51
|
Scaffolds adoption in one shot: seeds a starter .spec tree (project root + .plugins plugins), plants
|
|
51
52
|
spexcode.json, installs the git hooks, and materializes the harness artifacts (contract block +
|
|
52
|
-
shims).
|
|
53
|
+
shims). --harness is REQUIRED — the explicit choice of which harnesses materialize delivers into
|
|
54
|
+
(stamped as spexcode.json "harnesses"; only their launchers are seeded); a pre-existing "harnesses"
|
|
55
|
+
field satisfies it. Additive — never overwrites your files. --preset picks the .plugins plugin tier (cumulative).
|
|
53
56
|
Footprint is fixed: materialized artifacts are never tracked — hidden via the per-clone .git/info/exclude, with
|
|
54
57
|
a tracked/mixed CLAUDE.md/AGENTS.md covered by the clean/smudge filter (see spex guide footprint).`,
|
|
55
58
|
see: 'spex guide (the full setup workflow) · spex uninstall (the inverse) · spex spec lint (adoption TODO)',
|
|
@@ -69,24 +72,26 @@ hooks yet (CI, a cloud agent): generated and excluded, the artifacts never arriv
|
|
|
69
72
|
see: 'spex doctor (verify the materialized artifacts actually reach an agent)',
|
|
70
73
|
},
|
|
71
74
|
doctor: {
|
|
72
|
-
line: 'doctor diagnose whether the workflow
|
|
73
|
-
body: `Usage: spex doctor
|
|
74
|
-
hooks + handler existence · backend
|
|
75
|
+
line: 'doctor diagnose spec health and whether the workflow reaches this agent [--contract|--conflicts]',
|
|
76
|
+
body: `Usage: spex doctor spec-health findings + delivery report: preconditions · git-hook floor ·
|
|
77
|
+
contract · hooks + handler existence · backend · footprint
|
|
75
78
|
spex doctor --contract print the composed surface:system text any agent here reads
|
|
76
79
|
spex doctor --conflicts detect double-delivery (loose artifacts beside the managed ones)
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
Bare doctor is the opt-in, read-only health surface: it reports altitude and breadth findings without
|
|
82
|
+
putting them in the lint gate, then audits workflow delivery. Run it directly or let the tidy workflow
|
|
83
|
+
consume the same visible diagnosis.`,
|
|
84
|
+
see: 'spex spec lint (deterministic graph/contract gate) · spex materialize (repair delivery artifacts)',
|
|
81
85
|
},
|
|
82
86
|
uninstall: {
|
|
83
|
-
line: 'uninstall [dir]
|
|
87
|
+
line: 'uninstall [dir] remove all derived artifacts + local state; preserve tracked intent [--hooks]',
|
|
84
88
|
body: `Usage: spex uninstall [dir=cwd] [--hooks]
|
|
85
89
|
|
|
86
|
-
Removes
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
Removes all SpexCode-derived wiring and project-local state: contract blocks, harness shims,
|
|
91
|
+
generated skills/agents, plugin bundles, trust/filter/exclude entries, and the global per-project
|
|
92
|
+
store. Your tracked intent (.spec including .plugins, plus spexcode.json) and surrounding user prose
|
|
93
|
+
are preserved. Git hooks remain unless --hooks; that flag removes only unmodified canonical copies.`,
|
|
94
|
+
see: 'spex init (re-adopt later — your tracked intent survives)',
|
|
90
95
|
},
|
|
91
96
|
serve: {
|
|
92
97
|
line: 'serve [api|ui] api (default) = the backend :8787 · ui = the dashboard :5173 on top of it',
|
|
@@ -106,6 +111,30 @@ projects coexist on one host). Loopback-only by default; --host 0.0.0.0 opens it
|
|
|
106
111
|
still plain HTTP with no gate, so bind wide only on a network you trust.`,
|
|
107
112
|
see: 'GET /health (backend liveness probe)',
|
|
108
113
|
},
|
|
114
|
+
dashboard: {
|
|
115
|
+
line: 'dashboard ONE dashboard for every project you serve — no --api-port pairing [--port N=5173]',
|
|
116
|
+
body: `Usage: spex dashboard [--port N=5173] [--host H=127.0.0.1]
|
|
117
|
+
|
|
118
|
+
The HOST gateway: serves the built dashboard once and routes to EVERY backend the current user runs —
|
|
119
|
+
the multi-project hub engine plus the host registry on top. It continuously reconciles the per-project
|
|
120
|
+
endpoint records each \`spex serve\` publishes (validating each against the live backend's /api/instance
|
|
121
|
+
identity), keeps a durable known-project catalog, and proxies each project's API + SSE + terminal
|
|
122
|
+
socket under /p/<projectId>/* — the project is named in the path, so nothing is "current" and no
|
|
123
|
+
pairing flag exists.
|
|
124
|
+
|
|
125
|
+
Admin surface (hub-authorized: implicit from loopback until an admin password is set, then cookie
|
|
126
|
+
sessions): GET /projects (the validated list + gating state) · GET /projects/stream (SSE) ·
|
|
127
|
+
GET /projects/browse?path=… (read-only host folder picker) · POST /projects {root, initGit?, init?}
|
|
128
|
+
(explicit setup, then register) · GET|PUT /projects/<id>/config (raw portable
|
|
129
|
+
spexcode.json, revision-guarded) · POST /projects/<id>/init|doctor|serve
|
|
130
|
+
(run the real \`spex init\`/\`spex doctor\`, or start an offline project's backend, detached — a
|
|
131
|
+
backend never depends on this gateway staying up) · PUT|DELETE /projects/admin-password and
|
|
132
|
+
/projects/<id>/password (the gates). A gated project answers /p/<id>/login with the designed page.
|
|
133
|
+
|
|
134
|
+
Loopback-only by default; --host widens the bind — the admin surface stays locked to loopback until
|
|
135
|
+
an admin password exists, and ungated projects serve open.`,
|
|
136
|
+
see: 'spex serve (each project\'s backend) · spex serve ui (explicit one-backend pairing)',
|
|
137
|
+
},
|
|
109
138
|
|
|
110
139
|
// ── the noun drawers ──────────────────────────────────────────────────────
|
|
111
140
|
spec: {
|
|
@@ -131,7 +160,7 @@ commit since the spec's version touched an ANCHORED unit's lines, unacked — th
|
|
|
131
160
|
drift; same-file selectors OR'd, one error naming the hit selectors) · one-govern (a node
|
|
132
161
|
governs >1 DISTINCT file) · living (a "## vN" changelog heading) · id-format (an
|
|
133
162
|
id char outside the whitelist — ascii [a-z0-9-] or a non-ascii unicode letter/number, CJK ok — or a
|
|
134
|
-
leaf id reused) · mention (a [[id]] naming no node). Warns:
|
|
163
|
+
leaf id reused) · mention (a [[id]] naming no node). Warns: coverage · drift
|
|
135
164
|
(UNANCHORED drift — always advisory, never blocks; on a scoped file's MISS, \`lint.scopedCodeMiss:
|
|
136
165
|
"ignore"\` may silence it) · anchor (anchoring a type) · related-drift (a scoped related row warns
|
|
137
166
|
per selector HIT, misses silent) · owners (whole-file governors only; scoped don't count) ·
|
|
@@ -147,10 +176,10 @@ edit the spec instead — same commit as the code.`,
|
|
|
147
176
|
session: {
|
|
148
177
|
line: 'session <verb> the worktree state machine: new · ls · watch · wait · review · merge · send · …',
|
|
149
178
|
body: `Manager verbs (dispatch, monitor, land):
|
|
150
|
-
spex session new "<prompt>" [--prompt-file <path>|-] [--
|
|
179
|
+
spex session new "<prompt>" [--prompt-file <path>|-] [--launcher <name>]
|
|
151
180
|
Launch a worker in its own node worktree. Give it ONLY its task — the dev-flow contract
|
|
152
|
-
reaches it through the materialized system prompt.
|
|
153
|
-
|
|
181
|
+
reaches it through the materialized system prompt. The prompt's first [[id]] mention binds
|
|
182
|
+
the session to that node. --prompt-file <path>|- carries a long prompt
|
|
154
183
|
without shell quoting (exclusive with the inline prompt). Then MONITOR it (wait/watch below).
|
|
155
184
|
spex session ls [SEL…] [--status a,b] [--json] one-shot table of living sessions
|
|
156
185
|
spex session watch [SEL…] [--as NAME] [--idle] [--interval N=5]
|
|
@@ -202,15 +231,16 @@ ${MENTION_NOTE}`,
|
|
|
202
231
|
see: 'spex eval ls --session <SEL> (the session’s measured loss) · spex help eval',
|
|
203
232
|
},
|
|
204
233
|
eval: {
|
|
205
|
-
line: 'eval <verb> the measurement system: add · ls · scenario ls · lint · ok · retract · clean',
|
|
234
|
+
line: 'eval <verb> the measurement system: add · ls · scenario ls · matrix · lint · ok · retract · clean',
|
|
206
235
|
body: `Usage: spex eval add [<node>|.] [--scenario <name>] (--pass|--fail) [--note <text>]
|
|
207
236
|
[--image <png> …repeatable] [--result <path|->] [--video <webm|mp4>] [--timeline <json>]
|
|
208
237
|
spex eval ls [<node>|.] [--json] a node's eval timeline, newest first
|
|
209
238
|
spex eval ls --session <SEL> [--json] a session's aggregate: its changed nodes' scores
|
|
210
239
|
spex eval ls --session <SEL> --export [--open | --out <path>]
|
|
211
240
|
spex eval scenario ls [<node>|.] [--unmeasured] [--json] declared scenarios; bare = every node
|
|
241
|
+
spex eval matrix <launcher> [--node <id>] [--rows k1,k2] the harness live-behavior matrix
|
|
212
242
|
spex eval lint [--changed] measurement-layer findings (advisory, always exit 0)
|
|
213
|
-
spex eval ok <node> [--scenario <name>] the HUMAN sign-off on the scenario's latest
|
|
243
|
+
spex eval ok <node> [--scenario <name>] the HUMAN sign-off on the scenario's latest measurement
|
|
214
244
|
spex eval retract [<node>|.] [--scenario <name>] [--last | --ts <iso>] [--note <why>]
|
|
215
245
|
spex eval clean [--keep-latest | --all] GC the content-addressed evidence cache
|
|
216
246
|
|
|
@@ -227,16 +257,22 @@ HTML artifact (diff · evidence inlined · gates) for CI/sharing.
|
|
|
227
257
|
scenario ls — the DECLARED contracts (name · tags · normalized test reference · latest verdict), no evals: bare lists every
|
|
228
258
|
measurable node's scenarios; --unmeasured keeps only the never-measured — the blind-spot worklist.
|
|
229
259
|
|
|
260
|
+
matrix — run the eight-row harness live-behavior matrix against a REAL dispatched session of the named
|
|
261
|
+
launcher (the harness-adapter acceptance rule, defined once in spec-eval/src/matrix.ts): it syncs the
|
|
262
|
+
rows into the \`<harness>-harness\` node's eval.md scenarios, drives one worker through undeclared-stop ·
|
|
263
|
+
pretooluse-block · ask-note · deliver-steer · resume · liveness · commit-gate · close-residue, and files
|
|
264
|
+
a per-row measurement with its evidence transcript. A new harness needs only its launcher + spec node.
|
|
265
|
+
|
|
230
266
|
lint — the measurement layer's findings: malformed eval.md (eval-schema) · unmeasured (eval-missing) ·
|
|
231
267
|
stale (eval-drift) · orphaned remark tracks (eval-dangling) · governed source with no eval.md
|
|
232
268
|
(eval-coverage — the same name and shape as spec lint's coverage, one rule per layer) · over-owned
|
|
233
269
|
files (eval-owners). --changed scopes to the nodes THIS branch touched. spec lint's errors block
|
|
234
270
|
commits; eval lint is PURE ADVISORY, always exit 0 — a measurement gap never blocks anyone.
|
|
235
271
|
|
|
236
|
-
ok — the human's reviewed-and-agreed mark on the scenario's LATEST
|
|
237
|
-
sign-off bound to that one immutable
|
|
272
|
+
ok — the human's reviewed-and-agreed mark on the scenario's LATEST measurement: an appended, monotonic
|
|
273
|
+
sign-off bound to that one immutable measurement (a newer measurement or staleness releases it on its own —
|
|
238
274
|
no un-ok exists). The evals feed default-hides a fresh, ok'd scenario; a governed session is refused
|
|
239
|
-
(an agent's judgment on a
|
|
275
|
+
(an agent's judgment on a measurement is a remark, never a self-blessing).
|
|
240
276
|
|
|
241
277
|
retract — the sanctioned undo for a botched filing: APPENDS a retraction event (traceable, never
|
|
242
278
|
deletes a line); the previous eval becomes latest again, or the scenario honestly returns to
|
|
@@ -363,6 +399,7 @@ Project verbs (implicit object = this project)
|
|
|
363
399
|
${ENTRIES.doctor.line}
|
|
364
400
|
${ENTRIES.uninstall.line}
|
|
365
401
|
${ENTRIES.serve.line}
|
|
402
|
+
${ENTRIES.dashboard.line}
|
|
366
403
|
|
|
367
404
|
Noun drawers
|
|
368
405
|
${ENTRIES.spec.line}
|