spexcode 0.2.8 → 0.3.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/attach.ts +2 -2
- package/spec-cli/src/cli.ts +695 -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 +2 -2
- 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 +123 -96
- package/spec-cli/src/harness-select.ts +2 -2
- package/spec-cli/src/harness.ts +30 -14
- package/spec-cli/src/help.ts +289 -384
- package/spec-cli/src/hooks.ts +1 -1
- package/spec-cli/src/index.ts +130 -103
- package/spec-cli/src/init.ts +9 -9
- package/spec-cli/src/issues.ts +89 -33
- package/spec-cli/src/layout.ts +5 -5
- package/spec-cli/src/lint.ts +73 -8
- package/spec-cli/src/localIssues.ts +42 -60
- package/spec-cli/src/materialize.ts +1 -1
- package/spec-cli/src/mentions.ts +15 -15
- package/spec-cli/src/migrate-table.ts +397 -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 +10 -10
- package/spec-cli/src/search.ts +1 -1
- package/spec-cli/src/sessions.ts +244 -138
- package/spec-cli/src/specs.ts +25 -15
- 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 +4 -4
- package/spec-cli/templates/spec/project/{.config → .plugins}/forge-link/spec.md +1 -1
- package/spec-cli/templates/spec/project/{.config → .plugins}/regroup/spec.md +2 -2
- package/spec-cli/templates/spec/project/{.config → .plugins}/reproduce-before-fix/spec.md +3 -3
- package/spec-cli/templates/spec/project/{.config → .plugins}/spec.md +4 -4
- 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-C7Bzsv86.js} +9 -9
- package/spec-dashboard/dist/assets/EvalsPage-DKZZIdHq.js +2 -0
- package/spec-dashboard/dist/assets/{FoldToggle-BuQ0lokE.js → FoldToggle-D5iB4Ac2.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-H-D8aHEl.js → IssuesPage-CMFTsQhg.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-oZXIeCPb.js → MobileApp-DwuTKgdP.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-Blr_MEdU.js → SessionInterface-CBS5_cmK.js} +3 -3
- package/spec-dashboard/dist/assets/{SessionWindow-LcCzBMU7.js → SessionWindow-CqAnjWfI.js} +2 -2
- package/spec-dashboard/dist/assets/{Settings-_yOye-In.js → Settings-BW5f0OaW.js} +1 -1
- package/spec-dashboard/dist/assets/{index-uGs9v_9o.css → index-Cc26X4ce.css} +1 -1
- package/spec-dashboard/dist/assets/{index-BhIslAau.js → index-Ce0wDyQS.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 +164 -95
- package/{spec-yatsu → spec-eval}/src/evaltab.ts +24 -24
- package/{spec-yatsu → spec-eval}/src/filing.ts +7 -5
- package/{spec-yatsu → spec-eval}/src/freshness.ts +44 -22
- 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 +7 -1
- 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/spec/project/.config/core/spec.md +0 -17
- 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/presets/careful/{.config → .plugins}/clarify-before-code/spec.md +0 -0
- /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}/memory-hygiene/spec.md +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// never written into it. One thread = one PLAIN markdown file at <main>/.spec/.issues/<id>.md; there is
|
|
4
4
|
// deliberately no content-kind taxonomy (a change suggestion, an annotation, a Q&A are the same mechanism —
|
|
5
5
|
// the prose says what it is). Others reply/discuss like an async chatroom; a supervisor drains it via
|
|
6
|
-
// `spex
|
|
6
|
+
// `spex issue ls` (reading is the port's job, issues.ts — this module owns only the store + its write verbs).
|
|
7
7
|
// Because a thread file is NOT named spec.md, the spec walk never nodes it and isSpecMd ignores it —
|
|
8
8
|
// invisible to lint / drift / deriveStatus / board with ZERO exemption. The store lives on the TRUNK, not
|
|
9
9
|
// per-branch: reads and writes target the main checkout and commit STRAIGHT to it (--no-verify, provably
|
|
@@ -28,22 +28,12 @@ const LEGACY_STORE_REL = '.spec/.forum'
|
|
|
28
28
|
// truth is `spexcode.json`'s `issues.enabled` (the same settings file that carries every other toggle),
|
|
29
29
|
// read via readConfig so a machine-local `spexcode.local.json` can override it. OFF silences the post-merge
|
|
30
30
|
// nudge (and, in the dashboard, hides the issues view); the raw write verbs stay usable, since running one
|
|
31
|
-
// is explicit consent.
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
export const issuesEnabled = (): boolean =>
|
|
36
|
-
|
|
37
|
-
return cfg.issues?.enabled ?? (cfg as { proposals?: { enabled?: boolean } }).proposals?.enabled ?? true
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function setEnabled(on: boolean): void {
|
|
41
|
-
const f = join(mainCheckout(), 'spexcode.json')
|
|
42
|
-
const cfg = existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : {}
|
|
43
|
-
cfg.issues = { ...(cfg.issues || {}), enabled: on }
|
|
44
|
-
delete cfg.proposals // the pre-rename key, superseded by `issues` on this write
|
|
45
|
-
writeFileSync(f, JSON.stringify(cfg, null, 2) + '\n')
|
|
46
|
-
}
|
|
31
|
+
// is explicit consent. There is deliberately NO CLI toggle verb (v0.3.0 — "no `spex config set`" is this
|
|
32
|
+
// project's standing rule): flip the key by editing the JSON, effective immediately because readConfig
|
|
33
|
+
// reads the working tree. A legacy pre-rename `proposals.enabled` key is NOT read (no fallback — fail
|
|
34
|
+
// toward the default); `spex doctor` reports it so an old settings file gets repaired, not silently obeyed.
|
|
35
|
+
export const issuesEnabled = (): boolean =>
|
|
36
|
+
readConfig(mainCheckout()).issues?.enabled ?? true
|
|
47
37
|
|
|
48
38
|
const list = (v: string | undefined): string[] =>
|
|
49
39
|
v ? v.split(',').map((s) => s.trim()).filter(Boolean) : []
|
|
@@ -79,7 +69,7 @@ function requirePrimaryStore(action: string): void {
|
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
// the local issue store dir — a fixed path directly under the trunk's .spec (name-independent, unlike the
|
|
82
|
-
// .
|
|
72
|
+
// .plugins system which nests under the named root node), OR the disposable override. Every read and write goes here.
|
|
83
73
|
const localStoreDir = (): string => overrideStoreDir() ?? join(mainCheckout(), LOCAL_STORE_REL)
|
|
84
74
|
// the author's signature: the effective governed session id (envSessionId handles the claude/codex split).
|
|
85
75
|
const currentSession = (): string => envSessionId() || 'unknown'
|
|
@@ -183,7 +173,7 @@ export function loadLocalIssues(): Issue[] {
|
|
|
183
173
|
export function loadOne(id: string): Issue {
|
|
184
174
|
ensureStoreMigrated()
|
|
185
175
|
const f = join(localStoreDir(), `${id}.md`)
|
|
186
|
-
if (!existsSync(f)) throw new Error(`no local issue '${id}' (see \`spex
|
|
176
|
+
if (!existsSync(f)) throw new Error(`no local issue '${id}' (see \`spex issue ls --all --store local\`)`)
|
|
187
177
|
return parse(id, readFileSync(f, 'utf8'))
|
|
188
178
|
}
|
|
189
179
|
|
|
@@ -368,13 +358,13 @@ export async function replyLocalIssue(id: string, body: string, author: string,
|
|
|
368
358
|
// every filer is offline/absent, the NODE's governing session, so an unresolved remark still REACHES an agent
|
|
369
359
|
// who can act on it. A broken/absent worktree sidecar falls through silently — one bad worktree never fails
|
|
370
360
|
// the remark write. This is notification only; it resolves nothing (R3: resolve is a deliberate
|
|
371
|
-
// `spex resolve`). Non-eval threads pay nothing (no
|
|
361
|
+
// `spex remark resolve`). Non-eval threads pay nothing (no eval/specs/sessions import).
|
|
372
362
|
const EVAL_CONCERN_RE = /^eval: (.+?) · (.+)$/ // node first (never contains ' · '), then the scenario (may)
|
|
373
363
|
async function threadOriginators(thread: Issue): Promise<(string | null)[]> {
|
|
374
364
|
const m = EVAL_CONCERN_RE.exec(thread.concern)
|
|
375
365
|
if (!m) return [thread.by]
|
|
376
366
|
const node = m[1].trim(), scenario = m[2].trim()
|
|
377
|
-
const { evalReadingFiler } = await import('../../spec-
|
|
367
|
+
const { evalReadingFiler } = await import('../../spec-eval/src/filing.js')
|
|
378
368
|
const chain: (string | null)[] = [evalReadingFiler(node, scenario)]
|
|
379
369
|
try {
|
|
380
370
|
const { listSessions } = await import('./sessions.js')
|
|
@@ -453,7 +443,7 @@ function resolveRemarkHost(host: { issue?: string; node?: string; scenario?: str
|
|
|
453
443
|
return loadOne(host.issue).id // throws loudly if the issue doesn't exist
|
|
454
444
|
}
|
|
455
445
|
|
|
456
|
-
// author a remark on a host — the ONE write both the CLI (`spex remark`) and the server call. Stamps the
|
|
446
|
+
// author a remark on a host — the ONE write both the CLI (`spex remark add`) and the server call. Stamps the
|
|
457
447
|
// codeSha it was authored against (the worktree HEAD by default — R2). Returns the `<thread-id>#<rid>` ref.
|
|
458
448
|
export async function remarkOnHost(
|
|
459
449
|
host: { issue?: string; node?: string; scenario?: string },
|
|
@@ -471,7 +461,7 @@ export async function remarkOnHost(
|
|
|
471
461
|
// a remark ref is `<thread-id>#<rid>`; the thread id (a store slug) never contains '#', so split on the last.
|
|
472
462
|
function parseRemarkRef(ref: string): { id: string; rid: string } {
|
|
473
463
|
const i = ref.lastIndexOf('#')
|
|
474
|
-
if (i <= 0 || i === ref.length - 1) throw new Error(`bad remark ref '${ref}' — expected <thread-id>#<rid> (the id \`spex remark\` printed)`)
|
|
464
|
+
if (i <= 0 || i === ref.length - 1) throw new Error(`bad remark ref '${ref}' — expected <thread-id>#<rid> (the id \`spex remark add\` printed)`)
|
|
475
465
|
return { id: ref.slice(0, i), rid: ref.slice(i + 1) }
|
|
476
466
|
}
|
|
477
467
|
|
|
@@ -525,15 +515,15 @@ export function nudge(node: string): string {
|
|
|
525
515
|
'',
|
|
526
516
|
'1. CLOSE what you finished. An issue whose work just landed is closed, not',
|
|
527
517
|
' left open — the open set is the OUTSTANDING work, so a stale open reads as a',
|
|
528
|
-
' lie: spex
|
|
518
|
+
' lie: spex issue ls --store local then spex issue close <id>',
|
|
529
519
|
'',
|
|
530
520
|
'2. RECORD only what OUTLIVES this task — a concern you are NOT acting on now:',
|
|
531
521
|
' an off-mainline smell / awkward boundary / wish, or a trivial-but-must-not-',
|
|
532
522
|
" forget to-do that doesn't earn a spec node. NOT a bug tracker (that is the",
|
|
533
523
|
' spec graph + the forge), NOT your assigned task or a fix you are about to',
|
|
534
524
|
' make — those need no issue. Only the taste that would otherwise evaporate:',
|
|
535
|
-
' spex
|
|
536
|
-
' spex
|
|
525
|
+
' spex issue ls # read first — reply if already raised',
|
|
526
|
+
' spex issue open "<concern>" [--node <id>] # else open one',
|
|
537
527
|
'A supervisor drains the store later. (Advisory — skip if nothing is owed.)',
|
|
538
528
|
'───────────────────────────────────────────────────────────────────',
|
|
539
529
|
].join('\n')
|
|
@@ -553,7 +543,7 @@ export function closeoutNudge(sessionId: string | null | undefined): string {
|
|
|
553
543
|
t.status === 'open' && !EVAL_CONCERN_RE.test(t.concern) &&
|
|
554
544
|
(t.by === sessionId || t.replies.some((r) => r.by === sessionId)))
|
|
555
545
|
if (!mine.length) return ''
|
|
556
|
-
return `\n\nIssue closeout — ${mine.length} still-open local issue(s) you touched (opened or replied): ${mine.map((t) => t.id).join(', ')}. For each, close it now if its work is finished (\`spex
|
|
546
|
+
return `\n\nIssue closeout — ${mine.length} still-open local issue(s) you touched (opened or replied): ${mine.map((t) => t.id).join(', ')}. For each, close it now if its work is finished (\`spex issue close <id>\`), or reply why it should stay open past this session (\`spex issue reply <id> --body "<why>"\`). Some issues rightly outlive their session — this is a reminder to sweep, not a gate.`
|
|
557
547
|
}
|
|
558
548
|
|
|
559
549
|
// ───────────────────────── CLI ─────────────────────────
|
|
@@ -582,24 +572,16 @@ function readBody(args: string[]): string | undefined {
|
|
|
582
572
|
const repeated = (args: string[], name: string): string[] =>
|
|
583
573
|
args.flatMap((a, i) => (a === `--${name}` ? [args[i + 1]] : [])).filter(Boolean) as string[]
|
|
584
574
|
|
|
585
|
-
// the local-issue WRITE verbs
|
|
586
|
-
//
|
|
587
|
-
// [--evidence hash…] [--body -|text], the id-based reply, and the feature toggle
|
|
588
|
-
// on | off | status. `nudge` is internal (the post-merge hook's caller). Store is a property of the issue,
|
|
575
|
+
// the local-issue WRITE verbs of the issue drawer (`spex issue <verb>`): open "<concern>" [--store local|<host>] [--node id…]
|
|
576
|
+
// [--evidence hash…] [--body -|text], and the id-based reply. Store is a property of the issue,
|
|
589
577
|
// never a second command — open and reply route by it (issues.ts createIssue/replyIssue).
|
|
590
578
|
export async function runIssueWrite(args: string[]): Promise<number> {
|
|
591
579
|
const sub = args[0]
|
|
592
580
|
try {
|
|
593
|
-
if (sub === 'on' || sub === 'off') {
|
|
594
|
-
setEnabled(sub === 'on')
|
|
595
|
-
console.log(`issues workflow ${sub.toUpperCase()} — spexcode.json issues.enabled = ${sub === 'on'}${sub === 'on' ? '' : ' (post-merge nudge silenced; dashboard issues view hidden)'}`)
|
|
596
|
-
return 0
|
|
597
|
-
}
|
|
598
|
-
if (sub === 'status') { console.log(`issues workflow is ${issuesEnabled() ? 'ON' : 'OFF'}`); return 0 }
|
|
599
581
|
if (sub === 'reply') {
|
|
600
582
|
const id = bare(args.slice(1))[0]
|
|
601
583
|
const body = readBody(args)
|
|
602
|
-
if (!id || !body) { console.error('usage: spex
|
|
584
|
+
if (!id || !body) { console.error('usage: spex issue reply <issue-id> --body -|<text> [--evidence <hash>…]'); return 2 }
|
|
603
585
|
// the ONE store-routed reply verb ([[issues]]): a forge id posts a real comment through the driver,
|
|
604
586
|
// a local id commits to the store — the same command either way (dynamic import: no static cycle).
|
|
605
587
|
const r = await (await import('./issues.js')).replyIssue(id, body, { evidence: repeated(args, 'evidence') })
|
|
@@ -610,19 +592,13 @@ export async function runIssueWrite(args: string[]): Promise<number> {
|
|
|
610
592
|
if (s) console.log(` ${s}`)
|
|
611
593
|
return 0
|
|
612
594
|
}
|
|
613
|
-
if (sub === 'nudge') {
|
|
614
|
-
// internal: the post-merge hook calls this to print the (toggle-aware) nudge for a merged node.
|
|
615
|
-
const text = nudge(bare(args.slice(1))[0] || '')
|
|
616
|
-
if (text) console.log(text)
|
|
617
|
-
return 0
|
|
618
|
-
}
|
|
619
595
|
// `open`: start a new issue — STORE-ROUTED through the one creation port ([[issues]] createIssue, the
|
|
620
596
|
// same routine POST /api/issues runs): default local commits to the trunk store; `--store <host>`
|
|
621
597
|
// creates the real forge issue through that store's driver (no promote round-trip when the concern is
|
|
622
598
|
// born forge-visible). The concern is the bare positional(s) after the sub.
|
|
623
599
|
const concern = sub === 'open' ? bare(args.slice(1)).join(' ').trim() : ''
|
|
624
600
|
if (!concern) {
|
|
625
|
-
console.error('usage: spex
|
|
601
|
+
console.error('usage: spex issue open "<concern>" [--store local|<host>] [--node <id>…] [--evidence <hash>…] [--body -|<text>]\n spex issue reply|close|promote <issue-id> …')
|
|
626
602
|
return 2
|
|
627
603
|
}
|
|
628
604
|
const r = await (await import('./issues.js')).createIssue(concern, {
|
|
@@ -633,23 +609,25 @@ export async function runIssueWrite(args: string[]): Promise<number> {
|
|
|
633
609
|
})
|
|
634
610
|
const re = r.nodes.length ? ` (re: ${r.nodes.join(', ')})` : ''
|
|
635
611
|
console.log(r.store === 'local'
|
|
636
|
-
? `opened '${r.id}'${re} — committed to the local issue store; read it with \`spex
|
|
612
|
+
? `opened '${r.id}'${re} — committed to the local issue store; read it with \`spex issue ls\``
|
|
637
613
|
: `opened '${r.id}' on ${r.store}${re} — ${r.url}`)
|
|
638
614
|
const s = summarize(r.outcomes)
|
|
639
615
|
if (s) console.log(` ${s}`)
|
|
640
616
|
return 0
|
|
641
617
|
} catch (e) {
|
|
642
|
-
console.error(`spex
|
|
618
|
+
console.error(`spex issue: ${e instanceof Error ? e.message : e}`)
|
|
643
619
|
return 1
|
|
644
620
|
}
|
|
645
621
|
}
|
|
646
622
|
|
|
647
|
-
// the first positionals runIssueWrite handles — the
|
|
648
|
-
//
|
|
649
|
-
|
|
623
|
+
// the first positionals runIssueWrite handles — the issue drawer routes these to it. Exported so the
|
|
624
|
+
// router and the runner can never drift. (`nudge` is not here: it is machine plumbing, called only by the
|
|
625
|
+
// post-merge hook as `spex internal nudge`; the on|off|status toggle verbs died in v0.3.0 — the switch is
|
|
626
|
+
// the `issues.enabled` settings key.)
|
|
627
|
+
export const ISSUE_WRITE_SUBS = new Set(['open', 'reply'])
|
|
650
628
|
|
|
651
629
|
// ── remark CLI ([[remark-substrate]]) — CLI-first: the whole author→resolve→retract loop, no server needed ──
|
|
652
|
-
// `spex remark <
|
|
630
|
+
// `spex remark add <issue-id | <node> --scenario <name>> --body -|<text> [--code-sha <sha>] [--evidence <hash>…]`
|
|
653
631
|
// host = a local issue id, OR a <node> with --scenario <name>. Records targetCodeSha (default: worktree HEAD).
|
|
654
632
|
export async function runRemark(args: string[]): Promise<number> {
|
|
655
633
|
try {
|
|
@@ -657,40 +635,44 @@ export async function runRemark(args: string[]): Promise<number> {
|
|
|
657
635
|
const positional = bare(args)[0]
|
|
658
636
|
const body = readBody(args)
|
|
659
637
|
if (!positional || !body) {
|
|
660
|
-
console.error('usage: spex remark <issue-id | node --scenario name> --body -|<text> [--code-sha <sha>] [--evidence <hash>…]')
|
|
638
|
+
console.error('usage: spex remark add <issue-id | node --scenario name> --body -|<text> [--code-sha <sha>] [--evidence <hash>…]')
|
|
661
639
|
return 2
|
|
662
640
|
}
|
|
641
|
+
// THE FLAG DECIDES THE PARSE ([[cli-surface]] §1): `--scenario` present ⇒ the positional is a NODE id
|
|
642
|
+
// (the remark pins to that node's scenario track); absent ⇒ it is an ISSUE id. Never type-sniffed —
|
|
643
|
+
// a node id and an issue id are both bare slugs, so any "looks like" guess would misroute; the flag
|
|
644
|
+
// is the one unambiguous discriminator, and a wrong host fails loud downstream (unknown issue/node).
|
|
663
645
|
const host = scenario ? { node: positional, scenario } : { issue: positional }
|
|
664
646
|
const r = await remarkOnHost(host, body, { codeSha: fl(args, 'code-sha'), evidence: repeated(args, 'evidence') })
|
|
665
|
-
console.log(`remark ${r.ref} (against ${r.codeSha.slice(0, 7) || 'HEAD'}) — read it with \`spex
|
|
647
|
+
console.log(`remark ${r.ref} (against ${r.codeSha.slice(0, 7) || 'HEAD'}) — read it with \`spex issue ls --all\``)
|
|
666
648
|
const s = summarize(r.outcomes, r.loopIn)
|
|
667
649
|
if (s) console.log(` ${s}`)
|
|
668
650
|
return 0
|
|
669
651
|
} catch (e) {
|
|
670
|
-
console.error(`spex remark: ${e instanceof Error ? e.message : e}`)
|
|
652
|
+
console.error(`spex remark add: ${e instanceof Error ? e.message : e}`)
|
|
671
653
|
return 1
|
|
672
654
|
}
|
|
673
655
|
}
|
|
674
656
|
|
|
675
|
-
// `spex resolve <remark-ref>` — flip resolved=true (agent-only, never the author, monotonic — see resolveRemark).
|
|
657
|
+
// `spex remark resolve <remark-ref>` — flip resolved=true (agent-only, never the author, monotonic — see resolveRemark).
|
|
676
658
|
export async function runResolve(args: string[]): Promise<number> {
|
|
677
659
|
const ref = bare(args)[0]
|
|
678
|
-
if (!ref) { console.error('usage: spex resolve <remark-ref> (the <thread-id>#<rid> `spex remark` printed)'); return 2 }
|
|
660
|
+
if (!ref) { console.error('usage: spex remark resolve <remark-ref> (the <thread-id>#<rid> `spex remark add` printed)'); return 2 }
|
|
679
661
|
try {
|
|
680
662
|
const by = currentSession()
|
|
681
663
|
resolveRemark(ref, by)
|
|
682
664
|
console.log(`resolved remark ${ref} — by ${by}`)
|
|
683
665
|
return 0
|
|
684
|
-
} catch (e) { console.error(`spex resolve: ${e instanceof Error ? e.message : e}`); return 1 }
|
|
666
|
+
} catch (e) { console.error(`spex remark resolve: ${e instanceof Error ? e.message : e}`); return 1 }
|
|
685
667
|
}
|
|
686
668
|
|
|
687
|
-
// `spex retract <remark-ref>` — the author withdraws their OWN remark, removing it (author-only — see retractRemark).
|
|
669
|
+
// `spex remark retract <remark-ref>` — the author withdraws their OWN remark, removing it (author-only — see retractRemark).
|
|
688
670
|
export async function runRetract(args: string[]): Promise<number> {
|
|
689
671
|
const ref = bare(args)[0]
|
|
690
|
-
if (!ref) { console.error('usage: spex retract <remark-ref>'); return 2 }
|
|
672
|
+
if (!ref) { console.error('usage: spex remark retract <remark-ref>'); return 2 }
|
|
691
673
|
try {
|
|
692
674
|
retractRemark(ref, currentSession())
|
|
693
675
|
console.log(`retracted remark ${ref}`)
|
|
694
676
|
return 0
|
|
695
|
-
} catch (e) { console.error(`spex retract: ${e instanceof Error ? e.message : e}`); return 1 }
|
|
677
|
+
} catch (e) { console.error(`spex remark retract: ${e instanceof Error ? e.message : e}`); return 1 }
|
|
696
678
|
}
|
|
@@ -39,7 +39,7 @@ const DISPATCH = join(PKG, 'hooks', 'dispatch.sh')
|
|
|
39
39
|
const SPEX = join(PKG, 'bin', 'spex.mjs')
|
|
40
40
|
// the manifest + content-hash marker + plugin-folder ledger land in the materialized TREE's own slot of the
|
|
41
41
|
// GLOBAL per-project store (layout.treeSlotDir — trees/<enc-worktree>), NOT the worktree and NOT one shared
|
|
42
|
-
// per-project file: each is a pure function of ONE tree's .
|
|
42
|
+
// per-project file: each is a pure function of ONE tree's .plugins, and the old single slot let the last-
|
|
43
43
|
// materialized tree's hook set reach every other tree's dispatch ([[hook-dispatch]]). The worktree keeps
|
|
44
44
|
// zero SpexCode-materialized runtime; only the harness-discovered contract files + shims (which the harness
|
|
45
45
|
// must find in-tree) are written under proj below.
|
package/spec-cli/src/mentions.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// node) and `@session` (an ACTOR — a live session, or `@new` for a fresh worker). The same parser resolves
|
|
3
3
|
// them in ANY input box; the resolve+dispatch live HERE (CLI-first) so the issues page, the composer, and an agent's
|
|
4
4
|
// own prompt share one implementation. An `@` "just auto-sends a prompt": resolve it against the live board
|
|
5
|
-
// sessions and dispatch via [[dispatch]]'s
|
|
5
|
+
// sessions and dispatch via [[dispatch]]'s sendText / [[launch]]'s newSession — storage and delivery stay
|
|
6
6
|
// separate, and sessions.ts is imported LAZILY so a mention-free post pays nothing.
|
|
7
7
|
|
|
8
8
|
// ── parse (pure) ──────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -17,8 +17,8 @@ const uniq = (xs: string[]): string[] => [...new Set(xs)]
|
|
|
17
17
|
|
|
18
18
|
// ── CLI sigil tolerance ───────────────────────────────────────────────────────────────────────────────
|
|
19
19
|
// In FREE TEXT the sigils are required — they are what marks a reference apart from prose. In a CLI
|
|
20
|
-
// ARGUMENT the whole token IS the reference, so the sigil is optional: `spex review @graph` ≡
|
|
21
|
-
// `spex review graph`, `spex
|
|
20
|
+
// ARGUMENT the whole token IS the reference, so the sigil is optional: `spex session review @graph` ≡
|
|
21
|
+
// `spex session review graph`, `spex eval add [[cli-surface]]` ≡ `spex eval add cli-surface`. One shared strip,
|
|
22
22
|
// applied by the session-selector matcher and every node-arg read site, so the habit a user learns in the
|
|
23
23
|
// dashboard's input boxes works verbatim on the CLI — never a second grammar to learn.
|
|
24
24
|
export function stripRefSigil(token: string): string {
|
|
@@ -78,8 +78,8 @@ export type DispatchOutcome = { token: string; result: 'sent' | 'spawned' | 'off
|
|
|
78
78
|
function mentionPrompt(threadId: string, node: string | null, author: string, text: string): string {
|
|
79
79
|
const re = node ? ` (re: ${node})` : ''
|
|
80
80
|
return `You were @-mentioned in issue thread "${threadId}"${re} by ${author}:\n\n ${text.trim()}\n\n` +
|
|
81
|
-
`Read the thread and act as the comment asks (often just a look): \`spex
|
|
82
|
-
`reply with \`spex
|
|
81
|
+
`Read the thread and act as the comment asks (often just a look): \`spex issue ls --all\` lists them; ` +
|
|
82
|
+
`reply with \`spex issue reply ${threadId} --body -\`.`
|
|
83
83
|
}
|
|
84
84
|
// A non-open thread is settled work: a fresh worker spawned onto it must not re-implement what already
|
|
85
85
|
// landed, so the prompt leads with the status and a verify-on-main-first instruction.
|
|
@@ -92,7 +92,7 @@ export function newWorkerPrompt(threadId: string, node: string | null, author: s
|
|
|
92
92
|
: ''
|
|
93
93
|
return `Issue thread "${threadId}"${on} @-mentioned @new (by ${author}) for a fresh look:\n\n ${text.trim()}\n\n` +
|
|
94
94
|
settled +
|
|
95
|
-
`Read the thread (\`spex
|
|
95
|
+
`Read the thread (\`spex issue ls --all\`, find ${threadId}) and act on it${node ? `; the relevant node is ${node}` : ''}.`
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Parse a committed issue post's text for `@` actors and deliver to each. Best-effort and LOUD: the thread is
|
|
@@ -104,7 +104,7 @@ export async function dispatchMentions(
|
|
|
104
104
|
): Promise<DispatchOutcome[]> {
|
|
105
105
|
const { actors } = parseMentions(text)
|
|
106
106
|
if (!actors.length) return []
|
|
107
|
-
const {
|
|
107
|
+
const { sendText, listSessions, newSession } = await import('./sessions.js')
|
|
108
108
|
const sessions = await listSessions()
|
|
109
109
|
const resolved = resolveActors(actors, sessions as unknown as ActorSession[])
|
|
110
110
|
const out: DispatchOutcome[] = []
|
|
@@ -120,7 +120,7 @@ export async function dispatchMentions(
|
|
|
120
120
|
} catch (e) { out.push({ token: r.token, result: 'failed', detail: e instanceof Error ? e.message : String(e) }) }
|
|
121
121
|
continue
|
|
122
122
|
}
|
|
123
|
-
const res = await
|
|
123
|
+
const res = await sendText(r.session.id, mentionPrompt(ctx.threadId, ctx.node, ctx.author, text), 'issues')
|
|
124
124
|
out.push(res.ok ? { token: r.token, result: 'sent', detail: r.session.id }
|
|
125
125
|
: { token: r.token, result: 'offline', detail: res.error })
|
|
126
126
|
}
|
|
@@ -131,9 +131,9 @@ export async function dispatchMentions(
|
|
|
131
131
|
// A committed reply is ALSO auto-delivered as a COURTESY — never an assignment — to a FALLBACK CHAIN of
|
|
132
132
|
// candidates, in order, stopping at the FIRST one that can be reached: for a remark this is the reading's
|
|
133
133
|
// filer session, then the node's governing session, then nobody (it still surfaces on the board via the
|
|
134
|
-
// teeth). This is a NOTIFICATION chain only — it resolves NOTHING (resolve stays a deliberate `spex resolve`,
|
|
134
|
+
// teeth). This is a NOTIFICATION chain only — it resolves NOTHING (resolve stays a deliberate `spex remark resolve`,
|
|
135
135
|
// R3); it just reaches an agent who can act. It is the same delivery pipe as dispatchMentions (one
|
|
136
|
-
// online-resolution + one
|
|
136
|
+
// online-resolution + one sendText), with the same cuts that keep courtesy ≠ assignment: deliver ONLY to an
|
|
137
137
|
// ONLINE session (an unreachable link is skipped for the next, NEVER spawns a worker, NEVER drains — only an
|
|
138
138
|
// explicit @new spawns); SKIP a candidate that is the replier (no self-notify); a candidate already reached by
|
|
139
139
|
// an explicit @-target of this same text counts as delivered, so the chain STOPS (no double-delivery, no
|
|
@@ -146,14 +146,14 @@ function originatorPrompt(threadId: string, node: string | null, replier: string
|
|
|
146
146
|
const re = node ? ` (re: ${node})` : ''
|
|
147
147
|
return `A new reply landed on a thread you originated — "${threadId}"${re}, from ${replier}:\n\n ${text.trim()}\n\n` +
|
|
148
148
|
`This is a courtesy heads-up (you started this thread), not an assignment. Look if it concerns you; ` +
|
|
149
|
-
`\`spex
|
|
149
|
+
`\`spex issue ls --all\` lists them, reply with \`spex issue reply ${threadId} --body -\`.`
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// The pure fallback decision (testable without sessions.ts): walk the ordered chain (nulls/dupes/the-replier
|
|
153
153
|
// pruned) and return the FIRST link that resolves to an online session — that is who the courtesy goes to. A
|
|
154
154
|
// link already reached by an explicit @-target of this same text short-circuits to `reached` (stop, no
|
|
155
155
|
// double-delivery — the actor already has it); an offline/absent link falls through to the next. `none` means
|
|
156
|
-
// the chain ran dry (nobody online). This is the whole fallback logic; delivery is a thin
|
|
156
|
+
// the chain ran dry (nobody online). This is the whole fallback logic; delivery is a thin sendText around it.
|
|
157
157
|
export type LoopInPick =
|
|
158
158
|
| { kind: 'deliver'; originator: string; session: ActorSession }
|
|
159
159
|
| { kind: 'reached' }
|
|
@@ -177,7 +177,7 @@ export function pickLoopIn(
|
|
|
177
177
|
|
|
178
178
|
// `chain` is the ordered fallback list. We deliver the courtesy to the first online link and STOP; an
|
|
179
179
|
// offline/failed link falls through to the next. NOTIFICATION ONLY — this never touches a `resolved` bit
|
|
180
|
-
// (resolve is a deliberate `spex resolve`), never spawns (only `@new` spawns).
|
|
180
|
+
// (resolve is a deliberate `spex remark resolve`), never spawns (only `@new` spawns).
|
|
181
181
|
export async function notifyOriginator(
|
|
182
182
|
chain: (string | null)[],
|
|
183
183
|
replier: string,
|
|
@@ -186,10 +186,10 @@ export async function notifyOriginator(
|
|
|
186
186
|
): Promise<LoopIn | null> {
|
|
187
187
|
const seen = new Set<string>()
|
|
188
188
|
if (!chain.some((c) => c && c !== replier && !seen.has(c) && (seen.add(c), true))) return null // nothing to do → no session load
|
|
189
|
-
const {
|
|
189
|
+
const { sendText, listSessions } = await import('./sessions.js')
|
|
190
190
|
const pick = pickLoopIn(chain, replier, await listSessions() as unknown as ActorSession[], ctx.alreadyDelivered)
|
|
191
191
|
if (pick.kind !== 'deliver') return null // reached via @ / nobody online → silent
|
|
192
|
-
const res = await
|
|
192
|
+
const res = await sendText(pick.session.id, originatorPrompt(ctx.threadId, ctx.node, replier, text), 'issues')
|
|
193
193
|
return res.ok ? { originator: pick.originator } : null // a failed send behaves like offline: silent
|
|
194
194
|
}
|
|
195
195
|
|