@haus-tech/haus-workflow 4.7.0 → 4.9.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.
@@ -140,7 +140,9 @@
140
140
  "myid",
141
141
  "oidc",
142
142
  "azure-ad",
143
- "saml2"
143
+ "saml2",
144
+ "auth",
145
+ "next-auth"
144
146
  ],
145
147
  "intentsRequiredTags": [
146
148
  "stripe",
@@ -150,7 +152,9 @@
150
152
  "myid",
151
153
  "oidc",
152
154
  "azure-ad",
153
- "saml2"
155
+ "saml2",
156
+ "auth",
157
+ "next-auth"
154
158
  ],
155
159
  "alwaysAllowedTags": [
156
160
  "haus",
@@ -2,7 +2,7 @@
2
2
  name: request-classifier
3
3
  description: Kör request-classify-skillen isolerat på spec + påverkansanalys och returnerar klassnings-JSON enligt skillens kontrakt. Dispatchas av /request-skillen (uppgift bedöm, parallellt med request-scorer). Saknar medvetet sök- och shellverktyg (Grep/Glob/Bash); Read finns kvar för skillens referensfil och är promptbegränsad till skillkatalogen — därför redovisas varje läst sökväg i svaret. Underlaget kommer bytetroget i prompten; output är JSON-kuvertet plus svansraderna, inget annat.
4
4
  model: fable
5
- effort: max
5
+ effort: high
6
6
  tools: Skill, Read
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  name: request-scorer
3
3
  description: Kör request-score-skillen isolerat på spec + påverkansanalys och returnerar poäng-JSON med inringning enligt skillens kontrakt. Dispatchas av /request-skillen (uppgift bedöm, parallellt med request-classifier). Saknar medvetet sök- och shellverktyg (Grep/Glob/Bash); Read finns kvar för skillens referensfil och är promptbegränsad till skillkatalogen — därför redovisas varje läst sökväg i svaret. Underlaget kommer bytetroget i prompten; output är JSON-kuvertet plus svansraderna, inget annat.
4
4
  model: fable
5
- effort: max
5
+ effort: high
6
6
  tools: Skill, Read
7
7
  ---
8
8
 
@@ -5,7 +5,7 @@
5
5
  "id": "hook.guard.file-access",
6
6
  "gate": "keep",
7
7
  "event": "PreToolUse",
8
- "matcher": "Read|Edit|Write",
8
+ "matcher": "Read|Edit|Write|NotebookEdit",
9
9
  "command": "haus guard file-access --from-hook"
10
10
  },
11
11
  {
@@ -15,11 +15,12 @@ the agent; collect answers with AskUserQuestion and pass everything as flags.
15
15
  before anything else: "Add the new project as a member of this workspace, or scaffold it
16
16
  standalone elsewhere?"
17
17
 
18
- - **Add as workspace member** — scaffold under the workspace root (default folder: the
19
- project name, sibling to the other member repos) instead of the cwd. After Phase 3
20
- creates the repo, tell the user to add an entry for it to `haus.workspace.yaml` /
21
- `repos.manifest.json` themselves — this flow only scaffolds and does not write either
22
- file (no CLI command mutates them yet).
18
+ - **Add as workspace member** — pass `--dir <workspace-root>` in Phase 3 so the project
19
+ lands beside the other member repos (folder name: the project name) instead of nested
20
+ inside whatever repo this session is standing in. The cwd is the only other placement
21
+ lever, and it is not yours to change. After Phase 3 creates the repo, tell the user to
22
+ add an entry for it to `haus.workspace.yaml` / `repos.manifest.json` themselves — this
23
+ flow only scaffolds and does not write either file (no CLI command mutates them yet).
23
24
  - **Standalone** — proceed exactly as below, unaffected by the workspace.
24
25
 
25
26
  Skip this ask entirely when Step 1.5 found Project or Not-set-up scope — proceed straight
@@ -65,13 +66,16 @@ Check the registry first: `haus new --list --json`.
65
66
  **Stack in the registry (fast path):**
66
67
 
67
68
  1. Preview:
68
- `haus new <stack> <name> --json --yes --dry-run [--org <org>] [--no-github]`
69
+ `haus new <stack> <name> --json --yes --dry-run [--dir <path>] [--org <org>] [--no-github]`
69
70
  and show the planned steps in plain language. Phase 2's approval already covers
70
71
  running this for real — no further confirmation needed.
71
72
  2. Execute the same command without `--dry-run`. Parse the JSON:
72
73
  - `ok: true` → note project path, repo URL, and `installHint`.
73
- - `ok: false` → report the failed step's `detail` verbatim — it contains the exact
74
- resume commands. Do NOT retry destructively; the project directory is never
74
+ - `ok: false` → report the `detail` of every step whose status is not `done`,
75
+ verbatim — they contain the exact resume commands. A step may be `failed` (it did
76
+ not run), `skipped` (it did not happen, e.g. setup was declined), or `partial`
77
+ (it ran and part of it did not land, e.g. the config files were copied but the
78
+ follow-up push failed). Do NOT retry destructively; the project directory is never
75
79
  auto-deleted.
76
80
 
77
81
  **Stack not in the registry:**
@@ -109,9 +113,12 @@ stay there.
109
113
 
110
114
  ## Phase 5 — Build the walking skeleton
111
115
 
112
- 1. Install dependencies: run the install command for the chosen package manager (fast
113
- path: the `installHint` from the JSON — it does not exist off-registry). The test
114
- loop needs them installed either way.
116
+ 1. Install dependencies. On the fast path the JSON's `installHint` is written for the
117
+ parent directory — it starts with `cd <name>`, so running it verbatim from inside
118
+ the project (where Phase 4 left you) fails. From in here, run the bare command for the
119
+ chosen package manager instead: `yarn` / `npm install` / `pnpm install`. Off-registry
120
+ there is no hint; run the same bare command. The test loop needs them installed either
121
+ way, so confirm the install actually succeeded before continuing.
115
122
  2. Read the new project's `CLAUDE.md` and `.haus-workflow/WORKFLOW.md`; they are
116
123
  authoritative from here on.
117
124
  3. Decide the project's file/folder structure BEFORE dispatching any build agent:
@@ -21,6 +21,7 @@ respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen
21
21
  | `godkänn <nr eller gid>` | Flytta Offer status framåt via request-sync — se grindavsnittet |
22
22
  | `underkänn <nr eller gid> <skäl>` | Skäl som Asana-kommentar + iterationsräknare via request-sync |
23
23
  | `detaljer <gid, slug eller nr> [steg]` | Skriv ut arkiverad JSON/dokument för posten |
24
+ | `utvärdera <post…> [modell]` | A/B-kör hela flödet per modellvariant till experimentytan; `sammanställ` ger svepvyn |
24
25
  | `hjälp` | Förklara uppgifterna, rör inga filer |
25
26
 
26
27
  ## Hårda regler
@@ -50,6 +51,11 @@ respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen
50
51
  - **Arkivet är cache, Asana är sanningen.** Vid konflikt vinner brädan.
51
52
  - **Godkännande antas aldrig.** Att en spec finns i arkivet betyder inte att den
52
53
  är granskad. Se grinden under `bedöm`.
54
+ - **Experimentdata når aldrig Asana.** `utvärdera` skriver aldrig via
55
+ request-sync; dess enda Asana-kontakt är läsande hämtning av önskemålet i
56
+ huvudkonversationen. `experiments/`-trädet är inte postkataloger: sync,
57
+ batch och guard läser bara `.haus/requests/<gid-eller-slug>/` på toppnivån
58
+ och rör aldrig `experiments/`.
53
59
 
54
60
  ## Arkivet: `.haus/requests/`
55
61
 
@@ -94,6 +100,7 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
94
100
  "bundle": { "parent_slug": null, "part": null, "of": null },
95
101
  "split": { "parent": null, "part": null, "of": null },
96
102
  "approval": null,
103
+ "granskarutslag": [],
97
104
  "note": null,
98
105
  "runs": []
99
106
  }
@@ -106,14 +113,36 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
106
113
  `{ "source": "asana" | "manual", "by": "<namn, krävs vid manual>",
107
114
  "status_read": "<värde>", "date": "YYYY-MM-DD" }`.
108
115
  Anteckningen är spårbarhet, aldrig själva godkännandet.
116
+ - `granskarutslag` är en lista, en rad per viktningsutslag (granskaren
117
+ konstaterar att en körning hade rätt, utan att någon sakuppgift ändras).
118
+ `bedöm` steg 6 APPENDAR en rad, skriver ALDRIG över tidigare rader:
119
+ `{ "varv": N, "date": "YYYY-MM-DD", "beslut": "<granskarens beslut,
120
+ ordagrant>" }`. Tom lista (`[]`) när inget viktningsutslag ännu skett.
121
+ Listan är ett revisionsspår och läses medvetet ALDRIG tillbaka in i någon
122
+ prompt: utslaget är granskarens dom över körningarna, och når den agenterna
123
+ styr den nästa bedömning i stället för att dokumentera den föregående.
124
+ Nollas (töms) vid respec, av samma skäl som `approval` nollas (se `speca`
125
+ steg 5): ett utslag mot en ersatt spec är inte längre giltigt. Bor ENDAST
126
+ i `state.json`. Ett sakändringsutslag går i stället in i `impact.md` som
127
+ Granskarutslag-blocket (se `bedöm` steg 6), aldrig hit.
109
128
  - `runs[]` får en rad per dispatch, även spec- och analyskörningar:
110
129
  `{ "file": "spec.md" | "impact.md" | "<run>.json", "skill": "...",
111
130
  "agent": "...", "model": "<ur MODELL-raden>", "effort": "<ur agentdefinitionen>",
112
131
  "date": "YYYY-MM-DD",
113
132
  "reference_sha256": { "<referensfil>": "<sha256>" },
114
133
  "underlag_sha256": { "spec.md": "<sha256>", "impact.md": "<sha256>" },
115
- "double_run": "bekräftad" | "olika" | null,
116
- "superseded_by": null }`.
134
+ "double_run": "bekräftad" | "olika" | "tolerans" | null,
135
+ "superseded_by": null, "partial_rerun": false, "pairs_with": null,
136
+ "quote_repairs": <antal> }`.
137
+ `quote_repairs` är frivilligt (utelämnas när det är 0): antal RÄTTAT-rader
138
+ från `quote-repair.mjs` för den körningen.
139
+ `partial_rerun` och `pairs_with` är frivilliga och sätts bara på en
140
+ pricing-rad ur ett riktat varv (se `bedöm` steg 6): `partial_rerun: true`
141
+ markerar att raden kom ur en enaxlig omkörning, och `pairs_with` namnger
142
+ den stående axelns fil den paras med. Pekaren bor på den omkörda axelns
143
+ nya rad, aldrig på den stående axelns äldre rad, som aldrig skrivs om.
144
+ Saknas (eller står `false`/`null`) på spec- och analysrader och på rader
145
+ ur ett fullt varv.
117
146
  `underlag_sha256` är underlagsproveniensen: hashen för de dokument körningen
118
147
  faktiskt såg. Precheck vägrar bedöma när analysens spec-hash inte matchar
119
148
  nuvarande `spec.md`, och guard ska vägra diffa mot en körning vars underlag
@@ -127,10 +156,45 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
127
156
  eller `~/.claude`). En senare guard-körning jämför hash och vägrar diffa vid
128
157
  skevhet ("omklassning krävs, skalan har ändrats").
129
158
  - `status` blir ALDRIG `priced` när något stopputfall finns i varvets
130
- körningar, eller när dubbelkörningen gav OLIKA (`double_run: "olika"`).
159
+ körningar, eller när det GÄLLANDE paret har `double_run: "olika"` olyft.
160
+ Spärren gäller det gällande paret, inte varje enskild rad för sig: har ett
161
+ senare riktat varv landat LIKA eller LIKA_TOLERANS, är spärren lyft för
162
+ hela paret, även om den oeniga axelns äldre rader fortfarande bär
163
+ `double_run: "olika"` (de skrivs aldrig om, se `partial_rerun`/
164
+ `pairs_with` ovan). Läs upplåsningen på den omkörda axelns senaste rad,
165
+ aldrig på en äldre. `double_run: "tolerans"` räknas som bekräftad och
166
+ blockerar inte `priced`.
131
167
  - `spec_synced` sätts av speca steg 7 (posten lagd på brädan);
132
168
  `guard_run` reserveras för guard-kommandot (senare byggsteg).
133
169
 
170
+ ### Experimentytan: `experiments/`
171
+
172
+ A/B-körningar från `utvärdera` bor i ett eget träd och är aldrig prisposter:
173
+
174
+ ```
175
+ <repo-rot>/.haus/requests/experiments/<slug>/
176
+ ├── experiment.json ticketreferens, datum, kandidatmodell,
177
+ │ modell per agent per varv, manifest-hash, status
178
+ ├── jamforelse.md skrivs när båda varven är klara
179
+ ├── baseline/ full standardstruktur enligt arkivkontraktet ovan
180
+ └── <kandidatmodell>/ samma struktur; delkataloger del-N/ vid DELA_UPP
181
+ ```
182
+
183
+ - Katalognamnet `experiments` är reserverat — en spec vars rubrik skulle
184
+ sluggas till `experiments` får suffixet `-post`.
185
+ - Variantkatalogerna följer postkatalogens kontrakt bytetroget (spec.md,
186
+ granskarblock.md, impact.md, pricing-JSON, state.json, `runs[]`-reglerna) —
187
+ därför fungerar `precheck.mjs` och `contract-check.mjs` oförändrade, och
188
+ proveniensen (modell, effort, hashar) följer med gratis.
189
+ - `state.json` i en variantkatalog bär markören `"experiment": true`.
190
+ `approval` förblir `null` och `status` blir ALDRIG `priced` — ett
191
+ experimentresultat kan inte förväxlas med en prispost.
192
+ - `experiment.json` bär `status` per variant: `"klar"` (båda stegen
193
+ arkiverade och jämförelse-underlag komplett), `"ofärdig"` (avbruten,
194
+ subagentdöd, kontraktsbrott eller MODELL-avvikelse), `"stopp"` (arkiverat
195
+ stopputfall).
196
+ - `experiments/` ärver gitignore-skyddet från `.haus/requests/`.
197
+
134
198
  ## Uppgift: `speca`
135
199
 
136
200
  1. **Underlag.** Rått önskemål inklistrat, eller ett gid — hämta då posten via
@@ -158,9 +222,10 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
158
222
  tomt) i varje berörd state.json.
159
223
  **Respec över befintlig katalog invaliderar analysen:** när `spec.md`
160
224
  ersätts döps ev. `impact.md` om till `impact-superseded-<datum>.md`,
161
- status sätts till `spec_draft` och `approval` nollas — en ny spec är
162
- ogranskad per definition, och en gammal analys mot ny spec är exakt den
163
- skevhet proveniens-kontrollen finns för att stoppa.
225
+ status sätts till `spec_draft`, `approval` nollas och `granskarutslag`
226
+ töms — en ny spec är ogranskad per definition, ett utslag mot en ersatt
227
+ spec är inte längre giltigt, och en gammal analys mot ny spec är exakt
228
+ den skevhet proveniens-kontrollen finns för att stoppa.
164
229
  6. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog>`
165
230
  per katalog. BRISTER i spec/granskarblock → dispatcha om specifiern EN gång
166
231
  enligt omdispatch-regeln; BRISTER i `state.json` är wrapperns egna filer och
@@ -217,7 +282,19 @@ oenighetsgrind.
217
282
  5. **Kontraktskontroll.** Strippa svansraderna (`MODELL:`, `LÄSTA:`, ev.
218
283
  `AVVIKELSE:`) ur varje svar, skriv den rena JSON:en till en temporär fil och
219
284
  kör `node <skillkatalog>/scripts/contract-check.mjs <svar.json> <spec.md> <impact.md> --expect-id <katalogens id>`.
220
- BROTT → dispatcha om just den agenten EN gång enligt omdispatch-regeln;
285
+ BROTT → kör först
286
+ `node <skillkatalog>/scripts/quote-repair.mjs <svar.json> <spec.md> <impact.md>`
287
+ (deterministisk citaträttning: ett nästan-ordagrant belägg ersätts med sin
288
+ entydigt närmaste källmening, allt annat lämnas orört; utan citatbrott är
289
+ scriptet en no-op). Exit 0: kör kontraktskontrollen igen och anteckna
290
+ `quote_repairs: <antal RÄTTAT-rader>` i runs-raden.
291
+ **VARJE RÄTTAT-rad (före → efter) visas dessutom ordagrant för användaren i
292
+ BLUF-svaret**, på samma sätt som en `AVVIKELSE:`-rad alltid visas. En
293
+ räknare går inte att granska: rättningen byter belägg i en bedömning som
294
+ redan är gjord, och den som läser svaret ska kunna se exakt vilket citat som
295
+ byttes mot vilken källmening. Aldrig bara antalet. Kvarstår BROTT efter
296
+ rättningen, eller gav rättningen exit 1 (orättbara citat): dispatcha om just
297
+ den agenten EN gång enligt omdispatch-regeln;
221
298
  kvarstår brott, visa dem och stanna utan att arkivera det svaret som giltigt
222
299
  varv — men släng det inte: arkivera det som `<typ>-pricing-vN-avvisad.json`
223
300
  (utanför N-räkningen) med kontrollens felrader i en runs-rad, så även
@@ -245,15 +322,124 @@ oenighetsgrind.
245
322
  `detaljer` läser alltid filen utan suffix.
246
323
  - **Någon OLIKA** → behandla posten som ESKALERA. Det beskriver routingen,
247
324
  aldrig artefakten: ingen fil ändras, ingen JSON skrivs om, inget
248
- ESKALERA-värde patchas in. Arkivera båda paren (körning 2 som `-run2`),
249
- sätt `double_run: "olika"` och `superseded_by: null` på alla berörda
250
- runs-rader, status `assessed`. Visa diffen (rad · körning 1 · körning 2)
325
+ ESKALERA-värde patchas in. Arkivera båda körningarna för båda axlarna
326
+ (den som inte blir axelns huvudfil arkiveras som `-run2`) och stämpla
327
+ runs-raderna PER AXEL, aldrig per
328
+ varv. Det finns exakt en regel, och den går efter vad axelns eget
329
+ jämförelsescript skrev:
330
+ - **Den OENIGA axeln** (den vars script skrev OLIKA) får
331
+ `double_run: "olika"` och `superseded_by: null` på bägge sina
332
+ runs-rader. Körning 1 är huvudfil utan suffix och körning 2 arkiveras
333
+ som `-run2`, men ingen av dem är utslagen: oenigheten är
334
+ oavgjord tills en människa eller ett riktat varv avgör den, och
335
+ `superseded_by` förblir därför `null` på båda.
336
+ - **Den ENIGA axeln** behåller sitt vanliga utfall och sitt vanliga
337
+ `superseded_by`: LIKA ger `double_run: "bekräftad"` med körning 1 som
338
+ suffixfri huvudfil, LIKA_TOLERANS ger `double_run: "tolerans"` med
339
+ verdiktradens utpekade körning (högsta poängen) som suffixfri
340
+ huvudfil och den andra som `-run2` med `superseded_by` mot den, precis
341
+ enligt LIKA_TOLERANS-punkten nedan, oavsett vilken av dem som var
342
+ körning 1. Regeln "högsta poängen gäller" är densamma oavsett vilken
343
+ punkt som routar posten.
344
+
345
+ Arkivet namnger därmed vilken axel som var oenig: `"olika"` står bara på
346
+ den axelns rader. Jämförelsescriptens verdikter lever bara i stdout och
347
+ försvinner med sessionen, så utan den stämplingen kunde ett senare varv
348
+ köra om fel axel och släppa igenom en oavgjord klassflip till pris.
349
+ Var BÅDA axlarna OLIKA bär bägge axlarnas rader `"olika"`.
350
+ Status blir `assessed`. Visa diffen (rad · körning 1 · körning 2)
251
351
  för granskaren. Inget `customer`-block ur någon av körningarna får gå
252
- till request-sync så länge `double_run` står på `"olika"`. Välj ALDRIG
253
- en av körningarna själv och räkna ALDRIG fram ett medelvärde.
254
- **Återställningen är ett nytt varv:** efter granskarens beslut (justera
255
- underlaget, eller konstatera att en körning hade rätt) körs `bedöm` om —
256
- spärren sitter på varvets rader, ett nytt varv med LIKA låser upp.
352
+ till request-sync medan det GÄLLANDE paret har `double_run: "olika"`
353
+ olyft (samma scope som `priced`-spärren, se state.json-reglerna ovan):
354
+ har ett senare riktat varv landat LIKA eller LIKA_TOLERANS är spärren
355
+ lyft för hela paret, även om den oeniga axelns äldre rader fortfarande
356
+ bär `"olika"`. Läs upplåsningen på den omkörda axelns senaste rad,
357
+ aldrig på en äldre. Välj ALDRIG en av körningarna själv och räkna
358
+ ALDRIG fram ett medelvärde.
359
+ **Återställningen är ett riktat nytt varv:** spärren (`double_run:
360
+ "olika"`) sitter på den oeniga axelns rader, och ett nytt varv med LIKA
361
+ (eller LIKA_TOLERANS) låser upp hela paret. VILKEN axel som körs om är
362
+ ingen bedömning: det är den axel vars runs-rader bär `"olika"`.
363
+ **Den omkörda axeln dubbelkörs ALLTID i ett riktat varv**, oavsett
364
+ triggervillkoren i steg 6: spärren kan bara lyftas av ett
365
+ jämförelseutslag, och en ensam körning ger inget att jämföra mot. En
366
+ klassomkörning som landar SMALL med stående poäng under 4 skulle annars
367
+ aldrig få något utslag och raderna förbli låsta för alltid. Hur MÅNGA
368
+ axlar som körs om beror på vilket slags utslag granskaren gav:
369
+ - **Viktningsutslag** (granskaren konstaterar att en körning hade rätt,
370
+ utan att någon sakuppgift ändras): appendas som en ny rad i
371
+ `state.json.granskarutslag` (se ovan), skriver ALDRIG över en
372
+ tidigare rad, och går ALDRIG in i `impact.md`. Se
373
+ Granskarutslag-punkten nedan för varför en fri text i underlaget vore
374
+ fel. Underlaget är därmed oförändrat, så kör ENDAST den axel om
375
+ (klass eller poäng) som var OLIKA, och bara den omkörda axelns agent
376
+ dispatchas, aldrig båda. Triggerns villkor läses fortfarande mot den
377
+ omkörda axelns nya svar plus den stående axelns gällande fil, aldrig
378
+ mot ett nytt körningspar, men det är inte triggern som avgör om axeln
379
+ dubbelkörs: i ett riktat varv dubbelkörs den alltid. Var BÅDA axlarna
380
+ OLIKA finns ingen enig axel att hålla stilla: kör då BÅDA om ändå,
381
+ `partial_rerun` förblir `false` på bägge nya rader. Vid enaxlig
382
+ omkörning paras den stående axelns gällande fil med det nya resultatet
383
+ (se steg 8) utan att skrivas om. Landar det riktade varvet
384
+ LIKA/LIKA_TOLERANS lyfts spärren för hela paret, antecknat på den
385
+ omkörda axelns nya runs-rad (`partial_rerun: true`, `pairs_with` mot
386
+ den stående filens namn). Pekaren bor på den nya raden, den stående
387
+ axelns rad skrivs aldrig om, så guard och `detaljer` läser
388
+ upplåsningen via `pairs_with` på den nya raden, aldrig tvärtom.
389
+ - **Sakändringsutslag**, eller ett beslut som av annat skäl måste stå i
390
+ underlaget (se Granskarutslag-punkten nedan): appendas i `impact.md`,
391
+ och vid en faktisk sakändring körs assessorn om (ny `impact.md`).
392
+ Underlaget har då ändrats, en körning mot gammalt underlag får aldrig
393
+ paras med en mot nytt, `underlag_sha256` skulle skilja och
394
+ proveniens-kontrollen finns just för den skevheten, så kör BÅDA
395
+ axlarna om, `partial_rerun` `false` på båda nya rader.
396
+
397
+ - **Granskarutslag skrivs som citat, aldrig som prosa.** När granskarens
398
+ beslut ska in i underlaget, alltså när det ändrar eller preciserar en
399
+ sakuppgift i analysen, eller av annat skäl måste stå i underlaget för
400
+ spårbarhet: appendas det sist i `impact.md` som ett avgränsat block
401
+ enligt mallen nedan, beslutet ordagrant citerat, aldrig omformulerat av
402
+ dispatchern och aldrig invävt i analysens löptext. En fritt formulerad
403
+ utslagsmening läses av nästa körning som sakuppgift och styr om
404
+ klassningen, det var precis felet i pilotkörningarna, därför alltid
405
+ mallen, aldrig fri prosa. Kräver beslutet en faktisk sakändring i
406
+ analysen: kör om assessorn (ny `impact.md`). Underlaget har i båda
407
+ fallen ändrats, så `bedöm` kör om BÅDA axlarna (se riktade-varv-stycket
408
+ ovan). Ett rent viktningsutslag, där granskaren bara konstaterar att en
409
+ körning hade rätt utan att röra någon sakuppgift, hör ALDRIG hemma här:
410
+ det appendas i stället som en rad i `state.json.granskarutslag`, se
411
+ samma stycke, just för att underlaget ska förbli oförändrat och den
412
+ riktade omkörningen giltig. Blocket är internt underlag med samma status som
413
+ Till granskaren-blocket och följer aldrig med i kundtext.
414
+
415
+ ```
416
+ ## Granskarutslag (varv <N>, <YYYY-MM-DD>)
417
+
418
+ > <granskarens beslut, ordagrant>
419
+
420
+ Utslaget gäller endast viktningen mellan körningarna och ändrar inga
421
+ sakuppgifter i analysen ovan.
422
+ ```
423
+
424
+ - **Klassjämförelsen LIKA men poängjämförelsen skriver LIKA_TOLERANS**
425
+ (poängskillnad ≤ 0,5 med samma outcome och konfidens; klassjämförelsen
426
+ har ingen tolerans, en klassflip är alltid OLIKA och routar posten som
427
+ ESKALERA oavsett poängens tolerans, se föregående punkt) → paret räknas
428
+ som bekräftat. Bytet gäller ENDAST poängaxelns filer: klassaxeln följer
429
+ LIKA-regeln som vanligt, klass-körning 1-filen blir huvudfil utan suffix
430
+ och klass-körning 2 arkiveras som `-run2` med `superseded_by`. För
431
+ poängaxeln blir verdiktradens utpekade körning (högsta poängen)
432
+ huvudfilen `score-pricing-vN.json`, och den andra arkiveras som `-run2`
433
+ med `superseded_by` satt mot huvudfilen, oavsett vilken av dem som var
434
+ körning 1. N är fortfarande gemensamt för de axlar som kördes i varvet
435
+ (här: klass och poäng, båda).
436
+ Huvudfilerna kan alltså komma från olika körningar, till exempel klass ur
437
+ körning 1 och poäng ur körning 2. Det är avsiktligt: båda körningarna har
438
+ passerat kontraktskontrollen och skillnaden ligger inom toleransen. Sätt
439
+ `double_run: "tolerans"` på poängaxelns bägge runs-rader; klassaxelns
440
+ runs-rader får `double_run: "bekräftad"` som vanligt. Toleransen är en
441
+ medveten prisregel: högsta värdet skyddar marginalen, och ett medelvärde
442
+ räknas fortfarande aldrig fram. Visa toleransraderna i BLUF-svaret.
257
443
  - Exit 2 från ett jämförelsescript är ett dispatcherfel (ojämförbara
258
444
  körningar) — visa felet och stanna, rapportera aldrig LIKA.
259
445
  7. **Oenighetsgrind klass ↔ poäng.** Deterministiska regler, i ordning:
@@ -264,16 +450,25 @@ oenighetsgrind.
264
450
  ankarbanden — klass TINY/SMALL med poäng ≥ 4, klass MEDIUM med poäng
265
451
  ≤ 0.75 eller ≥ 13, klass LARGE/OFFERT med poäng ≤ 3. Flaggan är
266
452
  information till granskaren — ingen automatisk åtgärd, ingen omkörning.
267
- 8. **Arkivera** godkända JSON-svar. Paret arkiveras atomärt: båda eller inget,
268
- och `N` i `-pricing-vN.json` är gemensamt (nästa N = högsta befintliga för
269
- någon av filtyperna + 1; `-run2`- och `-avvisad`-filer räknas aldrig;
453
+ 8. **Arkivera** godkända JSON-svar. Paret arkiveras atomärt: båda eller inget
454
+ ("båda eller inget" gäller de axlar som kördes i varvet: vid riktad
455
+ omkörning arkiveras den omkörda axelns fil ensam, och dess runs-rad får
456
+ `partial_rerun: true` samt `pairs_with: "<den stående filens namn>"`.
457
+ Pekaren bor på den nya raden, aldrig på den stående axelns äldre rad, som
458
+ aldrig skrivs om, så guard och `detaljer` hittar paret genom att läsa den
459
+ nya raden),
460
+ och `N` i `-pricing-vN.json` är gemensamt för de axlar som kördes i
461
+ varvet (nästa N = högsta befintliga för någon av filtyperna + 1, räknat
462
+ bland de filtyper som kördes; vid riktad omkörning bumpar alltså bara den
463
+ omkörda filtypens N; `-run2`- och `-avvisad`-filer räknas aldrig;
270
464
  guard-filer har en EGEN N-sekvens, skild från pricing). Uppdatera
271
465
  `state.json.runs` med modell, effort, referens-hash och `underlag_sha256`
272
466
  (hash för `spec.md` och `impact.md` som körningen såg) per fil, status →
273
467
  `priced` (eller `assessed` vid stopputfall).
274
468
  9. **Svara BLUF:** tabell `post · klass · poäng · utfall · konfidens`, en rad
275
469
  per avvikelse värd att se (oenighetsflagga, ESKALERA, DELA_UPP,
276
- INPUT_SAKNAS, AVVIKELSE-rader). JSON:en visas inte i chatten —
470
+ INPUT_SAKNAS, AVVIKELSE-rader, samt varje RÄTTAT-rad från citaträttningen
471
+ enligt steg 5). JSON:en visas inte i chatten —
277
472
  `detaljer <post>` gör det. Skriv-tillbaka till brädan sker via request-sync
278
473
  i huvudkonversationen, bara på uttrycklig begäran — och det är den flytten
279
474
  som tar posten ✅ Spec ok → ⚖️ Poängsättning redo för granskning enligt
@@ -336,9 +531,95 @@ steg en lista över vad som finns, med steg (`spec`, `granskarblock`, `analys`,
336
531
  `klass`, `poäng`, `state`) filens innehåll rått. För `klass`/`poäng`: senaste
337
532
  `-pricing-vN.json` utan suffix (aldrig `-run2`/`-avvisad`).
338
533
 
534
+ ## Uppgift: `utvärdera`
535
+
536
+ A/B-utvärdering av modellvalet: kör en post genom hela flödet två varv —
537
+ `baseline` med agentdefaults och ett varv med alla agenter tvingade till en
538
+ kandidatmodell (default `opus`) — och arkivera all output sida vid sida under
539
+ experimentytan. Uppgiften samlar underlag; människan analyserar. Ingen dom
540
+ fälls över vilken modell som är bäst.
541
+
542
+ 1. **Underlag.** Som `speca` steg 1: rått önskemål eller gid — vid gid hämtas
543
+ posten läsande via request-sync i huvudkonversationen, uppgiftens ENDA
544
+ Asana-kontakt. Skapa `experiments/<slug>/` och `experiment.json` med
545
+ ticketreferens, datum och kandidatmodell.
546
+ 2. **Källmanifest EN gång.** Bygg manifestet som i `speca` steg 2 och
547
+ återanvänd det byteidentiskt i båda varven. Anteckna manifest-hash i
548
+ `experiment.json` (`ref-hash.mjs` på manifesttexten). Enda avsedda
549
+ skillnaden mellan varven är modellen.
550
+ 3. **Eskaleringsvalet görs en gång** enligt `speca`:s regler och gäller båda
551
+ varven — även eskaleringsvalet är en del av det som hålls konstant.
552
+ 4. **Varv `baseline`.** Kör speca-stegen (dispatch, kontraktskontroll,
553
+ arkivering) och därefter bedöm-stegen (analys, klassning ∥ poäng,
554
+ dubbelkörning, citaträttning, riktad omkörning) enligt ordinarie
555
+ uppgifter, men mot variantkatalogen `baseline/` och utan
556
+ godkännandegrind: `approval` förblir `null`, `state.json` får
557
+ `"experiment": true`. Dispatcherna använder ordinarie promptmallar och
558
+ agenternas defaults — ingen override. `REQUEST_ID` och `--expect-id`
559
+ använder alltid ticketens gid/slug, aldrig variantkatalogens namn
560
+ (`baseline`/modellnamnet).
561
+ 5. **Varv `<kandidatmodell>`.** Exakt samma steg, prompter och underlag, men
562
+ varje Agent-anrop får modell-override till kandidatmodellen.
563
+ Agentdefinitionerna röres aldrig; effort tas ur definitionen som vanligt.
564
+ Verifiera via MODELL-svansraden att overriden slog igenom och redovisa
565
+ avvikelsen om den finns. Generellt för hela uppgiften: subagentdöd,
566
+ kontraktsbrott (kvarstående BROTT efter citaträttning) eller
567
+ MODELL-avvikelse — i vilket steg det än inträffar — sätter variantens
568
+ `status` till `"ofärdig"` i `experiment.json`.
569
+ 6. **DELA_UPP körs autonomt.** Ett DELA_UPP-utfall följer bedöms ordinarie
570
+ delningsregler men utan paus för granskare: delkataloger
571
+ `<variant>/del-1/ … del-N/` med `split` satt, förälderns `spec.md` +
572
+ `granskarblock.md` kopierade in, ny analys + klassning + poäng per del.
573
+ En del utan tak blir `OVER_SKALAN` och delas aldrig vidare. Övriga
574
+ stopputfall (`TILLBAKA_TILL_SPEC`, `KONCEPT`, `DIAGNOS`, `ESKALERA`,
575
+ `INPUT_SAKNAS`) kräver människa eller kund: arkivera utfallet, avbryt det
576
+ varvet och redovisa i jämförelsen — det andra varvet fortsätter oberoende.
577
+ 7. **Jämförelse.** När båda varven är klara, skriv
578
+ `experiments/<slug>/jamforelse.md`: delningsbeslutet per variant (delade
579
+ sig / delade sig inte), tabell per post/delpost med klass, poäng,
580
+ inringning, dubbelkörningsutfall, stopputfall och `quote_repairs` per
581
+ variant, samt kort notis om specskillnader (rubrik, omfång, Ingår
582
+ inte-avvikelser). Ingen värdering av vilken variant som är bättre —
583
+ rådatan per katalog är analysunderlaget.
584
+ 8. **Rapport.** BLUF i chatten: klart/ofärdigt per varv, stopputfall, och
585
+ sökvägen till `experiments/<slug>/`. Ingen skrivning mot Asana.
586
+
587
+ ### Batch: flera poster
588
+
589
+ `utvärdera <post1> <post2> … [kandidatmodell]` kör varje ticket i en EGEN
590
+ headless-process, sekventiellt:
591
+
592
+ ```
593
+ claude -p "/request utvärdera <gid> <kandidatmodell>"
594
+ ```
595
+
596
+ - **En ticket = ett färskt kontextfönster.** Allt tillstånd bor i arkivet på
597
+ disk; huvudsessionen samlar bara exitstatus och en radssammanfattning per
598
+ ticket. `/clear` mellan tickets behövs inte — processgränsen är
599
+ motsvarigheten. Headless-körningen förutsätter att uppgiften kan köra utan
600
+ interaktiva permission-frågor, vilket håller: den läser repot och skriver
601
+ enbart under `.haus/requests/` — plus EN läsande Asana-hämtning per ticket
602
+ (steg 1), så MCP-behörigheten för läsning måste täcka headless-körning.
603
+ - **Kör vidare vid fel.** En ticket vars gid inte går att hämta eller vars
604
+ process kraschar noteras i sammanfattningen; resten av listan fortsätter.
605
+ - **Återupptagbart:** innan ett varv startas kontrolleras arkivet —
606
+ skippet styrs av variantens `status` i `experiment.json`: `"klar"` eller
607
+ `"stopp"` hoppas över med en notis, `"ofärdig"` körs om från varvets
608
+ början. Varven är billiga relativt risken att lappa ihop halva körningar.
609
+ Ett svep över många tickets kan därmed fortsätta i en senare session utan
610
+ omkörningar.
611
+
612
+ ### Sammanställning
613
+
614
+ `utvärdera sammanställ` läser alla `experiments/*/` och skriver
615
+ `experiments/sammanstallning.md`: tabell per ticket med klass/poäng per
616
+ variant, delnings- och stopputfall, och avvikelsemarkering där varianterna
617
+ skiljer sig. Halvfärdiga kataloger märks `ofullständig`, aldrig tyst
618
+ utelämnade. Bara läsning — inga agentdispatcher, inga Asana-anrop.
619
+
339
620
  ## Mekaniska kontroller: `scripts/`
340
621
 
341
- Sex deterministiska script (fyra kontroller + `lib.mjs` med de delade
622
+ Sju deterministiska script (fem kontroller + `lib.mjs` med de delade
342
623
  byggstenarna + `ref-hash.mjs` för stabil hashning) ersätter modellkörningar
343
624
  där kontrollen är ren form —
344
625
  billigare, snabbare och utan omdömesglidning. Varje script bär sha256 för de
@@ -361,9 +642,20 @@ referensfiler vars regler det speglar och VARNAR när en katalogbump ändrat dem
361
642
  om 0.8-gränsen. Skriver `LIKA` eller `OLIKA` + diffrader, exit 0/1; exit 2 =
362
643
  ojämförbara körningar (dispatcherfel, aldrig LIKA).
363
644
  - `scripts/compare-scores.mjs <a.json> <b.json>` — samma för poäng: outcome,
364
- score, confidence, golv- och takankare. Aldrig prosa. Samma exit-semantik.
365
- Båda compare-scripten kräver mode `pricing`; guard-jämförelser är en medveten
366
- utbyggnad i guard-steget.
645
+ score, confidence, golv- och takankare. Aldrig prosa. Skriver `LIKA`,
646
+ `LIKA_TOLERANS` (poängskillnad ≤ 0,5 med samma outcome och confidence, och
647
+ ankarband som överlappar eller angränsar; exit 0, verdiktraden pekar ut
648
+ körningen med högst poäng) eller `OLIKA` + diffrader; i övrigt samma
649
+ exit-semantik. Båda compare-scripten kräver mode `pricing`; guard-jämförelser
650
+ är en medveten utbyggnad i guard-steget.
651
+ - `scripts/quote-repair.mjs <run.json> <spec.md> [impact.md]`: deterministisk
652
+ citaträttning som körs FÖRE omdispatch när contract-check fällt ett citat: ett
653
+ nästan-ordagrant belägg (eller citattecken-omgärdat driver-segment) ersätts med
654
+ sin entydigt närmaste källmening, allt annat lämnas orört. Rättar aldrig över
655
+ en polaritetsskillnad (negationsord) och skriver allt eller inget, så en
656
+ halvrättad fil aldrig arkiveras. Exit 0 = inga orättbara citat kvar (rättade
657
+ eller inga), 1 = orättbara kvar och filen orörd (omdispatch), 2 =
658
+ användningsfel eller oläsbar input (dispatcherfel).
367
659
 
368
660
  Kontrollerna bedömer aldrig innehåll och returnerar aldrig protokollutfall —
369
661
  en post som passerar kan fortfarande stoppas av protokollen själva, och det är
@@ -6,9 +6,17 @@
6
6
  * prosa) och inringningens golv- och takankare. Prosa (why, drivers,
7
7
  * rationale) jämförs ALDRIG.
8
8
  *
9
- * Skriver LIKA (exit 0) eller OLIKA + diffrader (exit 1) på stdout; varningar
10
- * går på stderr. Vid OLIKA väljer dispatchern ALDRIG en körning och räknar
11
- * ALDRIG medelvärde — posten eskaleras till granskaren med båda inringningarna.
9
+ * Toleransregel: en poängskillnad ≤ 0,5 med samma outcome och confidence är
10
+ * prisbrus, inte oenighet. En ankarskillnad tolereras dessutom bara när banden
11
+ * överlappar eller angränsar; icke-angränsande band är hård diff även vid samma
12
+ * poäng. LIKA_TOLERANS hör till LIKA-familjen (exit 0) med
13
+ * en deterministisk vinnare (högsta poängen, skyddar marginalen). Den
14
+ * skriver aldrig om en fil och räknar aldrig fram ett medelvärde.
15
+ *
16
+ * Skriver LIKA, LIKA_TOLERANS (exit 0) eller OLIKA + diffrader (exit 1) på
17
+ * stdout; varningar går på stderr. Vid OLIKA väljer dispatchern ALDRIG en
18
+ * körning och räknar ALDRIG medelvärde — posten eskaleras till granskaren med
19
+ * båda inringningarna.
12
20
  *
13
21
  * Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
14
22
  * rälsen bor i lib.mjs och delas med compare-classifications.
@@ -37,14 +45,56 @@ if (a.outcome !== b.outcome) diffs.push(`outcome: ${show(a.outcome)} ↔ ${show(
37
45
  // säkerhet är information granskaren ska se, precis som klassningens konfidenssida.
38
46
  if (a.confidence !== b.confidence)
39
47
  diffs.push(`confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}`)
48
+ // Toleransregel (beslut 2026-08-28, ADR-0078): en poängskillnad inom 0,5 med
49
+ // samma outcome och confidence är prisbrus, inte oenighet. Båda pilotfallens
50
+ // eskaleringar gällde Δ = 0,25 och kostade ett helt nytt varv styck. Högsta
51
+ // poängen gäller (skyddar marginalen); ett medelvärde räknas fortfarande aldrig.
52
+ const TOLERANS = 0.5
53
+
54
+ /**
55
+ * Ankarnummer på skalan A1..A17. Golv null (under-A1-fallet) läses som 0, alltså
56
+ * steget under A1. Ett ankare som inte går att tolka ger null och behandlas som
57
+ * icke-angränsande, så en oläslig inringning aldrig kan tystas som brus.
58
+ */
59
+ const ankarNr = (v) => {
60
+ if (v === null || v === undefined) return 0
61
+ const m = /^A(\d+)$/.exec(String(v))
62
+ return m ? Number(m[1]) : null
63
+ }
64
+ /**
65
+ * Inringningarna får toleransbehandling bara när de faktiskt pekar på samma del
66
+ * av skalan: överlappande eller angränsande band. Två körningar som landar på
67
+ * samma poäng men ringar in A5/A6 respektive A11/A12 är inte prisbrus, det är
68
+ * precis den drift dubbelkörningen finns för att visa, även när summan råkar bli
69
+ * densamma.
70
+ */
71
+ const nastanSammaBand = (fa, ta, fb, tb) => {
72
+ const [l1, h1, l2, h2] = [ankarNr(fa), ankarNr(ta), ankarNr(fb), ankarNr(tb)]
73
+ if ([l1, h1, l2, h2].some((n) => n === null)) return false
74
+ return l1 <= h2 + 1 && l2 <= h1 + 1
75
+ }
76
+
77
+ const tolerans = []
40
78
  if (a.outcome === 'POANG' && b.outcome === 'POANG') {
41
- if (a.score !== b.score) diffs.push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
79
+ const delta = Math.abs(a.score - b.score)
80
+ const inomTolerans = Number.isFinite(delta) && delta <= TOLERANS
81
+ if (a.score !== b.score)
82
+ (inomTolerans ? tolerans : diffs).push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
42
83
  const fa = a.bracket?.floor?.anchor ?? null
43
84
  const fb = b.bracket?.floor?.anchor ?? null
44
85
  const ta = a.bracket?.ceiling?.anchor ?? null
45
86
  const tb = b.bracket?.ceiling?.anchor ?? null
46
- if (fa !== fb) diffs.push(`bracket.floor.anchor: ${show(fa)} ↔ ${show(fb)}`)
47
- if (ta !== tb) diffs.push(`bracket.ceiling.anchor: ${show(ta)} ↔ ${show(tb)}`)
87
+ const ankarDiffs = []
88
+ if (fa !== fb) ankarDiffs.push(`bracket.floor.anchor: ${show(fa)} ↔ ${show(fb)}`)
89
+ if (ta !== tb) ankarDiffs.push(`bracket.ceiling.anchor: ${show(ta)} ↔ ${show(tb)}`)
90
+ ;(inomTolerans && nastanSammaBand(fa, ta, fb, tb) ? tolerans : diffs).push(...ankarDiffs)
48
91
  }
49
92
 
50
- reportDiffs(diffs)
93
+ if (diffs.length === 0 && tolerans.length > 0) {
94
+ const winner = b.score > a.score ? 2 : 1
95
+ console.log(`LIKA_TOLERANS: högsta gäller ${Math.max(a.score, b.score)} (körning ${winner})`)
96
+ for (const t of tolerans) console.log(`- ${t}`)
97
+ process.exit(0)
98
+ }
99
+ // Hårda diffar visas tillsammans med toleransraderna: granskaren ska se helheten.
100
+ reportDiffs([...diffs, ...tolerans])