@haus-tech/haus-workflow 4.5.0 → 4.7.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,446 @@
1
+ ---
2
+ name: request
3
+ description: Enda ingången till Haus requestflöde. Använd när användaren säger "/request", "kör requestflödet", "speca och bedöm", "prisa den här posten", "ta önskemålet hela vägen", "vad ligger i kön", "detaljer <post>", eller när flera request-skills ska kedjas i en följd. Enskilda request-skills (request-write-spec, request-classify m.fl.) fungerar fortfarande fristående; den här skillen äger kedjan, arkivet och dispatchen till subagenter.
4
+ ---
5
+
6
+ # /request — requestflödets ingång
7
+
8
+ Tunn orkestrering ovanpå de fem request-skillsen. Den här skillen innehåller
9
+ **noll domänregler**: allt om specformat, klassning, poäng och Asana-fält bor i
10
+ respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen.
11
+
12
+ ## Uppgifter
13
+
14
+ `/request [uppgift]` — utan argument: visa menyn nedan.
15
+
16
+ | Uppgift | Gör |
17
+ | -------------------------------------- | ---------------------------------------------------------------------------------------- |
18
+ | `speca <önskemål eller gid>` | Källmanifest → dispatcha `request-specifier` → arkivera spec + granskarblock |
19
+ | `bedöm <gid, slug eller nr>` | Grind → säkra analys → dispatcha `request-classifier` ∥ `request-scorer` → arkivera JSON |
20
+ | `status` | Kö-läge via request-sync, BLUF-tabell med radnummer |
21
+ | `godkänn <nr eller gid>` | Flytta Offer status framåt via request-sync — se grindavsnittet |
22
+ | `underkänn <nr eller gid> <skäl>` | Skäl som Asana-kommentar + iterationsräknare via request-sync |
23
+ | `detaljer <gid, slug eller nr> [steg]` | Skriv ut arkiverad JSON/dokument för posten |
24
+ | `hjälp` | Förklara uppgifterna, rör inga filer |
25
+
26
+ ## Hårda regler
27
+
28
+ - **request-sync körs ALDRIG i en subagent.** Uppsättning och brädverifiering är
29
+ interaktiva, fältkartan är sessionsstate, och en enda skrivare mot Asana
30
+ eliminerar races. All Asana-läsning/skrivning sker i huvudkonversationen via
31
+ Skill-verktyget (`request-sync`).
32
+ - **En skrivare i taget mot Asana.** Batchresultat skrivs tillbaka sekventiellt,
33
+ en post i taget, aldrig parallellt.
34
+ - **Dokument klistras bytetroget** in i subagent-prompter. Klassningens och
35
+ poängens belägg är ordagranna citat; en trunkerad eller omformaterad inklistring
36
+ ger tyst fel svar. Klistra aldrig in en sammanfattning.
37
+ - **Prislistan och repot hålls borta från classifier/scorer.** Deras
38
+ agentdefinitioner saknar sök- och shellverktyg (Grep/Glob/Bash), men Read
39
+ finns kvar för skillens referensfil — läs-scopet är alltså en promptregel,
40
+ inte en fysisk spärr. Därför: lägg aldrig prislistans innehåll i prompten,
41
+ och lägg inga egna sökvägar i dispatcherns rader utöver etiketterna i mallen.
42
+ Sökvägar som redan står i det bytetrogna underlaget (analysen namnger filer,
43
+ det är dess jobb) är inte ett brott — regeln gäller det dispatchern
44
+ lägger till. Granska `LÄSTA:`-raden i svaret (se dispatch-kontraktet).
45
+ - **Granskarblocken når aldrig kunden.** Specens granskarblock arkiveras separat
46
+ (`granskarblock.md`). Analysens "## Till granskaren"-block ligger kvar i
47
+ `impact.md` (det är del av underlaget för klassningen) — men inget
48
+ granskarblock får någonsin skickas med i material som går till request-sync
49
+ för kundsynliga fält.
50
+ - **Arkivet är cache, Asana är sanningen.** Vid konflikt vinner brädan.
51
+ - **Godkännande antas aldrig.** Att en spec finns i arkivet betyder inte att den
52
+ är granskad. Se grinden under `bedöm`.
53
+
54
+ ## Arkivet: `.haus/requests/`
55
+
56
+ Orkestreringens kuvert. De haus-hanterade request-skillsen känner inte till det;
57
+ den här skillen (och agenterna via sina prompter) underhåller det.
58
+
59
+ **En katalog = en spec = en prissättningsbar post.** En bunt (flera specar ur
60
+ samma önskemål) blir flera kataloger — klassningen räknar omfattning per spec
61
+ och poängen sätts per post, så en katalog med flera specar går inte att bedöma.
62
+
63
+ ```
64
+ <repo-rot>/.haus/requests/<gid-eller-slug>/
65
+ ├── spec.md EN spec, bytetrogen — det granskaren godkänner
66
+ ├── granskarblock.md "## Till granskaren" (bunt: samma block
67
+ │ kopieras till varje delkatalog)
68
+ ├── impact.md påverkansanalysen, ordagrann inkl. sitt
69
+ │ Till granskaren-block
70
+ ├── classification-pricing-vN.json nytt varv = ny fil, skriv aldrig över
71
+ ├── score-pricing-vN.json N är gemensamt för paret (se bedöm steg 8)
72
+ ├── classification-guard-vN.json guard-utfall synkas aldrig, bara arkiveras
73
+ ├── score-guard-vN.json
74
+ └── state.json
75
+ ```
76
+
77
+ - Katalognamn: Asana-gid när posten finns på brädan, annars en kebab-slug av
78
+ spec-rubriken. Bunt utan gid: `<slug>-1`, `<slug>-2`, … När gid myntas senare:
79
+ döp om katalogen till gid och notera sluggen i `state.json.former_slug`.
80
+ - **Första gången arkivet skapas i ett repo:** säkerställ att `.haus/requests/`
81
+ står i repots `.gitignore` (lägg till raden om den saknas, säg det).
82
+ Prisunderlag ska inte in i kundens historik. `.haus/request.json` (syncens
83
+ config) förblir spårad.
84
+
85
+ ### `state.json`
86
+
87
+ ```json
88
+ {
89
+ "request_id": "<gid eller null>",
90
+ "slug": "<kebab-rubrik>",
91
+ "former_slug": null,
92
+ "title": "<spec-rubrik>",
93
+ "status": "spec_draft | spec_synced | assessed | priced | guard_run",
94
+ "bundle": { "parent_slug": null, "part": null, "of": null },
95
+ "split": { "parent": null, "part": null, "of": null },
96
+ "approval": null,
97
+ "note": null,
98
+ "runs": []
99
+ }
100
+ ```
101
+
102
+ - `bundle` = specar ur samma önskemål (sätts av speca vid bunt).
103
+ `split` = delposter ur ett DELA_UPP-utfall (sätts av bedöm). Båda formen
104
+ `{parent…, part: N, of: M}`.
105
+ - `approval` skrivs av grinden i bedöm:
106
+ `{ "source": "asana" | "manual", "by": "<namn, krävs vid manual>",
107
+ "status_read": "<värde>", "date": "YYYY-MM-DD" }`.
108
+ Anteckningen är spårbarhet, aldrig själva godkännandet.
109
+ - `runs[]` får en rad per dispatch, även spec- och analyskörningar:
110
+ `{ "file": "spec.md" | "impact.md" | "<run>.json", "skill": "...",
111
+ "agent": "...", "model": "<ur MODELL-raden>", "effort": "<ur agentdefinitionen>",
112
+ "date": "YYYY-MM-DD",
113
+ "reference_sha256": { "<referensfil>": "<sha256>" },
114
+ "underlag_sha256": { "spec.md": "<sha256>", "impact.md": "<sha256>" },
115
+ "double_run": "bekräftad" | "olika" | null,
116
+ "superseded_by": null }`.
117
+ `underlag_sha256` är underlagsproveniensen: hashen för de dokument körningen
118
+ faktiskt såg. Precheck vägrar bedöma när analysens spec-hash inte matchar
119
+ nuvarande `spec.md`, och guard ska vägra diffa mot en körning vars underlag
120
+ bytts ut — utan detta är ett respec-varv mot gammal analys osynligt.
121
+ Referens-hashen bor i run-raden (inte på toppnivå) och räknas vid körning med
122
+ `node <skillkatalog>/scripts/ref-hash.mjs <referensfil>` — ALDRIG rå
123
+ `shasum` på hela filen: installern stämplar filerna med paketversionen, så en
124
+ hel-fils-hash byter värde vid varje haus-release och skulle ogiltigförklara
125
+ alla arkiverade körningar. Samma verktyg används för `underlag_sha256`.
126
+ Referensfilerna ligger under Claude-konfigkatalogen (`CLAUDE_CONFIG_DIR`
127
+ eller `~/.claude`). En senare guard-körning jämför hash och vägrar diffa vid
128
+ skevhet ("omklassning krävs, skalan har ändrats").
129
+ - `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"`).
131
+ - `spec_synced` sätts av speca steg 7 (posten lagd på brädan);
132
+ `guard_run` reserveras för guard-kommandot (senare byggsteg).
133
+
134
+ ## Uppgift: `speca`
135
+
136
+ 1. **Underlag.** Rått önskemål inklistrat, eller ett gid — hämta då posten via
137
+ request-sync (huvudkonversationen) och använd `## Ursprunglig förfrågan`
138
+ ordagrant.
139
+ 2. **Källmanifest.** Isolerade subagenter kan inte "leta innan de antar" utan
140
+ hjälp. Bygg ett manifest innan dispatch: relevanta grep-träffar i repot
141
+ (vyer/termer önskemålet nämner), tidigare specar för samma kund (arkivet +
142
+ brädan), ev. brief. **Alla sökvägar i manifestet absoluta.** Finns inget repo
143
+ i sessionen: skriv `KÄLLOR: inga — allt blir antaganden` så specifiern vet.
144
+ 3. **Eskaleringsbedömning.** Misstänkt bunt (flera beslut), behörighet/betalning/
145
+ oåterkallelig data/personuppgifter, eller offertunderlag → utfallet är
146
+ prisbärande och eskaleringen är ett MODELLBYTE, inte en flagga: dispatcha
147
+ `request-specifier-eskalerad` (frontier-modell) i stället för
148
+ `request-specifier`, med `ESKALERAD: ja` plus orsaken i prompten.
149
+ 4. **Dispatcha `request-specifier`** (eller den eskalerade varianten enligt
150
+ steg 3) enligt promptmallen i dispatch-kontraktet.
151
+ 5. **Arkivera.** Dela svaret vid `## Till granskaren`. En spec → en katalog:
152
+ allt före blocket är specarna; VARJE spec får sin egen katalog med egen
153
+ `spec.md` och eget `state.json` (bunt: `bundle`-fältet ifyllt, blocket
154
+ kopieras till varje katalog som `granskarblock.md`). Bunt ur en gid-post:
155
+ del 1 behåller gid-katalogen, resterande delar får slug-kataloger med
156
+ `bundle.parent_slug` satt till gid:t; syskontasks på brädan mintas först
157
+ vid sync. Lägg en `runs`-rad (`file: "spec.md"`, med `underlag_sha256`
158
+ tomt) i varje berörd state.json.
159
+ **Respec över befintlig katalog invaliderar analysen:** när `spec.md`
160
+ 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.
164
+ 6. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog>`
165
+ per katalog. BRISTER i spec/granskarblock → dispatcha om specifiern EN gång
166
+ enligt omdispatch-regeln; BRISTER i `state.json` är wrapperns egna filer och
167
+ rättas av wrappern själv, aldrig via omdispatch. Kvarstår brister, visa dem
168
+ och stanna. VARNING:ar visas men stoppar inget.
169
+ 7. **Svara BLUF:** en rad per spec (rubrik + ev. flaggor), sedan granskarblocket
170
+ i sin helhet. Erbjud att lägga specarna på brädan via request-sync (🔍) — gör
171
+ det bara på uttrycklig begäran. När en post läggs på brädan: sätt status
172
+ `spec_synced`, döp om slug-katalogen till det myntade gid:t, notera sluggen
173
+ i `former_slug` och uppdatera `request_id` i `state.json`.
174
+
175
+ ## Uppgift: `bedöm`
176
+
177
+ Kör prissättningsbenet: påverkansanalys om den saknas, sedan klass ∥ poäng med
178
+ oenighetsgrind.
179
+
180
+ 1. **Grind — obligatorisk, aldrig överhoppbar.** Bedömning kräver en granskad
181
+ spec, och godkännandet bor i Asana:
182
+ - **Posten har gid:** läs Offer status via request-sync (huvudkonversationen)
183
+ omedelbart före körningen — aldrig från minnet eller en tidigare listning.
184
+ Endast `✅ Spec ok` (normaliserad på alfabetiska kärnan) släpper igenom.
185
+ Annan status: visa den och stanna. En inklistrad spec för en post som har
186
+ gid räknas inte — det är brädstatusen som gäller, läs den ändå.
187
+ - **Posten saknar gid MEN repot har `.haus/request.json`:** vägra — repot är
188
+ kopplat till en bräda, så en gid-lös post ska in via intaget och granskas
189
+ där, inte prissättas vid sidan av. Grinden är värd något bara om den inte
190
+ går att kliva runt.
191
+ - **Posten saknar gid OCH repot saknar brädkoppling:** fråga uttryckligen
192
+ "Är specen godkänd av en människa — och av vem?" via AskUserQuestion och
193
+ kör bara på ja med namn. Anta aldrig, och låt aldrig "specen ser klar ut"
194
+ eller ett tidigare samtal ersätta frågan.
195
+ - **Posten har gid men saknar lokal arkivkatalog:** stöds inte ännu
196
+ (hydrering från brädan kommer med batch-steget) — säg det och stanna;
197
+ granskarblocket går inte att återskapa ur Asana.
198
+ - Anteckna passerad grind i `state.json.approval` (manual-fallet med `by`).
199
+ Nästa `bedöm` läser om statusen igen.
200
+ 2. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog> --impact`
201
+ (utan `--impact` om analysen ännu inte finns). En `spec.md` med mer än ett
202
+ specblock är en BRIST — dela posten enligt arkivregeln innan något dispatchas.
203
+ Övriga BRISTER → åtgärda ledet som brister (om spec: tillbaka till
204
+ speca-steget) innan något dyrt dispatchas.
205
+ 3. **Analys.** Saknas `impact.md`, eller fäller precheck den på
206
+ underlagsproveniens (analysen gjordes mot en annan spec): kräver repo i
207
+ sessionen (annars stopp — en gissad analys är förbjuden av skillen själv).
208
+ Dispatcha `request-assessor` enligt promptmallen — specens granskarblock
209
+ skickas med som internt underlag (samma förtroendedomän; flaggorna där är
210
+ ofta analysens viktigaste ingång). Arkivera svaret som `impact.md`
211
+ (inklusive dess "## Till granskaren"-block — visa blocket för användaren,
212
+ aldrig till kundfält), lägg en `runs`-rad (`file: "impact.md"`,
213
+ `underlag_sha256` med hash för `spec.md`). Kör precheck igen med `--impact`.
214
+ 4. **Prissättning.** Dispatcha `request-classifier` och `request-scorer`
215
+ **parallellt i samma meddelande** enligt promptmallarna. Ingen prislista,
216
+ inget repo, inga andra posters dokument i prompterna.
217
+ 5. **Kontraktskontroll.** Strippa svansraderna (`MODELL:`, `LÄSTA:`, ev.
218
+ `AVVIKELSE:`) ur varje svar, skriv den rena JSON:en till en temporär fil och
219
+ 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;
221
+ kvarstår brott, visa dem och stanna utan att arkivera det svaret som giltigt
222
+ varv — men släng det inte: arkivera det som `<typ>-pricing-vN-avvisad.json`
223
+ (utanför N-räkningen) med kontrollens felrader i en runs-rad, så även
224
+ misslyckade körningar har kvitto. En `AVVIKELSE:`-rad visas alltid för
225
+ användaren.
226
+ 6. **Dubbelkörning (driftvakt).** Körs BARA när inget av svaren är ett
227
+ stopputfall — stopproutningen i steg 7 går alltid först, en post som ändå
228
+ går till människa ska inte kosta fyra extra körningar. Trigger: klassningens
229
+ **utfall** är MEDIUM, LARGE eller OFFERT, ELLER poängen är ≥ 4 (poängen
230
+ multipliceras direkt till pris — en hög poäng på låg klass förtjänar samma
231
+ vakt). Dispatcha då `request-classifier` OCH `request-scorer` EN gång till,
232
+ samma prompter, nya isolerade körningar, parallellt i samma meddelande.
233
+ Kostnaden (ett extra par körningar) läggs medvetet exakt där pengarna står
234
+ på spel — poängen multipliceras direkt till pris och båda skillsens eget
235
+ acceptanstest är att två oberoende körningar ger samma resultat.
236
+ - Kör kontraktskontrollen (steg 5) på båda nya svaren först, med egen
237
+ omdispatch-budget (EN per agent).
238
+ - Jämför sedan deterministiskt:
239
+ `node <skillkatalog>/scripts/compare-classifications.mjs <klass1.json> <klass2.json>`
240
+ `node <skillkatalog>/scripts/compare-scores.mjs <poäng1.json> <poäng2.json>`
241
+ - **Båda LIKA** → körning 1-paret gäller: `double_run: "bekräftad"` på dess
242
+ runs-rader. Körning 2-filerna arkiveras som `...-pricing-vN-run2.json`
243
+ med SAMMA N som paret och `superseded_by` satt till motsvarande körning
244
+ 1-fil. `-run2`-filer räknas ALDRIG när nästa N bestäms, och guard och
245
+ `detaljer` läser alltid filen utan suffix.
246
+ - **Någon OLIKA** → behandla posten som ESKALERA. Det beskriver routingen,
247
+ 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)
251
+ 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.
257
+ - Exit 2 från ett jämförelsescript är ett dispatcherfel (ojämförbara
258
+ körningar) — visa felet och stanna, rapportera aldrig LIKA.
259
+ 7. **Oenighetsgrind klass ↔ poäng.** Deterministiska regler, i ordning:
260
+ - Endera svaret är ett stopputfall → hela posten routas enligt stopplistan
261
+ nedan. Det andra svaret arkiveras men markeras `superseded_by` mot
262
+ stopputfallets fil, och statusen blir `assessed`, aldrig `priced`.
263
+ - Båda levererar (klass + poäng): flagga oenighet i BLUF-tabellen mot
264
+ ankarbanden — klass TINY/SMALL med poäng ≥ 4, klass MEDIUM med poäng
265
+ ≤ 0.75 eller ≥ 13, klass LARGE/OFFERT med poäng ≤ 3. Flaggan är
266
+ 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;
270
+ guard-filer har en EGEN N-sekvens, skild från pricing). Uppdatera
271
+ `state.json.runs` med modell, effort, referens-hash och `underlag_sha256`
272
+ (hash för `spec.md` och `impact.md` som körningen såg) per fil, status →
273
+ `priced` (eller `assessed` vid stopputfall).
274
+ 9. **Svara BLUF:** tabell `post · klass · poäng · utfall · konfidens`, en rad
275
+ per avvikelse värd att se (oenighetsflagga, ESKALERA, DELA_UPP,
276
+ INPUT_SAKNAS, AVVIKELSE-rader). JSON:en visas inte i chatten —
277
+ `detaljer <post>` gör det. Skriv-tillbaka till brädan sker via request-sync
278
+ i huvudkonversationen, bara på uttrycklig begäran — och det är den flytten
279
+ som tar posten ✅ Spec ok → ⚖️ Poängsättning redo för granskning enligt
280
+ syncens regler.
281
+
282
+ **Stopputfall routas, aldrig tolkas:**
283
+
284
+ - `DELA_UPP` → föreslå ny analysrunda per del; delarna blir egna kataloger med
285
+ `split` satt (`part`/`of`), och en del utan tak blir `OVER_SKALAN`, aldrig
286
+ DELA_UPP igen. Delposter ärver enligt skillen postens spec och skriver aldrig
287
+ en egen: förälderns `spec.md` och `granskarblock.md` KOPIERAS in i varje
288
+ delkatalog (som buntens granskarblock), medan `impact.md` skrivs per del av
289
+ den nya analysrundan.
290
+ - `OVER_SKALAN` → posten offereras separat; delas aldrig vidare (delningen görs
291
+ ett steg, skillens egen regel).
292
+ - `TILLBAKA_TILL_SPEC` / `KONCEPT` / `DIAGNOS` → tillbaka till spec-steget
293
+ respektive kunddialog; visa skälet.
294
+ - `ESKALERA` / `INPUT_SAKNAS` → visa skälet och stanna.
295
+
296
+ ## Uppgift: `status`
297
+
298
+ Kör request-sync:s kö-läsning i huvudkonversationen för lägena
299
+ 📥 Inkommet, 🔍 Spec redo för granskning, ✅ Spec ok och ⚖️ Poängsättning redo
300
+ för granskning. Svara som BLUF-tabell med **radnummer**: `nr · post · Offer
301
+ status`. Spara nr → gid-mappningen i sessionen (gid behöver inte visas i
302
+ tabellen) — numren är vad `godkänn`/`underkänn`/`bedöm`/`detaljer` refererar
303
+ till, och varje verb slår upp och agerar på **gid**, aldrig på numret självt.
304
+ Ingen JSON i chatten.
305
+
306
+ ## Uppgifter: `godkänn` / `underkänn`
307
+
308
+ Genvägar till samma spak som i Asana — **Offer status på brädan förblir enda
309
+ beslutet**. Verben gör beslutet snabbare att verkställa, aldrig lättare att
310
+ missta:
311
+
312
+ - **Adressering:** radnummer ur senaste `status`-listningen i samma session
313
+ (slås upp till gid via den sparade mappningen), eller ett gid direkt. ALDRIG
314
+ fuzzy rubrikmatchning — fel post godkänd är värre än ett stopp. Finns ingen
315
+ färsk listning och inget gid: kör `status` först.
316
+ - **En post per verb-anrop.** "Godkänn 2 och 4" är två anrop i följd, vart och
317
+ ett läser och flyttar sin post. Aldrig batch-godkännande på "allt ser bra ut".
318
+ - **Lägesgrind för båda verben:** läs postens nuvarande Offer status via
319
+ request-sync först. Verben verkar bara från gransknings-lägena
320
+ (🔍 Spec redo för granskning, ⚖️ Poängsättning redo för granskning). Står
321
+ posten någon annanstans: visa status och stanna i stället för att gissa.
322
+ - **`godkänn <nr|gid>`:** 🔍 → ✅ Spec ok, respektive ⚖️ → 📤 Klar – till kund.
323
+ - **`underkänn <nr|gid> <skäl>`:** skäl är obligatoriskt — utan skäl, fråga
324
+ efter det. request-sync lägger skälet som kommentar på posten, bumpar
325
+ iterationsräknaren och flyttar status bakåt enligt sina bakåtflyttsregler.
326
+ - **Post utan bräda** (bara arkiv): `godkänn` antecknar
327
+ `approval: {source: "manual", date}` i `state.json` efter en uttrycklig
328
+ bekräftelsefråga. Säg tydligt att detta inte syns för någon annan än arkivet.
329
+ - Verben rör Offer status och kommentarer — aldrig Score, Class eller
330
+ beskrivningsfält.
331
+
332
+ ## Uppgift: `detaljer`
333
+
334
+ `detaljer <gid|slug|nr> [steg]` — skriv ut arkiverade filer för posten: utan
335
+ steg en lista över vad som finns, med steg (`spec`, `granskarblock`, `analys`,
336
+ `klass`, `poäng`, `state`) filens innehåll rått. För `klass`/`poäng`: senaste
337
+ `-pricing-vN.json` utan suffix (aldrig `-run2`/`-avvisad`).
338
+
339
+ ## Mekaniska kontroller: `scripts/`
340
+
341
+ Sex deterministiska script (fyra kontroller + `lib.mjs` med de delade
342
+ byggstenarna + `ref-hash.mjs` för stabil hashning) ersätter modellkörningar
343
+ där kontrollen är ren form —
344
+ billigare, snabbare och utan omdömesglidning. Varje script bär sha256 för de
345
+ referensfiler vars regler det speglar och VARNAR när en katalogbump ändrat dem
346
+ (kontraktsdrift ska upptäckas av en check, inte av en kund):
347
+
348
+ - `scripts/precheck.mjs <arkivkatalog> [--impact]` — formkontroll FÖRE dyra
349
+ dispatcher: rubriker, exakt ett specblock, 2–5 punkter, förbjuden vokabulär,
350
+ granskarblocksrader, state.json-fält. Skiljer på BRISTER (exit 1, stoppar)
351
+ och VARNING (heuristik — visas, stoppar inte).
352
+ - `scripts/contract-check.mjs <run.json> <spec.md> [impact.md] [--expect-id <id>]`
353
+ — kontraktskontroll av classify/score-JSON EFTER körning: enums,
354
+ konfidensintervall, poängupplösning, inringningens närvaro (även per
355
+ alternativ; under-A1-fallet med floor null och tak A1 är giltigt), belägg/
356
+ driver-citat som ordagranna substrängar av underlaget, protokollterm-
357
+ svartlista i kundtextfält, sluten kuvertnyckellista, request_id mot katalogen.
358
+ INPUT_SAKNAS kräver saknas-lista i stället för frågerader.
359
+ - `scripts/compare-classifications.mjs <a.json> <b.json>` — deterministisk
360
+ dubbelkörningsdiff: utfall, basklass, varje frågas svar och konfidensens sida
361
+ om 0.8-gränsen. Skriver `LIKA` eller `OLIKA` + diffrader, exit 0/1; exit 2 =
362
+ ojämförbara körningar (dispatcherfel, aldrig LIKA).
363
+ - `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.
367
+
368
+ Kontrollerna bedömer aldrig innehåll och returnerar aldrig protokollutfall —
369
+ en post som passerar kan fortfarande stoppas av protokollen själva, och det är
370
+ deras jobb. Vid script-fel (exit 2 eller krasch): visa felet och stanna, kör
371
+ aldrig vidare som om kontrollen passerat.
372
+
373
+ ## Dispatch-kontrakt (gäller alla fyra agenterna)
374
+
375
+ - Prompten är självbärande: agenten ser inte den här konversationen.
376
+ - **Promptmallar, etiketterna ordagrant:**
377
+
378
+ `request-specifier`:
379
+
380
+ ```
381
+ ÖNSKEMÅL:
382
+ <rått önskemål, ordagrant>
383
+
384
+ KÄLLOR:
385
+ <manifest med absoluta sökvägar, eller "inga — allt blir antaganden">
386
+
387
+ REPO-ROT: <absolut sökväg eller "saknas">
388
+ ESKALERAD: <ja|nej>
389
+ ```
390
+
391
+ `request-assessor`:
392
+
393
+ ```
394
+ REPO-ROT: <absolut sökväg>
395
+
396
+ SPEC (godkänd):
397
+ <spec.md bytetroget>
398
+
399
+ GRANSKARBLOCK (internt underlag, följer aldrig med i analysens kundtext):
400
+ <granskarblock.md bytetroget>
401
+ ```
402
+
403
+ `request-classifier` / `request-scorer`:
404
+
405
+ ```
406
+ REQUEST_ID: <gid eller slug>
407
+ MODE: <pricing|guard>
408
+ DELPOST: <nej | ja, del N av M>
409
+
410
+ SPEC:
411
+ <spec.md bytetroget>
412
+
413
+ PÅVERKANSANALYS:
414
+ <impact.md bytetroget>
415
+ ```
416
+
417
+ (guard-läge lägger till `FULLPLAN:` och `TIDIGARE_KLASSNING:` /
418
+ `TIDIGARE_POANG:` — guard-kommandot kommer i ett senare steg.)
419
+
420
+ - **Svansrader efter huvudinnehållet**, i denna ordning: `MODELL: <modell-id>`
421
+ (alla agenter), `LÄSTA: <kommaseparerade absoluta sökvägar eller "inga">`
422
+ (endast classifier/scorer — deras Read är promptbegränsad, raden gör regeln
423
+ granskbar), `AVVIKELSE: <text>` (endast om något i prompten bröt mot agentens
424
+ regler, t.ex. en prislista i underlaget). Svansraderna strippas före varje
425
+ maskinell kontroll och före arkivering; MODELL antecknas i runs-raden,
426
+ LÄSTA verifieras ligga under agentens tillåtna katalog, AVVIKELSE visas
427
+ alltid för användaren.
428
+ - Agentens svar i övrigt är rådata enligt skillens kontrakt (prosa-dokument
429
+ eller JSON), ingen kommentar runt om.
430
+ - **Omdispatch (max EN per agent och varv):** skicka HELA ursprungsprompten
431
+ oförändrad, följd av ett block:
432
+ ```
433
+ RÄTTNINGAR:
434
+ <felraderna från kontrollen, citerade ordagrant>
435
+ ```
436
+ En ny subagent minns inget — enbart felrader utan underlag är obrukbart.
437
+
438
+ ## Använd inte när
439
+
440
+ - Användaren vill köra en enskild skill fristående ("skriv bara en spec") —
441
+ invokera den skillen direkt.
442
+ - Frågan gäller Asana-uppsättning ("koppla repot") — det är request-sync:s
443
+ uppsättningsflöde, kör den direkt.
444
+ - Guard-läget ("kör guarden", före första kodrad) — körs tills vidare via
445
+ request-classify/request-score direkt; ett `guard`-kommando kommer i ett
446
+ senare steg.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Deterministisk dubbelkörningsdiff för klassningar (driftvakten på MEDIUM+).
3
+ * Jämför utfall, basklass, varje frågas svar och vilken sida om KONFIDENSGRÄNS
4
+ * (0.8) konfidensen ligger, mellan två oberoende körningar på samma underlag.
5
+ * Prosafält (belägg, motiveringar) jämförs INTE — två korrekta körningar
6
+ * formulerar sig olika, det är svaren som ska vara stabila. Konfidensens SIDA
7
+ * om gränsen är däremot inte prosa: den avgör ESKALERA enligt protokollet.
8
+ *
9
+ * Skriver LIKA (exit 0) eller OLIKA + diffrader (exit 1) på stdout; varningar
10
+ * går på stderr så en verdiktparser aldrig sväljer dem. Vid OLIKA väljer
11
+ * dispatchern ALDRIG en körning och räknar ALDRIG medelvärde — posten
12
+ * eskaleras till granskaren med båda körningarna.
13
+ *
14
+ * Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
15
+ * rälsen bor i lib.mjs och delas med compare-scores.
16
+ *
17
+ * Användning: node compare-classifications.mjs <körning1.json> <körning2.json>
18
+ */
19
+ import { loadRunPair, refDriftWarning, reportDiffs } from './lib.mjs'
20
+
21
+ // KONFIDENSGRANS speglar klassningsinstruktionen — varna (stderr) vid katalogdrift.
22
+ const REF_SHA = '811f0bff62c749255e86da7ea8db0ea868ebc9df14262f9889101c110d9d14ff'
23
+ {
24
+ const w = refDriftWarning('request-classify/references/klassningsinstruktion.md', REF_SHA)
25
+ if (w) console.error(`VARNING: ${w} — kontrollera KONFIDENSGRANS`)
26
+ }
27
+ const KONFIDENSGRANS = 0.8
28
+
29
+ const { a: docA, b: docB } = loadRunPair(
30
+ 'användning: node compare-classifications.mjs <körning1.json> <körning2.json>',
31
+ )
32
+ const a = docA.classification
33
+ const b = docB.classification
34
+ if (!a || !b) {
35
+ console.error('minst en fil saknar classification-nyckeln — är båda classify-körningar?')
36
+ process.exit(2)
37
+ }
38
+
39
+ // Frågelagret är jämförelsens kärna — utan kompletta, unikt id-satta rader kan
40
+ // en äkta oenighet tyst försvinna. Då vägrar vi hellre än gissar.
41
+ const validateFragor = (rows, label) => {
42
+ if (!Array.isArray(rows) || rows.length === 0) {
43
+ console.error(`${label}: fragor saknas eller är tom — kan inte jämföra`)
44
+ process.exit(2)
45
+ }
46
+ const seen = new Set()
47
+ for (const f of rows) {
48
+ if (typeof f?.id !== 'string' || f.id.trim() === '') {
49
+ console.error(`${label}: en frågerad saknar id — kan inte jämföra`)
50
+ process.exit(2)
51
+ }
52
+ if (seen.has(f.id)) {
53
+ console.error(`${label}: dubblett-id "${f.id}" — kan inte jämföra`)
54
+ process.exit(2)
55
+ }
56
+ seen.add(f.id)
57
+ }
58
+ }
59
+ validateFragor(a.fragor, 'körning 1')
60
+ validateFragor(b.fragor, 'körning 2')
61
+
62
+ const show = (v) => JSON.stringify(v ?? null)
63
+ const normSvar = (v) =>
64
+ String(v ?? '')
65
+ .trim()
66
+ .toUpperCase()
67
+
68
+ const diffs = []
69
+ if (a.utfall !== b.utfall) diffs.push(`utfall: ${show(a.utfall)} ↔ ${show(b.utfall)}`)
70
+ if ((a.basklass ?? null) !== (b.basklass ?? null))
71
+ diffs.push(`basklass: ${show(a.basklass)} ↔ ${show(b.basklass)}`)
72
+
73
+ const byId = (rows) => new Map(rows.map((f) => [f.id, f]))
74
+ const mapA = byId(a.fragor)
75
+ const mapB = byId(b.fragor)
76
+ for (const id of new Set([...mapA.keys(), ...mapB.keys()])) {
77
+ const fa = mapA.get(id)
78
+ const fb = mapB.get(id)
79
+ if (!fa || !fb) {
80
+ // Körningarna stannade på olika steg — samma fakta gav olika väg.
81
+ diffs.push(`fråga ${id}: besvarad i ${fa ? 'körning 1' : 'körning 2'} men inte i den andra`)
82
+ continue
83
+ }
84
+ if (normSvar(fa.svar) !== normSvar(fb.svar))
85
+ diffs.push(`fråga ${id}: ${show(fa.svar)} ↔ ${show(fb.svar)}`)
86
+ const sideA = (fa.konfidens ?? 0) >= KONFIDENSGRANS
87
+ const sideB = (fb.konfidens ?? 0) >= KONFIDENSGRANS
88
+ if (sideA !== sideB)
89
+ diffs.push(
90
+ `fråga ${id}: konfidens på olika sida om ${KONFIDENSGRANS} (${show(fa.konfidens)} ↔ ${show(fb.konfidens)})`,
91
+ )
92
+ }
93
+
94
+ reportDiffs(diffs)
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Deterministisk dubbelkörningsdiff för poängsättningar (driftvakten på
3
+ * MEDIUM+). Poängen har större driftyta än klassens enum och multipliceras
4
+ * direkt till pris — request-score:s eget acceptanstest är att två oberoende
5
+ * körningar ger samma poäng. Jämförs: outcome, score, confidence (enum, inte
6
+ * prosa) och inringningens golv- och takankare. Prosa (why, drivers,
7
+ * rationale) jämförs ALDRIG.
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.
12
+ *
13
+ * Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
14
+ * rälsen bor i lib.mjs och delas med compare-classifications.
15
+ *
16
+ * Användning: node compare-scores.mjs <körning1.json> <körning2.json>
17
+ */
18
+ import { loadRunPair, refDriftWarning, reportDiffs } from './lib.mjs'
19
+
20
+ // Jämförelsefälten speglar poängskalans kontrakt — varna (stderr) vid katalogdrift.
21
+ const REF_SHA = 'cb5a6c221b5d39c445ca31a427fd3f96c648388ac5441c22ea2ea5f72e1f8e31'
22
+ {
23
+ const w = refDriftWarning('request-score/references/poangskala.md', REF_SHA)
24
+ if (w) console.error(`VARNING: ${w}`)
25
+ }
26
+
27
+ const { a, b } = loadRunPair('användning: node compare-scores.mjs <körning1.json> <körning2.json>')
28
+ if (a.outcome === undefined || b.outcome === undefined) {
29
+ console.error('minst en fil saknar outcome-nyckeln — är båda score-körningar?')
30
+ process.exit(2)
31
+ }
32
+
33
+ const show = (v) => JSON.stringify(v ?? null)
34
+ const diffs = []
35
+ if (a.outcome !== b.outcome) diffs.push(`outcome: ${show(a.outcome)} ↔ ${show(b.outcome)}`)
36
+ // confidence är ett enum (hog/medel/lag), inte prosa — samma poäng med olika
37
+ // säkerhet är information granskaren ska se, precis som klassningens konfidenssida.
38
+ if (a.confidence !== b.confidence)
39
+ diffs.push(`confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}`)
40
+ if (a.outcome === 'POANG' && b.outcome === 'POANG') {
41
+ if (a.score !== b.score) diffs.push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
42
+ const fa = a.bracket?.floor?.anchor ?? null
43
+ const fb = b.bracket?.floor?.anchor ?? null
44
+ const ta = a.bracket?.ceiling?.anchor ?? null
45
+ 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)}`)
48
+ }
49
+
50
+ reportDiffs(diffs)