@stackbone/cli 0.2.10 → 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.
@@ -0,0 +1,68 @@
1
+ -- 0041_eval_criterion_workflow_name.sql — write the name that used to be implicit
2
+ -- onto every stored `custom_workflow` criterion (STA-521, PRD STA-393).
3
+ --
4
+ -- The criterion "judged by your own workflow" used to resolve ONE reserved workflow
5
+ -- name and store nothing about it: the role WAS the workflow name, so a suite only
6
+ -- had to say "custom_workflow" and the box knew where to go. STA-521 makes the
7
+ -- judging workflow a setting picked from a dropdown of what the box holds, which
8
+ -- means the name is now something a suite has to CARRY.
9
+ --
10
+ -- WHY THIS SHIPS IN THE SAME SLICE AS THE CATALOG CHANGE, not after it. The catalog
11
+ -- rejects settings it does not declare, so a backfill landing FIRST would break
12
+ -- every subsequent save; and once `workflowName` is required, an unmigrated suite
13
+ -- can neither be saved nor judged. Either order on its own opens a window in which
14
+ -- a stored suite is broken. Both halves land together, so there is no window.
15
+ --
16
+ -- THE LITERAL BELOW MUST MATCH `EVAL_CRITERION_ROLE` in
17
+ -- libs/shared/evals/src/lib/custom/custom-criterion.ts. SQL cannot import the
18
+ -- constant, and that constant now exists for exactly this reason: it is the value
19
+ -- every suite meant while the name was implicit, and both sides have to mean the
20
+ -- same thing by it. The TS side keeps a comment pointing back here.
21
+ --
22
+ -- ONLY WHERE THE SETTING IS ABSENT OR BLANK. A box that already had the picker has
23
+ -- an operator's own choice in that field; overwriting it would silently re-point
24
+ -- their judge at a workflow they never picked, and nothing on the screen would say
25
+ -- so. `COALESCE(... ->> 'workflowName', '') = ''` covers all three ways of having
26
+ -- said nothing — the key missing, a JSON null, and an empty string — and makes the
27
+ -- statement idempotent: run it twice and the second pass matches nothing.
28
+ --
29
+ -- `scorers` is a jsonb ARRAY of `{type, params}`, so the rewrite goes element by
30
+ -- element through `jsonb_array_elements` and reassembles the list with `jsonb_agg`.
31
+ -- Element ORDER is preserved through `WITH ORDINALITY` … `ORDER BY`, because a
32
+ -- criterion list is read positionally on the screen; the co-located spec pins that
33
+ -- against a suite whose criterion is neither first nor alone. The `COALESCE(…,
34
+ -- '[]')` around the aggregate is belt-and-braces only — `jsonb_agg` over no rows
35
+ -- returns NULL, and the `EXISTS` guard below already means no row without criteria
36
+ -- is ever updated. It stays because writing NULL into a NOT NULL column is the one
37
+ -- mistake here that would take a boot down.
38
+ --
39
+ -- A separate file rather than an edit to 0038: the migrator hashes each body and
40
+ -- verifies it on every boot, so rewriting a landed migration bricks a box that
41
+ -- already ran it. Comments count towards that hash too.
42
+ UPDATE stackbone_platform.eval_suites AS s
43
+ SET scorers = (
44
+ SELECT COALESCE(jsonb_agg(rewritten.scorer ORDER BY rewritten.position), '[]'::jsonb)
45
+ FROM (
46
+ SELECT element.position,
47
+ CASE
48
+ WHEN element.scorer->>'type' = 'custom_workflow'
49
+ AND COALESCE(element.scorer->'params'->>'workflowName', '') = ''
50
+ THEN jsonb_set(
51
+ element.scorer,
52
+ '{params}',
53
+ COALESCE(element.scorer->'params', '{}'::jsonb)
54
+ || jsonb_build_object('workflowName', 'eval-criterion')
55
+ )
56
+ ELSE element.scorer
57
+ END AS scorer
58
+ FROM jsonb_array_elements(s.scorers)
59
+ WITH ORDINALITY AS element(scorer, position)
60
+ ) AS rewritten
61
+ )
62
+ WHERE jsonb_typeof(s.scorers) = 'array'
63
+ AND EXISTS (
64
+ SELECT 1
65
+ FROM jsonb_array_elements(s.scorers) AS scorer
66
+ WHERE scorer->>'type' = 'custom_workflow'
67
+ AND COALESCE(scorer->'params'->>'workflowName', '') = ''
68
+ );
@@ -0,0 +1,64 @@
1
+ -- 0042_eval_case_results_ended.sql — record HOW a measured case ended, next to
2
+ -- the free-form answer the verdict already keeps (STA-526, PRD STA-393).
3
+ --
4
+ -- 0038's own comment already promised this: "a case stopped by a guardrail or
5
+ -- parked on a human approval is distinguishable from one that simply answered
6
+ -- badly". It was only ever true by convention — the ending was a TypeScript
7
+ -- union translated away before the insert, and the sole trace left behind was a
8
+ -- string inside the `error` blob. Studio then had to guess the verdict by
9
+ -- matching the front of that string, which is how a parked workflow ended up
10
+ -- painted as an error.
11
+ --
12
+ -- A COLUMN rather than a code inside `error`, for two reasons:
13
+ -- - an agent that correctly ESCALATED is not an error, and archiving it as one
14
+ -- makes a suite of held cases read as a broken box;
15
+ -- - "3 of 15 escalated" has to be one query, not a JSON read per row.
16
+ --
17
+ -- THE SIX VALUES, and why the two parks are separate:
18
+ -- completed reached a terminal state with an answer
19
+ -- escalated parked on an operator approval
20
+ -- asked_user parked on a resume hook, waiting on a person
21
+ -- blocked_by_guardrail refused at the border, never started
22
+ -- errored broke
23
+ -- cancelled stopped before it could finish
24
+ -- `escalated` and `asked_user` are NOT folded together: telling "escalated to a
25
+ -- human" from "asked the end user" is the whole distinction the pause criteria
26
+ -- are being built to measure.
27
+ --
28
+ -- NULLABLE, and deliberately never backfilled. This is an APPEND-ONLY results
29
+ -- table: every verdict written before today belongs to a sealed run, and there
30
+ -- is no honest value to invent for it — `completed` would put words in that
31
+ -- run's mouth. A NULL reads as "this run could not say", which is the truth.
32
+ -- It also keeps a rolling upgrade safe: a box that applies this migration a
33
+ -- moment before its runners learn to write the column still writes verdicts,
34
+ -- rather than failing every one of them on a NOT NULL.
35
+ --
36
+ -- A CHECK and not free text, unlike `runs.entity_kind` (0037) or the suite's
37
+ -- `target_kind`. Those two are discriminators a box on an older build must be
38
+ -- able to STORE without executing, so rejecting an unknown value would lose an
39
+ -- operator's configuration. This column is the opposite: the box WRITES it, one
40
+ -- of six values decided in this repository, and a screen that groups on it must
41
+ -- not have to defend against a seventh spelling invented by a typo.
42
+ --
43
+ -- A separate file rather than an edit to 0038: the migrator hashes each body and
44
+ -- verifies it on every boot, so rewriting a landed migration bricks a box that
45
+ -- already ran it. Comments count towards that hash too.
46
+ ALTER TABLE stackbone_platform.eval_case_results
47
+ ADD COLUMN IF NOT EXISTS ended text;
48
+
49
+ -- Added separately from the column so the statement above stays re-runnable:
50
+ -- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
51
+ -- on a box where the column somehow already exists.
52
+ ALTER TABLE stackbone_platform.eval_case_results
53
+ DROP CONSTRAINT IF EXISTS eval_case_results_ended_check;
54
+
55
+ ALTER TABLE stackbone_platform.eval_case_results
56
+ ADD CONSTRAINT eval_case_results_ended_check
57
+ CHECK (ended IS NULL OR ended IN (
58
+ 'completed',
59
+ 'escalated',
60
+ 'asked_user',
61
+ 'blocked_by_guardrail',
62
+ 'errored',
63
+ 'cancelled'
64
+ ));
@@ -0,0 +1,50 @@
1
+ -- 0043_eval_suite_approval_policy.sql — what a suite does about an approval one
2
+ -- of its cases parks on (STA-530, PRD STA-393).
3
+ --
4
+ -- A measured case has NOBODY waiting on it. The thread belongs to one attempt of
5
+ -- one case and is never spoken to again, so an agent that correctly stops to ask
6
+ -- an operator is scored on the stop and everything it would have said afterwards
7
+ -- is never seen. Half of what an approval-bearing agent does is on the other side
8
+ -- of that gate, and today a suite cannot measure any of it.
9
+ --
10
+ -- THE FOUR VALUES:
11
+ -- leave_waiting the case closes on the park and the request it raised is
12
+ -- taken back out of the inbox. What the product did before this
13
+ -- column existed.
14
+ -- reject answer it no, then keep watching the turn.
15
+ -- approve answer it yes, then keep watching the turn.
16
+ -- model_decides ask the judging model which of the two, then answer it.
17
+ --
18
+ -- NOT NULL with a DEFAULT, unlike 0042's `ended`. The opposite situation: that
19
+ -- one is an append-only results column with no honest value to invent for a row
20
+ -- written before it existed, while this is CONFIGURATION and the honest value for
21
+ -- every suite already stored is precisely what they do today — leave the case
22
+ -- waiting. Backfilling it is therefore not putting words in anybody's mouth.
23
+ --
24
+ -- A CHECK and not free text, for the same reason 0042's ending carries one: the
25
+ -- BOX writes and reads this, one of four values decided in this repository, and
26
+ -- the runner that branches on it must not have to defend against a fifth spelling
27
+ -- invented by a typo. The suite's own `target_kind` is deliberately open text by
28
+ -- contrast — that one is a discriminator a box on an older build has to be able
29
+ -- to store without executing.
30
+ --
31
+ -- A separate file rather than an edit to 0038: the migrator hashes each body and
32
+ -- verifies it on every boot, so rewriting a landed migration bricks a box that
33
+ -- already ran it. Comments count towards that hash too.
34
+ ALTER TABLE stackbone_platform.eval_suites
35
+ ADD COLUMN IF NOT EXISTS approval_policy text NOT NULL DEFAULT 'leave_waiting';
36
+
37
+ -- Added separately from the column so the statement above stays re-runnable:
38
+ -- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
39
+ -- on a box where the column somehow already exists.
40
+ ALTER TABLE stackbone_platform.eval_suites
41
+ DROP CONSTRAINT IF EXISTS eval_suites_approval_policy_check;
42
+
43
+ ALTER TABLE stackbone_platform.eval_suites
44
+ ADD CONSTRAINT eval_suites_approval_policy_check
45
+ CHECK (approval_policy IN (
46
+ 'leave_waiting',
47
+ 'reject',
48
+ 'approve',
49
+ 'model_decides'
50
+ ));
@@ -0,0 +1,88 @@
1
+ -- 0044_eval_persona_simulator.sql — a case carries a persona, and the suite
2
+ -- decides whether anybody plays it (STA-532, PRD STA-393).
3
+ --
4
+ -- The clarification loop is the part of the product no eval touched. An agent
5
+ -- that asks before it acts parks on the first question, so what it would have
6
+ -- done with the answer — the whole reason it asks at all — was never measured.
7
+ -- 0043 opened the half of that story that faces an OPERATOR; this one opens the
8
+ -- half that faces the END USER.
9
+ --
10
+ -- FOUR TABLES, and each column is on the table that owns the fact:
11
+ --
12
+ -- eval_cases.persona WHO answers, in what words, how many times.
13
+ -- Belongs to the case, so the same list can be
14
+ -- run by a suite that measures the first
15
+ -- question and one that measures the whole
16
+ -- conversation.
17
+ -- eval_suites.simulator WHETHER anybody plays it, and with which
18
+ -- model. Belongs to the suite, so a persona
19
+ -- with the simulator off is simply ignored.
20
+ -- eval_runs.simulator_model WHO ACTUALLY played it, sealed at launch.
21
+ -- eval_runs.reply_cap HOW MANY TIMES they were allowed to.
22
+ -- eval_runs.simulated_connectors WHICH connectors were stood in for.
23
+ -- eval_case_results.simulation WHAT was said, turn by turn, marking which
24
+ -- lines were scripted and which improvised.
25
+ --
26
+ -- WHY THE RUN SEALS THREE MORE THINGS. Two runs of the same suite a month apart
27
+ -- have to stay comparable, and every one of these moves a score without the agent
28
+ -- changing at all: a different model plays the user differently, a cap raised
29
+ -- from three to five gives the agent two more chances, and a connector that was
30
+ -- real last month and simulated today is a different world. A run that did not
31
+ -- record them makes the next comparison read a change of setting as a change in
32
+ -- the agent.
33
+ --
34
+ -- A NOTE ON GRANULARITY, so it is not rediscovered as a surprise. `git_sha` and
35
+ -- `agent_version` on this table are already taken from the FIRST variant only,
36
+ -- while a run may carry two. The three columns below are added at the SAME single
37
+ -- value per run, deliberately — a per-variant seal for these alone would leave
38
+ -- the row half one shape and half another. When the variant scoping is fixed it
39
+ -- has to be fixed for all five together.
40
+ --
41
+ -- JSONB rather than columns for `persona` and `simulator`, matching `scorers` and
42
+ -- `variants` beside them: they are configuration payloads whose documented
43
+ -- defaults live in `@stackbone/validators` → `studio-evals.ts`, and a CHECK here
44
+ -- would be a second description that drifts from the form an operator fills in.
45
+ --
46
+ -- `simulated_connectors` is `text[]` rather than jsonb, matching `eval_cases.tags`
47
+ -- — it is a flat list of identifiers, and the array type is what makes "which
48
+ -- runs simulated Outlook?" an ordinary query.
49
+ --
50
+ -- NULLABLE where the fact is HISTORICAL and defaulted where it is CONFIGURATION,
51
+ -- exactly as 0042 and 0043 split it. A verdict written before this column existed
52
+ -- has no honest simulation to invent, and a run sealed before it has no honest
53
+ -- model to name — those read NULL, which is the truth. A suite, by contrast, has
54
+ -- a perfectly honest current value: nobody answers, which is what every stored
55
+ -- suite does today.
56
+ --
57
+ -- A separate file rather than an edit to 0038: the migrator hashes each body and
58
+ -- verifies it on every boot, so rewriting a landed migration bricks a box that
59
+ -- already ran it. Comments count towards that hash too.
60
+
61
+ ALTER TABLE stackbone_platform.eval_cases
62
+ ADD COLUMN IF NOT EXISTS persona jsonb;
63
+
64
+ ALTER TABLE stackbone_platform.eval_suites
65
+ ADD COLUMN IF NOT EXISTS simulator jsonb NOT NULL
66
+ DEFAULT '{"enabled": false, "model": null}'::jsonb;
67
+
68
+ ALTER TABLE stackbone_platform.eval_runs
69
+ ADD COLUMN IF NOT EXISTS simulator_model text;
70
+
71
+ ALTER TABLE stackbone_platform.eval_runs
72
+ ADD COLUMN IF NOT EXISTS reply_cap integer;
73
+
74
+ ALTER TABLE stackbone_platform.eval_runs
75
+ ADD COLUMN IF NOT EXISTS simulated_connectors text[] NOT NULL DEFAULT '{}';
76
+
77
+ ALTER TABLE stackbone_platform.eval_case_results
78
+ ADD COLUMN IF NOT EXISTS simulation jsonb;
79
+
80
+ -- Added separately from the column so the statement above stays re-runnable:
81
+ -- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
82
+ -- on a box where the column somehow already exists.
83
+ ALTER TABLE stackbone_platform.eval_runs
84
+ DROP CONSTRAINT IF EXISTS eval_runs_reply_cap_check;
85
+
86
+ ALTER TABLE stackbone_platform.eval_runs
87
+ ADD CONSTRAINT eval_runs_reply_cap_check
88
+ CHECK (reply_cap IS NULL OR reply_cap >= 0);
@@ -0,0 +1,34 @@
1
+ -- 0045_eval_case_description.sql — a case says, in prose, what it is for.
2
+
3
+ -- A case list is read far more often than it is written, and until now the only
4
+ -- thing a reader got was the `name`. A name has to be short and unique, so it
5
+ -- ends up as a slug — `prof-asunto-y-cuerpo-discrepan` — and everything that
6
+ -- makes the case worth having lives in whoever wrote it.
7
+ --
8
+ -- WHERE THIS WAS BEING KEPT INSTEAD, and why that is the actual argument for the
9
+ -- column. Case lists in the wild already carry the explanation; they just have
10
+ -- nowhere to put it, so it goes inside `expected` as an extra key. That field is
11
+ -- the REFERENCE ANSWER — the thing scorers compare against — so an explanation
12
+ -- parked there is prose sitting inside the ground truth, surviving only because
13
+ -- each scorer is taught to skip that one key by name. A scorer that compares the
14
+ -- whole object (`exact-match` does) fails every case that has one.
15
+ --
16
+ -- So this is not a new capability so much as moving a fact onto the row that owns
17
+ -- it, which leaves `expected` holding only what is actually compared.
18
+ --
19
+ -- `text` and NULLABLE, not `NOT NULL DEFAULT ''`:
20
+ --
21
+ -- * Every case stored before today genuinely has no description, and NULL is
22
+ -- what "nobody wrote one" looks like. A default of '' would claim all of them
23
+ -- had been described with an empty sentence, and a screen cannot tell that
24
+ -- apart from a description somebody cleared on purpose.
25
+ -- * No length CHECK, matching `eval_cases.name` beside it: the bound is a form
26
+ -- concern and lives with the form, in `@stackbone/validators` →
27
+ -- `studio-evals.ts`. A number repeated here is a second bound that drifts.
28
+ --
29
+ -- A separate file rather than an edit to 0038: the migrator hashes each body and
30
+ -- verifies it on every boot, so rewriting a landed migration bricks a box that
31
+ -- already ran it. Comments count towards that hash too.
32
+
33
+ ALTER TABLE stackbone_platform.eval_cases
34
+ ADD COLUMN IF NOT EXISTS description text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.2.10",
3
+ "version": "0.3.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {