@haus-tech/haus-workflow 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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`, `Original request title`, `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
 
@@ -116,6 +116,7 @@ Domän-JSON till vänster, fältnamn till höger. Ingen annan skill får se hög
116
116
  | --------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
117
117
  | `request_id` | task gid | — |
118
118
  | `original_request` | Original request + beskrivningens `## Ursprunglig förfrågan` | Skrivs en gång. Ändras aldrig, inte ens för stavfel |
119
+ | `original_request_title` | Original request title | Skrivs en gång. Ändras aldrig, inte ens för stavfel. Tomt när kunden aldrig skrev en rubrik |
119
120
  | `source` | Source | Kunden skrev själv · Mejl · Möte · Haus initierade |
120
121
  | `customer` | Customer | |
121
122
  | `requested_by` | Requested by | people-fält, kräver user gid |
@@ -130,7 +131,7 @@ Domän-JSON till vänster, fältnamn till höger. Ingen annan skill får se hög
130
131
  | `customer_rationale` | beskrivningens `## Lösning och omfattning` | Låst efter `Decision`, se nedan |
131
132
  | `assumptions_stated` ej tom | beskrivningens `## Lösning och omfattning` | Kunden godkänner dem med priset |
132
133
  | `alternatives` ej tom | beskrivningens `## Lösning och omfattning` | Egna vägar med var sitt pris |
133
- | `conditions` ej tom | Conditional classification | se nedan |
134
+ | `conditions` ej tom | Has conditions | se nedan |
134
135
  | `decision` | Decision | Beställd · Ej beställd · Uppskjuten |
135
136
  | `decision_reason` | Decision reason | |
136
137
 
@@ -139,16 +140,15 @@ där kunden faktiskt läser den. Att skriva samma text på två ställen gav tv
139
140
  som kunde glida isär, och fältet fanns bara för att kunna filtrera på en text ingen
140
141
  filtrerar på. Fältet får ligga kvar i Asana med sina gamla värden som historik.
141
142
 
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.
143
+ **Fältnamnen byttes 2026-09-08 (ADR-0121):** `Score iterations` hette `Class iterations`
144
+ och `Has conditions` hette `Conditional classification`. Innehållet är oförändrat i sak,
145
+ en räknare över poängsättningsvarv och en ja/nej-flagga för villkor. En bräda som
146
+ fortfarande bär de gamla namnen är inte uppdaterad: säg till i stället för att gissa.
147
147
 
148
148
  `Score` är ett taltfält. Skriv poängen som tal, inte som text, så att den går att
149
149
  sortera och summera i listvyer.
150
150
 
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.
151
+ `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
152
 
153
153
  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
154
 
@@ -217,7 +217,7 @@ körningen gjordes.
217
217
 
218
218
  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
219
 
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.
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), `Has conditions` och räknarna.
221
221
 
222
222
  ### Grindläget stöds inte
223
223
 
@@ -334,13 +334,14 @@ Ursprunget avgör vad du gör, och det avgör också hur underlaget ska tolkas s
334
334
 
335
335
  ### Kunden skrev själv på sin bräda
336
336
 
337
- Kundens ord ligger i beskrivningen, och beskrivningen ska sedan skrivas om till sektionsform. Det är den **enda destruktiva operationen i hela flödet**, och ordningen är inte förhandlingsbar:
337
+ Kundens ord ligger i beskrivningen och i task name, och båda skrivs över senare: beskrivningen görs om till sektionsform, namnet byts till det förändrade tillståndet när specen skrivs. Det är de **enda destruktiva operationerna i hela flödet**, och ordningen är inte förhandlingsbar:
338
338
 
339
- 1. Kopiera beskrivningen ordagrant till `Original request`
340
- 2. **Läs tillbaka fältet och verifiera att texten landat**
341
- 3. Först då skriv om beskrivningen — samma text ordagrant under `## Ursprunglig förfrågan`, specen under `## Specifikation`
339
+ 1. Kopiera beskrivningen ordagrant till `Original request` och task name ordagrant till `Original request title`
340
+ 2. **Läs tillbaka båda fälten och verifiera att texten landat**
341
+ 3. Först då skriv om beskrivningen, samma text ordagrant under `## Ursprunglig förfrågan`, specen under `## Specifikation`
342
+ 4. **Byt aldrig task name innan `Original request title` är skrivet och läst tillbaka.** Namnbytet sker när specen skrivs, ofta i en senare körning, och då finns kundens rubrik bara i det fältet
342
343
 
343
- Hoppar du över steg 2 och skrivningen tyst misslyckas är kundens egna ord borta för alltid — vilket är precis det `Original request` finns för att förhindra. `Source` = kunden skrev själv.
344
+ Hoppar du över steg 2 och skrivningen tyst misslyckas är kundens egna ord borta för alltid — vilket är precis det `Original request` finns för att förhindra, och ett namnbyte utan steg 4 gör samma sak med rubriken. `Source` = kunden skrev själv.
344
345
 
345
346
  ### Mejl, möte eller annat underlag
346
347
 
@@ -348,6 +349,8 @@ Här skapar agenten både task och spec i ett svep, och kundens ord ligger kvar
348
349
 
349
350
  **Klistra in det relevanta stycket ur mejlet eller anteckningen ordagrant** i `Original request` och under `## Ursprunglig förfrågan` i beskrivningen — inte en sammanfattning, inte en parafras. Risken för glid är störst just här, eftersom underlaget är råare och agenten tolkat mer.
350
351
 
352
+ **Rubriken** är mejlets ärenderad eller anteckningens egen rubrik, ordagrant, i `Original request title`. Skrev kunden aldrig någon rubrik lämnas fältet tomt. En rubrik vi formulerat själva ser i granskningen ut som kundens ord, och ett fält som blandar de två är sämre än ett tomt.
353
+
351
354
  Blir det flera requests ur samma underlag får var och en **sitt eget relevanta stycke**, inte hela mejlet klistrat en gång per post. Ett fält som innehåller allt håller ingenting.
352
355
 
353
356
  `Source` = mejl eller möte. Statusarna sätts enligt skapandesekvensen nedan. Notera bara att en task inte får hamna i ett granskningsläge utan att specen faktiskt är skriven — en tom task i granskningskön ser ut som ett fel i skillen när det är ett fel i regeln.
@@ -361,9 +364,10 @@ Kundbrädan bär bara det kunden ska se. Interna fält finns enbart på samlings
361
364
  | Bara samlingsbrädan | Delade, syns på båda |
362
365
  | ---------------------------------- | ------------------------------------------ |
363
366
  | Offer status · Source | Kundstatus |
364
- | Spec iterations · Class iterations | Original request · Customer · Requested by |
365
- | Conditional classification | Request type · Funding |
367
+ | Spec iterations · Score iterations | Original request · Customer · Requested by |
368
+ | Has conditions | Request type · Funding |
366
369
  | | Score · Decision · Decision reason |
370
+ | | Original request title |
367
371
 
368
372
  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.
369
373
 
@@ -371,7 +375,7 @@ Ett delat fält är **samma** fält på båda brädorna. Finns två fält med sa
371
375
 
372
376
  Ordningen spelar roll, eftersom de interna fälten inte existerar på kundbrädan.
373
377
 
374
- **1 — Skapa på kundbrädan** med det som finns där: task name, `Original request`, `Customer`, `Requested by`. Lägg tasken i rätt sektion (se nedan), och **sätt `Request status` redan här**, explicit:
378
+ **1 — Skapa på kundbrädan** med det som finns där: task name, `Original request`, `Original request title`, `Customer`, `Requested by`. Lägg tasken i rätt sektion (se nedan), och **sätt `Request status` redan här**, explicit:
375
379
 
376
380
  | Läge vid skapandet | Request status |
377
381
  | ------------------------------------ | --------------- |
@@ -395,7 +399,7 @@ Försöker du sätta `Offer status` eller `Source` i steg 1 avbryter uppslagning
395
399
 
396
400
  Efter skapandet rör du aldrig `Request status` igen. Då härleds den av Asana-regeln ur `Offer status`.
397
401
 
398
- Task name ska vara det förändrade tillståndet, inte uppgiften, samma regel som specrubriken. Finns ingen spec än: använd önskemålets egna ord, och byt namn när specen skrivs.
402
+ Task name ska vara det förändrade tillståndet, inte uppgiften, samma regel som specrubriken. Finns ingen spec än: använd önskemålets egna ord, och byt namn när specen skrivs. Men namnbytet förutsätter att `Original request title` bär den ursprungliga rubriken och att den är läst tillbaka, se _Kunden skrev själv på sin bräda_.
399
403
 
400
404
  ### Sektion på kundbrädan
401
405
 
@@ -428,7 +432,7 @@ Skälet: `Request status` är det enda i hela flödet en kund ser. Två fält so
428
432
 
429
433
  Feedback är en kommentar plus en flytt bakåt till närmaste AI-läge. Två regler:
430
434
 
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.
435
+ **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
436
 
433
437
  **Ä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
438
 
@@ -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.3.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {