@vllm-sr/dsh 0.4.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/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @vllm-sr/dsh
2
2
 
3
- **System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow. One
4
- key.*
3
+ **System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow.*
5
4
 
6
5
  An agent's LLM thinks slowly, in tokens. System One, a decision model, answers the many small questions around it in
7
6
  about half a second each, with calibrated probabilities: is this command dangerous, does this file try to give the
@@ -18,12 +17,14 @@ answers at `https://app.vllm-sr.ai/v1/systemone`, and `vllm-sr/auto` at `https:/
18
17
  ## Quick start
19
18
 
20
19
  ```bash
21
- npm install -g @deepseek-ai/dsh # DeepSeek Harness 0.2.1-rc.5 or a later 0.2 release, Node 24.9 or later
20
+ npm install -g @deepseek-ai/dsh pnpm
22
21
  dsh plugin --profile web add @vllm-sr/dsh
23
22
  dsh web
24
23
  ```
25
24
 
26
- The package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
25
+ The first line installs DeepSeek Harness (0.2.1-rc.5 or a later 0.2 release, on Node 24.9 or later) and pnpm, which
26
+ `dsh plugin` uses to install plugins; without pnpm, `dsh plugin add` stops with "pnpm was not found". On a machine
27
+ with git, the package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
27
28
 
28
29
  On its first start the plugin gets one free System Duo key, with the same small proof of work the site's "Get your
29
30
  API key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
@@ -75,9 +76,43 @@ the work folds, so a finished turn still shows what System One decided. Parallel
75
76
  card, and every decision pops in with a short animation the first time it appears (none with reduced motion). Each
76
77
  element shows the Decision API's own answer: the probabilities, the model and the measured latency.
77
78
 
78
- Every element carries `data-system-one` (`card`, `compact` or `tool`), `data-system-one-reflex` (`triage`, `guard`,
79
- `shield`, `done` or `tool`) and `data-system-one-verdict`, for tests and screen recordings. Set `systemOneUi` to
80
- `compact` for one line per decision, or to `off` to hide them.
79
+ Set `systemOneUi` to `compact` for one line per decision, or to `off` to hide them.
80
+
81
+ ### Selectors
82
+
83
+ For tests and screen recordings, each System One element in Chat is one root marked `data-system-one`, with stable
84
+ attributes that say what it shows:
85
+
86
+ | Attribute | Values |
87
+ | --- | --- |
88
+ | `data-system-one` | `card`, `compact` (one line) or `tool` (the `system_one` row) |
89
+ | `data-system-one-reflex` | `triage`, `guard`, `shield`, `done` or `tool` |
90
+ | `data-system-one-verdict` | triage `brief`; guard `allow`, `ask` or `deny`; shield `clean`, `removed` or `flagged`; done-check `stop` or `continue`; tool `running` or `done`; `error` when System One did not answer |
91
+ | `data-system-one-outcome` | guard, once you answer its approval: `allowed`, `rejected`, `cancelled` or `failed` |
92
+ | `data-system-one-why` | done-check: `complete`, `waiting`, `unfinished` or `unchecked` |
93
+ | `data-system-one-tone` | its colour: `ok` (green), `info` (ember), `warn` (amber), `bad` (red) or `muted` |
94
+ | `data-system-one-count` | how many decisions it holds (a batch: how many items) |
95
+ | `data-system-one-turn`, `data-system-one-step` | where it acted (the step for guard and shield) |
96
+
97
+ Inside a root, `data-system-one-part` marks `badge` (the ember **System One** pill), `latency`, `reflex`, `summary`,
98
+ `model`, `head`, `toggle` (the button of a one-line element), `rows`, `verdict`, `outcome`, `subject`, `facts`,
99
+ `note` and `more`, and in the batch row also `stack`, `legend`, `failed`, `results` and `raw`. Each number is a
100
+ `data-system-one-fact`: `kind`, `effort`, `unclear` and `risky` (triage), `risk`, `leaks` and `on-task` (guard),
101
+ `passages` and `injection` (shield), `complete`, `checked` and `waiting` (done-check). Decision rows carry
102
+ `data-system-one-decision` and `data-system-one-call`, batch questions `data-system-one-question`. For example:
103
+
104
+ | Element | Selector |
105
+ | --- | --- |
106
+ | Triage card | `[data-system-one-reflex="triage"]` |
107
+ | Guard card that asked you (amber) | `[data-system-one-reflex="guard"][data-system-one-verdict="ask"]` |
108
+ | The same card once you rejected the call (red) | `[data-system-one-reflex="guard"][data-system-one-outcome="rejected"]` |
109
+ | Injection removed (red) | `[data-system-one-reflex="shield"][data-system-one-verdict="removed"]` |
110
+ | A routine check (one line) | `[data-system-one="compact"][data-system-one-reflex="guard"]`, and `"shield"` |
111
+ | Done-check | `[data-system-one-reflex="done"]` |
112
+ | Batch row, finished | `[data-system-one="tool"][data-system-one-verdict="done"]` |
113
+ | The ember badge, and its latency | `[data-system-one-part="badge"]`, `[data-system-one-part="latency"]` |
114
+ | "Dangerous", and "You rejected it" | `[data-system-one-fact="risk"]`, `[data-system-one-part="outcome"]` |
115
+ | The reason | `[data-system-one-part="note"]` |
81
116
 
82
117
  ## Your data
83
118
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vllm-sr/dsh",
3
- "version": "0.4.0",
4
- "description": "System Duo for DeepSeek Harness. Thinking, fast and slow. One key: System One decision models guard, shield, triage and check your agent around System Two (vllm-sr/auto), with one free app.vllm-sr.ai key.",
3
+ "version": "0.4.1",
4
+ "description": "System Duo for DeepSeek Harness. Thinking, fast and slow: System One decision models guard, shield, triage and check your agent around System Two (vllm-sr/auto), with one free app.vllm-sr.ai key.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
package/src/index.js CHANGED
@@ -21,7 +21,7 @@ import * as dshSession from '@deepseek-ai/dsh-session'
21
21
  import { createDecisionClient, compactAnswer, yes } from './client.js'
22
22
  import { SERVICES, appendDotenv, ensureKeys, readDotenv } from './keys.js'
23
23
  import { DONE, EFFORT_LEVELS, GUARD, RISK_LEVELS, TRIAGE } from './questions.js'
24
- import { UI_MODES, approvalOutcome, createRecorder, subject } from './records.js'
24
+ import { UI_MODES, approvalOutcome, createRecorder, relativeTo, subject } from './records.js'
25
25
  import { screen } from './shield.js'
26
26
  import { systemOneTool } from './tool.js'
27
27
  import { clip, createTrace } from './trace.js'
@@ -335,12 +335,13 @@ export function apply(ctx, config) {
335
335
  ctx.on('tools/pre-execute', async (exec, next) => {
336
336
  if (!gated.has(exec.name) || !exec.agent) return next()
337
337
  const state = sessionState(exec.agent)
338
+ const workspace = await workspaceOf(exec)
338
339
  const situation = [
339
340
  `The user asked the agent:\n"""${clip(state?.user || '(unknown)', 1500)}"""`,
340
- `Workspace: ${await workspaceOf(exec)}`,
341
+ `Workspace: ${workspace}`,
341
342
  `The agent now wants to run:\n${describeCall(exec)}`,
342
343
  ].join('\n\n')
343
- const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
344
+ const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), workspace, homedir())) }
344
345
  let response
345
346
  try {
346
347
  response = await client.ask({ model: config.model, questions: GUARD, inputs: [{ state: situation }], signal: exec.signal, timeoutMs: config.hookTimeoutMs, retries: config.hookRetries })
@@ -414,7 +415,7 @@ export function apply(ctx, config) {
414
415
  const tail = text.slice(screened.length)
415
416
  const where = exec.name === 'web_fetch' ? `a web page (${exec.arguments?.url})` : exec.name === 'read' ? `the file ${exec.arguments?.file_path}` : `the output of: ${clip(exec.arguments?.command, 200)}`
416
417
  const prefix = `The user asked the agent: "${clip(state?.user || '(unknown)', 300)}"\nThe agent just read this passage from ${where}:`
417
- const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
418
+ const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), await workspaceOf(exec), homedir())) }
418
419
  const started = performance.now()
419
420
  let verdict
420
421
  try {
package/src/records.js CHANGED
@@ -20,6 +20,28 @@ export function subject(text, max = 200) {
20
20
  return redact(clip(text, max))
21
21
  }
22
22
 
23
+ const escaped = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
24
+ // a path starts after a separator and ends at one, so /work does not match inside /work2 or /x/work
25
+ const START = String.raw`(?<=^|[\s'"=:(])`
26
+ const END = String.raw`(?=$|[\s'"/;&|)<>,])`
27
+
28
+ /**
29
+ * A call as the cards show it: paths in the session's working directory relative to it, the home directory as `~`,
30
+ * and no leading `cd` into the working directory itself.
31
+ */
32
+ export function relativeTo(text, workspace, home) {
33
+ let out = String(text ?? '')
34
+ const root = dir => (typeof dir === 'string' && dir.replace(/\/+$/, '').length > 1 ? escaped(dir.replace(/\/+$/, '')) : undefined)
35
+ const work = root(workspace)
36
+ if (work) {
37
+ out = out.replace(new RegExp(`${START}${work}/`, 'g'), '').replace(new RegExp(`${START}${work}${END}`, 'g'), '.')
38
+ out = out.replace(/^cd \.\s*&&\s*/, '')
39
+ }
40
+ const own = root(home)
41
+ if (own) out = out.replace(new RegExp(`${START}${own}${END}`, 'g'), '~')
42
+ return out
43
+ }
44
+
23
45
  /**
24
46
  * @param {object} options
25
47
  * @param {((session: object, type: string, data: object) => unknown) | undefined} options.append - DeepSeek
package/src/tool.js CHANGED
@@ -12,20 +12,38 @@ You write the rubric as questions; System One answers every question for every i
12
12
 
13
13
  Question types:
14
14
  - "choice": pick one option. criteria: an object of option name -> short description (2 or more options).
15
- - "noul": yes or no. The answer is the probability of yes.
15
+ - "noul": yes or no, with no criteria: say in the instructions what counts as yes. The answer is the probability of yes.
16
16
  - "score": an ordered scale. criteria: a list of level descriptions, lowest first (2 to 10). The answer is the expected level (0 = first).
17
17
  - "box": where something is in an image (images only); instructions name the target.
18
18
 
19
19
  Example questions: {"team": {"type": "choice", "instructions": "Which team should handle this ticket?", "criteria": {"billing": "Payments, invoices, refunds", "engineering": "Bugs, errors, outages", "sales": "Plans and pricing"}}, "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["Low", "Normal", "Urgent"]}, "angry": {"type": "noul", "instructions": "Is the customer angry?"}}`
20
20
 
21
- function parseQuestions(value) {
21
+ /** The type a question's criteria imply: a list of levels is a score, named options a choice, anything else yes/no. */
22
+ function impliedType(criteria) {
23
+ if (Array.isArray(criteria)) return 'score'
24
+ if (!criteria || typeof criteria !== 'object') return 'noul'
25
+ return Object.keys(criteria).map(name => name.toLowerCase()).sort().join() === 'no,yes' ? 'noul' : 'choice'
26
+ }
27
+
28
+ export function parseQuestions(value) {
22
29
  const questions = typeof value === 'string' ? JSON.parse(value) : value
23
30
  if (!questions || typeof questions !== 'object' || Array.isArray(questions) || Object.keys(questions).length === 0) {
24
31
  throw new Error('questions must be an object of question id -> {type, instructions, criteria}')
25
32
  }
26
- for (const [id, question] of Object.entries(questions)) {
33
+ for (const [id, asked] of Object.entries(questions)) {
34
+ // System Two often leaves the type out of its first call
35
+ const question = asked && typeof asked === 'object' && asked.type === undefined ? { ...asked, type: impliedType(asked.criteria) } : asked
36
+ questions[id] = question
27
37
  if (!['choice', 'noul', 'score', 'box'].includes(question?.type)) throw new Error(`question "${id}" needs type choice, noul, score or box`)
28
38
  if (typeof question.instructions !== 'string' || !question.instructions.trim()) throw new Error(`question "${id}" needs instructions`)
39
+ // the Decision API does not answer a noul question that carries criteria
40
+ if (question.type === 'noul' && question.criteria !== undefined) {
41
+ const { criteria, ...rest } = question
42
+ const rules = typeof criteria === 'string' ? [criteria]
43
+ : Array.isArray(criteria) ? criteria.map(String)
44
+ : criteria && typeof criteria === 'object' ? Object.entries(criteria).map(([name, rule]) => `${name}: ${rule}`) : []
45
+ questions[id] = { ...rest, instructions: [rest.instructions.trim(), ...rules].join('\n') }
46
+ }
29
47
  }
30
48
  return questions
31
49
  }
package/web/client.js CHANGED
@@ -171,10 +171,10 @@ window.__ModuleLoader__.load({
171
171
  verdict: 'brief', label: kindLabel, short: kindLabel.toLowerCase(), tone: 'info',
172
172
  headline: [`${kindLabel} work`, `${effortLabel.toLowerCase()} effort`, ...flags].join(' · '),
173
173
  facts: [
174
- { label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
175
- { label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
176
- { label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
177
- { label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
174
+ { id: 'kind', label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
175
+ { id: 'effort', label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
176
+ { id: 'unclear', label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
177
+ { id: 'risky', label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
178
178
  ],
179
179
  note: hints.length ? `Brief for System Two: ${hints.join(' ')}` : undefined,
180
180
  }
@@ -184,21 +184,21 @@ window.__ModuleLoader__.load({
184
184
  const outcome = OUTCOME[row.outcome]
185
185
  if (row.error) {
186
186
  const fallback = { allow: { text: 'so the call ran', tone: 'muted' }, ask: { text: 'so you were asked', tone: 'warn' }, deny: { text: 'so the call was blocked', tone: 'bad' } }[row.verdict]
187
- return { ...errorView(row, fallback), outcome }
187
+ return { ...errorView(row, fallback), outcome, outcomeKey: outcome ? row.outcome : undefined }
188
188
  }
189
189
  const subject = { tool: row.tool, text: row.call }
190
190
  const verdict = GUARD[row.verdict]
191
- if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), subject, facts: [] }
191
+ if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), outcomeKey: outcome ? row.outcome : undefined, subject, facts: [] }
192
192
  const a = row.answers ?? {}
193
193
  const risk = scoreOf(a.risk)
194
194
  const leaks = yesOf(a.exfiltration)
195
195
  const onTask = yesOf(a.on_task)
196
196
  const facts = []
197
- if (risk !== undefined) facts.push({ label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
198
- if (leaks !== undefined) facts.push({ label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
199
- if (onTask !== undefined) facts.push({ label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
197
+ if (risk !== undefined) facts.push({ id: 'risk', label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
198
+ if (leaks !== undefined) facts.push({ id: 'leaks', label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
199
+ if (onTask !== undefined) facts.push({ id: 'on-task', label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
200
200
  return {
201
- verdict: row.verdict, ...verdict, subject, facts, outcome, verdictTone: verdict.tone,
201
+ verdict: row.verdict, ...verdict, subject, facts, outcome, outcomeKey: outcome ? row.outcome : undefined, verdictTone: verdict.tone,
202
202
  tone: outcome && SEVERITY[outcome.tone] > SEVERITY[verdict.tone] ? outcome.tone : verdict.tone,
203
203
  note: row.verdict !== 'allow' && row.why ? `Because ${row.why}.` : undefined,
204
204
  }
@@ -211,8 +211,8 @@ window.__ModuleLoader__.load({
211
211
  return {
212
212
  verdict: row.verdict, ...verdict, subject: { tool: row.tool, text: row.call },
213
213
  facts: [
214
- { label: 'Passages', value: String(num(row.passages) ?? '?') },
215
- { label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
214
+ { id: 'passages', label: 'Passages', value: String(num(row.passages) ?? '?') },
215
+ { id: 'injection', label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
216
216
  ],
217
217
  note: row.verdict === 'removed' ? `Removed ${plural(lines, 'line')} that gave orders to AI agents before System Two read the result.`
218
218
  : row.verdict === 'flagged' ? `Found ${plural(lines, 'line')} that give orders to AI agents; the result was withheld.` : undefined,
@@ -226,12 +226,12 @@ window.__ModuleLoader__.load({
226
226
  const verdict = DONE[why]
227
227
  const round = `${num(row.steers) ?? '?'} of ${num(row.maxSteers) ?? '?'}`
228
228
  return {
229
- verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, short: verdict.label.toLowerCase(),
229
+ verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, why, short: verdict.label.toLowerCase(),
230
230
  headline: row.verdict === 'continue' ? `${verdict.label} to System Two (${round})` : why === 'waiting' ? 'Waiting on you' : 'The work is done',
231
231
  facts: [
232
- { label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
233
- { label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
234
- { label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
232
+ { id: 'complete', label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
233
+ { id: 'checked', label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
234
+ { id: 'waiting', label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
235
235
  ],
236
236
  note: why === 'unfinished' ? `Not finished yet, so System Two goes back to work (${round}).`
237
237
  : why === 'unchecked' ? `The changes were not checked, so System Two goes back to verify them (${round}).` : undefined,
@@ -273,7 +273,7 @@ window.__ModuleLoader__.load({
273
273
  const label = view.outcome ? `${view.label} → ${view.outcome.label}` : view.label
274
274
  const rest = []
275
275
  if (view.subject?.text) rest.push(`${view.subject.tool ?? ''} ${view.subject.text}`.trim())
276
- const lead = view.facts.find(fact => fact.label === 'Risk' || fact.label === 'Injection' || fact.label === 'Complete' || fact.label === 'Effort')
276
+ const lead = view.facts.find(fact => ['risk', 'injection', 'complete', 'effort'].includes(fact.id))
277
277
  if (lead) rest.push(`${lead.label.toLowerCase()} ${lead.value}`)
278
278
  if (view.error) rest.push(view.error)
279
279
  return { label, tone: view.tone, rest: rest.join(' · ') }
@@ -313,18 +313,18 @@ window.__ModuleLoader__.load({
313
313
 
314
314
  function Badge({ id, ms, time }) {
315
315
  const fresh = useFresh(`badge:${id}`, time)
316
- return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh') },
316
+ return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh'), 'data-system-one-part': 'badge' },
317
317
  h('i', { className: 'vsr-s1__dot', 'aria-hidden': true }),
318
318
  h('span', { className: 'vsr-s1__name' }, 'System One'),
319
- secs(ms) ? h('span', { className: 'vsr-s1__ms' }, secs(ms)) : null)
319
+ secs(ms) ? h('span', { className: 'vsr-s1__ms', 'data-system-one-part': 'latency' }, secs(ms)) : null)
320
320
  }
321
321
 
322
- function Pill({ tone, children, arrow }) {
323
- return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then') }, children)
322
+ function Pill({ tone, children, arrow, part }) {
323
+ return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then'), 'data-system-one-part': part }, children)
324
324
  }
325
325
 
326
- function Fact({ label, value, bar, tone }) {
327
- return h('span', { className: 'vsr-s1__fact' },
326
+ function Fact({ id, label, value, bar, tone }) {
327
+ return h('span', { className: 'vsr-s1__fact', 'data-system-one-fact': id },
328
328
  h('span', { className: 'vsr-s1__fact-label' }, label),
329
329
  num(bar) === undefined ? null : h('span', { className: cx('vsr-s1__bar', tone && `vsr-s1__bar--${tone}`), style: { '--p': clamp01(bar) }, 'aria-hidden': true }, h('i')),
330
330
  h('b', null, value))
@@ -335,34 +335,43 @@ window.__ModuleLoader__.load({
335
335
  return h('div', {
336
336
  className: cx('vsr-s1__row', fresh && 'vsr-s1__row--fresh'),
337
337
  'data-system-one-decision': view.verdict,
338
+ 'data-system-one-outcome': view.outcomeKey,
338
339
  'data-system-one-call': row.callId,
339
340
  },
340
341
  h('span', { className: 'vsr-s1__verdicts' },
341
- h(Pill, { tone: view.verdictTone ?? view.tone }, view.label),
342
- view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true }, view.outcome.label) : null),
342
+ h(Pill, { tone: view.verdictTone ?? view.tone, part: 'verdict' }, view.label),
343
+ view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true, part: 'outcome' }, view.outcome.label) : null),
343
344
  h('div', { className: 'vsr-s1__main' },
344
- view.subject?.text ? h('div', { className: 'vsr-s1__subject' },
345
+ view.subject?.text ? h('div', { className: 'vsr-s1__subject', 'data-system-one-part': 'subject' },
345
346
  view.subject.tool ? h('span', { className: 'vsr-s1__tool' }, view.subject.tool) : null,
346
347
  h('code', { className: 'vsr-s1__code', title: view.subject.text }, view.subject.text)) : null,
347
- view.facts.length ? h('div', { className: 'vsr-s1__facts' }, view.facts.map(fact => h(Fact, { key: fact.label, ...fact }))) : null,
348
- view.note ? h('div', { className: 'vsr-s1__note' }, view.note) : null),
349
- !single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms' }, secs(row.ms)) : null)
348
+ view.facts.length ? h('div', { className: 'vsr-s1__facts', 'data-system-one-part': 'facts' }, view.facts.map(fact => h(Fact, { key: fact.id, ...fact }))) : null,
349
+ view.note ? h('div', { className: 'vsr-s1__note', 'data-system-one-part': 'note' }, view.note) : null),
350
+ !single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms', 'data-system-one-part': 'latency' }, secs(row.ms)) : null)
350
351
  }
351
352
 
352
353
  function Rows({ reflex, rows, views }) {
353
354
  const [all, setAll] = React.useState(false)
354
355
  const hidden = all ? 0 : Math.max(0, rows.length - ROWS_SHOWN)
355
- return h('div', { className: 'vsr-s1__rows' },
356
- hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
356
+ return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
357
+ hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', 'data-system-one-part': 'more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
357
358
  rows.slice(hidden).map((row, index) => h(Row, { key: idOf(row), row, view: views[hidden + index], single: rows.length === 1 })))
358
359
  }
359
360
 
360
- function attributes(kind, state, views, worst) {
361
+ const whole = value => (Number.isSafeInteger(value) ? String(value) : undefined)
362
+
363
+ // a public contract (README.md, "Selectors"); React leaves out an attribute whose value is undefined
364
+ function attributes(kind, state, worst) {
361
365
  return {
362
366
  'data-system-one': kind,
363
367
  'data-system-one-reflex': state.reflex,
364
368
  'data-system-one-verdict': worst.verdict,
369
+ 'data-system-one-outcome': worst.outcomeKey,
370
+ 'data-system-one-why': worst.why,
371
+ 'data-system-one-tone': worst.tone,
365
372
  'data-system-one-count': String(state.rows.length),
373
+ 'data-system-one-turn': whole(state.rows[0].turn),
374
+ 'data-system-one-step': state.reflex === 'guard' || state.reflex === 'shield' ? whole(state.rows[0].step) : undefined,
366
375
  }
367
376
  }
368
377
 
@@ -377,14 +386,14 @@ window.__ModuleLoader__.load({
377
386
  className: cx('vsr-s1', 'vsr-s1--card', `vsr-s1--${worst.tone}`, fresh && 'vsr-s1--fresh'),
378
387
  role: 'group',
379
388
  'aria-label': `System One ${reflex}: ${headline}`,
380
- ...attributes('card', state, views, worst),
389
+ ...attributes('card', state, worst),
381
390
  },
382
391
  h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
383
- h('div', { className: 'vsr-s1__head' },
392
+ h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
384
393
  h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
385
- h('span', { className: 'vsr-s1__reflex' }, reflex),
386
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
387
- last.model ? h('span', { className: 'vsr-s1__model', title: last.model }, shortModel(last.model)) : null),
394
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
395
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
396
+ last.model ? h('span', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: last.model }, shortModel(last.model)) : null),
388
397
  h(Rows, { reflex: state.reflex, rows: state.rows, views }))
389
398
  }
390
399
 
@@ -406,13 +415,13 @@ window.__ModuleLoader__.load({
406
415
  className: cx('vsr-s1', 'vsr-s1--compact', `vsr-s1--${worst.tone}`, open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'),
407
416
  role: 'group',
408
417
  'aria-label': `System One ${reflex}: ${said}`,
409
- ...attributes('compact', state, views, worst),
418
+ ...attributes('compact', state, worst),
410
419
  },
411
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
420
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
412
421
  h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
413
- h('span', { className: 'vsr-s1__reflex' }, reflex),
414
- h('span', { className: 'vsr-s1__summary', title: said },
415
- h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`) }, line.label),
422
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
423
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: said },
424
+ h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`), 'data-system-one-part': 'verdict' }, line.label),
416
425
  line.rest ? ` · ${line.rest}` : null),
417
426
  h(Chevron)),
418
427
  open ? h(Rows, { reflex: state.reflex, rows: state.rows, views }) : null)
@@ -455,7 +464,7 @@ window.__ModuleLoader__.load({
455
464
 
456
465
  function Stack({ parts, fresh }) {
457
466
  const total = parts.reduce((sum, part) => sum + part.n, 0)
458
- return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'aria-hidden': true },
467
+ return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'data-system-one-part': 'stack', 'aria-hidden': true },
459
468
  total > 0 ? parts.filter(part => part.n > 0).map((part, index) => h('i', { key: part.label, style: { flexGrow: part.n, background: part.color, animationDelay: `${0.12 + index * 0.06}s` } })) : null)
460
469
  }
461
470
 
@@ -463,12 +472,13 @@ window.__ModuleLoader__.load({
463
472
  const LABEL_CHARS = 28
464
473
  const shortLabel = label => (label.length > LABEL_CHARS ? `${label.slice(0, LABEL_CHARS - 1).trimEnd()}…` : label)
465
474
 
466
- function Legend({ parts, extra }) {
467
- return h('span', { className: 'vsr-s1__legend' },
475
+ function Legend({ parts, extra, failed }) {
476
+ return h('span', { className: 'vsr-s1__legend', 'data-system-one-part': 'legend' },
468
477
  parts.map(part => h('span', { key: part.label, className: 'vsr-s1__key', title: `${part.label}: ${part.n}` },
469
478
  h('i', { className: 'vsr-s1__swatch', style: { background: part.color } }),
470
479
  `${shortLabel(part.label)} `, h('b', null, String(part.n)))),
471
- extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null)
480
+ extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null,
481
+ failed ? h('span', { className: 'vsr-s1__key vsr-s1__key--bad', 'data-system-one-part': 'failed' }, `${failed} failed`) : null)
472
482
  }
473
483
 
474
484
  function questionParts(summary) {
@@ -501,14 +511,14 @@ window.__ModuleLoader__.load({
501
511
  h('code', { className: 'vsr-s1__qname', title: id }, id),
502
512
  h('div', { className: 'vsr-s1__qbody' },
503
513
  parts.length ? h(Stack, { parts, fresh }) : null,
504
- h(Legend, { parts, extra: [extra, summary?.failed ? `${summary.failed} failed` : undefined].filter(Boolean).join(' · ') || undefined })))
514
+ h(Legend, { parts, extra, failed: num(summary?.failed) || undefined })))
505
515
  }
506
516
 
507
517
  function ToolBody({ meta, fresh, cwd, openFile }) {
508
518
  const entries = Object.entries(meta.summary ?? {})
509
- return h('div', { className: 'vsr-s1__rows' },
519
+ return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
510
520
  entries.map(([id, summary]) => h(Question, { key: id, id, summary, fresh })),
511
- meta.output ? h('div', { className: 'vsr-s1__foot' },
521
+ meta.output ? h('div', { className: 'vsr-s1__foot', 'data-system-one-part': 'results' },
512
522
  'Results ',
513
523
  typeof openFile === 'function'
514
524
  ? h('button', { type: 'button', className: 'vsr-s1__link', onClick: () => openFile(meta.output) }, relative(meta.output, cwd))
@@ -524,36 +534,40 @@ window.__ModuleLoader__.load({
524
534
  const [open, setOpen] = React.useState(false)
525
535
  const source = args.file ? basename(args.file) : Array.isArray(args.images) ? plural(args.images.length, 'image') : Array.isArray(args.items) ? plural(args.items.length, 'item') : ''
526
536
  const asked = questionCount(args.questions)
527
- const common = { 'data-system-one': 'tool', 'data-system-one-reflex': 'tool', 'data-system-one-call': callId }
537
+ const common = { 'data-system-one-reflex': 'tool', 'data-system-one-call': callId }
528
538
 
529
539
  if (phase !== 'result') {
530
540
  const line = phase === 'preparing' ? 'Preparing a batch' : `Answering ${plural(asked, 'question')}${source ? ` over ${source}` : ''}`
531
- return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', ...common, 'data-system-one-verdict': 'running' },
541
+ return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', 'data-system-one': 'tool', ...common, 'data-system-one-verdict': 'running', 'data-system-one-tone': 'info' },
532
542
  h('div', { className: 'vsr-s1__chip vsr-s1__chip--static' },
533
543
  h(Badge, { id: `${callId}:running`, time: Date.now() }),
534
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
535
- h('span', { className: 'vsr-s1__summary vsr-s1__shimmer' }, line)))
544
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
545
+ h('span', { className: 'vsr-s1__summary vsr-s1__shimmer', 'data-system-one-part': 'summary' }, line)))
536
546
  }
537
547
 
538
548
  if (block?.isError || !meta || ui === 'off') {
539
549
  const body = resultText(block)
540
550
  const first = body.split('\n')[0] || (block?.isError ? 'The batch failed' : 'Done')
541
- return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--plain', block?.isError && 'vsr-s1--bad', open && 'vsr-s1--open'), ...common, 'data-system-one-verdict': block?.isError ? 'error' : 'done' },
542
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
543
- h('span', { className: 'vsr-s1__reflex' }, 'System One'),
544
- h('span', { className: cx('vsr-s1__summary', block?.isError && 'vsr-s1__summary--bad'), title: first }, first),
545
- h(Chevron)),
546
- open ? h('pre', { className: 'vsr-s1__raw' }, body) : null)
551
+ return h('div', {
552
+ className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--plain', block?.isError && 'vsr-s1--bad', open && 'vsr-s1--open'),
553
+ 'data-system-one': 'tool', ...common, 'data-system-one-verdict': block?.isError ? 'error' : 'done', 'data-system-one-tone': block?.isError ? 'bad' : 'muted',
554
+ },
555
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
556
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, 'System One'),
557
+ h('span', { className: cx('vsr-s1__summary', block?.isError && 'vsr-s1__summary--bad'), 'data-system-one-part': 'summary', title: first }, first),
558
+ h(Chevron)),
559
+ open ? h('pre', { className: 'vsr-s1__raw', 'data-system-one-part': 'raw' }, body) : null)
547
560
  }
548
561
 
549
562
  const ms = num(meta.seconds) === undefined ? undefined : meta.seconds * 1000
550
563
  const headline = [plural(num(meta.items) ?? 0, 'item'), plural(num(meta.questions) ?? 0, 'question'), plural(num(meta.requests) ?? 0, 'request')].join(' · ')
564
+ const done = { ...common, 'data-system-one-verdict': 'done', 'data-system-one-tone': 'ok', 'data-system-one-count': String(meta.items ?? '') }
551
565
  if (ui === 'compact') {
552
- return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'), ...common, 'data-system-one-verdict': 'done', 'data-system-one-count': String(meta.items ?? '') },
553
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
566
+ return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'), 'data-system-one': 'tool', ...done },
567
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
554
568
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
555
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
556
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
569
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
570
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
557
571
  h(Chevron)),
558
572
  open ? h(ToolBody, { meta, fresh, cwd, openFile }) : null)
559
573
  }
@@ -561,16 +575,15 @@ window.__ModuleLoader__.load({
561
575
  className: cx('vsr-s1', 'vsr-s1--card', 'vsr-s1--ok', 'vsr-s1--tool', fresh && 'vsr-s1--fresh'),
562
576
  role: 'group',
563
577
  'aria-label': `System One batch: ${headline}`,
564
- ...common,
565
- 'data-system-one-verdict': 'done',
566
- 'data-system-one-count': String(meta.items ?? ''),
578
+ 'data-system-one': 'tool',
579
+ ...done,
567
580
  },
568
581
  h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
569
- h('div', { className: 'vsr-s1__head' },
582
+ h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
570
583
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
571
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
572
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
573
- meta.model ? h('span', { className: 'vsr-s1__model', title: meta.model }, shortModel(meta.model)) : null),
584
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
585
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
586
+ meta.model ? h('span', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: meta.model }, shortModel(meta.model)) : null),
574
587
  h(ToolBody, { meta, fresh, cwd, openFile }))
575
588
  }
576
589
 
@@ -631,14 +644,15 @@ window.__ModuleLoader__.load({
631
644
  .vsr-s1--plain .vsr-s1__reflex{color:var(--vsr-s1-text-2)}
632
645
  .vsr-s1__raw{margin:4px 0 8px;padding:8px 10px;max-height:320px;overflow:auto;border-radius:8px;background:color-mix(in srgb,var(--vsr-s1-text-3) 10%,transparent);font-family:var(--vsr-s1-mono);font-size:12px;white-space:pre-wrap}
633
646
  .vsr-s1__q{display:grid;grid-template-columns:minmax(80px,140px) minmax(0,1fr);column-gap:14px;align-items:start}
634
- .vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:10px;white-space:nowrap;text-overflow:ellipsis;background:none;padding:0}
647
+ .vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;background:none;padding:0}
635
648
  .vsr-s1__qbody{display:grid;gap:4px;min-width:0}
636
- .vsr-s1__stack{display:flex;gap:2px;height:8px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
649
+ .vsr-s1__stack{display:flex;gap:2px;height:8px;margin-top:4px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
637
650
  .vsr-s1__stack>i{display:block;min-width:3px;height:100%;transform-origin:left center}
638
651
  .vsr-s1__legend{display:flex;flex-wrap:wrap;gap:1px 14px;color:var(--vsr-s1-text-2);font-size:12.5px}
639
652
  .vsr-s1__key{display:inline-flex;align-items:center;white-space:nowrap}
640
653
  .vsr-s1__key b{margin-left:4px;color:var(--vsr-s1-text);font-weight:600;font-variant-numeric:tabular-nums}
641
654
  .vsr-s1__key--note{color:var(--vsr-s1-text-3)}
655
+ .vsr-s1__key--bad{color:var(--vsr-s1-bad);font-weight:600}
642
656
  .vsr-s1__swatch{flex:none;width:8px;height:8px;margin-right:6px;border-radius:2px}
643
657
  .vsr-s1__foot{color:var(--vsr-s1-text-3);font-size:12.5px}
644
658
  .vsr-s1__foot code{font-family:var(--vsr-s1-mono)}