spexcode 0.3.0 → 0.4.1
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 +1 -1
- package/spec-cli/src/anchors.ts +341 -0
- package/spec-cli/src/cli.ts +20 -20
- package/spec-cli/src/gateway.ts +8 -1
- package/spec-cli/src/git.ts +21 -13
- package/spec-cli/src/graph.ts +13 -7
- package/spec-cli/src/guide.ts +63 -15
- package/spec-cli/src/harness.ts +85 -34
- package/spec-cli/src/help.ts +33 -17
- package/spec-cli/src/index.ts +39 -7
- package/spec-cli/src/init.ts +5 -4
- package/spec-cli/src/lint.ts +103 -28
- package/spec-cli/src/localIssues.ts +19 -0
- package/spec-cli/src/migrate-table.ts +27 -16
- package/spec-cli/src/search.bench.mjs +2 -2
- package/spec-cli/src/session-timeline.ts +148 -0
- package/spec-cli/src/sessions.ts +29 -4
- package/spec-cli/src/specs.ts +44 -21
- package/spec-cli/templates/hooks/prepare-commit-msg +6 -1
- package/spec-cli/templates/spec/project/.plugins/{extract → commands/extract}/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/commands/spec.md +16 -0
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
- package/spec-cli/templates/spec/project/.plugins/skills/spec.md +17 -0
- package/spec-cli/templates/spec/project/.plugins/spec.md +5 -2
- package/spec-cli/templates/spec/project/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-C7Bzsv86.js → Dashboard-C_fGmOKK.js} +3 -3
- package/spec-dashboard/dist/assets/EvalsPage-Cnr1s3bq.js +2 -0
- package/spec-dashboard/dist/assets/{FoldToggle-D5iB4Ac2.js → FoldToggle-x9gtO1OQ.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-CMFTsQhg.js → IssuesPage-5f_vL-JV.js} +1 -1
- package/spec-dashboard/dist/assets/MobileApp-DEO1jgGM.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-CAlbMOFR.js +66 -0
- package/spec-dashboard/dist/assets/SessionWindow-JYbpPwNB.js +13 -0
- package/spec-dashboard/dist/assets/{Settings-BW5f0OaW.js → Settings-DKb5Ji_X.js} +1 -1
- package/spec-dashboard/dist/assets/index-BQu-oJ8J.js +41 -0
- package/spec-dashboard/dist/assets/index-BbMkwuix.css +1 -0
- package/spec-dashboard/dist/assets/launch-BM9GgvkX.js +6 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-eval/src/cli.ts +82 -21
- package/spec-eval/src/evaltab.ts +15 -6
- package/spec-eval/src/humanok.ts +43 -0
- package/spec-eval/src/scenarios.ts +116 -4
- package/spec-eval/src/sidecar.ts +35 -9
- package/spec-cli/templates/presets/careful/.plugins/clarify-before-code/spec.md +0 -11
- package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +0 -8
- package/spec-dashboard/dist/assets/EvalsPage-DKZZIdHq.js +0 -2
- package/spec-dashboard/dist/assets/MobileApp-DwuTKgdP.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-CBS5_cmK.js +0 -71
- package/spec-dashboard/dist/assets/SessionWindow-CqAnjWfI.js +0 -9
- package/spec-dashboard/dist/assets/index-Cc26X4ce.css +0 -1
- package/spec-dashboard/dist/assets/index-Ce0wDyQS.js +0 -41
- /package/spec-cli/templates/spec/project/.plugins/{regroup → commands/regroup}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{tidy → commands/tidy}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{forge-link → prompts/forge-link}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{memory-hygiene → prompts/memory-hygiene}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{reproduce-before-fix → prompts/reproduce-before-fix}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/digest.mjs +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/spec.md +0 -0
|
@@ -7,12 +7,17 @@ import { mintIds } from '../../spec-cli/src/specs.js'
|
|
|
7
7
|
export const EVAL_FILE = 'eval.md'
|
|
8
8
|
export const SIDECAR_FILE = 'evals.ndjson'
|
|
9
9
|
|
|
10
|
+
export type ScenarioTestReference = {
|
|
11
|
+
path: string
|
|
12
|
+
name?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export type Scenario = {
|
|
11
16
|
name: string
|
|
12
17
|
description: string
|
|
13
18
|
expected: string
|
|
14
19
|
tags?: string[]
|
|
15
|
-
test?:
|
|
20
|
+
test?: ScenarioTestReference
|
|
16
21
|
code?: string[]
|
|
17
22
|
related?: string[]
|
|
18
23
|
}
|
|
@@ -28,10 +33,19 @@ export type EvalNode = {
|
|
|
28
33
|
const SCENARIO_KEYS = ['name', 'description', 'expected', 'tags', 'test', 'code', 'related'] as const
|
|
29
34
|
type ScenarioKey = (typeof SCENARIO_KEYS)[number]
|
|
30
35
|
const LIST_KEYS: readonly ScenarioKey[] = ['tags', 'code', 'related']
|
|
36
|
+
const TEST_KEYS = ['path', 'name'] as const
|
|
37
|
+
type TestKey = (typeof TEST_KEYS)[number]
|
|
38
|
+
|
|
39
|
+
type RawTestObject = {
|
|
40
|
+
fields: Partial<Record<TestKey, string>>
|
|
41
|
+
unknownKeys: string[]
|
|
42
|
+
duplicateKeys: string[]
|
|
43
|
+
malformed: string[]
|
|
44
|
+
}
|
|
31
45
|
|
|
32
46
|
// a raw scenario item straight off the frontmatter walk: the known fields it set, plus any UNKNOWN keys it
|
|
33
47
|
// carried — kept (not dropped) so the validator can name a typo'd field instead of silently swallowing it.
|
|
34
|
-
type RawItem = { fields: Partial<Record<ScenarioKey, string>>; unknownKeys: string[] }
|
|
48
|
+
type RawItem = { fields: Partial<Record<ScenarioKey, string>>; testObject?: RawTestObject; unknownKeys: string[] }
|
|
35
49
|
|
|
36
50
|
// tiny indentation parser for eval.md's frontmatter `scenarios:` block (no YAML dep), shared by parseScenarios and validateScenarios so they can't disagree; reports hasFrontmatter/hasKey so the validator can tell "none declared" from "malformed"
|
|
37
51
|
function walkScenarios(src: string): { hasFrontmatter: boolean; hasKey: boolean; items: RawItem[] } {
|
|
@@ -75,6 +89,32 @@ function assignField(cur: RawItem, kv: string, lines: string[], idx: number, key
|
|
|
75
89
|
const f = kv.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
76
90
|
if (!f) return idx
|
|
77
91
|
const key = f[1]
|
|
92
|
+
if (key === 'test') {
|
|
93
|
+
const raw = f[2].trim()
|
|
94
|
+
if (!raw) {
|
|
95
|
+
const parsed = emptyTestObject()
|
|
96
|
+
let childIndent = -1
|
|
97
|
+
let j = idx + 1
|
|
98
|
+
for (; j < lines.length; j++) {
|
|
99
|
+
const line = lines[j]
|
|
100
|
+
if (!line.trim()) continue
|
|
101
|
+
const indent = line.length - line.replace(/^\s+/, '').length
|
|
102
|
+
if (indent <= keyIndent) break
|
|
103
|
+
if (childIndent < 0) childIndent = indent
|
|
104
|
+
if (indent !== childIndent) {
|
|
105
|
+
parsed.malformed.push(`invalid nested test object entry \`${line.trim()}\``)
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
assignTestField(parsed, line.trim())
|
|
109
|
+
}
|
|
110
|
+
cur.testObject = parsed
|
|
111
|
+
return j - 1
|
|
112
|
+
}
|
|
113
|
+
if (raw.startsWith('{') || raw.endsWith('}')) {
|
|
114
|
+
cur.testObject = parseFlowTestObject(raw)
|
|
115
|
+
return idx
|
|
116
|
+
}
|
|
117
|
+
}
|
|
78
118
|
// a list field (`code:`/`related:`) may be a YAML block sequence (`- item` lines); the scalar reader can't see those, so collect them here into the comma form parseCodeList expects
|
|
79
119
|
if ((LIST_KEYS as readonly string[]).includes(key) && f[2].trim() === '') {
|
|
80
120
|
const items: string[] = []
|
|
@@ -118,6 +158,65 @@ function assignField(cur: RawItem, kv: string, lines: string[], idx: number, key
|
|
|
118
158
|
|
|
119
159
|
const unquote = (s: string) => s.replace(/^["'](.*)["']$/, '$1').trim()
|
|
120
160
|
|
|
161
|
+
const emptyTestObject = (): RawTestObject => ({ fields: {}, unknownKeys: [], duplicateKeys: [], malformed: [] })
|
|
162
|
+
|
|
163
|
+
function testValue(raw: string, opaque = false): string {
|
|
164
|
+
const value = raw.trim()
|
|
165
|
+
const quoted = value.match(/^(["'])([\s\S]*)\1$/)
|
|
166
|
+
return quoted ? quoted[2] : opaque ? value : unquote(value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function assignTestField(obj: RawTestObject, entry: string): void {
|
|
170
|
+
const f = entry.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
171
|
+
if (!f) { obj.malformed.push(`invalid test object entry \`${entry}\``); return }
|
|
172
|
+
const key = f[1]
|
|
173
|
+
if (!(TEST_KEYS as readonly string[]).includes(key)) { obj.unknownKeys.push(key); return }
|
|
174
|
+
if (key in obj.fields) obj.duplicateKeys.push(key)
|
|
175
|
+
obj.fields[key as TestKey] = testValue(f[2], key === 'name')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A flow mapping is accepted beside the more readable block form. Split only on commas outside quotes so
|
|
179
|
+
// an opaque case name such as "allows admin, user" survives byte-for-byte after YAML quote removal.
|
|
180
|
+
function parseFlowTestObject(raw: string): RawTestObject {
|
|
181
|
+
const obj = emptyTestObject()
|
|
182
|
+
if (!raw.startsWith('{') || !raw.endsWith('}')) {
|
|
183
|
+
obj.malformed.push('`test` object must be a mapping with exactly `path` and `name`')
|
|
184
|
+
return obj
|
|
185
|
+
}
|
|
186
|
+
const body = raw.slice(1, -1).trim()
|
|
187
|
+
if (!body) return obj
|
|
188
|
+
const entries: string[] = []
|
|
189
|
+
let start = 0
|
|
190
|
+
let quote = ''
|
|
191
|
+
let escaped = false
|
|
192
|
+
for (let i = 0; i < body.length; i++) {
|
|
193
|
+
const ch = body[i]
|
|
194
|
+
if (quote) {
|
|
195
|
+
if (escaped) { escaped = false; continue }
|
|
196
|
+
if (ch === '\\') { escaped = true; continue }
|
|
197
|
+
if (ch === quote) quote = ''
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if (ch === '"' || ch === "'") { quote = ch; continue }
|
|
201
|
+
if (ch === ',') { entries.push(body.slice(start, i).trim()); start = i + 1 }
|
|
202
|
+
}
|
|
203
|
+
if (quote) obj.malformed.push('`test` object has an unterminated quoted value')
|
|
204
|
+
entries.push(body.slice(start).trim())
|
|
205
|
+
for (const entry of entries) assignTestField(obj, entry)
|
|
206
|
+
return obj
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizedTest(it: RawItem): ScenarioTestReference | undefined {
|
|
210
|
+
if (it.testObject) {
|
|
211
|
+
const path = it.testObject.fields.path
|
|
212
|
+
if (!path) return undefined
|
|
213
|
+
const name = it.testObject.fields.name
|
|
214
|
+
return { path, ...(name ? { name } : {}) }
|
|
215
|
+
}
|
|
216
|
+
const path = it.fields.test
|
|
217
|
+
return path ? { path } : undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
121
220
|
// @@@scenario contract hash - the deterministic content hash of a scenario's SEMANTIC text, stamped on each
|
|
122
221
|
// reading at filing time ([[eval-core]]'s scenario freshness axis). Hashes ONLY the measurement contract —
|
|
123
222
|
// description (what to measure) + expected (what zero loss looks like) — never name/tags/test/code/related.
|
|
@@ -144,12 +243,13 @@ export function parseScenarios(src: string): Scenario[] {
|
|
|
144
243
|
const tags = it.fields.tags ? parseCodeList(it.fields.tags) : []
|
|
145
244
|
const code = it.fields.code ? parseCodeList(it.fields.code) : []
|
|
146
245
|
const related = it.fields.related ? parseCodeList(it.fields.related) : []
|
|
246
|
+
const test = normalizedTest(it)
|
|
147
247
|
return {
|
|
148
248
|
name: it.fields.name ?? '',
|
|
149
249
|
description: it.fields.description ?? '',
|
|
150
250
|
expected: it.fields.expected ?? '',
|
|
151
251
|
...(tags.length ? { tags } : {}),
|
|
152
|
-
...(
|
|
252
|
+
...(test ? { test } : {}),
|
|
153
253
|
...(code.length ? { code } : {}),
|
|
154
254
|
...(related.length ? { related } : {}),
|
|
155
255
|
}
|
|
@@ -161,7 +261,7 @@ export function parseScenarios(src: string): Scenario[] {
|
|
|
161
261
|
// Every scenario needs ≥1 tag; each tag must be IN the library — an out-of-library tag is rejected LOUD with
|
|
162
262
|
// the repair the user owns: pick an existing tag, or extend the library. An empty library (none configured)
|
|
163
263
|
// disables only the membership check, never the ≥1-tag requirement.
|
|
164
|
-
export function validateScenarios(src: string, tagLibrary: string[] = []): string[] {
|
|
264
|
+
export function validateScenarios(src: string, tagLibrary: string[] = [], pathRoot?: string): string[] {
|
|
165
265
|
const { hasFrontmatter, hasKey, items } = walkScenarios(src)
|
|
166
266
|
if (!hasFrontmatter) return ['no frontmatter block — an eval.md must declare a `scenarios:` list']
|
|
167
267
|
if (!hasKey) return ['frontmatter has no `scenarios:` key — declare at least one scenario']
|
|
@@ -183,6 +283,18 @@ export function validateScenarios(src: string, tagLibrary: string[] = []): strin
|
|
|
183
283
|
}
|
|
184
284
|
}
|
|
185
285
|
for (const u of it.unknownKeys) errs.push(`${label}: unknown field \`${u}\` (allowed: ${SCENARIO_KEYS.join(', ')})`)
|
|
286
|
+
if (it.testObject) {
|
|
287
|
+
for (const u of it.testObject.unknownKeys) errs.push(`${label}: unknown \`test\` field \`${u}\` (allowed: ${TEST_KEYS.join(', ')})`)
|
|
288
|
+
for (const d of it.testObject.duplicateKeys) errs.push(`${label}: duplicate \`test.${d}\` field`)
|
|
289
|
+
for (const e of it.testObject.malformed) errs.push(`${label}: ${e}`)
|
|
290
|
+
for (const k of TEST_KEYS) if (!it.testObject.fields[k]?.length) errs.push(`${label}: \`test\` object missing required field \`${k}\``)
|
|
291
|
+
} else if ('test' in it.fields && !it.fields.test?.length) {
|
|
292
|
+
errs.push(`${label}: \`test\` scalar path must not be empty`)
|
|
293
|
+
}
|
|
294
|
+
const test = normalizedTest(it)
|
|
295
|
+
if (test && pathRoot && !existsSync(join(pathRoot, test.path))) {
|
|
296
|
+
errs.push(`${label}: \`test.path\` not found: ${test.path}`)
|
|
297
|
+
}
|
|
186
298
|
if (it.fields.name) counts.set(it.fields.name, (counts.get(it.fields.name) ?? 0) + 1)
|
|
187
299
|
})
|
|
188
300
|
for (const [n, c] of counts) if (c > 1) errs.push(`duplicate scenario name '${n}' (${c}×) — names must be unique within an eval.md`)
|
package/spec-eval/src/sidecar.ts
CHANGED
|
@@ -72,18 +72,28 @@ export function isJsonBlob(b: Buffer): boolean {
|
|
|
72
72
|
|
|
73
73
|
// a RETRACTION is the sanctioned inverse of a filing — itself an appended event, never a deleted line
|
|
74
74
|
// (the sidecar stays append-only; git shows who retracted what, when). `retracts` is the target reading's
|
|
75
|
-
// `ts` within `scenario` (its natural key). The
|
|
76
|
-
// carries `retracts`, a reading carries `codeSha
|
|
77
|
-
// is the retracting session; `note` says why (a botched e2e
|
|
75
|
+
// `ts` within `scenario` (its natural key). The event kinds are told apart POSITIVELY — a retraction
|
|
76
|
+
// carries `retracts`, a reading carries `codeSha`, a human-ok carries `kind: 'human-ok'`; none is ever
|
|
77
|
+
// recognized by another field's absence. `by` is the retracting session; `note` says why (a botched e2e
|
|
78
|
+
// filing, a wrong verdict).
|
|
78
79
|
export type Retraction = { retracts: string; scenario: string; note?: string; by?: string; ts: string }
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
81
|
+
// a HUMAN-OK ([[human-ok]]) is the human's sign-off on ONE immutable reading — an appended event like the
|
|
82
|
+
// others, never a mutation. `okTs`+`okSha` anchor the blessed reading (its ts is the natural key within
|
|
83
|
+
// `scenario`, exactly retraction's join; the sha rides for the human reader). The ok is MONOTONIC — there
|
|
84
|
+
// is no un-ok event: a newer reading is a different object the ok never transfers to, and staleness is
|
|
85
|
+
// computed live, so both automatically bring the scenario back. A pre-human-ok toolchain skips these lines
|
|
86
|
+
// silently (no top-level `codeSha`, so its reading parse never claims them).
|
|
87
|
+
export type HumanOk = { kind: 'human-ok'; scenario: string; okTs: string; okSha: string; by: string; ts: string }
|
|
88
|
+
|
|
89
|
+
// parse the sidecar RAW: one event per non-blank line — a Reading, a Retraction (a line carrying a string
|
|
90
|
+
// `retracts`), or a HumanOk (kind 'human-ok'). A malformed line is skipped (the file is append-only and
|
|
91
|
+
// git-tracked, so a partial write or a hand-edit shouldn't sink the whole read) — fail soft per line.
|
|
92
|
+
export function readSidecar(sidecarPath: string): { readings: Reading[]; retractions: Retraction[]; oks: HumanOk[] } {
|
|
84
93
|
const readings: Reading[] = []
|
|
85
94
|
const retractions: Retraction[] = []
|
|
86
|
-
|
|
95
|
+
const oks: HumanOk[] = []
|
|
96
|
+
if (!existsSync(sidecarPath)) return { readings, retractions, oks }
|
|
87
97
|
for (const line of readFileSync(sidecarPath, 'utf8').split('\n')) {
|
|
88
98
|
const t = line.trim()
|
|
89
99
|
if (!t) continue
|
|
@@ -91,10 +101,11 @@ export function readSidecar(sidecarPath: string): { readings: Reading[]; retract
|
|
|
91
101
|
const r = JSON.parse(t)
|
|
92
102
|
if (!r || typeof r.scenario !== 'string') continue
|
|
93
103
|
if (typeof r.retracts === 'string') retractions.push(r as Retraction)
|
|
104
|
+
else if (r.kind === 'human-ok' && typeof r.okTs === 'string') oks.push(r as HumanOk)
|
|
94
105
|
else if (typeof r.codeSha === 'string') readings.push(r as Reading)
|
|
95
106
|
} catch { /* skip a malformed line */ }
|
|
96
107
|
}
|
|
97
|
-
return { readings, retractions }
|
|
108
|
+
return { readings, retractions, oks }
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
// the retraction join, shared by every effective-view reader: drop each reading a retraction targets by
|
|
@@ -127,6 +138,21 @@ export function appendRetraction(sidecarPath: string, r: Retraction): void {
|
|
|
127
138
|
appendFileSync(sidecarPath, JSON.stringify(r) + '\n')
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
// append ONE human-ok as a JSON line — the sign-off writes through the same append-only surface; the
|
|
142
|
+
// blessed reading stays untouched, the ok binds to it by (scenario, okTs).
|
|
143
|
+
export function appendHumanOk(sidecarPath: string, r: HumanOk): void {
|
|
144
|
+
appendFileSync(sidecarPath, JSON.stringify(r) + '\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// the ok that binds to a reading — the LAST ok row targeting (scenario, ts), or null. An ok anchored to a
|
|
148
|
+
// retracted/superseded reading is inert history: it binds to nothing current, so the join is by exact
|
|
149
|
+
// (scenario, okTs) against whichever readings the caller passes.
|
|
150
|
+
export function humanOkFor(oks: HumanOk[], scenario: string, readingTs: string): HumanOk | null {
|
|
151
|
+
let hit: HumanOk | null = null
|
|
152
|
+
for (const o of oks) if (o.scenario === scenario && o.okTs === readingTs) hit = o
|
|
153
|
+
return hit
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
// the latest reading per scenario (the file is chronological, so the LAST line for a name wins). clean's
|
|
131
157
|
// --keep-latest uses it to decide which blob to keep.
|
|
132
158
|
export function latestPerScenario(readings: Reading[]): Map<string, Reading> {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: clarify-before-code
|
|
3
|
-
surface: system
|
|
4
|
-
status: active
|
|
5
|
-
hue: 30
|
|
6
|
-
desc: A config plugin (careful preset) — before coding, the worker surfaces ambiguities as explicit assumptions in its proposal, and blocks for a live question only on a load-bearing one.
|
|
7
|
-
code:
|
|
8
|
-
---
|
|
9
|
-
Before you write code, enumerate the ambiguities, contradictions, and technical risks in your task. Most of them are cheap: resolve a cheap one by **stating your assumption explicitly** in the work you propose. SpexCode's manager-merge review is already the hard gate — a misread surfaces there, in the proposal, not in a live interrogation of the human. So clarification shifts **left into the artifact**: the diff and the spec body say what you assumed, and the reviewer catches a wrong assumption at merge.
|
|
10
|
-
|
|
11
|
-
Block for a live question (the `needs-input` channel) **only on a load-bearing ambiguity** — one where guessing wrong would waste the whole node. A small or clear task proceeds without asking. This is deliberately the opposite of "every agent asks the user": the default is to proceed on a stated assumption, and the human is interrupted only when proceeding blind would burn the work.
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: supervisor
|
|
3
|
-
surface: command
|
|
4
|
-
status: active
|
|
5
|
-
hue: 280
|
|
6
|
-
desc: Launch a supervisor agent that manages other agents from the main checkout to drive a goal to completion.
|
|
7
|
-
---
|
|
8
|
-
You are a SpexCode supervisor — a **manager**, not a feature worker. Your work base is the main checkout (the repository root), NOT your own worktree: do all git via `git -C <root>`, everything else via the `spex` CLI, and never write feature code. **FIRST, read `<root>/CLAUDE.md` — specifically its "Supervising — the manager loop" section** — that is your complete playbook (dispatch → monitor → review → merge → close, and how to parallelize). Then drive the goal: decompose it into worker-sized tasks and dispatch one worker per independent task (`spex session new "<task>"` — give each ONLY its task; a task about one specific node mentions it as `[[<id>]]`, which only sets the branch name and board attribution; the session's real node links come from what it edits), monitor with `spex session watch`, review proposals with `spex session review <id>`, merge good ones with `git -C <root> merge --no-ff <branch>`, then close. Never let a worker self-merge; keep `spex spec lint` at 0 errors. To WAIT on a worker, POLL one-shot (`spex session review <id>` or `spex session ls` — both return immediately); never block on `spex session watch`, which STREAMS forever and will freeze your turn. One footgun that bites a fresh supervisor: before `spex session close <id>`, confirm the merge landed (`git -C <root> log -1` shows HEAD at the new merge commit) — closing an unmerged branch discards the work. Report progress as you go and when the goal is complete. Your goal follows:
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{u as pe,r as a,s as We,e as Ye,j as n,f as Ge}from"./index-Ce0wDyQS.js";import{V as Ze,W as Je,X as Qe,Y as et,Z as tt,g as fe,n as nt,_ as Q,$ as st,a0 as Ae,a1 as at,a2 as lt,u as ot,z as he,a3 as it,o as rt,a4 as ct}from"./SessionWindow-CqAnjWfI.js";import{F as me}from"./FoldToggle-D5iB4Ac2.js";const Ie=(l,c)=>{let d=null;for(const v of l)if(v.at<=c)d=v;else break;return d},ut=l=>{if(!l||!Array.isArray(l.events))return null;const c=l.v===1?"time":typeof l.axis=="string"?l.axis:"time",d=l.v===1?v=>v.tMs:v=>v.at;return{axis:c,events:l.events.map(v=>({at:d(v),step:v.step,...v.node?{node:v.node}:{}}))}},dt=(l,c,d)=>l==="time"?Q(c):l==="frame"?`#${c}`:l==="line"?`L${c}`:l==="index"?d?`${c}/${d}`:`${c}`:String(c);function Pe({events:l,axis:c,extent:d,activeStepIdx:v,onSeek:$}){return n.jsx("div",{className:"an-ruler",children:l.map((i,f)=>n.jsxs("button",{className:`an-step ${v===f?"on":""}`,onClick:()=>$(i.at),"data-tip":i.node?`→ ${i.node}`:void 0,children:[dt(c,i.at,d)," ",i.step]},f))})}const ve=l=>{var c,d;return((c=l.verdict)==null?void 0:c.status)==="pass"?"✓":((d=l.verdict)==null?void 0:d.status)==="fail"?"✗":"·"},Fe=l=>{var c,d;return((c=l.verdict)==null?void 0:c.status)==="pass"?"pass":((d=l.verdict)==null?void 0:d.status)==="fail"?"fail":"legacy"};function ft({entry:l,specs:c=[],sessions:d=[],onWrite:v,onOpenSession:$}){var Re,Le,Te,De,Ce;const i=pe(),f=a.useRef(null),x=a.useRef(null),j=a.useRef(null),k=a.useRef(null),g=a.useRef(0),[u,M]=a.useState([]),[w,y]=a.useState("time"),[r,b]=a.useState(null),[S,T]=a.useState(""),[D,X]=a.useState(!1),[ee,z]=a.useState(null),C=a.useRef(null),h=a.useRef(null),I=a.useRef(null),te=a.useRef(0),[Be,xe]=a.useState(0),[ne,ge]=a.useState(-1),[ye,be]=a.useState(null),[H,se]=a.useState(!1),[we,ae]=a.useState(!1),[le,oe]=a.useState(null),[_,ie]=a.useState(null),[E,je]=a.useState(null),[A,q]=a.useState(0),p=E&&E[A]||l;a.useEffect(()=>{b(null),T(""),M([]),z(null),q(0),je(null);let e=!0;return fetch(We(l.node,"evals")).then(t=>t.ok?t.json():null).then(t=>{e&&Array.isArray(t==null?void 0:t.readings)&&je(t.readings.filter(s=>s.scenario===l.scenario))}).catch(()=>{}),()=>{e=!1}},[l.node,l.scenario,l.ts,l.blob]),a.useEffect(()=>{b(null),z(null)},[A]);const O=Ze(p),P=O.find(e=>e.kind==="video"&&e.state==="present"),re=O.filter(e=>e.kind==="image"),Ve=O.filter(e=>e.kind!=="video"&&e.kind!=="image"),K=!!P,B=l.thread??null,ce=a.useMemo(()=>B?[{by:B.by,at:B.created,body:B.body},...B.replies||[]]:[],[B]),Xe=E&&((Re=E[0])==null?void 0:Re.by)||l.by||null,R=a.useMemo(()=>ce.map((e,t)=>{const s=Je(Qe(e.body),u);return s&&s.seekable?{i:t,tMs:s.tMs,step:s.step,label:s.label}:null}).filter(Boolean).sort((e,t)=>e.tMs-t.tMs),[ce,u]),ke=a.useRef(u);ke.current=u;const Me=a.useRef(R);Me.current=R;const W=a.useCallback(()=>{const e=te.current,t=ke.current;let s=-1;for(let m=0;m<t.length&&t[m].at<=e;m++)s=m;ge(m=>m===s?m:s);let o=null;for(const m of Me.current)if(m.tMs<=e)o=m.i;else break;be(m=>m===o?m:o)},[]);a.useEffect(()=>{W()},[R,u,W]),a.useEffect(()=>{te.current=0,C.current&&(C.current.style.width="0%"),h.current&&(h.current.style.left="0%"),I.current&&(I.current.textContent=""),ge(-1),be(null),xe(0),se(!1),ae(!1),oe(null),ie(null)},[l.blob,l.scenario,l.node]),a.useEffect(()=>{if(M([]),y("time"),!p.timelineBlob)return;let e=!0;return fetch(`/api/evidence/${p.timelineBlob}`).then(t=>t.ok?t.json():null).then(t=>{const s=e&&ut(t);s&&(M(s.events),y(s.axis))}).catch(()=>{}),()=>{e=!1}},[p.timelineBlob]),a.useEffect(()=>{const e=f.current;if(!e)return;const t=()=>{const N=Math.round((e.currentTime||0)*1e3),L=Math.round((e.duration||0)*1e3);te.current=N;const J=L?N/L*100:0;C.current&&(C.current.style.width=`${J}%`),h.current&&(h.current.style.left=`${J}%`),I.current&&(I.current.textContent=`${Q(N)} / ${Q(L)}`),W()},s=()=>{xe(e.duration||0),t()},o=()=>se(!0),m=()=>se(!1);return e.addEventListener("timeupdate",t),e.addEventListener("seeked",t),e.addEventListener("loadedmetadata",s),e.addEventListener("durationchange",s),e.addEventListener("play",o),e.addEventListener("pause",m),s(),()=>{e.removeEventListener("timeupdate",t),e.removeEventListener("seeked",t),e.removeEventListener("loadedmetadata",s),e.removeEventListener("durationchange",s),e.removeEventListener("play",o),e.removeEventListener("pause",m)}},[P==null?void 0:P.hash,l.node,l.scenario,A,W]);const F=Math.round(Be*1e3),Y=u[ne]||null,Ne=w==="time"?F:w==="frame"?re.length:w==="index"?u.length:0,V=a.useCallback(e=>{const t=f.current;t&&(t.currentTime=e/1e3)},[]),G=a.useCallback(()=>{const e=f.current;e&&(e.paused?e.play():e.pause())},[]),Ee=(e,t)=>{ie(e),t!=null&&V(t)},Z=a.useCallback(async e=>{const t=f.current;if(!(t!=null&&t.videoWidth))return null;X(!0),T(i("annotator.capturing"));try{t.seeking&&await new Promise(L=>t.addEventListener("seeked",L,{once:!0}));const s=document.createElement("canvas");s.width=t.videoWidth,s.height=t.videoHeight;const o=s.getContext("2d");o.drawImage(t,0,0,s.width,s.height),e&&(o.strokeStyle="#ff9a3c",o.lineWidth=Math.max(2,s.width/300),o.strokeRect(e.x/100*s.width,e.y/100*s.height,e.w/100*s.width,e.h/100*s.height));const m=await new Promise(L=>s.toBlob(L,"image/png")),{hash:N}=await Ye(m);if(!N)throw new Error("no hash");return T(""),N}catch{return T(i("annotator.failed")),null}finally{X(!1)}},[i]),ze=a.useCallback(async()=>{var s;const e=f.current;if(!e)return null;e.pause();const t=Math.round((e.currentTime??0)*1e3);return{tMs:t,step:((s=Ie(u,t))==null?void 0:s.step)??null,frame:await Z(null)}},[u,Z]),ue=a.useCallback(async e=>{const t=f.current;if(!t)return;t.pause();const s=Math.round((t.currentTime||0)*1e3),o=Ie(u,s),m=await Z(e),N=[et(s,o==null?void 0:o.step)];m&&N.push(``),o!=null&&o.node&&o.node!==l.node&&N.push(`re: [[${o.node}]]`),N.push(""),z({seq:++g.current,body:N.join(`
|
|
2
|
-
`)})},[u,l.node,Z]),de=a.useCallback(e=>{var o;if(!R.length)return;let t=R.findIndex(m=>m.i===_);if(t<0){const m=Math.round((((o=f.current)==null?void 0:o.currentTime)||0)*1e3);t=R.reduce((N,L,J)=>L.tMs<=m?J:N,-1)}t=Math.min(R.length-1,Math.max(0,t+e));const s=R[t];ie(s.i),V(s.tMs)},[R,_,V]);a.useEffect(()=>{if(!K)return;const e=t=>{const s=document.activeElement;if(s&&(s.tagName==="INPUT"||s.tagName==="TEXTAREA"||s.tagName==="SELECT"||s.isContentEditable))return;const o=f.current;o&&(t.key===" "?(t.preventDefault(),G()):t.key==="ArrowRight"?(t.preventDefault(),o.currentTime=Math.min(o.duration||o.currentTime,o.currentTime+(t.shiftKey?1:5))):t.key==="ArrowLeft"?(t.preventDefault(),o.currentTime=Math.max(0,o.currentTime-(t.shiftKey?1:5))):t.key===","?(t.preventDefault(),o.currentTime=Math.max(0,o.currentTime-1/30)):t.key==="."?(t.preventDefault(),o.duration&&(o.currentTime=Math.min(o.duration,o.currentTime+1/30))):t.key==="ArrowDown"?(t.preventDefault(),de(1)):t.key==="ArrowUp"?(t.preventDefault(),de(-1)):(t.key==="a"||t.key==="A")&&(t.preventDefault(),ue(null)))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[K,G,de,ue]);const $e=e=>{var o;const t=(o=j.current)==null?void 0:o.getBoundingClientRect(),s=f.current;!t||!t.width||!s||!s.duration||(s.currentTime=Math.min(1,Math.max(0,(e-t.left)/t.width))*s.duration)},Oe=e=>{e.preventDefault(),ae(!0),$e(e.clientX)},Ue=e=>{var s;const t=(s=j.current)==null?void 0:s.getBoundingClientRect();t!=null&&t.width&&oe(Math.min(100,Math.max(0,(e.clientX-t.left)/t.width*100)))};a.useEffect(()=>{if(!we)return;const e=s=>$e(s.clientX),t=()=>ae(!1);return window.addEventListener("mousemove",e),window.addEventListener("mouseup",t),()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)}},[we]);const Se=e=>{const t=x.current.getBoundingClientRect();return{x:(e.clientX-t.left)/t.width*100,y:(e.clientY-t.top)/t.height*100}},He=e=>{if(e.button!==0||D)return;const t=Se(e);b({x0:t.x,y0:t.y,x:t.x,y:t.y})},_e=e=>{var s,o;if(!r)return;const t=Se(e);!((s=f.current)!=null&&s.paused)&&(Math.abs(t.x-r.x0)>1||Math.abs(t.y-r.y0)>1)&&((o=f.current)==null||o.pause()),b({...r,x:t.x,y:t.y})},qe=()=>{if(!r)return;const e={x:Math.min(r.x0,r.x),y:Math.min(r.y0,r.y),w:Math.abs(r.x-r.x0),h:Math.abs(r.y-r.y0)};if(b(null),e.w<1&&e.h<1){G();return}ue(e)},U=r&&{x:Math.min(r.x0,r.x),y:Math.min(r.y0,r.y),w:Math.abs(r.x-r.x0),h:Math.abs(r.y-r.y0)};return n.jsxs("div",{className:"an-detail",children:[n.jsxs("header",{className:"an-head",children:[n.jsx("span",{className:"an-title",children:l.scenario}),n.jsx("span",{className:"an-node",children:l.node}),n.jsx("span",{className:`an-verdict-badge ${Fe(p)}`,children:ve(p)}),p.evaluator&&n.jsx("span",{className:"an-meta",children:p.evaluator}),n.jsx("span",{className:"an-meta",children:new Date(p.ts).toLocaleString()}),n.jsx(tt,{originator:Xe,sessions:d,kind:"eval",onOpenSession:$}),E&&E.length>1&&n.jsxs("div",{className:"an-ab",children:[n.jsx(fe,{icon:"chevron-left",size:13,className:"an-ab-nav",disabled:A>=E.length-1,onClick:()=>q(e=>Math.min(E.length-1,e+1)),label:i("annotator.abOlder")}),n.jsx("div",{className:"an-ab-track",children:E.slice().reverse().map((e,t)=>{const s=E.length-1-t;return n.jsx("button",{type:"button",className:`an-ab-pip ${Fe(e)} ${s===A?"on":""}`,onClick:()=>q(s),"data-tip":`${ve(e)} ${new Date(e.ts).toLocaleString()}`,children:ve(e)},`${e.ts}-${s}`)})}),n.jsx(fe,{icon:"chevron-right",size:13,className:"an-ab-nav",disabled:A<=0,onClick:()=>q(e=>Math.max(0,e-1)),label:i("annotator.abNewer")}),n.jsx("span",{className:"an-ab-pos",children:A===0?i("annotator.abLatest"):i("annotator.abPos",{i:E.length-A,n:E.length})})]})]}),n.jsxs("div",{className:"an-work",children:[n.jsxs("div",{className:"an-stage-col",children:[p.expected&&n.jsxs("div",{className:"an-expected",children:[n.jsx("b",{children:i("nodeView.eval.expected")})," ",p.expected]}),O.length>0&&((Le=p.verdict)==null?void 0:Le.note)&&n.jsxs("div",{className:"an-expected an-prior-note",children:[n.jsx("b",{children:i("nodeView.eval.noteLabel")})," ",p.verdict.note]}),!p.fresh&&(((Te=p.staleAxes)==null?void 0:Te.length)??0)>0&&n.jsxs("div",{className:"an-expected an-stale","data-tip":i("nodeView.eval.staleReadoutTitle"),children:[n.jsx("b",{children:i("nodeView.eval.staleLabel")})," ",p.staleAxes.join(" · "),(((De=p.codeDrift)==null?void 0:De.length)??0)>0&&n.jsxs("span",{className:"an-stale-files",children:[" — ",p.codeDrift.map(e=>`${e.file.split("/").pop()} +${e.behind}`).join(", ")]})]}),P&&n.jsxs(n.Fragment,{children:[n.jsxs("div",{className:"an-player",ref:k,children:[n.jsxs("div",{className:`an-stage ${H?"playing":"paused"}`,ref:x,onMouseDown:He,onMouseMove:_e,onMouseUp:qe,children:[n.jsx("video",{className:"an-video",ref:f,src:`/api/evidence/${P.hash}`,preload:"metadata",playsInline:!0}),U&&n.jsx("div",{className:"an-rect live",style:{left:`${U.x}%`,top:`${U.y}%`,width:`${U.w}%`,height:`${U.h}%`}}),!H&&!r&&n.jsx("div",{className:"an-bigplay","aria-hidden":!0,children:n.jsx(nt,{name:"play",size:22})})]}),n.jsxs("div",{className:"an-bar",children:[n.jsx(fe,{icon:H?"pause":"play",size:14,className:"an-play",label:i(H?"annotator.pause":"annotator.play"),onClick:G}),n.jsxs("div",{className:"an-seek",ref:j,onMouseDown:Oe,onMouseMove:Ue,onMouseLeave:()=>oe(null),children:[n.jsx("div",{className:"an-seek-trk"}),F>0&&w==="time"&&u.map((e,t)=>n.jsx("div",{className:"an-band",style:{left:`${e.at/F*100}%`},"data-tip":e.step},`band-${t}`)),n.jsx("div",{className:"an-seek-play",ref:C}),F>0&&R.map(e=>n.jsx("button",{type:"button",className:`an-mk ${_===e.i?"on":""} ${ye===e.i?"active":""}`,style:{left:`${e.tMs/F*100}%`},"data-tip":e.label,onMouseDown:t=>t.stopPropagation(),onClick:t=>{t.stopPropagation(),Ee(e.i,e.tMs)}},`mk-${e.i}`)),n.jsx("div",{className:"an-knob",ref:h}),le!=null&&F>0&&n.jsx("div",{className:"an-seek-hov",style:{left:`${le}%`},children:Q(le/100*F)})]}),n.jsx("span",{className:"an-time",ref:I}),Y&&n.jsx("span",{className:"an-curstep","data-tip":Y.node?`→ ${Y.node}`:void 0,children:Y.step}),n.jsx(st,{target:k})]})]}),u.length>0&&n.jsx(Pe,{events:u,axis:w,extent:Ne,activeStepIdx:ne,onSeek:V}),n.jsx("div",{className:"an-hint",children:i("annotator.hint")}),n.jsx("div",{className:"an-keys",children:i("annotator.keys")}),S&&n.jsx("div",{className:"an-flash",children:S})]}),!P&&u.length>0&&n.jsx(Pe,{events:u,axis:w,extent:Ne,activeStepIdx:ne,onSeek:V}),re.length>0&&n.jsx("div",{className:"an-gallery",children:re.map((e,t)=>n.jsx(Ae,{e,alt:l.scenario},`${e.hash}-${t}`))}),Ve.map((e,t)=>n.jsx(Ae,{e,alt:l.scenario},`${e.hash}-${t}`)),O.length===0&&((Ce=p.verdict)!=null&&Ce.note?n.jsx("pre",{className:"eval-transcript",children:p.verdict.note}):n.jsx("div",{className:"an-hint",children:i("nodeView.eval.noImage")}))]}),n.jsx(ht,{entry:l,comments:ce,specs:c,sessions:d,onWrite:v,codeSha:p.codeSha,seekMs:K?V:null,anchorNow:K?ze:null,draft:ee,selIdx:_,activeIdx:ye,onSelect:K?Ee:null,events:K?u:null})]})]})}function ht({entry:l,comments:c,codeSha:d,specs:v,sessions:$,onWrite:i,seekMs:f,anchorNow:x,draft:j,selIdx:k,activeIdx:g,onSelect:u,events:M}){var r;const w=pe(),y=(b,S)=>Ge({node:l.node,scenario:l.scenario,body:b,codeSha:d,evidence:S});return n.jsxs("aside",{className:"an-rail",children:[n.jsx("div",{className:"an-comments-head",children:w("annotator.comments",{n:c.length})}),n.jsx("div",{className:"an-rail-list",children:n.jsx(at,{replies:c,onSeek:f,selIdx:k,activeIdx:g,onSelect:u,events:M,threadId:((r=l.thread)==null?void 0:r.id)??null,onRemarkChange:()=>i==null?void 0:i("")})}),n.jsx("div",{className:"an-rail-compose",children:n.jsx(lt,{onSend:y,specs:v,sessions:$,focusId:l.node,onDone:i,anchorNow:x,draft:j},`${l.node}·${l.scenario}`)})]})}function Ke({rowKeys:l,sel:c,onSel:d,detail:v,children:$}){const[i,f]=a.useState(!1),x=a.useRef({});x.current={rowKeys:l,sel:c},a.useEffect(()=>{const k=g=>{var b;const u=(b=g.target)==null?void 0:b.tagName;if(u==="INPUT"||u==="TEXTAREA"||g.metaKey||g.ctrlKey||g.altKey||g.key!=="j"&&g.key!=="k")return;g.preventDefault(),g.stopPropagation();const{rowKeys:M,sel:w}=x.current;if(!M.length)return;const y=M.indexOf(w),r=y<0?g.key==="j"?0:M.length-1:Math.max(0,Math.min(M.length-1,y+(g.key==="j"?1:-1)));d(M[r])};return window.addEventListener("keydown",k,!0),()=>window.removeEventListener("keydown",k,!0)},[d]),a.useEffect(()=>{var k;(k=document.querySelector(".fv-list-col .sel"))==null||k.scrollIntoView({block:"nearest"})},[c]);const j=n.jsx(me,{className:"fv-fold-inline",onToggle:()=>f(!0)});return n.jsxs("div",{className:`fv-master ${i?"folded":""}`,children:[i&&n.jsx(me,{className:"fv-unfold",folded:!0,onToggle:()=>f(!1)}),n.jsx("div",{className:"fv-list-col",style:i?{display:"none"}:void 0,children:typeof $=="function"?$(j):n.jsxs(n.Fragment,{children:[n.jsx(me,{className:"fv-fold",onToggle:()=>f(!0)}),$]})}),n.jsx("div",{className:"fv-detail",children:v})]})}function mt({specs:l=[],sessions:c=[],reloadBoard:d,onOpenSession:v}){const $=pe(),{page:i,param:f}=ot(),[x,j]=a.useState(null),[k,g]=a.useState(""),[u,M]=a.useState([]),w=a.useRef([]),y=a.useMemo(()=>new Map(u.map(h=>[he(h),h])),[u]);w.current=u.map(he);const r=x&&y.has(x)?x:w.current[0]??null,b=a.useMemo(()=>{if(!f)return null;const h=f.indexOf("/");return h>0?`eval:${f.slice(0,h)}·${f.slice(h+1)}`:null},[f]),[S,T]=a.useState(null);a.useEffect(()=>{i==="evals"&&b&&(j(b),T(b))},[i,b]),a.useEffect(()=>{S&&y.has(S)&&T(null)},[S,y]);const D=x&&!y.has(x),X=a.useMemo(()=>!D||it(l).some(h=>he(h)===x),[D,l,x]);a.useEffect(()=>{D&&!X&&(j(null),T(null))},[D,X]),a.useEffect(()=>{x&&!S&&u.length&&!y.has(x)&&j(null)},[x,S,u,y]),a.useEffect(()=>{if(i!=="evals"||!r||D)return;const h=/^eval:([^·]+)·(.+)$/.exec(r);h&&rt("evals",`${h[1]}/${h[2]}`,{replace:!0})},[i,r,D]);const ee=a.useCallback(h=>M(h),[]),z=h=>{h&&(g(h),setTimeout(()=>g(""),6e3))},C=r?y.get(r):null;return n.jsx(Ke,{rowKeys:w.current,sel:r,onSel:j,detail:C?n.jsx(ft,{entry:C,specs:l,sessions:c,onOpenSession:v,onWrite:async h=>{z(h),await(d==null?void 0:d())}}):n.jsx("div",{className:"fv-note",children:$("evalsFeed.empty")}),children:h=>n.jsxs(n.Fragment,{children:[k&&n.jsx("div",{className:"fv-notice",children:k}),n.jsx(ct,{nodes:l,sessions:c,sel:r,onSel:I=>j(I),onRows:ee,mustShow:S,lead:h})]})})}const gt=Object.freeze(Object.defineProperty({__proto__:null,EvalMasterDetail:Ke,default:mt},Symbol.toStringTag,{value:"Module"}));export{ft as E,Ke as a,gt as b};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as k,r as b,j as s}from"./index-Ce0wDyQS.js";import{q as M,t as A,A as y,j as g,v as H,H as V,w as R,E as _,c as $,G as B,x as G,S}from"./SessionWindow-CqAnjWfI.js";const L={spec:"nodeView.paneSpec",history:"nodeView.paneHistory",issues:"nodeView.paneIssues",eval:"nodeView.paneEval",edit:"nodeView.paneEdit"},C=(e,l)=>l.filter(u=>{var h;return(h=u.ops)==null?void 0:h.some(n=>n.nodeId===e.id)});function T({status:e}){const l=S[e]||S.pending;return s.jsx("span",{className:"m-dot",style:{background:l.color}})}function U({node:e,kids:l,editors:u,onTap:h}){return s.jsxs("button",{className:"m-row",onClick:h,children:[s.jsx(T,{status:e.status}),s.jsx("span",{className:"m-row-title",children:e.title}),u.length>0&&s.jsx("span",{className:"m-row-faces",children:u.slice(0,3).map(n=>s.jsx(y,{seed:n.id,status:n.status,title:g(n)},n.id))}),e.version?s.jsxs("span",{className:"m-row-ver",children:["v",e.version]}):null,l>0&&s.jsxs("span",{className:"m-row-kids",children:["▸",l]}),s.jsx("span",{className:"m-row-chev",children:"›"})]})}function q({node:e,childrenOf:l,sessions:u,onOpenChild:h}){const n=k(),m=l(e.id),d=M(e),i=[...m.length?[{key:"children",label:n("mobile.childrenTab",{n:m.length})}]:[],...d.map(a=>({key:a.key,label:n(L[a.key])}))],[r,x]=b.useState(null);b.useEffect(()=>{x(null)},[e.id]);const c=r&&i.some(a=>a.key===r)?r:i[0].key,j=A(e.id,c==="history"),N=C(e,u);return s.jsxs("div",{className:"m-node",children:[s.jsxs("div",{className:"m-node-head",children:[s.jsx(T,{status:e.status}),s.jsx("span",{className:"m-node-title",children:e.title}),e.version?s.jsxs("span",{className:"m-node-ver",children:["v",e.version]}):null,s.jsx("span",{className:"m-node-status",children:n(`status.${e.status}`)}),N.length>0&&s.jsx("span",{className:"m-node-faces",title:n("mobile.liveEditors",{n:N.length}),children:N.map(a=>s.jsx(y,{seed:a.id,status:a.status,title:g(a)},a.id))})]}),e.desc&&s.jsx("div",{className:"m-node-desc",children:e.desc}),s.jsx("div",{className:"m-tabs",children:i.map(a=>s.jsx("button",{className:a.key===c?"m-tab on":"m-tab",onClick:()=>x(a.key),children:a.label},a.key))}),s.jsxs("div",{className:c==="history"?"m-pane-host fixed":"m-pane-host",children:[c==="children"&&s.jsx("div",{className:"m-children",children:m.map(a=>s.jsx(U,{node:a,kids:l(a.id).length,editors:C(a,u),onTap:()=>h(a.id)},a.id))}),c==="spec"&&s.jsx(H,{node:e}),c==="history"&&s.jsx(V,{node:e,rows:j}),c==="issues"&&s.jsx(R,{node:e}),c==="edit"&&s.jsx(_,{node:e})]})]})}function D({sessions:e,openId:l,setOpenId:u,byId:h,goToNode:n}){const m=k(),d=l?e.find(i=>i.id===l):null;if(d){const i=d.ops||[];return s.jsxs("div",{className:"m-sessdetail",children:[s.jsxs("div",{className:"m-sess-card",children:[s.jsx(y,{seed:d.id,status:d.status}),s.jsxs("div",{className:"m-sess-meta",children:[s.jsx("span",{className:"m-sess-name",children:g(d)}),s.jsx("span",{className:"m-sess-status",style:{color:$[d.status]},children:m(`status.${d.status}`)})]})]}),d.activity&&s.jsx("div",{className:"m-sess-activity",children:d.activity}),s.jsx("div",{className:"m-sess-section",children:m("mobile.changing",{n:i.length})}),i.length===0?s.jsx("div",{className:"m-empty",children:m("mobile.noChanges")}):i.map((r,x)=>{const c=h[r.nodeId];return s.jsxs("button",{className:"m-row",disabled:!c,onClick:()=>c&&n(r.nodeId),children:[s.jsx("span",{className:`ov-mark ov-${r.op}`,children:B[r.op]||"•"}),s.jsx("span",{className:"m-row-title",children:c?c.title:r.nodeId}),c&&s.jsx("span",{className:"m-row-chev",children:"›"})]},x)})]})}return e.length?s.jsx("div",{className:"m-sesslist",children:e.map(i=>s.jsx("button",{className:"m-sess-row",onClick:()=>u(i.id),children:s.jsx(G,{s:i,locked:!1})},i.id))}):s.jsx("div",{className:"m-empty big",children:m("mobile.noSessions")})}function z({specs:e,sessions:l,project:u}){const h=k(),n=b.useMemo(()=>Object.fromEntries(e.map(t=>[t.id,t])),[e]),m=b.useMemo(()=>e.find(t=>!t.parent)||e[0],[e]),d=b.useMemo(()=>{const t={};return e.forEach(o=>{var p;o.parent&&(t[p=o.parent]??(t[p]=[])).push(o)}),o=>t[o]||[]},[e]),i=b.useMemo(()=>t=>{const o=[];for(let p=n[t];p;p=p.parent?n[p.parent]:null)o.unshift(p.id);return o},[n]),[r,x]=b.useState("specs"),[c,j]=b.useState(()=>m?[m.id]:[]),[N,a]=b.useState(null),v=c.filter(t=>n[t]),w=v[v.length-1]||(m==null?void 0:m.id),f=w?n[w]:null,E=t=>j(o=>[...o.filter(p=>n[p]),t]),I=t=>{j(i(t)),a(null),x("specs")},P=r==="specs"?v.length>1:!!N,O=()=>{r==="specs"?j(t=>t.slice(0,-1)):a(null)};return s.jsxs("div",{className:"m-app",children:[s.jsxs("header",{className:"m-topbar",children:[P?s.jsx("button",{className:"m-back",onClick:O,"aria-label":h("mobile.back"),children:"‹"}):s.jsx("span",{className:"m-back ghost","aria-hidden":"true"}),s.jsx("span",{className:"m-brand",children:u||"SpexCode"})]}),s.jsx("main",{className:"m-main",children:r==="specs"?s.jsxs("div",{className:"m-specs",children:[s.jsx("nav",{className:"m-crumbs",children:v.map((t,o)=>s.jsxs("span",{className:"m-crumb",children:[o>0&&s.jsx("span",{className:"m-crumb-sep",children:"›"}),s.jsx("button",{className:o===v.length-1?"m-crumb-btn cur":"m-crumb-btn",onClick:()=>j(v.slice(0,o+1)),children:n[t].title})]},t))}),f&&s.jsx(q,{node:f,childrenOf:d,sessions:l,onOpenChild:E},f.id)]}):s.jsx(D,{sessions:l,openId:N,setOpenId:a,byId:n,goToNode:I})}),s.jsxs("nav",{className:"m-tabbar",children:[s.jsxs("button",{className:r==="specs"?"m-tabbar-btn on":"m-tabbar-btn",onClick:()=>x("specs"),children:[s.jsx("span",{className:"m-tabbar-ico",children:"❯_"}),h("mobile.specsTab")]}),s.jsxs("button",{className:r==="sessions"?"m-tabbar-btn on":"m-tabbar-btn",onClick:()=>x("sessions"),children:[s.jsx("span",{className:"m-tabbar-ico",children:"◐"}),h("mobile.sessionsTab"),l.length>0&&s.jsx("span",{className:"m-tabbar-badge",children:l.length})]})]})]})}export{z as default};
|