@haus-tech/haus-workflow 5.1.0 → 5.2.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.
@@ -219,10 +219,32 @@ const KOSTNADSFALT = [
219
219
  // sa 5,5 minuter om en sonnetkörning som tog omkring 34. Ett `wall_ms` som
220
220
  // saknas eller inte är ett tal är omätt, aldrig noll: en nolla skulle läsas
221
221
  // som en ögonblicklig körning.
222
- ['Väggtid', 'klocka', (m) => (Number.isFinite(m.wall_ms) ? minuter(m.wall_ms) : null)],
222
+ // #526: väggtiden mättes under samtidig last. Lasten står i korning.json:s
223
+ // steg (`samtidiga_processer`, maxvärdet över stegen: det är den värsta
224
+ // trängseln körningen köade bakom); saknas fältet (äldre körningar, eller
225
+ // en räknare som föll i varje steg) är samtidigheten okänd, inte noll.
226
+ [
227
+ 'Väggtid',
228
+ 'klocka',
229
+ (m, k) =>
230
+ Number.isFinite(m.wall_ms) ? `${minuter(m.wall_ms)} (${samtidighet(k.matning)})` : null,
231
+ ],
223
232
  ['Dispatcher', 'matning', (m) => tal(m.subagent_stats?.spawned)],
233
+ // #523: nekade verktygsanrop, summan av `permission_denials` över stegen.
234
+ // Ett steg utan fältet (null, eller äldre korning.json) räknas inte; bär
235
+ // inget steg fältet är summan omätt, aldrig noll.
236
+ ['Nekade anrop', 'matning', (m) => nekadeAnrop(m)],
224
237
  ]
225
238
 
239
+ /** Summan av nekade verktygsanrop över stegen, null när inget steg bär
240
+ * fältet. */
241
+ const nekadeAnrop = (matning) => {
242
+ const tal = (matning?.steg ?? [])
243
+ .map((s) => s?.permission_denials)
244
+ .filter((n) => Number.isFinite(n))
245
+ return tal.length === 0 ? null : String(tal.reduce((a, b) => a + b, 0))
246
+ }
247
+
226
248
  /** Kanoniska modellnycklar i en körnings korning.json, sorterade. Flera
227
249
  * fakturerade nycklar (t.ex. med `[1m]`-suffix eller ett datumstämplat
228
250
  * snapshot-namn) kan kanonisera till samma sträng; unionen dedupar dem. */
@@ -240,6 +262,17 @@ const kostnadPerNyckel = (m) => {
240
262
  return ut
241
263
  }
242
264
 
265
+ /** Maxantalet samtidiga agentprocesser över stegen, som text. Steg med null
266
+ * (räknaren föll) räknas inte; utan ett enda tal är samtidigheten okänd. */
267
+ const samtidighet = (matning) => {
268
+ const tal = (matning?.steg ?? [])
269
+ .map((s) => s?.samtidiga_processer)
270
+ .filter((n) => Number.isFinite(n))
271
+ if (tal.length === 0) return 'okänd samtidighet'
272
+ const max = Math.max(...tal)
273
+ return `${max} ${max === 1 ? 'samtidig' : 'samtidiga'}`
274
+ }
275
+
243
276
  const kostnadsrader = (k, nycklar = modellNycklar(k.matning)) =>
244
277
  KOSTNADSFALT.flatMap(([namn, källa, hämta]) => {
245
278
  // En del är ingen egen process: mätningen kommer ur headless-svaret för
@@ -259,7 +292,9 @@ const kostnadsrader = (k, nycklar = modellNycklar(k.matning)) =>
259
292
  // ljuger inte om de andra. Radlistan har ändå alltid samma längd och
260
293
  // samma fältnamn, mätt eller omätt.
261
294
  const dok = k[källa]
262
- const värde = dok ? hämta(dok) : null
295
+ // Hela körningen följer med som andra argument: Väggtid läser klockan ur
296
+ // matning.json men samtidigheten ur korning.json:s steg.
297
+ const värde = dok ? hämta(dok, k) : null
263
298
  const rad = [namn, värde === null ? '<em>omätt</em>' : esc(värde)]
264
299
  if (namn !== 'USD') return [rad]
265
300
  // #494: orkestratorns (hjälpmodellens) kostnad låg förr inbakad i
@@ -8,10 +8,13 @@
8
8
  * (de rena hjälparna exporteras, huvudloopen körs bara när scriptet startas
9
9
  * direkt).
10
10
  *
11
- * Två tickets åt gången, varje ticket kör alla valda modeller samtidigt, och
12
- * nästa ticket startar först när en tickets samtliga modeller är klara
13
- * (beslut 2026-09-02). `experiment.json` skrivs ALDRIG härifrån direkt, bara
14
- * via `archive.mjs experiment` (låst läs-modifiera-skriv), så samtidiga varv
11
+ * Två tickets åt gången, varje ticket kör sina valda modeller med högst
12
+ * `--modeller-parallellt` (default 4) i flykt, och nästa ticket startar först
13
+ * när en tickets samtliga modeller är klara (beslut 2026-09-02; modelltaket
14
+ * #526, ADR-0120: sex agentprocesser per modell utan tak gav en last som
15
+ * wall_ms mättes under utan att bokföras). `experiment.json` skrivs ALDRIG
16
+ * härifrån direkt, bara via `archive.mjs experiment` (låst
17
+ * läs-modifiera-skriv), så samtidiga varv
15
18
  * aldrig tappar varandras rader (#493). Övriga arkivsteg (ny-korning,
16
19
  * proveniens, sparr) anropas som kommandon av samma skäl som ADR-0094: ett
17
20
  * steg som ålägger startaren en skrivning måste peka ut vem som utför den.
@@ -19,8 +22,8 @@
19
22
  * Användning:
20
23
  * node svep.mjs --repo-rot <abs> --konto <namn> --modell <m> [--modell <m>]...
21
24
  * --exp <experiments/<slug>> [--exp ...]...
22
- * [--tickets-parallellt 2] [--om | --hoppa-över]
23
- * [--claude <kommando>]
25
+ * [--tickets-parallellt 2] [--modeller-parallellt 4]
26
+ * [--om | --hoppa-över] [--claude <kommando>]
24
27
  *
25
28
  * Ett relativt --exp löses mot <repo-rot>/.haus-workflow/requests/. Varje
26
29
  * experimentkatalog måste bära onskemal.md, kallmanifest.md och experiment.json
@@ -58,14 +61,27 @@ export const STATUSAR = ['klar', 'ofärdig', 'stopp', 'väntar på dom']
58
61
  /** Statusar den inre processen själv får sätta och som startaren behåller. */
59
62
  const INRE_BEHALLS = ['stopp', 'väntar på dom', 'ofärdig']
60
63
  const STANDARD_TAK = 2
64
+ /** Modeller per ticket i flykt. Fyra täcker det vanliga svepet (opus, sonnet,
65
+ * fable plus en till) utan att sex agentprocesser per modell staplas
66
+ * obegränsat (#526). */
67
+ const STANDARD_MODELLTAK = 4
61
68
  const SIGINT_EXIT = 130
62
- const VARDEFLAGGOR = ['repo-rot', 'konto', 'modell', 'exp', 'tickets-parallellt', 'claude']
69
+ const VARDEFLAGGOR = [
70
+ 'repo-rot',
71
+ 'konto',
72
+ 'modell',
73
+ 'exp',
74
+ 'tickets-parallellt',
75
+ 'modeller-parallellt',
76
+ 'claude',
77
+ ]
63
78
  const BOOLFLAGGOR = ['om', 'hoppa-över']
64
79
 
65
80
  export const ANVANDNING = [
66
81
  'användning: svep.mjs --repo-rot <abs> --konto <namn> --modell <m> [--modell <m>]...',
67
82
  ' --exp <experiments/<slug>> [--exp ...]...',
68
- ` [--tickets-parallellt ${STANDARD_TAK}] [--om | --hoppa-över] [--claude <kommando>]`,
83
+ ` [--tickets-parallellt ${STANDARD_TAK}] [--modeller-parallellt ${STANDARD_MODELLTAK}]`,
84
+ ' [--om | --hoppa-över] [--claude <kommando>]',
69
85
  ].join('\n')
70
86
 
71
87
  // ---------------------------------------------------------------------------
@@ -112,12 +128,17 @@ export function tolkaArgv(argv) {
112
128
  const taket = Number(takText)
113
129
  if (!/^\d+$/.test(takText) || taket < 1)
114
130
  return { fel: `--tickets-parallellt måste vara ett heltal >= 1 (fick "${takText}")` }
131
+ const modelltakText = enkla['modeller-parallellt'] ?? String(STANDARD_MODELLTAK)
132
+ const modellerParallellt = Number(modelltakText)
133
+ if (!/^\d+$/.test(modelltakText) || modellerParallellt < 1)
134
+ return { fel: `--modeller-parallellt måste vara ett heltal >= 1 (fick "${modelltakText}")` }
115
135
  return {
116
136
  repoRot: enkla['repo-rot'],
117
137
  konto: enkla.konto,
118
138
  modeller: listor.modell,
119
139
  exps: listor.exp,
120
140
  taket,
141
+ modellerParallellt,
121
142
  om: bool.om,
122
143
  hoppaOver: bool['hoppa-över'] || !bool.om,
123
144
  claude: enkla.claude ?? 'claude',
@@ -174,19 +195,46 @@ export function senasteKorning(experiment, katalognamn) {
174
195
  if (!m) continue
175
196
  const n = Number(m[1])
176
197
  if (basta === null || n > basta.n)
177
- basta = { n, nyckel, status: rad?.status ?? null, orsak: rad?.orsak ?? null }
198
+ basta = {
199
+ n,
200
+ nyckel,
201
+ status: rad?.status ?? null,
202
+ orsak: rad?.orsak ?? null,
203
+ // Startarens pid medan körningen pågår (#516); null i slutstatusen
204
+ // och i äldre filer.
205
+ pid: Number.isInteger(rad?.pid) ? rad.pid : null,
206
+ }
178
207
  }
179
208
  return basta
180
209
  }
181
210
 
211
+ /** Lever processen? Signal 0 levererar inget men prövar att pid:n finns.
212
+ * EPERM betyder att den finns men tillhör någon annan (det andra kontots
213
+ * svep, ADR-0093), alltså levande. ESRCH och allt annat är död. */
214
+ export const processLever = (pid) => {
215
+ try {
216
+ process.kill(pid, 0)
217
+ return true
218
+ } catch (e) {
219
+ return e?.code === 'EPERM'
220
+ }
221
+ }
222
+
182
223
  /** Återupptagningsregeln ur SKILL.md:s Batch-avsnitt. `klar` och `stopp` är
183
224
  * resultat värda att skydda: de hoppas utan --om. `ofärdig` är avbruten data
184
- * och körs alltid om. `väntar på dom` omvärderas med `archive.mjs sparr`,
185
- * svepet är den enda aktören för den statusen. Ingen körning alls
186
- * (status undefined) körs. */
187
- export function aterupptagning(status, { om = false } = {}) {
225
+ * och körs om, UTOM när raden bär en pid som lever: då är det ingen avbruten
226
+ * körning utan en som pågår i ett annat svep (två konton mot samma checkout,
227
+ * ADR-0093), och en omkörning vore en dubblett av en dyr körning (#516). En
228
+ * död pid eller ingen pid alls (äldre rad, korning.mjs:s eget avbrott) körs
229
+ * om som förut, så #493:s skydd står kvar. `--om` rör inte det: flaggan
230
+ * gäller resultat, inte pågående processer. `väntar på dom` omvärderas med
231
+ * `archive.mjs sparr`, svepet är den enda aktören för den statusen. Ingen
232
+ * körning alls (status undefined) körs. `levande` är injicerbar för
233
+ * testerna. */
234
+ export function aterupptagning(status, { om = false, pid = null, levande = processLever } = {}) {
188
235
  if (status === 'väntar på dom') return 'prova-sparr'
189
236
  if (status === 'klar' || status === 'stopp') return om ? 'kör' : 'hoppa'
237
+ if (status === 'ofärdig' && pid !== null && levande(pid)) return 'hoppa'
190
238
  return 'kör'
191
239
  }
192
240
 
@@ -355,8 +403,10 @@ function korInre({ claude, gid, modell, katalog, expDir, repoRot, pagaende, nyck
355
403
 
356
404
  /** #493: en körning som ny-korning skapat men som dör innan slutstatusen
357
405
  * skrivs fanns förr inte i experiment.json alls. Raden skrivs vid start som
358
- * ofärdig och skrivs över av slutstatusen; ett fel här är en varning. */
359
- export function registreraStart({ expDir, nyckel, arkiv: arkivFn }) {
406
+ * ofärdig och skrivs över av slutstatusen; ett fel här är en varning. Raden
407
+ * bär startarens pid (#516) så ett andra svep ser att körningen pågår;
408
+ * slutstatusen skrivs utan --pid och tar bort den. */
409
+ export function registreraStart({ expDir, nyckel, arkiv: arkivFn, pid = process.pid }) {
360
410
  const r = arkivFn([
361
411
  'experiment',
362
412
  expDir,
@@ -366,6 +416,8 @@ export function registreraStart({ expDir, nyckel, arkiv: arkivFn }) {
366
416
  'ofärdig',
367
417
  '--orsak',
368
418
  'startad, inte avslutad',
419
+ '--pid',
420
+ String(pid),
369
421
  ])
370
422
  return { varning: r.status === 0 ? null : `experiment --status ofärdig: ${felText(r)}` }
371
423
  }
@@ -499,7 +551,30 @@ function provaSparr({ gid, modell, expDir, repoRot, nyckel, ut }) {
499
551
  }
500
552
  }
501
553
 
502
- /** En ticket: underlagskontroll, varianter, sedan alla modeller samtidigt. */
554
+ /** Modellerna för en ticket, högst `taket` i flykt (#526): samma
555
+ * schemaläggare som tickets, en nivå ner. `korModell(modell)` kastar aldrig
556
+ * (korVarv fångar allt), men ett kast landar ändå som en `ofärdig`-rad för
557
+ * den modellen i stället för att stoppa de andra. Raderna kommer i
558
+ * modellordning. */
559
+ export async function korModeller(modeller, taket, korModell) {
560
+ const resultat = await schemalagg(modeller, taket, korModell)
561
+ return resultat.map((r, i) =>
562
+ r?.fel === undefined
563
+ ? r
564
+ : {
565
+ modell: modeller[i],
566
+ status: 'ofärdig',
567
+ orsak: r.fel,
568
+ korning: null,
569
+ wall_ms: null,
570
+ usd: null,
571
+ varningar: [],
572
+ },
573
+ )
574
+ }
575
+
576
+ /** En ticket: underlagskontroll, varianter, sedan modellerna med högst
577
+ * `--modeller-parallellt` i flykt. */
503
578
  async function korTicket(exp, val, pagaende, ut) {
504
579
  const namn = path.basename(exp)
505
580
  const u = kontrolleraUnderlag(exp)
@@ -518,43 +593,60 @@ async function korTicket(exp, val, pagaende, ut) {
518
593
  val.repoRot,
519
594
  )
520
595
  if (va.status !== 0) varningar.push(`experiment --variant: ${felText(va)}`)
521
- const rader = await Promise.all(
522
- val.modeller.map(async (modell) => {
523
- const senaste = senasteKorning(u.experiment, modellKatalog(modell))
524
- const beslut = aterupptagning(senaste?.status ?? undefined, { om: val.om })
525
- if (beslut === 'hoppa') {
526
- ut(`HOPPAR ${u.ticket} ${modell} ${senaste.status} (${senaste.nyckel})`)
527
- return {
528
- modell,
529
- status: senaste.status,
530
- orsak: senaste.orsak,
531
- korning: senaste.nyckel,
532
- wall_ms: null,
533
- usd: null,
534
- varningar: [],
535
- }
596
+ const rader = await korModeller(val.modeller, val.modellerParallellt, async (modell) => {
597
+ const senaste = senasteKorning(u.experiment, modellKatalog(modell))
598
+ const beslut = aterupptagning(senaste?.status ?? undefined, {
599
+ om: val.om,
600
+ pid: senaste?.pid ?? null,
601
+ })
602
+ if (beslut === 'hoppa' && senaste.status === 'ofärdig') {
603
+ // #516: körningen pågår i ett annat svep. Raden heter `pågår`, inte
604
+ // `ofärdig`: tabellen ska inte påstå att den föll.
605
+ ut(
606
+ `HOPPAR ${u.ticket} ${modell} pågår i en annan process (pid ${senaste.pid}, ${senaste.nyckel})`,
607
+ )
608
+ return {
609
+ modell,
610
+ status: 'pågår',
611
+ orsak: `i en annan process (pid ${senaste.pid})`,
612
+ korning: senaste.nyckel,
613
+ wall_ms: null,
614
+ usd: null,
615
+ varningar: [],
616
+ }
617
+ }
618
+ if (beslut === 'hoppa') {
619
+ ut(`HOPPAR ${u.ticket} ${modell} ${senaste.status} (${senaste.nyckel})`)
620
+ return {
621
+ modell,
622
+ status: senaste.status,
623
+ orsak: senaste.orsak,
624
+ korning: senaste.nyckel,
625
+ wall_ms: null,
626
+ usd: null,
627
+ varningar: [],
536
628
  }
537
- if (beslut === 'prova-sparr')
538
- return provaSparr({
539
- gid: u.ticket,
540
- modell,
541
- expDir: exp,
542
- repoRot: val.repoRot,
543
- nyckel: senaste.nyckel,
544
- ut,
545
- })
546
- return korVarv({
629
+ }
630
+ if (beslut === 'prova-sparr')
631
+ return provaSparr({
547
632
  gid: u.ticket,
548
633
  modell,
549
634
  expDir: exp,
550
635
  repoRot: val.repoRot,
551
- konto: val.konto,
552
- claude: val.claude,
553
- pagaende,
636
+ nyckel: senaste.nyckel,
554
637
  ut,
555
638
  })
556
- }),
557
- )
639
+ return korVarv({
640
+ gid: u.ticket,
641
+ modell,
642
+ expDir: exp,
643
+ repoRot: val.repoRot,
644
+ konto: val.konto,
645
+ claude: val.claude,
646
+ pagaende,
647
+ ut,
648
+ })
649
+ })
558
650
  return { ticket: u.ticket, rader, varningar }
559
651
  }
560
652
 
@@ -619,9 +711,14 @@ async function main(argv) {
619
711
  ut('')
620
712
  ut(renderaTabell(rader))
621
713
  // Saknat underlag är också exit 1: ett svep som tyst hoppar en ticket ser
622
- // annars lyckat ut i en loggrad ingen läser.
714
+ // annars lyckat ut i en loggrad ingen läser. Detsamma gäller `pågår`
715
+ // (#516): svepet har inget resultat för den raden, det andra svepet har.
623
716
  return rader.some(
624
- (r) => r.status === 'ofärdig' || r.status === 'fel' || r.status === 'underlag saknas',
717
+ (r) =>
718
+ r.status === 'ofärdig' ||
719
+ r.status === 'fel' ||
720
+ r.status === 'underlag saknas' ||
721
+ r.status === 'pågår',
625
722
  )
626
723
  ? 1
627
724
  : 0
@@ -141,6 +141,15 @@ Efter etiketten följer fritexten som förut. Flera avvikelser: etiketten är de
141
141
  allvarligaste av dem (motsägelse väger över precisering), och varje avvikelse
142
142
  får en egen mening.
143
143
 
144
+ ### Kundens språk
145
+
146
+ Prompten bär `SPRÅK: svenska | engelska`, kundens språk. Analysen är
147
+ Haus-intern, men delar av den går vidare till kunden genom poängsättaren: det
148
+ preciserade på avvikelseraden blir en förutsättning i förslaget, och
149
+ alternativen under _Obesvarade utfallsval_ blir kundens alternativ. Namnge
150
+ dem i kundens språk ur promptens `SPRÅK:`-rad; i en fristående körning utan
151
+ raden är det specens språk.
152
+
144
153
  ### Beroenden i arbetskopian
145
154
 
146
155
  Prompten bär `DEPS: installerade | saknas`. Vid `saknas` finns inga
@@ -167,6 +176,10 @@ delarna ovan (alternativen, om de skiljer sig i vad som måste byggas, vilket
167
176
  alternativet analysen är skriven utifrån) skrivs på indenterade underrader
168
177
  under den punkten, inte som egna listpunkter. Formkontrollen räknar bara
169
178
  listpunkter på kolumn 0; en underrad räknas inte som ett eget alternativ.
179
+ Alternativ i löptext ("(a) text i bilden, (b) egen komponent") räknas inte
180
+ alls: formkontrollen fäller `motsägelse` med sådan prosa i avsnittet som en
181
+ brist och varnar vid `precisering`, så skriv listan eller skriv `inga`, aldrig
182
+ en uppräkning i en mening.
170
183
 
171
184
  **Struket alternativ.** Ett alternativ som inte löser specens _Problem_ raderas inte,
172
185
  det stryks: den översta listpunkten (samma nivå som alternativets "- ") börjar med
@@ -304,6 +304,12 @@ ser rimlig ut.
304
304
  hur skalan räknade. Nämn aldrig poäng, referensuppdrag, ankare eller skalan. Kunden ska
305
305
  kunna invända mot omfattningen, inte mot modellen.
306
306
 
307
+ **Kundens språk** är, överallt i den här skillen, det promptens `SPRÅK:`-rad anger
308
+ (`svenska` eller `engelska`); i en fristående körning utan raden är det specens språk.
309
+ Det gäller `customer_rationale`, `conditions`, `assumptions_stated`, `alternatives`
310
+ (`label` och `adds`) och `coverage_post.yta`. Kontrollen fäller kundtext på fel språk,
311
+ och en sådan post dispatchas om i sin helhet.
312
+
307
313
  Finns alternativ: beskriv dem sist, som egna vägar med var sitt pris. En mening var
308
314
  räcker — vad kunden får i stället, och vad som tillkommer. Priset räknas ur poängen
309
315
  någon annanstans; skriv aldrig ut poängen.
@@ -65,7 +65,7 @@ Hämta brädans sektioner. **En sektion:** ingen fråga, använd den. **Flera se
65
65
 
66
66
  Hämta fälten på båda brädorna och gå igenom tre saker:
67
67
 
68
- - **Finns fälten?** Kundbrädan behöver `Original request`, `Customer`, `Requested by` och kundstatusfältet. Samlingsbrädan behöver dessa plus `Offer status`, `Source`, `Request type`, `Funding`, `Score`, `Conditional classification`, `Decision`, `Decision reason`, `Spec iterations`, `Class iterations`.
68
+ - **Finns fälten?** Kundbrädan behöver `Original request`, `Customer`, `Requested by` och kundstatusfältet. Samlingsbrädan behöver dessa plus `Offer status`, `Source`, `Request type`, `Funding`, `Score`, `Has conditions`, `Decision`, `Decision reason`, `Spec iterations`, `Score iterations`.
69
69
  - **Är de delade fälten samma fält?** Jämför gid, inte namn. Två fält med samma namn och olika gid är en dubblett — de driftar isär omedelbart, och felet syns först hos kunden.
70
70
  - **Går alternativen att slå upp?** Normaliserat, enligt regeln ovan. Rapportera vilka som inte gick att matcha och vilka alternativ som faktiskt finns.
71
71
 
@@ -130,7 +130,7 @@ Domän-JSON till vänster, fältnamn till höger. Ingen annan skill får se hög
130
130
  | `customer_rationale` | beskrivningens `## Lösning och omfattning` | Låst efter `Decision`, se nedan |
131
131
  | `assumptions_stated` ej tom | beskrivningens `## Lösning och omfattning` | Kunden godkänner dem med priset |
132
132
  | `alternatives` ej tom | beskrivningens `## Lösning och omfattning` | Egna vägar med var sitt pris |
133
- | `conditions` ej tom | Conditional classification | se nedan |
133
+ | `conditions` ej tom | Has conditions | se nedan |
134
134
  | `decision` | Decision | Beställd · Ej beställd · Uppskjuten |
135
135
  | `decision_reason` | Decision reason | |
136
136
 
@@ -139,16 +139,15 @@ där kunden faktiskt läser den. Att skriva samma text på två ställen gav tv
139
139
  som kunde glida isär, och fältet fanns bara för att kunna filtrera på en text ingen
140
140
  filtrerar på. Fältet får ligga kvar i Asana med sina gamla värden som historik.
141
141
 
142
- **Två fältnamn är historiska.** `Class iterations` och `Conditional classification`
143
- döptes när modellen satte klasser i stället för poäng. Innehållet är oförändrat i sak —
144
- en varvräknare och en ja/nej-flagga för villkor — så de används vidare under sina gamla
145
- namn hellre än att data flyttas. Döps de om i Asana måste den här mappningen uppdateras
146
- samtidigt.
142
+ **Fältnamnen byttes 2026-09-08 (ADR-0121):** `Score iterations` hette `Class iterations`
143
+ och `Has conditions` hette `Conditional classification`. Innehållet är oförändrat i sak,
144
+ en räknare över poängsättningsvarv och en ja/nej-flagga för villkor. En bräda som
145
+ fortfarande bär de gamla namnen är inte uppdaterad: säg till i stället för att gissa.
147
146
 
148
147
  `Score` är ett taltfält. Skriv poängen som tal, inte som text, så att den går att
149
148
  sortera och summera i listvyer.
150
149
 
151
- `Conditional classification` är härledd: villkorslistan är tom eller inte. Sätt alltid ett värde när en poängsättning skrivs — aldrig tomt, eftersom tomt är reserverat för "ingen har poängsatt än". Bär fältet flera värden samtidigt är det felkonfigurerat som flerval; skriv ett värde och säg till.
150
+ `Has conditions` är härledd: villkorslistan är tom eller inte. Sätt alltid ett värde när en poängsättning skrivs — aldrig tomt, eftersom tomt är reserverat för "ingen har poängsatt än". Bär fältet flera värden samtidigt är det felkonfigurerat som flerval; skriv ett värde och säg till.
152
151
 
153
152
  Asana konverterar markdown till `html_notes`. Använd `html_notes` för beskrivningen, för subtaskarnas innehåll och för kommentarer, inte `notes`, annars tappas struktur — rubriker, kursiv text och de vågräta linjerna mellan sektionerna försvinner alla med `notes`.
154
153
 
@@ -217,7 +216,7 @@ körningen gjordes.
217
216
 
218
217
  Skälet: en poäng som stiger är en prishöjning. Sker den genom att beskrivningen skrivs om finns inget spår av vad kunden faktiskt godkände, och den fria exiten — modellens centrala kundlöfte — går inte att lösa ut, eftersom det inte längre går att visa vad som ändrats.
219
218
 
220
- Låset gäller beskrivningen och `Score`, som bär den poäng priset räknades ur. Det som är internt får alltid skrivas — subtaskarna (det är hela poängen med dem: de är löpnumrerade och ärver inte kundbrädan, så en poängsättning efter kundbeslut landar där som vanligt), `Conditional classification` och räknarna.
219
+ Låset gäller beskrivningen och `Score`, som bär den poäng priset räknades ur. Det som är internt får alltid skrivas — subtaskarna (det är hela poängen med dem: de är löpnumrerade och ärver inte kundbrädan, så en poängsättning efter kundbeslut landar där som vanligt), `Has conditions` och räknarna.
221
220
 
222
221
  ### Grindläget stöds inte
223
222
 
@@ -361,8 +360,8 @@ Kundbrädan bär bara det kunden ska se. Interna fält finns enbart på samlings
361
360
  | Bara samlingsbrädan | Delade, syns på båda |
362
361
  | ---------------------------------- | ------------------------------------------ |
363
362
  | Offer status · Source | Kundstatus |
364
- | Spec iterations · Class iterations | Original request · Customer · Requested by |
365
- | Conditional classification | Request type · Funding |
363
+ | Spec iterations · Score iterations | Original request · Customer · Requested by |
364
+ | Has conditions | Request type · Funding |
366
365
  | | Score · Decision · Decision reason |
367
366
 
368
367
  Ett delat fält är **samma** fält på båda brädorna. Finns två fält med samma namn och olika gid är det en dubblett, inte en delning — de driftar isär omedelbart och felet syns först när en kund tittar på något som inte uppdaterats. Upptäcker du en sådan: skriv inte till någon av dem, säg till.
@@ -428,7 +427,7 @@ Skälet: `Request status` är det enda i hela flödet en kund ser. Två fält so
428
427
 
429
428
  Feedback är en kommentar plus en flytt bakåt till närmaste AI-läge. Två regler:
430
429
 
431
- **Bumpa räknaren.** Bakåt till 📥 höjer `Spec iterations`. Bakåt till ✅ höjer `Class iterations` (fältets namn är historiskt; det räknar poängsättningsvarv). Läs nuvarande värde och skriv värdet plus ett — sätt aldrig ett absolut tal från minnet.
430
+ **Bumpa räknaren.** Bakåt till 📥 höjer `Spec iterations`. Bakåt till ✅ höjer `Score iterations` (räknar poängsättningsvarv). Läs nuvarande värde och skriv värdet plus ett — sätt aldrig ett absolut tal från minnet.
432
431
 
433
432
  **Ändrad spec ogiltigförklarar poängsättningen.** Går en post bakåt till 📥 ska den passera ⚖️ igen på vägen tillbaka. Skriv inte över `Score` med ett nytt värde utan att posten gjort den resan.
434
433
 
@@ -68,6 +68,8 @@ Det är hela inputen. Ditt jobb är beredningen — att växa en spec ur den. En
68
68
 
69
69
  **Leta innan du antar.** Inventera vad sessionen ger dig tillgång till och använd det. Vilka källor som finns beror på uppdraget: i ett befintligt system är kodbasen bäst (finns vyn redan? vilket mönster används för liknande listor? vad heter begreppen i koden?), följt av tidigare specar och tickets för samma kund (hur har liknande poster lösts? vilka termer använder kunden — "skattning", "skift", "verksamhet"?). I ett nytt projekt finns ingen kodbas — då är briefen, offertunderlaget eller annat projektmaterial källan, och lösningen förankras i vad det materialet säger om målgrupp, flöden och befintliga system hos kunden. Det du hittar i källorna är inte antaganden — det är fakta, och de gör specen träffsäker på riktigt. Först när en fråga inte går att besvara ur det som finns blir den ett antagande i granskarblocket. Skriv gärna i granskarblocket _var_ du hämtat de viktigaste faktana ("mönstret finns i användarhanteringsvyn", "enligt briefen sid 2") — det låter granskaren skilja verifierat från gissat på en sekund.
70
70
 
71
+ **Leta efter det som redan finns innan du skriver Lösning.** Sök i källmanifestet (`KÄLLOR:`) och med Grep i repot efter en inbyggd funktion i plattformen eller ett redan installerat paket som gör det önskemålet ber om: en väljare, en meny, en export, en behörighetsmodell som redan finns. Redovisa fyndet i granskarblocket under _Vägval jag gjort_ eller _Prisavgörande öppet val_, även när du inte hittade något ("ingen inbyggd yta för X funnen i KÄLLOR eller repot"). Det du inte letat efter kan du inte välja bort medvetet, och ett omedvetet egenbygge är det dyraste valet specen kan göra åt kunden (se regeln under Output).
72
+
71
73
  Sedan gäller för själva skrivandet:
72
74
 
73
75
  **Härled problemet ur önskemålet.** "Kan själv lägga till" betyder att någon idag inte kan själv — det görs via Haus eller inte alls. Det är problemraden. Du behöver inte veta detaljerna; skriv den mest sannolika vardagen och redovisa den som antagande om du är osäker.
@@ -101,7 +103,7 @@ Skriv varje spec som ett eget block, exakt enligt formatet. Efter VARJE spec: et
101
103
  **Antaganden:** [tolkningar av otydligt underlag, en rad var — "tom" om inga]
102
104
  **Vägval jag gjort:** [beslut du fattat på eget bevåg + en rads motivering — märk ut de som rör vad användaren får, de är specbeslut och det är dem granskaren måste titta på]
103
105
  **Öppna utfallsval:** [frågor om vad användaren får som inte gick att avgöra ur underlaget — "inga" om inga. Rena byggbeslut hör inte hit]
104
- **Prisavgörande öppet val:** [nej · eller "ja: <valet i en mening, i kundens språk>" när alternativen skiljer sig i vad som måste byggas och du inte kan välja ett utan att gissa kundens avsikt. Ett ja stoppar posten före analys tills valet är avgjort; ett val som redan står under "Avgjorda val" i KÄLLOR flaggas aldrig]
106
+ **Prisavgörande öppet val:** [nej · eller "ja: <valet i en mening, i kundens språk>" när alternativen skiljer sig i vad som måste byggas och du inte kan välja ett utan att gissa kundens avsikt. Inbyggd funktion kontra egenbygge är ALLTID ett ja med båda alternativen namngivna, se nedan. Ett ja stoppar posten före analys tills valet är avgjort; ett val som redan står under "Avgjorda val" i KÄLLOR flaggas aldrig]
105
107
  **Flaggor:** [Koncept-/Diagnos-kandidat med skäl (specen själv bär markörraden, det här är granskarens sammanfattning) · misstänkt bunt/beroende till annan post · sådant som ser ut att röra behörighet, betalning, oåterkallelig data eller personuppgifter]
106
108
  **Struket ur underlaget:** [tidsord, belopp, interna anteckningar som inte hör hemma i spec]
107
109
  ```
@@ -114,6 +116,8 @@ De två vägvalsraderna är åtskilda för att de kräver olika sak av granskare
114
116
 
115
117
  Prisavgörande öppet val är det enda i granskarblocket som stoppar posten. Vanliga öppna utfallsval löses av poängsättarens fyrstegsregel; den här raden är för valet där två alternativ ger två olika priser och bara kunden kan välja.
116
118
 
119
+ **Egen lösning kontra plattformsfunktion.** Finns i källmanifestet, eller i din egen sökning i repot, en inbyggd funktion i plattformen eller ett redan installerat paket som täcker önskemålet, och du väljer att bygga en egen yta i stället, eller tvärtom väljer bort en fungerande inbyggd yta för en egen, är det ALLTID `Prisavgörande öppet val: ja` med båda alternativen namngivna (inbyggd funktion respektive egenbygge), aldrig ett Vägval jag gjort. Skillnaden i pris mellan de två är som störst just där, och bara kunden kan välja. Ett egenbygge som inte ens nämner den inbyggda ytan ser ut som det enda sättet, och kunden betalar tre gånger priset utan att ha valt det.
120
+
117
121
  ## Exempel i korthet
118
122
 
119
123
  Rått önskemål: _"Som chef vill jag själv kunna radera en skattning så att olämpligt innehåll kan tas bort direkt utan att ringa Haus."_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {