spexcode 0.2.8 → 0.4.0
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 +56 -44
- package/package.json +3 -3
- package/spec-cli/bin/spex.mjs +2 -2
- package/spec-cli/hooks/dispatch.sh +1 -1
- package/spec-cli/hooks/harness.sh +26 -6
- package/spec-cli/src/anchors.ts +300 -0
- package/spec-cli/src/attach.ts +2 -2
- package/spec-cli/src/cli.ts +691 -536
- package/spec-cli/src/client.ts +31 -30
- package/spec-cli/src/contract-filter.ts +1 -1
- package/spec-cli/src/doctor.ts +40 -13
- package/spec-cli/src/gateway.ts +11 -7
- package/spec-cli/src/git.ts +23 -15
- package/spec-cli/src/{board.ts → graph.ts} +44 -14
- package/spec-cli/src/{boardCache.ts → graphCache.ts} +41 -21
- package/spec-cli/src/{boardDelta.ts → graphDelta.ts} +1 -1
- package/spec-cli/src/graphStream.ts +288 -0
- package/spec-cli/src/guide.ts +142 -98
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +30 -14
- package/spec-cli/src/help.ts +300 -385
- package/spec-cli/src/hooks.ts +1 -1
- package/spec-cli/src/index.ts +144 -103
- package/spec-cli/src/init.ts +14 -13
- package/spec-cli/src/issues.ts +89 -33
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +128 -30
- package/spec-cli/src/localIssues.ts +61 -60
- package/spec-cli/src/materialize.ts +1 -1
- package/spec-cli/src/mentions.ts +15 -15
- package/spec-cli/src/migrate-table.ts +398 -0
- package/spec-cli/src/migrate.ts +386 -0
- package/spec-cli/src/ranker.ts +30 -4
- package/spec-cli/src/reaper.ts +117 -0
- package/spec-cli/src/search.bench.mjs +12 -12
- package/spec-cli/src/search.ts +1 -1
- package/spec-cli/src/sessions.ts +244 -138
- package/spec-cli/src/specs.ts +40 -25
- package/spec-cli/src/supervise.ts +2 -2
- package/spec-cli/src/tree.ts +5 -5
- package/spec-cli/src/uninstall.ts +4 -4
- package/spec-cli/templates/hooks/post-checkout +1 -1
- package/spec-cli/templates/hooks/post-merge +3 -3
- package/spec-cli/templates/hooks/pre-commit +9 -9
- package/spec-cli/templates/hooks/prepare-commit-msg +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/idle.sh +2 -2
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/mark-active.sh +7 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/mark-active/spec.md +2 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/fail.sh +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec-first.sh +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec-of-file.sh +2 -2
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +21 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/stop-gate.sh +21 -21
- package/spec-cli/templates/spec/project/{.config → .plugins}/distill/spec.md +2 -2
- package/spec-cli/templates/spec/project/{.config → .plugins}/extract/spec.md +5 -5
- package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/forge-link/spec.md +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/reproduce-before-fix/spec.md +3 -3
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/regroup/spec.md +2 -2
- package/spec-cli/templates/spec/project/.plugins/spec.md +18 -0
- package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +8 -0
- package/spec-cli/templates/spec/project/{.config → .plugins}/tidy/spec.md +1 -1
- package/spec-cli/templates/spec/project/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-P0B9ukSG.js → Dashboard-CTcH2eW9.js} +9 -9
- package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +2 -0
- package/spec-dashboard/dist/assets/{FoldToggle-BuQ0lokE.js → FoldToggle-CVFbBpyW.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-H-D8aHEl.js → IssuesPage-kULjonqj.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-oZXIeCPb.js → MobileApp-B0ZJju8K.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-Blr_MEdU.js → SessionInterface-BRKJqU2U.js} +3 -3
- package/spec-dashboard/dist/assets/{SessionWindow-LcCzBMU7.js → SessionWindow-CDhEL7wO.js} +7 -7
- package/spec-dashboard/dist/assets/{Settings-_yOye-In.js → Settings-BL6FV_8S.js} +1 -1
- package/spec-dashboard/dist/assets/{index-uGs9v_9o.css → index-DmQsNYKK.css} +1 -1
- package/spec-dashboard/dist/assets/{index-BhIslAau.js → index-DulGPk6A.js} +8 -8
- package/spec-dashboard/dist/index.html +2 -2
- package/{spec-yatsu → spec-eval}/src/cache.ts +8 -5
- package/{spec-yatsu → spec-eval}/src/cli.ts +212 -96
- package/{spec-yatsu → spec-eval}/src/evaltab.ts +35 -27
- package/{spec-yatsu → spec-eval}/src/filing.ts +7 -5
- package/{spec-yatsu → spec-eval}/src/freshness.ts +44 -22
- package/spec-eval/src/humanok.ts +43 -0
- package/{spec-yatsu → spec-eval}/src/scenariofresh.ts +22 -17
- package/{spec-yatsu/src/yatsu.ts → spec-eval/src/scenarios.ts} +41 -26
- package/{spec-yatsu/src/proof.ts → spec-eval/src/sessioneval.ts} +59 -59
- package/{spec-yatsu → spec-eval}/src/sidecar.ts +42 -10
- package/{spec-yatsu → spec-eval}/src/timeline.ts +1 -1
- package/spec-forge/src/__fixtures__/github-forge.json +9 -9
- package/spec-forge/src/cli.ts +14 -13
- package/spec-forge/src/{needs-yatsu-eval.ts → needs-eval.ts} +6 -6
- package/spec-cli/src/boardStream.ts +0 -179
- package/spec-cli/templates/presets/careful/.config/clarify-before-code/spec.md +0 -11
- package/spec-cli/templates/spec/project/.config/core/spec.md +0 -17
- package/spec-cli/templates/spec/project/.config/spec.md +0 -15
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +0 -8
- package/spec-dashboard/dist/assets/EvalsPage-BrvAGyc4.js +0 -2
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/idle/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/session-fail/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-first/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/spec-of-file/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/core/stop-gate/spec.md +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins}/distill/digest.mjs +0 -0
- /package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/memory-hygiene/spec.md +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "Fixture mirroring githubDriver output (the vendor-neutral ForgeIssue[]/ForgePR[] a `gh issue/pr list` collapses to) for a sample repo. Drives the needs-
|
|
2
|
+
"_note": "Fixture mirroring githubDriver output (the vendor-neutral ForgeIssue[]/ForgePR[] a `gh issue/pr list` collapses to) for a sample repo. Drives the needs-eval resolver test with no network.",
|
|
3
3
|
"issues": [
|
|
4
4
|
{
|
|
5
5
|
"number": 1,
|
|
6
6
|
"title": "login redirect is broken on /logout",
|
|
7
|
-
"body": "Steps to repro: click logout, land on a 404.\n\nSpec: sessions\nneeds-
|
|
7
|
+
"body": "Steps to repro: click logout, land on a 404.\n\nSpec: sessions\nneeds-eval\n",
|
|
8
8
|
"url": "https://github.com/acme/repo/issues/1",
|
|
9
9
|
"state": "open",
|
|
10
10
|
"labels": []
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"body": "The open-issue badge over-counts.\n\nSpec: dashboard-issues",
|
|
16
16
|
"url": "https://github.com/acme/repo/issues/2",
|
|
17
17
|
"state": "open",
|
|
18
|
-
"labels": ["bug", "needs-
|
|
18
|
+
"labels": ["bug", "needs-eval"]
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"number": 3,
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"number": 4,
|
|
30
|
-
"title": "old
|
|
31
|
-
"body": "Spec:
|
|
30
|
+
"title": "old forge-cache drift already verified",
|
|
31
|
+
"body": "Spec: forge-cache\nNeeds-Eval\n",
|
|
32
32
|
"url": "https://github.com/acme/repo/issues/4",
|
|
33
33
|
"state": "closed",
|
|
34
34
|
"labels": []
|
|
@@ -36,18 +36,18 @@
|
|
|
36
36
|
{
|
|
37
37
|
"number": 5,
|
|
38
38
|
"title": "flagged but names no node",
|
|
39
|
-
"body": "This one is flagged but carries no Spec: marker and no closing PR.\n\nneeds-
|
|
39
|
+
"body": "This one is flagged but carries no Spec: marker and no closing PR.\n\nneeds-eval\n",
|
|
40
40
|
"url": "https://github.com/acme/repo/issues/5",
|
|
41
41
|
"state": "open",
|
|
42
|
-
"labels": ["needs-
|
|
42
|
+
"labels": ["needs-eval"]
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"number": 6,
|
|
46
46
|
"title": "fixed via an open PR, no marker on the issue",
|
|
47
|
-
"body": "A worked bug; the node comes transitively from the closing PR.\n\nneeds-
|
|
47
|
+
"body": "A worked bug; the node comes transitively from the closing PR.\n\nneeds-eval: ignore-the-trailing-text",
|
|
48
48
|
"url": "https://github.com/acme/repo/issues/6",
|
|
49
49
|
"state": "open",
|
|
50
|
-
"labels": ["needs-
|
|
50
|
+
"labels": ["needs-eval"]
|
|
51
51
|
}
|
|
52
52
|
],
|
|
53
53
|
"prs": [
|
package/spec-forge/src/cli.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { loadSpecs } from '../../spec-cli/src/specs.js'
|
|
|
2
2
|
import type { ForgeDriver, ForgeIssue, ForgePR } from './port.js'
|
|
3
3
|
import { FORGE_DRIVERS, forgeDriverFor, resolveForgeHost } from './drivers.js'
|
|
4
4
|
import { resolveLinks, type NodeLinks } from './links.js'
|
|
5
|
-
import { resolveEvalPending, type NodeEvalPending } from './needs-
|
|
5
|
+
import { resolveEvalPending, type NodeEvalPending } from './needs-eval.js'
|
|
6
6
|
|
|
7
7
|
// tiny flag reader over this command's own arg slice (everything after `forge`), so cli.ts stays routing-only.
|
|
8
8
|
function flag(args: string[], name: string): string | undefined {
|
|
@@ -14,10 +14,12 @@ const has = (args: string[], name: string) => args.includes(`--${name}`)
|
|
|
14
14
|
async function readForge(
|
|
15
15
|
args: string[],
|
|
16
16
|
): Promise<{ driver: ForgeDriver; nodeIds: string[]; issues: ForgeIssue[]; prs: ForgePR[] } | null> {
|
|
17
|
-
|
|
17
|
+
// the forge is a VALUE, never a command ([[cli-surface]]): `--store <host>` names it, mirroring the
|
|
18
|
+
// store axis every other issue verb uses.
|
|
19
|
+
const host = flag(args, 'store') ?? resolveForgeHost()
|
|
18
20
|
const driver = forgeDriverFor(host)
|
|
19
21
|
if (!driver) {
|
|
20
|
-
console.error(`
|
|
22
|
+
console.error(`spex issue links: unknown --store '${host}' (known: ${FORGE_DRIVERS.map((d) => d.host).join(', ')})`)
|
|
21
23
|
return null
|
|
22
24
|
}
|
|
23
25
|
const nodeIds = (await loadSpecs()).map((s) => s.id)
|
|
@@ -49,7 +51,7 @@ async function links(args: string[]): Promise<number> {
|
|
|
49
51
|
|
|
50
52
|
const only = flag(args, 'node')
|
|
51
53
|
if (only) {
|
|
52
|
-
if (!nodeIds.includes(only)) { console.error(`
|
|
54
|
+
if (!nodeIds.includes(only)) { console.error(`spex issue links: no such node '${only}'`); return 1 }
|
|
53
55
|
resolved = resolved.filter((n) => n.node === only)
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -58,7 +60,7 @@ async function links(args: string[]): Promise<number> {
|
|
|
58
60
|
const nPRs = resolved.reduce((a, n) => a + n.prs.length, 0)
|
|
59
61
|
console.log(
|
|
60
62
|
`spec-forge · ${driver.host} · ${resolved.length} linked node(s) · ${nIssues} issue(s), ${nPRs} pr(s)` +
|
|
61
|
-
` ·
|
|
63
|
+
` · traced ${issues.length} issue(s), ${prs.length} pr(s)`,
|
|
62
64
|
)
|
|
63
65
|
if (resolved.length) console.log(render(resolved))
|
|
64
66
|
return 0
|
|
@@ -81,7 +83,7 @@ async function evalPending(args: string[]): Promise<number> {
|
|
|
81
83
|
|
|
82
84
|
const only = flag(args, 'node')
|
|
83
85
|
if (only) {
|
|
84
|
-
if (!nodeIds.includes(only)) { console.error(`
|
|
86
|
+
if (!nodeIds.includes(only)) { console.error(`spex issue links: no such node '${only}'`); return 1 }
|
|
85
87
|
resolved = resolved.filter((n) => n.node === only)
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -89,16 +91,15 @@ async function evalPending(args: string[]): Promise<number> {
|
|
|
89
91
|
const nPending = resolved.reduce((a, n) => a + n.pending.length, 0)
|
|
90
92
|
console.log(
|
|
91
93
|
`spec-forge · ${driver.host} · ${resolved.length} node(s) with eval pending · ${nPending} issue(s)` +
|
|
92
|
-
` ·
|
|
94
|
+
` · traced ${issues.length} issue(s), ${prs.length} pr(s)`,
|
|
93
95
|
)
|
|
94
96
|
if (resolved.length) console.log(renderPending(resolved))
|
|
95
97
|
return 0
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return 2
|
|
100
|
+
// `spex issue links [--pending]` ([[cli-surface]]): the read-only forge→spec trace, folded into the issue
|
|
101
|
+
// drawer (the forge drawer is dissolved — a forge is a value, `--store`). Bare = every linked node's open
|
|
102
|
+
// issues/PRs; --pending narrows to the threads still awaiting an eval reading (the old eval-pending view).
|
|
103
|
+
export async function runIssueLinks(args: string[]): Promise<number> {
|
|
104
|
+
return has(args, 'pending') ? evalPending(args) : links(args)
|
|
104
105
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ForgeIssue, ForgePR } from './port.js'
|
|
2
2
|
import { resolveLinks, type LinkedIssue } from './links.js'
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const NEEDS_EVAL = 'needs-eval'
|
|
5
5
|
|
|
6
|
-
// a bare-marker body line: the name alone, case-insensitive, any indent, optional trailing colon — content after it (`needs-
|
|
7
|
-
const BODY_MARKER = new RegExp(`^\\s*${
|
|
6
|
+
// a bare-marker body line: the name alone, case-insensitive, any indent, optional trailing colon — content after it (`needs-eval: foo`) is NOT a match
|
|
7
|
+
const BODY_MARKER = new RegExp(`^\\s*${NEEDS_EVAL}\\s*:?\\s*$`, 'im')
|
|
8
8
|
|
|
9
|
-
export function
|
|
10
|
-
if (issue.labels.some((l) => l.trim().toLowerCase() ===
|
|
9
|
+
export function isNeedsEval(issue: ForgeIssue): boolean {
|
|
10
|
+
if (issue.labels.some((l) => l.trim().toLowerCase() === NEEDS_EVAL)) return true
|
|
11
11
|
return BODY_MARKER.test(issue.body || '')
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -19,7 +19,7 @@ export function resolveEvalPending(
|
|
|
19
19
|
prs: ForgePR[],
|
|
20
20
|
nodeIds: string[],
|
|
21
21
|
): NodeEvalPending[] {
|
|
22
|
-
const flagged = new Set(issues.filter(
|
|
22
|
+
const flagged = new Set(issues.filter(isNeedsEval).map((i) => i.number))
|
|
23
23
|
if (!flagged.size) return []
|
|
24
24
|
const out: NodeEvalPending[] = []
|
|
25
25
|
for (const { node, issues: linked } of resolveLinks(issues, prs, nodeIds)) {
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { streamSSE } from 'hono/streaming'
|
|
2
|
-
import type { Context } from 'hono'
|
|
3
|
-
import { watch, mkdirSync, type FSWatcher } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { sessionsRoot, gitCommonDir } from './layout.js'
|
|
6
|
-
import { sessionSignature } from './sessions.js'
|
|
7
|
-
import { getBoard, invalidateBoard } from './boardCache.js'
|
|
8
|
-
import { unitize, tagOf, diffUnits, type Units } from './boardDelta.js'
|
|
9
|
-
|
|
10
|
-
// @@@ board-stream — the board's freshness is PUSHED, not polled. A dashboard subscribes here ONCE; in
|
|
11
|
-
// plain mode it gets a bare `board-changed` and refetches /api/board (the legacy protocol, kept verbatim
|
|
12
|
-
// for old clients); in DELTA mode (`?mode=delta`) the server itself rebuilds on change and streams the
|
|
13
|
-
// hash-chained patch ([[board-delta]]): a `board-full {to, board}` on connect, then `board-delta
|
|
14
|
-
// {from, to, set, del}` per change — a few KB against the ~600KB snapshot, with a full-snapshot send
|
|
15
|
-
// whenever the patch wouldn't win (bigger than the board, or the unit decomposition's id-uniqueness
|
|
16
|
-
// precondition failed), so a delta subscriber is NEVER worse off than a full refetch.
|
|
17
|
-
//
|
|
18
|
-
// Event sources, ALL funneled into one debounced pipeline: (1) fs.watch on the per-user session store —
|
|
19
|
-
// every lifecycle transition lands as a sessions/<id>/session.json write; (2) fs.watch on the shared git
|
|
20
|
-
// dir's refs (+ packed-refs/HEAD) — a commit or merge moves a ref, so tree reshapes push instead of
|
|
21
|
-
// waiting out a poll; (3) a subscriber-gated ~2s poll of the CHEAP tmux session signature ([[sessions]])
|
|
22
|
-
// for liveness/activity, which never touch a file; (4) a delta-gated ~15s cold tick that rebuilds and
|
|
23
|
-
// diffs server-side, catching what no watcher sees (uncommitted worktree spec edits, forge issues) — ONE
|
|
24
|
-
// rebuild per tick total, replacing every open dashboard's own 15s full refetch. Plain mode without delta
|
|
25
|
-
// subscribers keeps its zero-build behavior: sources just fan out `board-changed`.
|
|
26
|
-
|
|
27
|
-
type Notify = () => void
|
|
28
|
-
type Frame = { event: string; data: string }
|
|
29
|
-
type DeltaSend = (frame: Frame) => void
|
|
30
|
-
const plainSubs = new Set<Notify>()
|
|
31
|
-
const deltaSubs = new Set<DeltaSend>()
|
|
32
|
-
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
33
|
-
|
|
34
|
-
// ---- the rebuild→diff→broadcast pipeline (runs only while delta subscribers exist) ----
|
|
35
|
-
// last successfully-broadcast snapshot: the delta chain's anchor. `lastFullFrame` is what a fresh
|
|
36
|
-
// subscriber gets instantly; `lastUnits`+`lastTag` are what the next diff chains from. A snapshot that
|
|
37
|
-
// failed the unitize precondition anchors nothing (lastUnits=null) so every following send is a full.
|
|
38
|
-
let lastUnits: Units | null = null
|
|
39
|
-
let lastTag = ''
|
|
40
|
-
let lastFullFrame: Frame | null = null
|
|
41
|
-
let building = false
|
|
42
|
-
let dirty = false
|
|
43
|
-
|
|
44
|
-
async function rebuildAndBroadcast(): Promise<void> {
|
|
45
|
-
if (building) { dirty = true; return }
|
|
46
|
-
building = true
|
|
47
|
-
try {
|
|
48
|
-
do {
|
|
49
|
-
dirty = false
|
|
50
|
-
let board: unknown
|
|
51
|
-
// share the route's single-flight build ([[board-cache]]); fireChanged() already invalidated the
|
|
52
|
-
// cache, so this gets a fresh build (or joins one a concurrent poll already started).
|
|
53
|
-
try { board = await getBoard() } catch { for (const n of [...plainSubs]) { try { n() } catch { /* swept on abort */ } }; continue }
|
|
54
|
-
const boardJson = JSON.stringify(board)
|
|
55
|
-
const { units, ok } = unitize(board as Record<string, unknown>)
|
|
56
|
-
const tag = tagOf(units)
|
|
57
|
-
if (tag === lastTag) continue
|
|
58
|
-
const fullFrame: Frame = { event: 'board-full', data: `{"to":"${tag}","board":${boardJson}}` }
|
|
59
|
-
let frame = fullFrame
|
|
60
|
-
if (lastUnits && ok) {
|
|
61
|
-
const { set, del } = diffUnits(lastUnits, units)
|
|
62
|
-
const deltaData = JSON.stringify({ from: lastTag, to: tag, set, del })
|
|
63
|
-
// guaranteed win: ship the patch only when it actually beats the snapshot
|
|
64
|
-
if (deltaData.length < fullFrame.data.length) frame = { event: 'board-delta', data: deltaData }
|
|
65
|
-
}
|
|
66
|
-
lastUnits = ok ? units : null
|
|
67
|
-
lastTag = tag
|
|
68
|
-
lastFullFrame = fullFrame
|
|
69
|
-
for (const send of [...deltaSubs]) { try { send(frame) } catch { /* swept on abort */ } }
|
|
70
|
-
for (const n of [...plainSubs]) { try { n() } catch { /* swept on abort */ } }
|
|
71
|
-
} while (dirty)
|
|
72
|
-
} finally { building = false }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// a merge/launch/close touches several record files at once; collapse the burst into ONE signal. With
|
|
76
|
-
// delta subscribers the debounced fire rebuilds and broadcasts (plain subs then ride the same tag-moved
|
|
77
|
-
// gate — no spurious refetches); without them it stays the zero-build legacy notify.
|
|
78
|
-
function fireChanged(): void {
|
|
79
|
-
// invalidate the route's board cache ([[board-cache]]) on EVERY change signal, before the debounce guard
|
|
80
|
-
// — a plain-mode client that polls /api/board (no delta rebuild here) must still see fresh data on its
|
|
81
|
-
// next poll, and a delta rebuild below re-reads the same now-stale cache.
|
|
82
|
-
invalidateBoard()
|
|
83
|
-
if (debounce) return
|
|
84
|
-
debounce = setTimeout(() => {
|
|
85
|
-
debounce = null
|
|
86
|
-
if (deltaSubs.size) void rebuildAndBroadcast()
|
|
87
|
-
else for (const notify of [...plainSubs]) { try { notify() } catch { /* swept on abort */ } }
|
|
88
|
-
}, 150)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- event source 0: an EXPLICIT server-side nudge ----
|
|
92
|
-
// for a server-side mutation that must show instantly regardless of watcher health: /rename writes the
|
|
93
|
-
// session's global record (`session.json` — [[session-rename]]), which lives INSIDE the watched store, so
|
|
94
|
-
// source 1 normally sees the write too. The explicit route call stays because that fs watch is best-effort
|
|
95
|
-
// (it can fail to attach), and the nudge makes the sub-second rename guarantee deterministic. Same
|
|
96
|
-
// debounced funnel as every other source.
|
|
97
|
-
export const notifyBoardChanged = (): void => fireChanged()
|
|
98
|
-
|
|
99
|
-
// ---- event source 1: the session store (lifecycle status writes) ----
|
|
100
|
-
let watcher: FSWatcher | null = null
|
|
101
|
-
function ensureWatcher(): void {
|
|
102
|
-
if (watcher) return
|
|
103
|
-
const root = sessionsRoot()
|
|
104
|
-
try { mkdirSync(root, { recursive: true }) } catch { /* best-effort; the watch below still tries */ }
|
|
105
|
-
try { watcher = watch(root, { recursive: true }, () => fireChanged()) } catch { watcher = null }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---- event source 2: git refs (a commit/merge reshapes the tree the moment the ref moves) ----
|
|
109
|
-
// refs/ recursively for loose refs (heads, worktree branches), plus the common dir itself non-recursively
|
|
110
|
-
// for packed-refs rewrites and HEAD flips. Best-effort like every source: no watch → the cold tick covers.
|
|
111
|
-
let refsWatchers: FSWatcher[] | null = null
|
|
112
|
-
function ensureRefsWatcher(): void {
|
|
113
|
-
if (refsWatchers) return
|
|
114
|
-
refsWatchers = []
|
|
115
|
-
try {
|
|
116
|
-
const common = gitCommonDir()
|
|
117
|
-
try { refsWatchers.push(watch(join(common, 'refs'), { recursive: true }, () => fireChanged())) } catch { /* loose refs unwatched */ }
|
|
118
|
-
try { refsWatchers.push(watch(common, (_e, f) => { if (f === 'packed-refs' || f === 'HEAD') fireChanged() })) } catch { /* packed refs unwatched */ }
|
|
119
|
-
} catch { /* not a repo? the cold tick still covers */ }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ---- event source 3: the tmux-derived signature (liveness + activity — never a file write) ----
|
|
123
|
-
let poller: ReturnType<typeof setInterval> | null = null
|
|
124
|
-
let lastSig = ''
|
|
125
|
-
function ensureLivePoll(): void {
|
|
126
|
-
if (poller) return
|
|
127
|
-
poller = setInterval(() => {
|
|
128
|
-
void sessionSignature().then((sig) => { if (sig !== lastSig) { lastSig = sig; fireChanged() } }).catch(() => {})
|
|
129
|
-
}, 2000)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ---- event source 4: the cold tick — the server-side replacement for every client's slow fallback poll.
|
|
133
|
-
// Rebuild+diff on a relaxed timer so what NO watcher sees (an uncommitted worktree spec edit, a forge
|
|
134
|
-
// issue refresh) still lands; an unchanged tag broadcasts nothing. Delta-gated: plain-only clients keep
|
|
135
|
-
// their own client-side fallback, so without delta subscribers this must not burn builds.
|
|
136
|
-
let coldTick: ReturnType<typeof setInterval> | null = null
|
|
137
|
-
function ensureColdTick(): void {
|
|
138
|
-
if (coldTick) return
|
|
139
|
-
coldTick = setInterval(() => { if (deltaSubs.size) void rebuildAndBroadcast() }, 15000)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function stopSourcesIfIdle(): void {
|
|
143
|
-
if (plainSubs.size + deltaSubs.size > 0) return
|
|
144
|
-
if (poller) { clearInterval(poller); poller = null; lastSig = '' }
|
|
145
|
-
if (coldTick) { clearInterval(coldTick); coldTick = null }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// GET /api/board/stream — one SSE per dashboard tab, server→client only, with a periodic `ping` so an
|
|
149
|
-
// idle proxy never times the connection out. On a backend hot-reload the stream drops and EventSource
|
|
150
|
-
// auto-reconnects to the fresh child; a delta subscriber's reconnect lands a fresh `board-full`, so the
|
|
151
|
-
// chain re-anchors with no client-side repair logic.
|
|
152
|
-
export function boardStream(c: Context) {
|
|
153
|
-
const delta = c.req.query('mode') === 'delta'
|
|
154
|
-
ensureWatcher()
|
|
155
|
-
ensureRefsWatcher()
|
|
156
|
-
return streamSSE(c, async (stream) => {
|
|
157
|
-
let aborted = false
|
|
158
|
-
const send: DeltaSend = (frame) => { void stream.writeSSE(frame).catch(() => {}) }
|
|
159
|
-
const notify: Notify = () => { void stream.writeSSE({ event: 'board-changed', data: 'x' }).catch(() => {}) }
|
|
160
|
-
if (delta) { deltaSubs.add(send); ensureColdTick() } else { plainSubs.add(notify) }
|
|
161
|
-
ensureLivePoll()
|
|
162
|
-
const unsub = (): void => { deltaSubs.delete(send); plainSubs.delete(notify); stopSourcesIfIdle() }
|
|
163
|
-
stream.onAbort(() => { aborted = true; unsub() })
|
|
164
|
-
try {
|
|
165
|
-
await stream.writeSSE({ event: 'ready', data: 'x' })
|
|
166
|
-
if (delta) {
|
|
167
|
-
// seed the chain: the cached anchor snapshot immediately (same tag the next delta chains from),
|
|
168
|
-
// then a fire so a connect during a quiet stretch converges to truly-current within one build.
|
|
169
|
-
if (lastFullFrame) { await stream.writeSSE(lastFullFrame).catch(() => {}) ; fireChanged() }
|
|
170
|
-
else void rebuildAndBroadcast()
|
|
171
|
-
}
|
|
172
|
-
while (!aborted) {
|
|
173
|
-
await stream.sleep(25000)
|
|
174
|
-
if (aborted) break
|
|
175
|
-
await stream.writeSSE({ event: 'ping', data: 'x' })
|
|
176
|
-
}
|
|
177
|
-
} finally { unsub() }
|
|
178
|
-
})
|
|
179
|
-
}
|
|
@@ -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,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: core
|
|
3
|
-
surface: system
|
|
4
|
-
status: active
|
|
5
|
-
hue: 200
|
|
6
|
-
desc: A config plugin — the minimal spec-discipline contract folded into every launched agent.
|
|
7
|
-
code:
|
|
8
|
-
---
|
|
9
|
-
Commit your spec node and the code it justifies BEFORE you declare done or propose merge — the commit comes first, never as an afterthought to a declaration.
|
|
10
|
-
|
|
11
|
-
A spec body is a living current-state document: it states the node's PRESENT intent and is rewritten in place. Never accrete a "## vN" changelog heading, and never add current-state or verdict sections — version history is git's job, not the body's.
|
|
12
|
-
|
|
13
|
-
An independently-scoped feature gets its OWN spec node: if you build something separately scoped while working, create a sibling node for it rather than bundling it into your assigned node's commit (cosmetic polish riding along is the smell).
|
|
14
|
-
|
|
15
|
-
Keep the loss signal honest for what you changed — yatsu is the signal the optimizer reads, so a gap is a blind spot. Changed a node that carries a `yatsu.md`? Re-measure it: run its scenario, compare to the expected, and file the result with `spex yatsu eval <node>`. Made an obvious frontend change to a node with NO `yatsu.md`? Give it one — a scenario (description + expected) — so its loss can be measured. A frontend scenario is measured through the **actual running product** — drive a real browser, read the real DOM and capture a screenshot (or video), never reason about the code — and that real observation is filed as the reading, not left as an ad-hoc check you ran but never recorded. `spex yatsu scan --changed` shows the gaps in exactly the nodes you touched.
|
|
16
|
-
|
|
17
|
-
Don't reverse-engineer the file formats: `spex guide spec` and `spex guide yatsu` print the full spec.md and yatsu.md schema on demand. This prompt is the clue; that manual carries the detail. The CLI explains itself the same way: `spex help` is the command map (grouped by the loop each verb serves), `spex help <command>` one command's usage — when unsure of a verb, ask the tool, don't guess.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: .config
|
|
3
|
-
status: active
|
|
4
|
-
hue: 110
|
|
5
|
-
desc: The instance home — this repo's DIY dev-flow plugins live here as skill-shaped config nodes.
|
|
6
|
-
---
|
|
7
|
-
`.config/` is the **instance** of the config system: the concrete dev-flow plugins this repo ships for
|
|
8
|
-
working in it. Each plugin is a skill-shaped node — its folder *is* the unit (a `spec.md` plus any
|
|
9
|
-
co-located scripts) — living as a flat child of `.config/` and carrying a `surface: command|system` field
|
|
10
|
-
that names where it plugs in.
|
|
11
|
-
|
|
12
|
-
The launcher's system gather and the new-session dropdown read from here. Only **active** plugins
|
|
13
|
-
gather: a `pending` node is declared intent, not yet an active plugin. The seed ships `core`
|
|
14
|
-
(`surface: system` — the spec-discipline contract folded into every agent) plus command presets like
|
|
15
|
-
`tidy`; add your own by creating a sibling node with a `surface` field.
|
|
@@ -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 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 watch`, review proposals with `spex review <id>`, merge good ones with `git -C <root> merge --no-ff <branch>`, then close. Never let a worker self-merge; keep `spex lint` at 0 errors. To WAIT on a worker, POLL one-shot (`spex review <id>` or `spex ls` — both return immediately); never block on `spex 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-BhIslAau.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-LcCzBMU7.js";import{F as me}from"./FoldToggle-BuQ0lokE.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/yatsu/blob/${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/yatsu/blob/${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};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/spec-cli/templates/spec/project/{.config → .plugins/prompts}/memory-hygiene/spec.md
RENAMED
|
File without changes
|