spexcode 0.5.1 → 0.5.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 +1 -1
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +25 -1
- package/spec-cli/hooks/dispatch.sh +24 -1
- package/spec-cli/src/anchors.ts +192 -58
- package/spec-cli/src/attach.ts +11 -8
- package/spec-cli/src/claude-headless.ts +116 -66
- package/spec-cli/src/cli.ts +221 -72
- package/spec-cli/src/client.ts +143 -5
- package/spec-cli/src/gateway-hub.ts +2 -6
- package/spec-cli/src/gateway.ts +96 -11
- package/spec-cli/src/git.ts +1395 -96
- package/spec-cli/src/graph.ts +21 -17
- package/spec-cli/src/graphCache.ts +405 -61
- package/spec-cli/src/graphStream.ts +581 -182
- package/spec-cli/src/guide.ts +13 -3
- package/spec-cli/src/harness.ts +965 -70
- package/spec-cli/src/headless-controller.ts +63 -0
- package/spec-cli/src/help.ts +120 -64
- package/spec-cli/src/host-resources.ts +706 -0
- package/spec-cli/src/host.ts +1 -0
- package/spec-cli/src/index.ts +95 -21
- package/spec-cli/src/init.ts +47 -8
- package/spec-cli/src/issues.ts +36 -3
- package/spec-cli/src/layout.ts +193 -37
- package/spec-cli/src/lint.ts +152 -27
- package/spec-cli/src/localIssues.ts +5 -1
- package/spec-cli/src/maintenance-wrapper.ts +276 -0
- package/spec-cli/src/materialize.ts +6 -7
- package/spec-cli/src/opencode-headless.ts +117 -7
- package/spec-cli/src/opencode.ts +8 -4
- package/spec-cli/src/pi-headless.ts +18 -60
- package/spec-cli/src/process-identity.ts +49 -0
- package/spec-cli/src/project-store.ts +17 -0
- package/spec-cli/src/pty-bridge.ts +37 -7
- package/spec-cli/src/pty-helper.mjs +16 -6
- package/spec-cli/src/pty-native-helper.mjs +22 -0
- package/spec-cli/src/reviews.ts +17 -13
- package/spec-cli/src/runtime-ownership.ts +99 -0
- package/spec-cli/src/session-maintenance.ts +750 -0
- package/spec-cli/src/session-timeline.ts +29 -23
- package/spec-cli/src/sessions.ts +1262 -200
- package/spec-cli/src/sh.ts +3 -0
- package/spec-cli/src/source-files.ts +27 -2
- package/spec-cli/src/specs.ts +58 -25
- package/spec-cli/src/supervise.ts +31 -11
- package/spec-cli/src/uninstall.ts +7 -6
- package/spec-cli/templates/hooks/commit-msg +14 -0
- package/spec-cli/templates/hooks/pre-commit +26 -6
- package/spec-cli/templates/hooks/prepare-commit-msg +20 -48
- package/spec-cli/templates/hooks/reference-transaction +105 -0
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +23 -21
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -10
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +15 -6
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +32 -0
- package/spec-dashboard/dist/assets/App-C5vbTw8Q.js +2 -0
- package/spec-dashboard/dist/assets/Dashboard-u8RIS3NY.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-BS7ITcNo.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-DXbqQFW_.js +1 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-DVLnk9hz.js +2 -0
- package/spec-dashboard/dist/assets/Modal-6mHq6fbZ.js +1 -0
- package/spec-dashboard/dist/assets/{PageScroll-B_dKCuXx.js → PageScroll-CAY4S4g4.js} +1 -1
- package/spec-dashboard/dist/assets/ProjectsPage-UQyzsTWN.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DKU4c1Z-.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-zGwJaGbR.js +1 -0
- package/spec-dashboard/dist/assets/Settings-igR17pns.js +1 -0
- package/spec-dashboard/dist/assets/Thread-B-ZUarN1.js +13 -0
- package/spec-dashboard/dist/assets/TimelineChat-Cp579UoJ.css +1 -0
- package/spec-dashboard/dist/assets/TimelineChat-sc49Qj5d.js +276 -0
- package/spec-dashboard/dist/assets/data-B1ot4PF0.js +1 -0
- package/spec-dashboard/dist/assets/index-BqBNCa1V.js +41 -0
- package/spec-dashboard/dist/assets/index-CzutlTDf.css +1 -0
- package/spec-dashboard/dist/assets/{terminalFont-DmTPNdbU.js → terminalFont-C6oGNKrf.js} +1 -1
- package/spec-dashboard/dist/index.html +5 -4
- package/spec-dashboard/src/reviewFilters.js +46 -8
- package/spec-dashboard/src/session.js +64 -9
- package/spec-eval/src/cli.ts +144 -32
- package/spec-eval/src/evaltab.ts +16 -9
- package/spec-eval/src/freshness.ts +336 -54
- package/spec-eval/src/scenariofresh.ts +51 -1
- package/spec-eval/src/scenarios.ts +324 -25
- package/spec-eval/src/sessioneval.ts +969 -173
- package/spec-cli/src/message-stream.ts +0 -147
- package/spec-dashboard/dist/assets/Dashboard-C_w_wdk5.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-5_nfIYll.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-By-u--95.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-CVEwjHr9.js +0 -2
- package/spec-dashboard/dist/assets/Modal-BqgvzMJD.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-RVP8AqK4.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-Bh3vq8SU.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-BuJ5mzjC.js +0 -13
- package/spec-dashboard/dist/assets/Settings-B8KFocsz.js +0 -1
- package/spec-dashboard/dist/assets/TimelineChat-K0wdlweB.js +0 -1
- package/spec-dashboard/dist/assets/index-BKaTHjmU.js +0 -41
- package/spec-dashboard/dist/assets/index-DcnCaBAC.css +0 -1
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { closeSync, fstatSync, openSync, readSync, watch, type FSWatcher } from 'node:fs'
|
|
2
|
-
import { streamSSE } from 'hono/streaming'
|
|
3
|
-
import type { Context } from 'hono'
|
|
4
|
-
|
|
5
|
-
import { readAliasedRawRecord, sessionArtifactPath, sessionStoreDir } from './layout.js'
|
|
6
|
-
|
|
7
|
-
export type NativeMessageEvent = Record<string, unknown>
|
|
8
|
-
export type MessageEnvelope = { cursor: number; event: NativeMessageEvent }
|
|
9
|
-
export type MessageBatch = { messages: MessageEnvelope[]; cursor: number }
|
|
10
|
-
|
|
11
|
-
function fileBytes(path: string, cursor: number): Buffer {
|
|
12
|
-
let fd: number
|
|
13
|
-
try { fd = openSync(path, 'r') }
|
|
14
|
-
catch (error) {
|
|
15
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT' && cursor === 0) return Buffer.alloc(0)
|
|
16
|
-
throw error
|
|
17
|
-
}
|
|
18
|
-
try {
|
|
19
|
-
const size = fstatSync(fd).size
|
|
20
|
-
if (cursor > size) throw new Error(`messages.ndjson cursor ${cursor} is past file size ${size}`)
|
|
21
|
-
const bytes = Buffer.alloc(size - cursor)
|
|
22
|
-
let read = 0
|
|
23
|
-
while (read < bytes.length) {
|
|
24
|
-
const count = readSync(fd, bytes, read, bytes.length - read, cursor + read)
|
|
25
|
-
if (count === 0) break
|
|
26
|
-
read += count
|
|
27
|
-
}
|
|
28
|
-
return read === bytes.length ? bytes : bytes.subarray(0, read)
|
|
29
|
-
} finally { closeSync(fd) }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Parse only newline-terminated records. The cursor is a byte offset, not a character count, so an SSE
|
|
33
|
-
// reconnect resumes correctly even when a native event contains non-ASCII text.
|
|
34
|
-
export function readMessageBatchFile(path: string, requestedCursor = 0): MessageBatch {
|
|
35
|
-
const cursor = Number.isSafeInteger(requestedCursor) && requestedCursor >= 0 ? requestedCursor : 0
|
|
36
|
-
const bytes = fileBytes(path, cursor)
|
|
37
|
-
|
|
38
|
-
const messages: MessageEnvelope[] = []
|
|
39
|
-
let lineStart = 0
|
|
40
|
-
while (lineStart < bytes.length) {
|
|
41
|
-
const newline = bytes.indexOf(0x0a, lineStart)
|
|
42
|
-
if (newline < 0) break
|
|
43
|
-
const nextCursor = cursor + newline + 1
|
|
44
|
-
const text = bytes.subarray(lineStart, newline).toString('utf8')
|
|
45
|
-
let event: unknown
|
|
46
|
-
try { event = JSON.parse(text) }
|
|
47
|
-
catch (error) {
|
|
48
|
-
throw new Error(`invalid messages.ndjson event at byte ${cursor + lineStart}: ${(error as Error).message}`)
|
|
49
|
-
}
|
|
50
|
-
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
51
|
-
throw new Error(`invalid messages.ndjson event at byte ${cursor + lineStart}: expected a JSON object`)
|
|
52
|
-
}
|
|
53
|
-
messages.push({ cursor: nextCursor, event: event as NativeMessageEvent })
|
|
54
|
-
lineStart = newline + 1
|
|
55
|
-
}
|
|
56
|
-
return { messages, cursor: cursor + lineStart }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// null means the id is unknown or is not a governed session. A known session with no adapter output yet is
|
|
60
|
-
// an ordinary empty stream: the adapter may create messages.ndjson after the browser has connected.
|
|
61
|
-
export function readSessionMessages(id: string, cursor = 0): MessageBatch | null {
|
|
62
|
-
const record = readAliasedRawRecord(id)
|
|
63
|
-
if (!record?.governed) return null
|
|
64
|
-
return readMessageBatchFile(sessionArtifactPath(record.session_id, 'messages.ndjson'), cursor)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type StreamSignal = 'append' | 'ping' | 'abort' | 'watch-error'
|
|
68
|
-
|
|
69
|
-
function signalQueue() {
|
|
70
|
-
const queued: StreamSignal[] = []
|
|
71
|
-
let waiter: ((signal: StreamSignal) => void) | null = null
|
|
72
|
-
const push = (signal: StreamSignal) => {
|
|
73
|
-
if (signal === 'append' && queued.includes('append')) return
|
|
74
|
-
if (waiter) {
|
|
75
|
-
const resolve = waiter
|
|
76
|
-
waiter = null
|
|
77
|
-
resolve(signal)
|
|
78
|
-
} else queued.push(signal)
|
|
79
|
-
}
|
|
80
|
-
const next = (): Promise<StreamSignal> => {
|
|
81
|
-
const ready = queued.shift()
|
|
82
|
-
return ready ? Promise.resolve(ready) : new Promise((resolve) => { waiter = resolve })
|
|
83
|
-
}
|
|
84
|
-
return { push, next }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function cursorParam(value: string | undefined): number | null {
|
|
88
|
-
if (!value || !/^\d+$/.test(value)) return null
|
|
89
|
-
const cursor = Number(value)
|
|
90
|
-
return Number.isSafeInteger(cursor) ? cursor : null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// GET /api/sessions/:id/messages/stream — the append-follow half of [[message-stream]]. REST establishes a
|
|
94
|
-
// complete snapshot + cursor; this stream starts there. Standard SSE ids let EventSource reconnect through
|
|
95
|
-
// Last-Event-ID even though its original URL still carries the older cursor.
|
|
96
|
-
export function sessionMessageStream(c: Context): Response {
|
|
97
|
-
const record = readAliasedRawRecord(c.req.param('id') as string)
|
|
98
|
-
if (!record?.governed) return c.json({ error: 'no such session' }, 404)
|
|
99
|
-
|
|
100
|
-
const lastEventCursor = cursorParam(c.req.header('Last-Event-ID'))
|
|
101
|
-
const queryCursor = cursorParam(c.req.query('cursor'))
|
|
102
|
-
const startCursor = lastEventCursor ?? queryCursor ?? 0
|
|
103
|
-
const id = record.session_id
|
|
104
|
-
|
|
105
|
-
return streamSSE(c, async (stream) => {
|
|
106
|
-
const signals = signalQueue()
|
|
107
|
-
let aborted = false
|
|
108
|
-
let watcher: FSWatcher | null = null
|
|
109
|
-
const ping = setInterval(() => signals.push('ping'), 10_000)
|
|
110
|
-
ping.unref()
|
|
111
|
-
stream.onAbort(() => { aborted = true; signals.push('abort') })
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
watcher = watch(sessionStoreDir(id), { persistent: false }, (_event, filename) => {
|
|
115
|
-
if (filename == null || String(filename) === 'messages.ndjson') signals.push('append')
|
|
116
|
-
})
|
|
117
|
-
watcher.on('error', () => signals.push('watch-error'))
|
|
118
|
-
|
|
119
|
-
let cursor = startCursor
|
|
120
|
-
await stream.writeSSE({ event: 'ready', data: JSON.stringify({ cursor }) })
|
|
121
|
-
const flush = async () => {
|
|
122
|
-
const batch = readSessionMessages(id, cursor)
|
|
123
|
-
if (!batch) throw new Error('session closed')
|
|
124
|
-
cursor = batch.cursor
|
|
125
|
-
for (const message of batch.messages) {
|
|
126
|
-
await stream.writeSSE({ event: 'message', id: String(message.cursor), data: JSON.stringify(message) })
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
await flush()
|
|
130
|
-
|
|
131
|
-
while (!aborted) {
|
|
132
|
-
const signal = await signals.next()
|
|
133
|
-
if (signal === 'abort') break
|
|
134
|
-
if (signal === 'watch-error') throw new Error('messages.ndjson watcher failed')
|
|
135
|
-
if (signal === 'ping') await stream.writeSSE({ event: 'ping', data: 'x' })
|
|
136
|
-
else await flush()
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (!aborted) {
|
|
140
|
-
await stream.writeSSE({ event: 'stream-error', data: JSON.stringify({ error: (error as Error).message }) }).catch(() => {})
|
|
141
|
-
}
|
|
142
|
-
} finally {
|
|
143
|
-
clearInterval(ping)
|
|
144
|
-
watcher?.close()
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
}
|