spexcode 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -25
- package/package.json +5 -6
- package/spec-cli/README.md +86 -0
- package/spec-cli/bin/spex.mjs +15 -3
- package/spec-cli/hooks/dispatch.sh +20 -8
- package/spec-cli/hooks/harness.sh +18 -11
- package/spec-cli/src/board.ts +47 -18
- package/spec-cli/src/boardCache.ts +70 -0
- package/spec-cli/src/boardDelta.ts +90 -0
- package/spec-cli/src/boardStream.ts +178 -0
- package/spec-cli/src/cli.ts +172 -119
- package/spec-cli/src/client.ts +6 -4
- package/spec-cli/src/gateway.ts +49 -13
- package/spec-cli/src/git.ts +105 -92
- package/spec-cli/src/guide.ts +175 -12
- package/spec-cli/src/harness-select.ts +63 -0
- package/spec-cli/src/harness.ts +506 -100
- package/spec-cli/src/help.ts +360 -0
- package/spec-cli/src/hooks.ts +0 -14
- package/spec-cli/src/index.ts +272 -32
- package/spec-cli/src/init.ts +41 -1
- package/spec-cli/src/issues.ts +250 -0
- package/spec-cli/src/layout.ts +70 -28
- package/spec-cli/src/lint.ts +12 -10
- package/spec-cli/src/listen.ts +28 -0
- package/spec-cli/src/localIssues.ts +683 -0
- package/spec-cli/src/materialize.ts +182 -27
- package/spec-cli/src/mentions.ts +192 -0
- package/spec-cli/src/plugin-harness.ts +145 -0
- package/spec-cli/src/pty-bridge.ts +378 -81
- package/spec-cli/src/self.ts +123 -20
- package/spec-cli/src/sessions.ts +461 -298
- package/spec-cli/src/specs.ts +55 -14
- package/spec-cli/src/supervise.ts +23 -3
- package/spec-cli/src/tsx-bin.ts +14 -5
- package/spec-cli/src/uninstall.ts +146 -0
- package/spec-cli/templates/hooks/post-merge +27 -0
- package/spec-cli/templates/hooks/pre-commit +51 -31
- package/spec-cli/templates/hooks/prepare-commit-msg +31 -8
- package/spec-cli/templates/presets/careful/.config/clarify-before-code/spec.md +11 -0
- package/spec-cli/templates/spec/project/.config/core/spec-of-file/spec-of-file.sh +26 -3
- package/spec-cli/templates/spec/project/.config/core/spec.md +4 -0
- package/spec-cli/templates/spec/project/.config/core/stop-gate/stop-gate.sh +16 -4
- package/spec-cli/templates/spec/project/.config/extract/spec.md +1 -1
- package/spec-cli/templates/spec/project/.config/regroup/spec.md +1 -1
- package/spec-cli/templates/spec/project/.config/reproduce-before-fix/spec.md +18 -0
- package/spec-cli/templates/spec/project/.config/spec.md +3 -3
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +2 -2
- package/spec-cli/templates/spec/project/.config/tidy/spec.md +1 -1
- package/spec-cli/templates/spec/project/spec.md +2 -2
- package/spec-dashboard/dist/assets/index-B0tgHeEQ.js +145 -0
- package/spec-dashboard/dist/assets/index-BTU-44Os.css +32 -0
- package/spec-dashboard/dist/index.html +17 -5
- package/spec-forge/src/cache.ts +16 -0
- package/spec-forge/src/drivers/github.ts +74 -4
- package/spec-forge/src/port.ts +25 -0
- package/spec-forge/src/resident.ts +40 -6
- package/spec-yatsu/src/cli.ts +227 -38
- package/spec-yatsu/src/evaltab.ts +169 -19
- package/spec-yatsu/src/filing.ts +48 -0
- package/spec-yatsu/src/freshness.ts +55 -20
- package/spec-yatsu/src/proof.ts +89 -3
- package/spec-yatsu/src/scenariofresh.ts +92 -0
- package/spec-yatsu/src/sidecar.ts +75 -11
- package/spec-yatsu/src/timeline.ts +47 -0
- package/spec-yatsu/src/yatsu.ts +47 -3
- package/spec-cli/src/relay.ts +0 -28
- package/spec-cli/templates/spec/project/.config/scenario/spec.md +0 -32
- package/spec-dashboard/dist/assets/index-Bk4E1EQy.js +0 -139
- package/spec-dashboard/dist/assets/index-Cq7hwngj.css +0 -32
package/spec-cli/src/self.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// AND codex are covered with no hardcoded paths). It catches the SILENT failure: a shim whose handler is
|
|
7
7
|
// missing, a PATH that can't resolve `spex`, a contract that never landed. Read-only today: `doctor`,
|
|
8
8
|
// `contract` (print the surface:system text any agent reads), `env`. install/uninstall are STAGED (noteStaged).
|
|
9
|
-
import { existsSync, readFileSync, accessSync, constants } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, accessSync, constants } from 'node:fs'
|
|
10
|
+
import { join, dirname, basename } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { execFileSync } from 'node:child_process'
|
|
13
13
|
import { homedir } from 'node:os'
|
|
14
|
-
import { loadSystemConfig } from './specs.js'
|
|
14
|
+
import { loadSystemConfig, loadSkillConfig } from './specs.js'
|
|
15
15
|
import { runtimeRoot, envSessionId, readAliasedRawRecord } from './layout.js'
|
|
16
16
|
|
|
17
17
|
// this file lives at <pkgRoot>/src/self.ts, so `..` is the package root — the same derivation init.ts/
|
|
@@ -74,6 +74,110 @@ function manifestScripts(text: string): string[] {
|
|
|
74
74
|
return [...out]
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// @@@ double-delivery - the SILENT conflict on the OTHER axis from under-delivery: not "did the contract
|
|
78
|
+
// land?" but "did it land TWICE?". A self-launched agent can be reached by BOTH the loose native delivery
|
|
79
|
+
// materialize() writes into the worktree AND a `spexcode` plugin bundle the user installed independently
|
|
80
|
+
// (Claude marketplace) or a stale leftover — doubling every hook (dispatch.sh fires per copy), shadowing
|
|
81
|
+
// skills, confusing the `/` menu. We never sniff payload: every count is by IDENTITY STAMP — a shim's
|
|
82
|
+
// `dispatch.sh` command line (the hook-routing stamp, the same one cleanHarness keys on), a plugin.json
|
|
83
|
+
// `name:"spexcode"` (the bundle stamp), and our own materialized skill NAMES. Per harness we count, on three
|
|
84
|
+
// channels, how many spexcode-stamped copies reach the agent; any channel >1 is a double-delivery conflict.
|
|
85
|
+
|
|
86
|
+
// a plugin bundle dir found under a harness's plugins root, carrying a spexcode stamp.
|
|
87
|
+
type Bundle = { dir: string; name: string; scope: string; hooksToDispatch: boolean; skillsDir: string }
|
|
88
|
+
|
|
89
|
+
// the bundle's declared name, from plugin.json (root or the .claude-plugin/ convention), or null when none.
|
|
90
|
+
function pluginName(dir: string): string | null {
|
|
91
|
+
for (const p of [join(dir, 'plugin.json'), join(dir, '.claude-plugin', 'plugin.json')]) {
|
|
92
|
+
if (!existsSync(p)) continue
|
|
93
|
+
try { const n = (JSON.parse(readFileSync(p, 'utf8')) as { name?: unknown }).name; if (typeof n === 'string') return n } catch { /* malformed → treat as nameless */ }
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
// does this bundle wire a hooks shim that routes to OUR dispatch.sh? (hooks/hooks.json or a root hooks.json)
|
|
98
|
+
function bundleHooksToDispatch(dir: string): boolean {
|
|
99
|
+
return [join(dir, 'hooks', 'hooks.json'), join(dir, 'hooks.json')].some((p) => /dispatch\.sh/.test(read(p)))
|
|
100
|
+
}
|
|
101
|
+
// scan one plugins root's immediate children for SpexCode-stamped bundles (name=="spexcode", a dispatch.sh
|
|
102
|
+
// shim, or one of our own skill names) — three stamps, any one suffices. A non-dir / missing root → [].
|
|
103
|
+
function scanBundles(root: string, scope: string, ourSkills: string[]): Bundle[] {
|
|
104
|
+
let names: string[] = []
|
|
105
|
+
try { names = readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) } catch { return [] }
|
|
106
|
+
const out: Bundle[] = []
|
|
107
|
+
for (const n of names) {
|
|
108
|
+
const dir = join(root, n)
|
|
109
|
+
const skillsDir = join(dir, 'skills')
|
|
110
|
+
const pname = pluginName(dir)
|
|
111
|
+
const hooksToDispatch = bundleHooksToDispatch(dir)
|
|
112
|
+
const hasOurSkill = ourSkills.some((s) => existsSync(join(skillsDir, s, 'SKILL.md')) || existsSync(join(skillsDir, s)))
|
|
113
|
+
if (pname === 'spexcode' || hooksToDispatch || hasOurSkill) out.push({ dir, name: pname ?? n, scope, hooksToDispatch, skillsDir })
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// per-harness double-delivery report. For each harness it derives the plugins roots FROM THE ADAPTER's own
|
|
119
|
+
// shim path (`<cfgdir>/plugins` in-tree + `~/<cfgdir>/plugins` global — no hardcoded `.claude`, so a new
|
|
120
|
+
// harness scans for free), finds the spexcode bundles, then counts three channels against the loose native
|
|
121
|
+
// delivery. Returns the printable lines + whether ANY channel >1 (a live double-delivery).
|
|
122
|
+
async function doubleDeliveryReport(base: string): Promise<{ lines: string[]; conflict: boolean }> {
|
|
123
|
+
const { HARNESSES } = await import('./harness.js')
|
|
124
|
+
const ourSkills = (() => { try { return loadSkillConfig().map((c) => c.name) } catch { return [] as string[] } })()
|
|
125
|
+
const L: string[] = []
|
|
126
|
+
const line = (k: string, v: string) => L.push(` ${k.padEnd(16)}: ${v}`)
|
|
127
|
+
let conflict = false
|
|
128
|
+
const rel = (f: string) => f.startsWith(base + '/') ? f.slice(base.length + 1) : f
|
|
129
|
+
|
|
130
|
+
for (const h of HARNESSES) {
|
|
131
|
+
const shimFile = h.shimFile(base)
|
|
132
|
+
const looseDispatch = /dispatch\.sh/.test(read(shimFile))
|
|
133
|
+
const looseSkillDir = h.skillDir(base)
|
|
134
|
+
const looseContract = h.contractFiles(base).some((f) => /<!--\s*spexcode:start\s*-->/.test(read(f)))
|
|
135
|
+
const looseSkill = (s: string) => !!looseSkillDir && existsSync(join(looseSkillDir, s, 'SKILL.md'))
|
|
136
|
+
const looseDelivery = looseDispatch || looseContract || ourSkills.some(looseSkill)
|
|
137
|
+
|
|
138
|
+
const cfgDir = dirname(shimFile) // <proj>/.claude (codex: <main>/.codex)
|
|
139
|
+
const roots: [string, string][] = [[join(cfgDir, 'plugins'), 'workspace'], [join(homedir(), basename(cfgDir), 'plugins'), 'global']]
|
|
140
|
+
const bundles = roots.flatMap(([r, scope]) => scanBundles(r, scope, ourSkills))
|
|
141
|
+
|
|
142
|
+
// channel 1 — hooks → dispatch.sh (loose shim + any bundle hooks shim)
|
|
143
|
+
const hookSrc = [
|
|
144
|
+
...(looseDispatch ? [`loose ${rel(shimFile)}`] : []),
|
|
145
|
+
...bundles.filter((b) => b.hooksToDispatch).map((b) => `plugin "${b.name}" (${b.scope})`),
|
|
146
|
+
]
|
|
147
|
+
// channel 2 — same-named skill in loose skillDir AND a bundle's skills dir
|
|
148
|
+
const skillHits: string[] = []
|
|
149
|
+
for (const s of ourSkills) {
|
|
150
|
+
let c = looseSkill(s) ? 1 : 0
|
|
151
|
+
c += bundles.filter((b) => existsSync(join(b.skillsDir, s, 'SKILL.md')) || existsSync(join(b.skillsDir, s))).length
|
|
152
|
+
if (c > 1) skillHits.push(`${s} ×${c}`)
|
|
153
|
+
}
|
|
154
|
+
// channel 3 — total delivery sources reaching this harness (loose + each spexcode bundle)
|
|
155
|
+
const sources = (looseDelivery ? 1 : 0) + bundles.length
|
|
156
|
+
|
|
157
|
+
const hConflict = hookSrc.length > 1 || skillHits.length > 0 || sources > 1
|
|
158
|
+
if (hConflict) conflict = true
|
|
159
|
+
|
|
160
|
+
L.push(`${h.id}${hConflict ? ' — DOUBLE-DELIVERY CONFLICT' : ''}`)
|
|
161
|
+
line('delivery srcs', `${sources}${sources > 1 ? ' (>1 → CONFLICT)' : sources === 1 ? ' (single — ok)' : ' (none)'}`)
|
|
162
|
+
line(' loose', looseDelivery ? `present (${[looseDispatch && 'shim→dispatch', looseContract && 'contract', ourSkills.some(looseSkill) && 'skills'].filter(Boolean).join('+')})` : 'absent')
|
|
163
|
+
for (const b of bundles) line(' plugin', `"${b.name}" (${b.scope}) — ${b.dir}`)
|
|
164
|
+
line('hooks→dispatch', `${hookSrc.length}${hookSrc.length > 1 ? ' (>1 → CONFLICT): ' + hookSrc.join(', ') : hookSrc.length === 1 ? ' (single — ok)' : ' (none wired)'}`)
|
|
165
|
+
line('skill shadowing', skillHits.length ? `CONFLICT: ${skillHits.join(', ')}` : 'none')
|
|
166
|
+
L.push('')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (conflict) {
|
|
170
|
+
L.push('Repair — SpexCode is reaching this agent through MORE THAN ONE discovery channel. Keep exactly one:')
|
|
171
|
+
L.push(' • remove the independently-installed plugin bundle (delete its dir, or `claude plugin uninstall spexcode`); or')
|
|
172
|
+
L.push(' • if you WANT the plugin, stop the native delivery: set spexcode.json "harnesses" to a plugin target')
|
|
173
|
+
L.push(' (e.g. ["plugin",{"plugin":".claude"}] → {"plugin":".claude"}) so `spex materialize` prunes the loose shim/contract/skills; or')
|
|
174
|
+
L.push(' • remove the loose copy directly (`spex self uninstall` [staged] / `spex uninstall`).')
|
|
175
|
+
} else {
|
|
176
|
+
L.push('No double-delivery: each harness is reached by at most one spexcode-stamped channel.')
|
|
177
|
+
}
|
|
178
|
+
return { lines: L, conflict }
|
|
179
|
+
}
|
|
180
|
+
|
|
77
181
|
// ping the backend (apiBase) with a short timeout so doctor never hangs on a dead/wrong SPEXCODE_API_URL.
|
|
78
182
|
async function backendReachable(): Promise<{ base: string; up: boolean }> {
|
|
79
183
|
let base = 'http://127.0.0.1:8787'
|
|
@@ -170,6 +274,11 @@ async function doctor(): Promise<number> {
|
|
|
170
274
|
L.push('\nLayer 4 — session orchestration (backend-only: dispatch · queue · comms)')
|
|
171
275
|
line('backend', up ? `reachable at ${backendBase}` : `not reachable at ${backendBase}`)
|
|
172
276
|
|
|
277
|
+
// --- double-delivery: the same agent reached through two discovery channels (loose + a plugin bundle) ---
|
|
278
|
+
const dd = await doubleDeliveryReport(base)
|
|
279
|
+
L.push('\nLayer 5 — double-delivery (one harness, two discovery channels)')
|
|
280
|
+
L.push(...dd.lines)
|
|
281
|
+
|
|
173
282
|
// --- verdict ---
|
|
174
283
|
L.push('\nCoverage verdict')
|
|
175
284
|
line('preconditions', resolveOnPath('spex') ? 'spex resolves' : 'BLOCKED — spex not on PATH (fix first)')
|
|
@@ -177,6 +286,7 @@ async function doctor(): Promise<number> {
|
|
|
177
286
|
line('layer 2', body.length === 0 ? 'ABSENT (no contract)' : 'see per-harness above')
|
|
178
287
|
line('layer 3', manifestText ? 'see handler-existence above' : 'ABSENT (no manifest — agent ungoverned)')
|
|
179
288
|
line('layer 4', up ? 'present' : managed ? 'EXPECTED but backend down' : 'absent (normal for bring-your-own-agent)')
|
|
289
|
+
line('layer 5', dd.conflict ? 'CONFLICT (double-delivery — see Layer 5; `spex self conflicts`)' : 'clean (single channel)')
|
|
180
290
|
|
|
181
291
|
// --- footprint: every artifact Spex wrote here, + any slot held by something not ours ---
|
|
182
292
|
L.push('\nFootprint (what Spex wrote into this environment)')
|
|
@@ -205,24 +315,17 @@ function contract(): number {
|
|
|
205
315
|
return 0
|
|
206
316
|
}
|
|
207
317
|
|
|
208
|
-
//
|
|
209
|
-
|
|
318
|
+
// the focused double-delivery check: JUST Layer 5, exit non-zero when a conflict is live so it gates a script
|
|
319
|
+
// / yatsu. Anchors at the repo root like doctor (the shims + contract + skills live there).
|
|
320
|
+
async function conflicts(): Promise<number> {
|
|
210
321
|
const cwd = process.cwd()
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
node: process.version,
|
|
216
|
-
CLAUDE_CODE_SESSION_ID: process.env.CLAUDE_CODE_SESSION_ID ?? '',
|
|
217
|
-
CODEX_THREAD_ID: process.env.CODEX_THREAD_ID ?? '',
|
|
218
|
-
SPEXCODE_API_URL: process.env.SPEXCODE_API_URL ?? '(default :8787)',
|
|
219
|
-
'PATH spex': resolveOnPath('spex') ?? '(not on PATH)',
|
|
220
|
-
tmux: process.env.TMUX ? 'inside tmux' : 'no',
|
|
221
|
-
}
|
|
222
|
-
for (const [k, v] of Object.entries(facts)) console.log(`${k.padEnd(24)}: ${v}`)
|
|
223
|
-
return 0
|
|
322
|
+
const base = repoRoot(cwd) ?? cwd
|
|
323
|
+
const { lines, conflict } = await doubleDeliveryReport(base)
|
|
324
|
+
console.log(['spex self conflicts — does SpexCode reach this agent through more than one discovery channel?\n', ...lines].join('\n'))
|
|
325
|
+
return conflict ? 1 : 0
|
|
224
326
|
}
|
|
225
327
|
|
|
328
|
+
|
|
226
329
|
// install/uninstall are STAGED: wiring layer-3 hooks into a standalone repo is only SAFE once the hooks
|
|
227
330
|
// detect a missing managed session and degrade. So the diagnosis ships first; the installer lands behind it.
|
|
228
331
|
function noteStaged(verb: string): number {
|
|
@@ -237,7 +340,7 @@ function usage(): number {
|
|
|
237
340
|
console.error(`spex self — diagnose how the SpexCode workflow reaches your agent
|
|
238
341
|
doctor per-layer report: preconditions · git-hook floor · contract · hooks(+handlers) · backend · footprint (default)
|
|
239
342
|
contract print the surface:system contract text (hand it to any agent)
|
|
240
|
-
|
|
343
|
+
conflicts detect double-delivery — the same agent reached via loose native delivery AND a plugin bundle (exits non-zero on conflict)
|
|
241
344
|
install [staged] wire the materialized contract + hooks into your agent (--agent claude, --minimal)
|
|
242
345
|
uninstall [staged] reverse exactly what install wrote`)
|
|
243
346
|
return 0
|
|
@@ -247,7 +350,7 @@ export async function runSelf(args: string[]): Promise<number> {
|
|
|
247
350
|
switch (args[0] ?? 'doctor') {
|
|
248
351
|
case 'doctor': return await doctor()
|
|
249
352
|
case 'contract': return contract()
|
|
250
|
-
case '
|
|
353
|
+
case 'conflicts': return await conflicts()
|
|
251
354
|
case 'install': return noteStaged('install')
|
|
252
355
|
case 'uninstall': return noteStaged('uninstall')
|
|
253
356
|
case 'help': case '--help': case '-h': return usage()
|