spexcode 0.2.1 → 0.2.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 +158 -103
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +24 -1
- package/spec-cli/src/attach.ts +50 -0
- package/spec-cli/src/cli.ts +217 -64
- package/spec-cli/src/client.ts +47 -9
- package/spec-cli/src/{self.ts → doctor.ts} +26 -25
- package/spec-cli/src/guide.ts +79 -21
- package/spec-cli/src/harness.ts +53 -29
- package/spec-cli/src/help.ts +137 -49
- package/spec-cli/src/index.ts +31 -11
- package/spec-cli/src/issues.ts +48 -21
- package/spec-cli/src/layout.ts +3 -5
- package/spec-cli/src/lint.ts +34 -5
- package/spec-cli/src/localIssues.ts +44 -60
- package/spec-cli/src/materialize.ts +4 -2
- package/spec-cli/src/mentions.ts +22 -1
- package/spec-cli/src/pty-bridge.ts +39 -4
- package/spec-cli/src/ranker.ts +31 -12
- package/spec-cli/src/search.bench.mjs +30 -7
- package/spec-cli/src/search.ts +39 -0
- package/spec-cli/src/sessions.ts +160 -69
- package/spec-cli/src/specs.ts +16 -4
- package/spec-cli/src/supervise.ts +30 -6
- package/spec-cli/src/tree.ts +118 -0
- package/spec-cli/templates/hooks/post-merge +2 -2
- package/spec-cli/templates/hooks/pre-commit +34 -15
- package/spec-cli/templates/hooks/prepare-commit-msg +8 -1
- package/spec-cli/templates/spexcode.json +7 -0
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +1 -0
- package/spec-dashboard/dist/assets/Dashboard-Dlg78cbC.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-CDxc1-in.js +3 -0
- package/spec-dashboard/dist/assets/FoldToggle-B5leylLf.js +1 -0
- package/spec-dashboard/dist/assets/IssuesPage-C2yFXiO-.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-RHNECU6x.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-YLD6IOmC.js +71 -0
- package/spec-dashboard/dist/assets/SessionWindow-CmKtpNUX.js +9 -0
- package/spec-dashboard/dist/assets/Settings-ZnOwskMZ.js +1 -0
- package/spec-dashboard/dist/assets/index-BdRQfrkR.js +41 -0
- package/spec-dashboard/dist/assets/index-DEc5Ru3l.css +1 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-yatsu/src/cli.ts +128 -26
- package/spec-yatsu/src/evaltab.ts +7 -6
- package/spec-yatsu/src/filing.ts +6 -3
- package/spec-yatsu/src/proof.ts +10 -0
- package/spec-yatsu/src/scenariofresh.ts +100 -30
- package/spec-yatsu/src/sidecar.ts +25 -3
- package/spec-yatsu/src/timeline.ts +53 -23
- package/README.zh-CN.md +0 -135
- package/spec-dashboard/dist/assets/index-Ct_ubwrd.css +0 -32
- package/spec-dashboard/dist/assets/index-DehTZ-h9.js +0 -145
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import net from 'node:net'
|
|
4
4
|
import http from 'node:http'
|
|
5
5
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
6
|
-
import { statSync, readdirSync, type Dirent } from 'node:fs'
|
|
6
|
+
import { statSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, type Dirent } from 'node:fs'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { dirname, join } from 'node:path'
|
|
9
9
|
import { installProcessGuards } from './resilience.js'
|
|
10
10
|
import { listenOrExit } from './listen.js'
|
|
11
11
|
import { resolvePublicConfig, startGateway, ensureDashboardBuilt, resolveDistDir } from './gateway.js'
|
|
12
12
|
import { tsxBin } from './tsx-bin.js'
|
|
13
|
+
import { runtimeRoot } from './layout.js'
|
|
13
14
|
|
|
14
15
|
// the supervisor OWNS the public port, so it must outlive any transient throw: an uncaught error here is
|
|
15
16
|
// logged and survived, never an exit that closes the port (and the tmux session) and takes the frontend down.
|
|
@@ -74,8 +75,11 @@ async function boot(): Promise<Backend | null> {
|
|
|
74
75
|
const port = await freePort()
|
|
75
76
|
// PORT pins the child's PRIVATE bind port; SPEXCODE_API_URL pins everything the child SPAWNS (launched
|
|
76
77
|
// sessions + their hooks) at the PUBLIC port, so a launched agent's own `spex` reaches the stable proxy
|
|
77
|
-
// and never inherits this ephemeral, soon-retired port
|
|
78
|
-
|
|
78
|
+
// and never inherits this ephemeral, soon-retired port. ALWAYS childApiBase, never the ambient
|
|
79
|
+
// process.env.SPEXCODE_API_URL: the env this serve itself inherited may carry ANOTHER project's backend
|
|
80
|
+
// (the exact misroute [[remote-client]]'s ladder exists to kill), and a worker's env is its routing
|
|
81
|
+
// LIFELINE — it must be a deterministic backend-injected fact, not an inheritance gamble.
|
|
82
|
+
const child = spawn(tsx, [entry], { stdio: 'inherit', env: { ...process.env, PORT: String(port), SPEXCODE_API_URL: childApiBase } })
|
|
79
83
|
// if the ACTIVE backend dies unexpectedly (crash, OOM), restart it so the public port keeps serving.
|
|
80
84
|
// Planned retirement sets current to the NEW child first, so the old child's exit fails this identity
|
|
81
85
|
// check and is ignored. boot()'s ~5s health budget rate-limits any crash loop.
|
|
@@ -130,7 +134,27 @@ const proxy = net.createServer((client) => {
|
|
|
130
134
|
client.pipe(up); up.pipe(client)
|
|
131
135
|
})
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
// @@@ endpoint record - this project's live backend endpoint, written into the per-project runtime tier
|
|
138
|
+
// (~/.spexcode/projects/<enc>/backend.json) only AFTER the public bind succeeds. It's what lets a bare
|
|
139
|
+
// `spex` run from this project's tree find ITS OWN backend instead of an env URL inherited from another
|
|
140
|
+
// project's ([[remote-client]]'s resolution ladder). Readers /health-probe before trusting, so a crash
|
|
141
|
+
// leaves at worst a dead record that is ignored — never followed. The recorded URL is the LOOPBACK face
|
|
142
|
+
// local agents reach (equals the public port when public mode is off), never the password-gated gateway.
|
|
143
|
+
const backendRecordPath = () => join(runtimeRoot(), 'backend.json')
|
|
144
|
+
function recordEndpoint(url: string): void {
|
|
145
|
+
try {
|
|
146
|
+
mkdirSync(runtimeRoot(), { recursive: true })
|
|
147
|
+
writeFileSync(backendRecordPath(), JSON.stringify({ url, pid: process.pid, startedAt: new Date().toISOString() }, null, 2) + '\n')
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`[supervisor] could not record the backend endpoint (${(e as Error).message}) — cwd-based \`spex\` discovery won't find this backend`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// best-effort removal on a clean stop, only if the record is OURS (a newer serve may have overwritten it).
|
|
153
|
+
function dropEndpoint(): void {
|
|
154
|
+
try { if (JSON.parse(readFileSync(backendRecordPath(), 'utf8'))?.pid === process.pid) rmSync(backendRecordPath()) } catch { /* not ours / already gone */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const shutdown = () => { dropEndpoint(); try { current?.child.kill('SIGTERM') } catch { /* */ } process.exit(0) }
|
|
134
158
|
process.on('SIGINT', shutdown)
|
|
135
159
|
process.on('SIGTERM', shutdown)
|
|
136
160
|
|
|
@@ -146,10 +170,10 @@ if (publicCfg) {
|
|
|
146
170
|
// public mode: the raw proxy stays on loopback; the password-gated gateway owns the public port.
|
|
147
171
|
const distDir = resolveDistDir() // bundled <pkg>/dashboard-dist when installed, else monorepo spec-dashboard/dist
|
|
148
172
|
ensureDashboardBuilt(repoRoot, distDir)
|
|
149
|
-
listenOrExit(proxy, proxyPort, { host: '127.0.0.1', label: 'supervisor (loopback proxy)', cleanup: reapChild, onListen: () => console.log(`spec-cli supervisor on loopback :${proxyPort} (zero-downtime reloads, backend :${first.port})`) })
|
|
173
|
+
listenOrExit(proxy, proxyPort, { host: '127.0.0.1', label: 'supervisor (loopback proxy)', cleanup: reapChild, onListen: () => { recordEndpoint(childApiBase); console.log(`spec-cli supervisor on loopback :${proxyPort} (zero-downtime reloads, backend :${first.port})`) } })
|
|
150
174
|
startGateway({ publicPort, upstreamPort: proxyPort, password: publicCfg.password, tls: publicCfg.tls, distDir, onBindFail: reapChild })
|
|
151
175
|
} else {
|
|
152
|
-
listenOrExit(proxy, publicPort, { label: 'supervisor', cleanup: reapChild, onListen: () => console.log(`spec-cli supervisor serving on http://localhost:${publicPort} (zero-downtime reloads, backend :${first.port})`) })
|
|
176
|
+
listenOrExit(proxy, publicPort, { label: 'supervisor', cleanup: reapChild, onListen: () => { recordEndpoint(childApiBase); console.log(`spec-cli supervisor serving on http://localhost:${publicPort} (zero-downtime reloads, backend :${first.port})`) } })
|
|
153
177
|
}
|
|
154
178
|
|
|
155
179
|
// watch every imported source tree; debounce a burst of writes (a merge touching several files across
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @@@ spex tree - the CLI's human-readable graph view: the same assembled board the dashboard's
|
|
2
|
+
// tidy-tree renders, as an indented terminal tree. Pure presentation over buildBoard()'s nodes —
|
|
3
|
+
// no read path of its own; status colours and badge semantics mirror the dashboard (drift /
|
|
4
|
+
// stale-yatsu / open-issues counts). Governed by the spex-tree spec node.
|
|
5
|
+
|
|
6
|
+
// the subset of a board node this view consumes (buildBoard attaches more; we read only these).
|
|
7
|
+
export type TreeNode = {
|
|
8
|
+
id: string
|
|
9
|
+
parent: string | null
|
|
10
|
+
title: string
|
|
11
|
+
status: string
|
|
12
|
+
version?: number
|
|
13
|
+
drift?: number
|
|
14
|
+
ghost?: boolean
|
|
15
|
+
openIssues?: unknown[]
|
|
16
|
+
scenarios?: { name: string }[]
|
|
17
|
+
evals?: { scenario: string; fresh?: boolean }[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TreeOpts = { node?: string; depth?: number; color?: boolean }
|
|
21
|
+
|
|
22
|
+
// dashboard status palette mapped onto ANSI: merged=green, active=cyan (live/in-flight, distinct
|
|
23
|
+
// from the warning yellow), drift=yellow (the dashboard's warning colour), pending=muted grey.
|
|
24
|
+
const STATUS_ANSI: Record<string, string> = { merged: '32', active: '36', drift: '33', pending: '90' }
|
|
25
|
+
|
|
26
|
+
// stale-yatsu count: declared scenarios whose LATEST reading exists but is no longer fresh.
|
|
27
|
+
// board `evals` is already latest-per-scenario, so this is a straight filter — the same freshness
|
|
28
|
+
// axis the dashboard's grey ✓/✗ badges read (score.jsx readingScore).
|
|
29
|
+
function staleYatsu(n: TreeNode): number {
|
|
30
|
+
if (!n.scenarios?.length || !n.evals?.length) return 0
|
|
31
|
+
const latest = new Map(n.evals.map((r) => [r.scenario, r]))
|
|
32
|
+
return n.scenarios.filter((s) => { const r = latest.get(s.name); return r && r.fresh === false }).length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function childrenIndex(nodes: TreeNode[]): Map<string | null, TreeNode[]> {
|
|
36
|
+
const byParent = new Map<string | null, TreeNode[]>()
|
|
37
|
+
for (const n of nodes) {
|
|
38
|
+
const list = byParent.get(n.parent) ?? []
|
|
39
|
+
list.push(n)
|
|
40
|
+
byParent.set(n.parent, list)
|
|
41
|
+
}
|
|
42
|
+
return byParent
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// resolve the display roots: --node picks one subtree; default is the forest of parentless nodes
|
|
46
|
+
// (a node whose parent id isn't on the board — a ghost mid-add, say — surfaces as a root rather
|
|
47
|
+
// than vanishing). Unknown --node throws (fail loud, caller prints + exits 2), never an empty tree.
|
|
48
|
+
function roots(nodes: TreeNode[], nodeId?: string): TreeNode[] {
|
|
49
|
+
if (nodeId) {
|
|
50
|
+
const hit = nodes.find((n) => n.id === nodeId)
|
|
51
|
+
if (!hit) throw new Error(`no spec node "${nodeId}" — spex tree lists every id; spex search <topic> finds one by intent`)
|
|
52
|
+
return [hit]
|
|
53
|
+
}
|
|
54
|
+
const ids = new Set(nodes.map((n) => n.id))
|
|
55
|
+
return nodes.filter((n) => n.parent === null || !ids.has(n.parent))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function renderTree(nodes: TreeNode[], opts: TreeOpts = {}): string {
|
|
59
|
+
const color = opts.color ?? false
|
|
60
|
+
const c = (code: string, t: string) => (color ? `\x1b[${code}m${t}\x1b[0m` : t)
|
|
61
|
+
const byParent = childrenIndex(nodes)
|
|
62
|
+
const lines: string[] = []
|
|
63
|
+
|
|
64
|
+
const badges = (n: TreeNode): string => {
|
|
65
|
+
const parts: string[] = []
|
|
66
|
+
if (n.ghost) parts.push(c('90', 'ghost'))
|
|
67
|
+
if (n.drift) parts.push(c('33', `drift:${n.drift}`))
|
|
68
|
+
const stale = staleYatsu(n)
|
|
69
|
+
if (stale) parts.push(c('90', `stale:${stale}`))
|
|
70
|
+
if (n.openIssues?.length) parts.push(c('31', `issues:${n.openIssues.length}`))
|
|
71
|
+
return parts.length ? ' ' + parts.join(' ') : ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const line = (n: TreeNode, prefix: string, branch: string) => {
|
|
75
|
+
const code = STATUS_ANSI[n.status] ?? '0'
|
|
76
|
+
// base state is invisible: merged (~80% of nodes) is the healthy ground state, already carried by
|
|
77
|
+
// the dot's colour — only deviations get a text label. Without colour the word IS the signal, so
|
|
78
|
+
// every status (merged included) prints in plain-text mode.
|
|
79
|
+
const label = color && n.status === 'merged' ? '' : ' ' + c(code, `[${n.status}]`)
|
|
80
|
+
const title = n.title && n.title !== n.id ? ' ' + c('90', '· ' + n.title) : ''
|
|
81
|
+
lines.push(`${prefix}${branch}${c(code, '●')} ${n.id}${label}${title}${badges(n)}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const walk = (n: TreeNode, prefix: string, branch: string, childPrefix: string, depth: number) => {
|
|
85
|
+
line(n, prefix, branch)
|
|
86
|
+
const kids = byParent.get(n.id) ?? []
|
|
87
|
+
if (!kids.length) return
|
|
88
|
+
if (opts.depth !== undefined && depth >= opts.depth) {
|
|
89
|
+
lines.push(`${childPrefix}${c('90', `└─ … ${kids.length} more (raise --depth)`)}`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
kids.forEach((k, i) => {
|
|
93
|
+
const last = i === kids.length - 1
|
|
94
|
+
walk(k, childPrefix, last ? '└─ ' : '├─ ', childPrefix + (last ? ' ' : '│ '), depth + 1)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const r of roots(nodes, opts.node)) walk(r, '', '', '', 0)
|
|
99
|
+
return lines.join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// the machine exit: the same filtered subtree as NESTED objects, badge counts precomputed — a
|
|
103
|
+
// shaped view, not a replacement for `spex board` (which stays the full flat payload). Pruned
|
|
104
|
+
// children degrade to their ids, so --depth still tells the machine what exists below the cut.
|
|
105
|
+
export function treeJson(nodes: TreeNode[], opts: TreeOpts = {}): object[] {
|
|
106
|
+
const byParent = childrenIndex(nodes)
|
|
107
|
+
const shape = (n: TreeNode, depth: number): object => {
|
|
108
|
+
const kids = byParent.get(n.id) ?? []
|
|
109
|
+
const pruned = opts.depth !== undefined && depth >= opts.depth
|
|
110
|
+
return {
|
|
111
|
+
id: n.id, title: n.title, status: n.status, version: n.version ?? 0,
|
|
112
|
+
drift: n.drift ?? 0, staleYatsu: staleYatsu(n), openIssues: n.openIssues?.length ?? 0,
|
|
113
|
+
...(n.ghost ? { ghost: true } : {}),
|
|
114
|
+
children: pruned ? kids.map((k) => k.id) : kids.map((k) => shape(k, depth + 1)),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return roots(nodes, opts.node).map((r) => shape(r, 0))
|
|
118
|
+
}
|
|
@@ -21,7 +21,7 @@ if command -v spex >/dev/null 2>&1; then
|
|
|
21
21
|
spex issues nudge "$node"
|
|
22
22
|
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
|
|
23
23
|
"$repo_root/node_modules/.bin/spex" issues nudge "$node"
|
|
24
|
-
elif [ -x "$main_root/spec-cli/
|
|
25
|
-
"$main_root/spec-cli/
|
|
24
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
|
|
25
|
+
"$main_root/spec-cli/bin/spex.mjs" issues nudge "$node"
|
|
26
26
|
fi
|
|
27
27
|
exit 0
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# no node_modules of its own, so the dogfood tsx path reaches the main checkout's. First that exists wins:
|
|
8
8
|
# 1. `spex` on PATH - installed globally / on the shell PATH.
|
|
9
9
|
# 2. <repo>/node_modules/.bin/spex - a project that did `npm i @spexcode/spec-cli` (the dep's bin).
|
|
10
|
-
# 3. monorepo
|
|
10
|
+
# 3. monorepo launcher by path - dogfood: bin/spex.mjs (owns tsx resolution + the mid-merge guard).
|
|
11
11
|
main_root=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")
|
|
12
12
|
repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
13
13
|
spex_kind=
|
|
@@ -15,21 +15,33 @@ if command -v spex >/dev/null 2>&1; then
|
|
|
15
15
|
spex_kind=path
|
|
16
16
|
elif [ -x "$repo_root/node_modules/.bin/spex" ]; then
|
|
17
17
|
spex_kind=local
|
|
18
|
-
elif [ -x "$main_root/spec-cli/
|
|
19
|
-
spex_kind=
|
|
18
|
+
elif [ -x "$main_root/spec-cli/bin/spex.mjs" ]; then
|
|
19
|
+
spex_kind=pkg
|
|
20
20
|
fi
|
|
21
21
|
spex_cli() {
|
|
22
22
|
case "$spex_kind" in
|
|
23
23
|
path) spex "$@" ;;
|
|
24
24
|
local) "$repo_root/node_modules/.bin/spex" "$@" ;;
|
|
25
|
-
|
|
25
|
+
pkg) "$main_root/spec-cli/bin/spex.mjs" "$@" ;;
|
|
26
26
|
*) return 127 ;;
|
|
27
27
|
esac
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
# @@@ tree-unchanged stamp - a commit whose tree EQUALS HEAD's tree (`git write-tree` over the commit's own
|
|
31
|
+
# index — the hook's GIT_INDEX_FILE, a temp HEAD-tree index for `commit --only --allow-empty`) adds NO
|
|
32
|
+
# content: it cannot smuggle code past the main-guard below and cannot introduce spec↔code drift, so the
|
|
33
|
+
# whole hook passes it. This is `spex ack`'s empty Spec-OK stamp — the reason ack works on a trunk merge
|
|
34
|
+
# commit (amending it would re-author the merge after MERGE_HEAD is gone and read as direct authoring) and
|
|
35
|
+
# with a DIRTY real index (the lint shim below reads the real index via git.ts's env-strip, which would
|
|
36
|
+
# otherwise block the stamp on the very drift it acknowledges). Deliberately a one-line tree compare, not an
|
|
37
|
+
# "is this an amend?" heuristic (undecidable at pre-commit time).
|
|
38
|
+
if [ "$(git write-tree 2>/dev/null)" = "$(git rev-parse 'HEAD^{tree}' 2>/dev/null || echo none)" ]; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
30
42
|
# @@@ main-guard - the worktree model: never author directly on the trunk; the trunk only RECEIVES merges.
|
|
31
|
-
# Reject a direct commit while HEAD is the trunk. Merges pass (MERGE_HEAD present)
|
|
32
|
-
# Escape hatch for seeding / eager topology: SPEXCODE_ALLOW_MAIN=1 git commit …
|
|
43
|
+
# Reject a direct commit while HEAD is the trunk. Merges pass (MERGE_HEAD present), and a tree-unchanged
|
|
44
|
+
# stamp already passed above. Escape hatch for seeding / eager topology: SPEXCODE_ALLOW_MAIN=1 git commit …
|
|
33
45
|
#
|
|
34
46
|
# The trunk is resolved the SAME way the rest of SpexCode resolves it — `spex internal trunk` = layout.ts
|
|
35
47
|
# mainBranch() (config override → the main checkout's current branch → 'main') — so the guard protects
|
|
@@ -48,7 +60,7 @@ trunk=$(spex_cli internal trunk 2>/dev/null | head -1)
|
|
|
48
60
|
if [ "$branch" = "$trunk" ] && [ ! -f "$git_dir/MERGE_HEAD" ] && [ -z "${SPEXCODE_ALLOW_MAIN:-}" ]; then
|
|
49
61
|
echo "✗ SpexCode: direct commits on $trunk (the trunk) are blocked." >&2
|
|
50
62
|
echo " Work in a worktree (.worktrees/<node>, branch node/<id>) and merge back." >&2
|
|
51
|
-
echo " Merges pass automatically; for seeding/topology: SPEXCODE_ALLOW_MAIN=1 git commit …" >&2
|
|
63
|
+
echo " Merges and tree-unchanged stamps (spex ack) pass automatically; for seeding/topology: SPEXCODE_ALLOW_MAIN=1 git commit …" >&2
|
|
52
64
|
exit 1
|
|
53
65
|
fi
|
|
54
66
|
|
|
@@ -58,16 +70,23 @@ fi
|
|
|
58
70
|
# hook's GIT_DIR, so lint validates THIS worktree's `.spec`).
|
|
59
71
|
if [ -z "${SPEXCODE_SKIP_LINT:-}" ]; then
|
|
60
72
|
if [ -n "$spex_kind" ]; then
|
|
61
|
-
|
|
73
|
+
spex_cli lint >&2
|
|
74
|
+
lint_rc=$?
|
|
75
|
+
# exit 75 = the launcher's mid-merge guard (spex's own source holds conflict markers — transient).
|
|
76
|
+
# Degrade to advisory rather than walling every commit behind a merge someone else is resolving.
|
|
77
|
+
if [ "$lint_rc" -eq 75 ]; then
|
|
78
|
+
echo "• SpexCode: spec-lint skipped — spex is paused mid-merge (exit 75); CI still enforces." >&2
|
|
79
|
+
elif [ "$lint_rc" -ne 0 ]; then
|
|
62
80
|
echo "✗ SpexCode: spec-lint failed — fix the spec↔code links or bypass with SPEXCODE_SKIP_LINT=1." >&2
|
|
63
81
|
exit 1
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
else
|
|
83
|
+
# @@@ yatsu backstop - reject a staged yatsu offender: a stray pixel blob copied into the tree (blobs
|
|
84
|
+
# belong in the shared git common dir, never committed) OR a malformed yatsu.md (a scenario schema
|
|
85
|
+
# violation). Logic lives in spec-yatsu; this is the lint-shim's twin. (this node's contract)
|
|
86
|
+
if ! spex_cli yatsu check-staged >&2; then
|
|
87
|
+
echo "✗ SpexCode: yatsu check-staged failed (see above) — fix it or bypass with SPEXCODE_SKIP_LINT=1." >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
71
90
|
fi
|
|
72
91
|
else
|
|
73
92
|
# Advisory hook: no CLI found (project hasn't installed @spexcode/spec-cli; monorepo not `npm install`ed).
|
|
@@ -33,5 +33,12 @@ if [ -z "$sid" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then # codex: alias the threa
|
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
35
|
[ -z "$sid" ] && exit 0
|
|
36
|
-
printf
|
|
36
|
+
# Stamp via interpret-trailers (git's own sample-hook mechanism), NOT a raw printf append: appending after a
|
|
37
|
+
# message that already carries a trailer block (e.g. `spex ack`'s Spec-OK) opens a new paragraph, and git then
|
|
38
|
+
# parses ONLY the last paragraph as trailers — the earlier trailers silently become body prose. interpret-trailers
|
|
39
|
+
# appends into the existing block, keeping ONE parseable block. It needs the file newline-terminated first:
|
|
40
|
+
# on an unterminated message (git merge -m writes MERGE_MSG without one) it folds the trailer into the subject
|
|
41
|
+
# paragraph, where git's trailer parser never looks — the reason merge commits' Session: never parsed.
|
|
42
|
+
[ -n "$(tail -c1 "$msg_file")" ] && echo >> "$msg_file"
|
|
43
|
+
git interpret-trailers --in-place --trailer "Session: $sid" "$msg_file"
|
|
37
44
|
exit 0
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"lint": {
|
|
3
3
|
"governedRoots": ["."]
|
|
4
|
+
},
|
|
5
|
+
"sessions": {
|
|
6
|
+
"launchers": {
|
|
7
|
+
"claude": { "harness": "claude", "cmd": "claude --dangerously-skip-permissions" },
|
|
8
|
+
"codex": { "harness": "codex", "cmd": "codex --yolo" }
|
|
9
|
+
},
|
|
10
|
+
"defaultLauncher": "claude"
|
|
4
11
|
}
|
|
5
12
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
|