spexcode 0.5.8 → 0.5.9
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/package.json +5 -3
- package/spec-cli/bin/spex.mjs +1 -13
- package/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +53 -0
- package/spec-cli/hooks/compat/mark-active-sed-v0.fixture +46 -0
- package/spec-cli/hooks/dispatch.sh +15 -19
- package/spec-cli/src/anchors.ts +185 -56
- package/spec-cli/src/attach.ts +7 -17
- package/spec-cli/src/cli.ts +144 -225
- package/spec-cli/src/client.ts +114 -132
- package/spec-cli/src/cockpit.ts +43 -0
- package/spec-cli/src/codex-runtime-generations.ts +556 -0
- package/spec-cli/src/commit-surgery.ts +0 -14
- package/spec-cli/src/contract-filter.ts +0 -14
- package/spec-cli/src/doctor.ts +0 -16
- package/spec-cli/src/gateway-auth.ts +0 -13
- package/spec-cli/src/gateway.ts +0 -22
- package/spec-cli/src/git.ts +151 -48
- package/spec-cli/src/graph.ts +28 -7
- package/spec-cli/src/graphCache.ts +253 -72
- package/spec-cli/src/graphDelta.ts +0 -13
- package/spec-cli/src/graphStream.ts +70 -37
- package/spec-cli/src/guide.ts +29 -6
- package/spec-cli/src/harness.ts +232 -248
- package/spec-cli/src/help.ts +27 -26
- package/spec-cli/src/host-resources.ts +11 -2
- package/spec-cli/src/index.ts +70 -95
- package/spec-cli/src/issues-cli.ts +276 -0
- package/spec-cli/src/issues.ts +15 -197
- package/spec-cli/src/layout.ts +78 -2
- package/spec-cli/src/lint.ts +5 -3
- package/spec-cli/src/localIssues.ts +22 -185
- package/spec-cli/src/loop-in.ts +78 -0
- package/spec-cli/src/materialize.ts +0 -42
- package/spec-cli/src/mentions.ts +49 -193
- package/spec-cli/src/opencode-headless.ts +7 -7
- package/spec-cli/src/opencode.ts +2 -5
- package/spec-cli/src/pi-headless.ts +5 -5
- package/spec-cli/src/pty-bridge.ts +4 -5
- package/spec-cli/src/reviewSnapshot.ts +1 -0
- package/spec-cli/src/reviews.ts +6 -1
- package/spec-cli/src/root-lru.ts +54 -0
- package/spec-cli/src/runtime-guard.ts +0 -9
- package/spec-cli/src/session-cursors.ts +97 -0
- package/spec-cli/src/session-follow.ts +182 -0
- package/spec-cli/src/session-timeline.ts +55 -127
- package/spec-cli/src/sessions.ts +207 -1022
- package/spec-cli/src/shim-runtime.ts +9 -16
- package/spec-cli/src/specs.ts +33 -10
- package/spec-cli/src/uninstall.ts +0 -9
- package/spec-cli/src/uploads.ts +240 -18
- package/spec-cli/src/worktree-sources.ts +0 -16
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +28 -0
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +43 -3
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +3 -1
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +2 -2
- package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +5 -0
- package/spec-cli/templates/spexcode.json +12 -0
- package/spec-dashboard/dist/assets/{App-u2P7KdSg.js → App-b8Nh0sgk.js} +2 -2
- package/spec-dashboard/dist/assets/{Dashboard-B8wp5_61.js → Dashboard-CvAjfRC2.js} +7 -7
- package/spec-dashboard/dist/assets/{EvalsPage-Bq1Tkb8y.js → EvalsPage-Bz-nMKoS.js} +1 -1
- package/spec-dashboard/dist/assets/IssuesPage-CAP64YWE.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-D9L1Va8Z.js +2 -0
- package/spec-dashboard/dist/assets/{Modal-bAkq9IIT.js → Modal-Drscez-d.js} +1 -1
- package/spec-dashboard/dist/assets/{PageScroll-px_rUZVJ.js → PageScroll-qW6uOJL8.js} +1 -1
- package/spec-dashboard/dist/assets/{ProjectsPage-8uGqYM12.js → ProjectsPage-CjybFBmR.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-CswwbewF.js → SessionInterface-Dl9v0JFM.js} +13 -13
- package/spec-dashboard/dist/assets/SessionWindow-iOk0yHoU.js +1 -0
- package/spec-dashboard/dist/assets/{Settings-bpAbfnmS.js → Settings-BZ1lGRJs.js} +1 -1
- package/spec-dashboard/dist/assets/Thread-D_kcDnfd.js +13 -0
- package/spec-dashboard/dist/assets/{data-CQFbQEMH.js → data-Bwd3kAVL.js} +1 -1
- package/spec-dashboard/dist/assets/{index-CixSnz1H.css → index-DAbQBBK_.css} +1 -1
- package/spec-dashboard/dist/assets/{index-Di1ch5dd.js → index-paP-z_Vd.js} +6 -6
- package/spec-dashboard/dist/assets/{TimelineChat-Ckmb1Ez2.js → launch-B-bYdWmh.js} +25 -25
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-dashboard/src/session.js +1 -28
- package/spec-eval/src/cache.ts +0 -14
- package/spec-eval/src/cli.ts +26 -5
- package/spec-eval/src/evaltab.ts +67 -108
- package/spec-eval/src/filing.ts +0 -18
- package/spec-eval/src/freshness.ts +78 -32
- package/spec-eval/src/humanok.ts +0 -15
- package/spec-eval/src/scenariofresh.ts +8 -80
- package/spec-eval/src/scenarios.ts +16 -67
- package/spec-eval/src/sessioneval.ts +157 -78
- package/spec-eval/src/sidecar.ts +1 -70
- package/spec-eval/src/timeline.ts +0 -23
- package/spec-forge/src/cache.ts +34 -12
- package/spec-forge/src/cli.ts +0 -6
- package/spec-forge/src/drivers/github.ts +1 -12
- package/spec-forge/src/drivers/gitlab.ts +4 -19
- package/spec-forge/src/drivers.ts +0 -18
- package/spec-forge/src/links.ts +0 -4
- package/spec-forge/src/needs-eval.ts +0 -1
- package/spec-forge/src/port.ts +0 -12
- package/spec-forge/src/resident.ts +4 -20
- package/spec-cli/src/maintenance-wrapper.ts +0 -276
- package/spec-cli/src/session-maintenance.ts +0 -751
- package/spec-dashboard/dist/assets/IssuesPage-BlkPSkmv.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-B1GxRZXK.js +0 -2
- package/spec-dashboard/dist/assets/SessionWindow-IspcLjFA.js +0 -1
- package/spec-dashboard/dist/assets/Thread-BpL3N3kw.js +0 -13
- /package/spec-dashboard/dist/assets/{TimelineChat-Cp579UoJ.css → launch-Cp579UoJ.css} +0 -0
|
@@ -4,7 +4,7 @@ import { watch, mkdirSync, readdirSync, readFileSync, type FSWatcher } from 'nod
|
|
|
4
4
|
import { join, dirname, relative, resolve } from 'node:path'
|
|
5
5
|
import { sessionsRoot, gitCommonDir } from './layout.js'
|
|
6
6
|
import { hotSignature, warmSignature, listSessions } from './sessions.js'
|
|
7
|
-
import { getBoard, invalidateBoard, patrolBoard } from './graphCache.js'
|
|
7
|
+
import { getBoard, getBoardForSessionRefresh, invalidateBoard, patrolBoard } from './graphCache.js'
|
|
8
8
|
import { unitize, tagOf, diffUnits, type Units } from './graphDelta.js'
|
|
9
9
|
import {
|
|
10
10
|
holdSessionEvalProjectionObserver,
|
|
@@ -14,27 +14,6 @@ import {
|
|
|
14
14
|
setSessionEvalProjectionWarmup,
|
|
15
15
|
} from '../../spec-eval/src/sessioneval.js'
|
|
16
16
|
|
|
17
|
-
// @@@ board-stream — the board's freshness is PUSHED, not polled. A dashboard subscribes here ONCE; in
|
|
18
|
-
// plain mode it gets a bare `graph-changed` and refetches /api/graph (the legacy protocol, kept verbatim
|
|
19
|
-
// for old clients); in DELTA mode (`?mode=delta`) the server itself rebuilds on change and streams the
|
|
20
|
-
// hash-chained patch ([[graph-delta]]): a `graph-full {to, graph}` on connect, then `graph-delta
|
|
21
|
-
// {from, to, set, del}` per change — a few KB against the ~600KB snapshot, with a full-snapshot send
|
|
22
|
-
// whenever the patch wouldn't win (bigger than the board, or the unit decomposition's id-uniqueness
|
|
23
|
-
// precondition failed), so a delta subscriber is NEVER worse off than a full refetch.
|
|
24
|
-
//
|
|
25
|
-
// Every source carries the DOMAIN of the change it saw — 'sessions' (only the session rows moved) or 'full'
|
|
26
|
-
// (anything could have) — and fireChanged funnels them into ONE debounced pipeline, escalating to the max
|
|
27
|
-
// scope seen in the window so the cache can splice sessions instead of rebuilding whole ([[graph-cache]]).
|
|
28
|
-
// Sources: (1) fs.watch on the per-user session store — every lifecycle transition lands as a
|
|
29
|
-
// sessions/<id>/session.json write → 'sessions'; (2) fs.watch on the shared git dir's refs (+
|
|
30
|
-
// packed-refs/HEAD) — a commit/merge moves a ref, reshaping the tree → 'full'; (3) fs.watch on the git
|
|
31
|
-
// worktree REGISTRY (+ each live worktree root and gitdir index) — dirty source/spec/sidecar/rename/stage → 'full';
|
|
32
|
-
// (4) two subscriber-gated pollers of the tmux-derived signatures ([[sessions]]) that never touch a file —
|
|
33
|
-
// a 100ms HOT syscall poll and a 1s WARM tmux poll, both → 'sessions'; (5) a delta-gated ~15s cold-tick
|
|
34
|
-
// PATROL that asks graph-cache to validate its owned input revision — unchanged inputs reuse the anchor,
|
|
35
|
-
// while moved inputs select sessions/full there and missed leaf signals stay loud through repair accounting.
|
|
36
|
-
// Plain mode without delta subscribers keeps its zero-build behavior: sources just fan out `graph-changed`.
|
|
37
|
-
|
|
38
17
|
type Scope = 'sessions' | 'full'
|
|
39
18
|
type EvalTarget = 'all' | { id?: string; path?: string }
|
|
40
19
|
type Notify = () => void
|
|
@@ -230,11 +209,19 @@ export class TreeWatcherRegistry {
|
|
|
230
209
|
const plainSubs = new Set<Notify>()
|
|
231
210
|
const deltaSubs = new Set<DeltaSend>()
|
|
232
211
|
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
233
|
-
let
|
|
234
|
-
|
|
212
|
+
let pendingFull = false
|
|
213
|
+
let pendingSessions = false
|
|
214
|
+
export type PendingGraphChanges = { full: boolean; sessions: boolean }
|
|
215
|
+
export const addPendingGraphChange = (pending: PendingGraphChanges, scope: Scope): PendingGraphChanges => ({
|
|
216
|
+
full: pending.full || scope === 'full',
|
|
217
|
+
sessions: pending.sessions || scope === 'sessions',
|
|
218
|
+
})
|
|
235
219
|
|
|
236
220
|
// under SPEXCODE_BOARD_DEBUG=1, every broadcast logs its changed unit keys + trigger tags + build ms.
|
|
237
221
|
const DEBUG = process.env.SPEXCODE_BOARD_DEBUG === '1'
|
|
222
|
+
function traceLatency(stage: 'sessions-signal' | 'session-projection-complete' | 'broadcast', detail: Record<string, unknown> = {}): void {
|
|
223
|
+
if (DEBUG) console.warn(`spec-cli: graph latency ${JSON.stringify({ at: Date.now(), stage, ...detail })}`)
|
|
224
|
+
}
|
|
238
225
|
// the set of trigger tags accrued SINCE THE LAST BROADCAST — each fireChanged adds its scope, the cold tick
|
|
239
226
|
// adds 'patrol'. Cleared on every broadcast. Its job: prove WHO caused a broadcast, so a change that only
|
|
240
227
|
// the patrol saw (tag set === {'patrol'}) is flagged as a repair — some leaf watcher was blind.
|
|
@@ -261,23 +248,60 @@ let lastFullFrame: Frame | null = null
|
|
|
261
248
|
let building = false
|
|
262
249
|
let dirty = false
|
|
263
250
|
let patrolPending = false
|
|
251
|
+
let sessionRefreshRequested = false
|
|
252
|
+
let wakeSessionRefresh: (() => void) | null = null
|
|
264
253
|
|
|
265
|
-
async function rebuildAndBroadcast(patrol = false): Promise<void> {
|
|
254
|
+
async function rebuildAndBroadcast(patrol = false, sessions = false, full = false): Promise<void> {
|
|
266
255
|
if (patrol) patrolPending = true
|
|
267
|
-
if (
|
|
256
|
+
if (sessions) sessionRefreshRequested = true
|
|
257
|
+
if (building) {
|
|
258
|
+
dirty = true
|
|
259
|
+
if (sessions) {
|
|
260
|
+
wakeSessionRefresh?.()
|
|
261
|
+
wakeSessionRefresh = null
|
|
262
|
+
}
|
|
263
|
+
return
|
|
264
|
+
}
|
|
268
265
|
building = true
|
|
269
266
|
try {
|
|
270
267
|
do {
|
|
271
268
|
dirty = false
|
|
272
269
|
const validate = patrolPending
|
|
273
270
|
patrolPending = false
|
|
271
|
+
const sessionsFirst = sessionRefreshRequested
|
|
272
|
+
sessionRefreshRequested = false
|
|
273
|
+
let servedSessionProjection = sessionsFirst
|
|
274
274
|
let board: unknown
|
|
275
275
|
// share the route's single-flight build ([[graph-cache]]); fireChanged() already invalidated the
|
|
276
276
|
// cache (at the accumulated scope), so this gets a fresh build/splice (or joins one a concurrent poll
|
|
277
277
|
// already started). The patrol instead asks that same cache flight to validate its input revision;
|
|
278
278
|
// equal inputs return the anchor without invoking a producer.
|
|
279
279
|
const t0 = Date.now()
|
|
280
|
-
try {
|
|
280
|
+
try {
|
|
281
|
+
let wake!: () => void
|
|
282
|
+
const sessionWake = new Promise<void>((resolve) => { wake = resolve })
|
|
283
|
+
wakeSessionRefresh = wake
|
|
284
|
+
// A sessions-first turn can still fall back to an active route-owned full. Keep its wait wakeable
|
|
285
|
+
// too: a later persisted session change must re-enter the cheap projection rather than queue behind it.
|
|
286
|
+
const boardWait = sessionsFirst
|
|
287
|
+
? getBoardForSessionRefresh()
|
|
288
|
+
: validate ? patrolBoard() : getBoard()
|
|
289
|
+
const outcome = await Promise.race([
|
|
290
|
+
boardWait.then((value) => ({ value })),
|
|
291
|
+
sessionWake.then(() => ({ value: null as unknown })),
|
|
292
|
+
])
|
|
293
|
+
if (wakeSessionRefresh === wake) wakeSessionRefresh = null
|
|
294
|
+
if (outcome.value === null) {
|
|
295
|
+
boardWait.catch(() => {})
|
|
296
|
+
board = await getBoardForSessionRefresh()
|
|
297
|
+
servedSessionProjection = true
|
|
298
|
+
if (validate) patrolPending = true
|
|
299
|
+
dirty = true // the full wait was preempted only for delivery; it remains owed.
|
|
300
|
+
} else {
|
|
301
|
+
board = outcome.value
|
|
302
|
+
if (sessionsFirst && validate) { patrolPending = true; dirty = true }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
catch {
|
|
282
306
|
// A failed refresh consumes no cause: graph-cache restores the producer scope, so its stream-side
|
|
283
307
|
// attribution must remain owed too. This also retains watcher causes that arrived while the failed
|
|
@@ -287,15 +311,18 @@ async function rebuildAndBroadcast(patrol = false): Promise<void> {
|
|
|
287
311
|
continue
|
|
288
312
|
}
|
|
289
313
|
const buildMs = Date.now() - t0
|
|
314
|
+
if (servedSessionProjection) traceLatency('session-projection-complete', { patrol: validate })
|
|
290
315
|
const boardJson = JSON.stringify(board)
|
|
291
316
|
const { units, ok } = unitize(board as Record<string, unknown>)
|
|
292
317
|
const tag = tagOf(units)
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// and went silent — the alarm suppressing itself on the very machines that need it.
|
|
318
|
+
// A session-first frame consumes its own cause, but a full/patrol cause remains owed until structural
|
|
319
|
+
// convergence. Otherwise the first cheap projection would erase patrol accountability before the full
|
|
320
|
+
// result could name it. A normal frame consumes its whole trigger set, including a no-op frame.
|
|
297
321
|
const tags = [...triggerTags]
|
|
298
322
|
triggerTags.clear()
|
|
323
|
+
if (servedSessionProjection)
|
|
324
|
+
for (const tag of tags) if (tag === 'full' || tag === 'patrol') triggerTags.add(tag)
|
|
325
|
+
if (sessionsFirst && full) dirty = true
|
|
299
326
|
if (tag === lastTag) continue
|
|
300
327
|
// the changed unit keys — computed against the prior anchor when we have one (a first paint has no
|
|
301
328
|
// anchor, so no repair claim can be made against it).
|
|
@@ -314,6 +341,7 @@ async function rebuildAndBroadcast(patrol = false): Promise<void> {
|
|
|
314
341
|
// (stopSourcesIfIdle cleared the anchor; leaving lastTag/lastUnits stale-cleared is consistent —
|
|
315
342
|
// rebuilds only run while delta subscribers exist, so nothing chains from them meanwhile).
|
|
316
343
|
if (deltaSubs.size) { lastUnits = ok ? units : null; lastTag = tag; lastFullFrame = fullFrame }
|
|
344
|
+
traceLatency('broadcast', { event: frame.event, sessionProjection: servedSessionProjection, tags, changedKeys })
|
|
317
345
|
for (const send of [...deltaSubs]) { try { send(frame) } catch { /* swept on abort */ } }
|
|
318
346
|
for (const n of [...plainSubs]) { try { n() } catch { /* swept on abort */ } }
|
|
319
347
|
// ---- repair accounting: a real (tag-moved) broadcast whose ONLY trigger was the cold-tick patrol
|
|
@@ -329,18 +357,21 @@ async function rebuildAndBroadcast(patrol = false): Promise<void> {
|
|
|
329
357
|
}
|
|
330
358
|
|
|
331
359
|
// a merge/launch/close touches several record files at once; collapse the burst into ONE signal. Each call
|
|
332
|
-
// carries its change SCOPE
|
|
360
|
+
// carries its own change SCOPE: full and sessions are independent obligations, not a max-scope replacement.
|
|
333
361
|
// With delta subscribers the debounced fire rebuilds and broadcasts (plain subs then ride the same
|
|
334
362
|
// tag-moved gate — no spurious refetches); without them it stays the zero-build legacy notify.
|
|
335
363
|
function fireChanged(scope: Scope = 'full', evalTarget?: EvalTarget): void {
|
|
336
364
|
// Advance eval input generations BEFORE invalidating/building the board, so the first frame caused by an
|
|
337
365
|
// input event is `updating(lastKnown)`. Summary completion calls this function without a target.
|
|
366
|
+
if (scope === 'sessions') traceLatency('sessions-signal')
|
|
338
367
|
if (evalTarget) invalidateSessionEvalProjections(evalTarget)
|
|
339
|
-
|
|
340
|
-
|
|
368
|
+
const pending = addPendingGraphChange({ full: pendingFull, sessions: pendingSessions }, scope)
|
|
369
|
+
pendingFull = pending.full
|
|
370
|
+
pendingSessions = pending.sessions
|
|
371
|
+
// invalidate the route's board cache ([[graph-cache]]) on EVERY change signal at its OWN scope,
|
|
341
372
|
// before the debounce guard — a plain-mode client that polls /api/graph (no delta rebuild here) must
|
|
342
373
|
// still see fresh data on its next poll, and a delta rebuild below re-reads the same now-stale cache.
|
|
343
|
-
invalidateBoard(
|
|
374
|
+
invalidateBoard(scope)
|
|
344
375
|
triggerTags.add(scope)
|
|
345
376
|
// DEBOUNCE = 25ms. Real fs-event bursts (a merge touching many records) were MEASURED to span 0–5ms, so a
|
|
346
377
|
// 25ms window collapses them with room to spare while shaving ~125ms off the old 150ms lag; anything
|
|
@@ -349,8 +380,10 @@ function fireChanged(scope: Scope = 'full', evalTarget?: EvalTarget): void {
|
|
|
349
380
|
if (debounce) return
|
|
350
381
|
debounce = setTimeout(() => {
|
|
351
382
|
debounce = null
|
|
352
|
-
|
|
353
|
-
|
|
383
|
+
const full = pendingFull, sessions = pendingSessions
|
|
384
|
+
pendingFull = false
|
|
385
|
+
pendingSessions = false
|
|
386
|
+
if (deltaSubs.size) void rebuildAndBroadcast(false, sessions, full)
|
|
354
387
|
else for (const notify of [...plainSubs]) { try { notify() } catch { /* swept on abort */ } }
|
|
355
388
|
}, 25)
|
|
356
389
|
}
|
package/spec-cli/src/guide.ts
CHANGED
|
@@ -39,8 +39,8 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
39
39
|
Look these up on demand — the formats an agent authors, and the settings it configures:
|
|
40
40
|
spex guide spec the spec.md format (frontmatter + body + the rules lint enforces)
|
|
41
41
|
spex guide eval the eval.md format (scenario schema + how loss is measured and filed)
|
|
42
|
-
spex guide settings the spexcode.json / spexcode.local.json settings (launchers, dashboard icon,
|
|
43
|
-
budgets, layout) — every field, and which of the two files it belongs in
|
|
42
|
+
spex guide settings the spexcode.json / spexcode.local.json settings (launchers, dashboard icon, upload,
|
|
43
|
+
lint budgets, layout) — every field, and which of the two files it belongs in
|
|
44
44
|
spex guide footprint the footprint model — what SpexCode plants in a repo, and who sees it
|
|
45
45
|
(committed | ignored | hidden), and every migration recipe`
|
|
46
46
|
|
|
@@ -252,21 +252,23 @@ code file or the scenario (the eval.md) moves since it was filed.
|
|
|
252
252
|
spex eval scenario ls [<node>] the declared contracts; text --unmeasured = blind-spot worklist; --json = canonical declaration index
|
|
253
253
|
spex eval clean GC the content-addressed evidence cache`
|
|
254
254
|
|
|
255
|
+
const UPLOAD_DEFAULTS = uploadPolicyDefaults()
|
|
256
|
+
|
|
255
257
|
const SETTINGS = `spex guide settings — SpexCode's runtime settings (spexcode.json / spexcode.local.json)
|
|
256
258
|
|
|
257
259
|
SpexCode reads PROJECT runtime settings from TWO optional JSON files at the repo root. There is no imperative
|
|
258
260
|
settings verb — an agent CONFIGURES SpexCode by EDITING these files directly. The two split by
|
|
259
261
|
PORTABILITY, and picking the right one is the whole discipline:
|
|
260
262
|
|
|
261
|
-
spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, policy,
|
|
262
|
-
identity and launcher visibility, lint policy, resource and doctor health budgets, launcher NAMES. "Git is the database": tracked so the
|
|
263
|
+
spexcode.json COMMITTED — portable, shared by everyone on the repo. Layout, upload policy,
|
|
264
|
+
dashboard identity and launcher visibility, lint policy, resource and doctor health budgets, launcher NAMES. "Git is the database": tracked so the
|
|
263
265
|
team shares ONE configuration.
|
|
264
266
|
spexcode.local.json GITIGNORED — host-specific, never committed. Absolute launcher paths, cert/secret
|
|
265
267
|
paths. Layered OVER spexcode.json (see MERGE
|
|
266
268
|
below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
|
|
267
269
|
|
|
268
270
|
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
269
|
-
icon or launcher-visibility policy, lint policy, resource and doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
271
|
+
icon or launcher-visibility policy, upload policy, lint policy, resource and doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
270
272
|
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
271
273
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
272
274
|
\`spex session new\` or the dashboard without an explicit launcher choice.
|
|
@@ -275,7 +277,7 @@ The host-wide gateway has one separate per-user setting, \`gateway.icon\` in
|
|
|
275
277
|
\`$SPEXCODE_HOME/config.json\`. It is documented below and never belongs to either project file.
|
|
276
278
|
|
|
277
279
|
MERGE: spexcode.local.json is layered over spexcode.json ONE LEVEL DEEP — per top-level section (dashboard,
|
|
278
|
-
sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
|
|
280
|
+
uploads, sessions, …), the two objects are shallow-merged with LOCAL WINNING per key; sections only one file names
|
|
279
281
|
pass through untouched. This is exactly what lets a launcher's portable NAME reference (defaultLauncher)
|
|
280
282
|
sit in the committed file while its host-specific DEFINITION (with the abs cmd) sits in the local file —
|
|
281
283
|
see LAUNCHERS.
|
|
@@ -336,6 +338,26 @@ The report is read-only: \`spex session resources [--json]\`. Reclaim eligibilit
|
|
|
336
338
|
projection never issues mutation authority or signals a process. Budgets are project policy and belong in committed spexcode.json;
|
|
337
339
|
host-specific tuning may override this top-level section in spexcode.local.json.
|
|
338
340
|
|
|
341
|
+
── UPLOADS (spexcode.json — portable transfer policy; local overrides are useful for one machine's disk/network) ──
|
|
342
|
+
uploads.maxBytes maximum bytes in one attached file. Default ${UPLOAD_DEFAULTS.maxBytes}.
|
|
343
|
+
uploads.chunkBytes maximum raw PATCH body and client slice size. Default ${UPLOAD_DEFAULTS.chunkBytes}.
|
|
344
|
+
uploads.concurrency simultaneous attachment streams from one dashboard batch. Default ${UPLOAD_DEFAULTS.concurrency}.
|
|
345
|
+
uploads.requestTimeoutMs browser timeout for one chunk or completion request. Default ${UPLOAD_DEFAULTS.requestTimeoutMs}.
|
|
346
|
+
uploads.retryLimit automatic retries after the initial transient chunk failure. Default ${UPLOAD_DEFAULTS.retryLimit}.
|
|
347
|
+
uploads.retryDelayMs wait between those retries. Default ${UPLOAD_DEFAULTS.retryDelayMs}.
|
|
348
|
+
uploads.incompleteTtlMs idle staging lifetime before an unfinished transfer expires. Default ${UPLOAD_DEFAULTS.incompleteTtlMs}.
|
|
349
|
+
uploads.cleanupIntervalMs stale-staging reaper cadence. Default ${UPLOAD_DEFAULTS.cleanupIntervalMs}.
|
|
350
|
+
uploads.minFreeBytes bytes retained on the backend filesystem while reserving a new attachment.
|
|
351
|
+
Default ${UPLOAD_DEFAULTS.minFreeBytes}.
|
|
352
|
+
uploads.evidenceMaxBytes retained POST-body ceiling for eval evidence. Default ${UPLOAD_DEFAULTS.evidenceMaxBytes}.
|
|
353
|
+
All fields are positive integers except retryLimit, retryDelayMs, and minFreeBytes, which may be zero.
|
|
354
|
+
The seed template is the one default source; omit a field to use it. The backend reads the merged files for
|
|
355
|
+
each transfer and cleanup pass. The dashboard receives chunk, concurrency, timeout, and retry policy from
|
|
356
|
+
the create/status response, so a portable project policy takes effect without another browser setting.
|
|
357
|
+
Put team-wide transfer policy in committed spexcode.json; use the same keys in gitignored spexcode.local.json
|
|
358
|
+
only when the backend machine needs a local override. The normal top-level shallow merge lets a local
|
|
359
|
+
uploads.chunkBytes replace only that key.
|
|
360
|
+
|
|
339
361
|
── LAUNCHERS (the profile block, split across the two files) ──
|
|
340
362
|
A named launcher profile fixes BOTH a session's harness AND its exact launch command; a create picks one
|
|
341
363
|
by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
|
|
@@ -566,3 +588,4 @@ export function guideText(topic?: string): string | null {
|
|
|
566
588
|
const t = TOPICS[topic]
|
|
567
589
|
return t ? t + FOOTER : null
|
|
568
590
|
}
|
|
591
|
+
import { uploadPolicyDefaults } from './layout.js'
|