spexcode 0.1.2 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpexCode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
6
6
  "license": "MIT",
7
- "bin": { "spex": "spec-cli/bin/spex.mjs" },
7
+ "bin": {
8
+ "spex": "spec-cli/bin/spex.mjs"
9
+ },
8
10
  "files": [
9
11
  "spec-cli/src",
10
12
  "!spec-cli/src/**/*.test.ts",
@@ -18,7 +20,9 @@
18
20
  "spec-dashboard/dist",
19
21
  "README.md"
20
22
  ],
21
- "engines": { "node": ">=22" },
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
22
26
  "scripts": {
23
27
  "api": "npm --prefix spec-cli run serve",
24
28
  "web": "npm --prefix spec-dashboard run dev",
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process'
7
7
  import { promisify } from 'node:util'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { claudeSlashCommands, codexSlashCommands, type SlashCommand } from './slash-commands.js'
10
- import { runtimeRoot, mainCheckout } from './layout.js'
10
+ import { runtimeRoot, mainCheckout, readConfig } from './layout.js'
11
11
  import { tsxBin } from './tsx-bin.js'
12
12
 
13
13
  // @@@ harness-adapter - the ONE seam between SpexCode and the coding-agent harness (Claude Code, Codex, …).
@@ -609,7 +609,7 @@ export const claudeHarness: Harness = {
609
609
  events: CLAUDE_EVENTS,
610
610
  ownsRendezvous: true, // reclaude opens the rendezvous control socket (prompt delivery + liveness)
611
611
  paneTitleIsSelfSummary: true, // claude writes its live task summary into the OSC pane title → headline derives from it
612
- launchCmd: () => process.env.SPEXCODE_CLAUDE_CMD || 'claude --dangerously-skip-permissions',
612
+ launchCmd: () => process.env.SPEXCODE_CLAUDE_CMD || readConfig(mainCheckout()).sessions?.claudeCmd || 'claude --dangerously-skip-permissions',
613
613
  sessionIdArg: (id) => `--session-id ${id}`, // the caller chooses the id
614
614
  sessionEnvVar: 'CLAUDE_CODE_SESSION_ID',
615
615
  shimFile: (proj) => join(proj, '.claude', 'settings.json'),
@@ -628,7 +628,7 @@ export const codexHarness: Harness = {
628
628
  events: CODEX_EVENTS,
629
629
  ownsRendezvous: false, // no reclaude daemon — liveness + prompts through the project app-server socket
630
630
  paneTitleIsSelfSummary: false, // codex's pane title is a spinner + the cwd folder name, NOT a task summary → headline uses the prompt
631
- launchCmd: (id, runtimeDir) => codexLaunchCommand(id, undefined, undefined, runtimeDir ?? runtimeRoot()), // ONE app-server per PROJECT
631
+ launchCmd: (id, runtimeDir) => codexLaunchCommand(id, process.env.SPEXCODE_CODEX_CMD || readConfig(mainCheckout()).sessions?.codexCmd, undefined, runtimeDir ?? runtimeRoot()), // env→config→default; ONE app-server per PROJECT
632
632
  sessionIdArg: () => '', // codex assigns its own id (the backend owns it via thread/start)
633
633
  sessionEnvVar: 'CODEX_THREAD_ID',
634
634
  // Codex discovers a LINKED worktree's PROJECT hooks from the ROOT CHECKOUT's `.codex`, NOT the worktree's
@@ -45,6 +45,19 @@ function resolveHooksDir(dir: string): string | null {
45
45
 
46
46
  export async function specInit(targetArg: string | undefined): Promise<void> {
47
47
  const targetDir = resolve(targetArg ?? process.cwd())
48
+
49
+ // SpexCode is git-backed: git IS the version database and the hooks live in `.git`. A non-git target
50
+ // would leave a HALF-STATE — specs on disk but no version history, no hooks, no harness shims, no
51
+ // sessions. So fail LOUD before writing anything, rather than seeding debris and warning past it. We do
52
+ // NOT `git init` for them: creating a repo is a side effect beyond init's remit (a subdir, a dir not
53
+ // meant as a repo root), and `git init` is one deliberate command.
54
+ try {
55
+ execFileSync('git', ['-C', targetDir, 'rev-parse', '--is-inside-work-tree'], { stdio: ['ignore', 'ignore', 'ignore'] })
56
+ } catch {
57
+ console.error(`spex init: ${targetDir} is not a git repository. SpexCode is git-backed (git is the version database; the hooks live in .git). Run \`git init\` there first, then \`spex init\`.`)
58
+ process.exit(1)
59
+ }
60
+
48
61
  if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true })
49
62
  console.log(`spex init → ${targetDir}`)
50
63
 
@@ -16,6 +16,8 @@ type Config = {
16
16
  }
17
17
  sessions?: {
18
18
  maxActive?: number // concurrency cap: max agents AUTONOMOUSLY PROGRESSING at once (default 6; see sessions.ts maxActive)
19
+ claudeCmd?: string // worker launcher for Claude (default 'claude --dangerously-skip-permissions'); env SPEXCODE_CLAUDE_CMD overrides. A host-specific ABS path belongs in the gitignored spexcode.local.json, not here.
20
+ codexCmd?: string // worker launcher for Codex (default 'codex --yolo'); env SPEXCODE_CODEX_CMD overrides. Same host-path rule.
19
21
  }
20
22
  serve?: {
21
23
  // public-exposure config for `spex serve --public` (resolved gateway-side; see [[public-mode]] / gateway.ts).
@@ -39,11 +41,24 @@ export type Worktree = {
39
41
  }
40
42
  export type Layout = { main: string; convention: Convention; worktrees: Worktree[] }
41
43
 
42
- export function readConfig(root: string): Config {
43
- const p = join(root, 'spexcode.json')
44
+ function readJsonOr(p: string): any {
44
45
  if (!existsSync(p)) return {}
45
46
  try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} }
46
47
  }
48
+ // committed `spexcode.json` with an OPTIONAL machine-local `spexcode.local.json` layered on top (gitignored).
49
+ // The local layer is the durable home for HOST-SPECIFIC values that must never be committed — e.g. an
50
+ // absolute worker-launcher path (the host-path leak the repo otherwise warns against). Precedence per field:
51
+ // local over committed; an env var (e.g. SPEXCODE_CLAUDE_CMD) still overrides both at its read site.
52
+ export function readConfig(root: string): Config {
53
+ const committed = readJsonOr(join(root, 'spexcode.json'))
54
+ const local = readJsonOr(join(root, 'spexcode.local.json'))
55
+ const out: any = { ...committed }
56
+ for (const k of Object.keys(local)) {
57
+ const b = committed[k], o = local[k]
58
+ out[k] = (b && o && typeof b === 'object' && typeof o === 'object' && !Array.isArray(o)) ? { ...b, ...o } : o
59
+ }
60
+ return out
61
+ }
47
62
 
48
63
  // the shared git common dir (env-stripped git() so a hook's exported GIT_DIR can't misdirect it). Memoized:
49
64
  // it's a process constant, but mainBranch()/mainRoot() resolve it per call (~60 git rev-parse forks per board build without the cache).