@haus-tech/haus-workflow 4.4.0 → 4.6.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: request-sync
3
- description: Läser och skriver requests mot ärendesystemet (idag Asana), och sätter upp kopplingen mellan ett repo och rätt kundbräda. Använd alltid när användaren vill "sätt upp", "koppla repot mot Asana", "kör uppsättningen", "hämta requests", "vad ligger för granskning", "lägg in klassningen i Asana", "skriv tillbaka", "flytta ärendet", "skapa en request", eller när output från `request-write-spec`, `request-assess-impact` eller `request-classify` ska landa på brädan. Detta är den enda skillen som känner till fältnamn, statusvärden och brädor — alla andra skills i request-familjen arbetar mot domän-JSON.
3
+ description: Läser och skriver requests mot ärendesystemet (idag Asana), och sätter upp kopplingen mellan ett repo och rätt kundbräda. Använd alltid när användaren vill "sätt upp", "koppla repot mot Asana", "kör uppsättningen", "hämta requests", "vad ligger för granskning", "lägg in klassningen i Asana", "skriv tillbaka", "flytta ärendet", "skapa en request", eller när output från `request-write-spec`, `request-assess-impact` eller `request-score` ska landa på brädan. Detta är den enda skillen som känner till fältnamn, statusvärden och brädor — alla andra skills i request-familjen arbetar mot domän-JSON.
4
4
  ---
5
5
 
6
6
  # request-sync
@@ -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`, `Class (offered)`, `Class (revised)`, `Class rationale`, `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`, `Conditional classification`, `Decision`, `Decision reason`, `Spec iterations`, `Class 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
 
@@ -112,24 +112,41 @@ Hittar du inte ett fält du ska skriva till: avbryt och säg vilket. Skapa aldri
112
112
 
113
113
  Domän-JSON till vänster, fältnamn till höger. Ingen annan skill får se högerkolumnen.
114
114
 
115
- | Domän | Fält | Not |
116
- | ------------------------------ | ------------------------------------------------------------ | --------------------------------------------------- |
117
- | `request_id` | task gid | — |
118
- | `original_request` | Original request + beskrivningens `## Ursprunglig förfrågan` | Skrivs en gång. Ändras aldrig, inte ens för stavfel |
119
- | `source` | Source | Kunden skrev själv · Mejl · Möte · Haus initierade |
120
- | `customer` | Customer | |
121
- | `requested_by` | Requested by | people-fält, kräver user gid |
122
- | `type` | Request type | Bug · Improvement · Wear |
123
- | `funding` | Funding | |
124
- | `spec` | beskrivningens `## Specifikation` | Bara den sektionen. Låst efter `Decision`, se nedan |
125
- | `impact` | subtask `Påverkansanalys vN` | Ny subtask varje varv, aldrig en redigering |
126
- | `classification` | subtask `Klassning vN` | Ny subtask varje varv, aldrig en redigering |
127
- | `customer.class`, läge pricing | Class (offered) + beskrivningens `## Klass och pris` | Skrivs aldrig över efter kundbeslut |
128
- | `customer.class`, läge guard | Class (revised) | |
129
- | `customer.rationale` | Class rationale + beskrivningens `## Klass och pris` | Låst efter `Decision`, se nedan |
130
- | `customer.conditions` ej tom | Conditional classification | se nedan |
131
- | `decision` | Decision | Beställd · Ej beställd · Uppskjuten |
132
- | `decision_reason` | Decision reason | |
115
+ | Domän | Fält | Not |
116
+ | --------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
117
+ | `request_id` | task gid | — |
118
+ | `original_request` | Original request + beskrivningens `## Ursprunglig förfrågan` | Skrivs en gång. Ändras aldrig, inte ens för stavfel |
119
+ | `source` | Source | Kunden skrev själv · Mejl · Möte · Haus initierade |
120
+ | `customer` | Customer | |
121
+ | `requested_by` | Requested by | people-fält, kräver user gid |
122
+ | `type` | Request type | Bug · Improvement · Wear |
123
+ | `funding` | Funding | |
124
+ | `spec` | beskrivningens `## Specifikation` | Bara den sektionen. Låst efter `Decision`, se nedan |
125
+ | `impact` | subtask `Påverkansanalys vN` | Ny subtask varje varv, aldrig en redigering |
126
+ | `bracket` + `drivers` | subtask `Poängsättning vN` | Ny subtask varje varv, aldrig en redigering |
127
+ | `score` | Score | Skrivs aldrig över efter kundbeslut. Aldrig på en förälder som har poängsatta delar |
128
+ | `outcome: DELA_UPP` | ingen `Score`, posten stannar på ✅ Spec ok | Se _Uppdelade poster_. Räknarna rörs inte — de räknar granskningsvarv, och ingen människa har skickat tillbaka posten |
129
+ | delposter | subtask per del, med `Score` | Se _Uppdelade poster_ |
130
+ | `customer_rationale` | beskrivningens `## Lösning och omfattning` | Låst efter `Decision`, se nedan |
131
+ | `assumptions_stated` ej tom | beskrivningens `## Lösning och omfattning` | Kunden godkänner dem med priset |
132
+ | `alternatives` ej tom | beskrivningens `## Lösning och omfattning` | Egna vägar med var sitt pris |
133
+ | `conditions` ej tom | Conditional classification | se nedan |
134
+ | `decision` | Decision | Beställd · Ej beställd · Uppskjuten |
135
+ | `decision_reason` | Decision reason | |
136
+
137
+ **`Class rationale` skrivs inte längre.** Motiveringen bor i beskrivningens `## Lösning och omfattning`,
138
+ där kunden faktiskt läser den. Att skriva samma text på två ställen gav två versioner
139
+ som kunde glida isär, och fältet fanns bara för att kunna filtrera på en text ingen
140
+ filtrerar på. Fältet får ligga kvar i Asana med sina gamla värden som historik.
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.
147
+
148
+ `Score` är ett taltfält. Skriv poängen som tal, inte som text, så att den går att
149
+ sortera och summera i listvyer.
133
150
 
134
151
  `Conditional classification` är härledd: villkorslistan är tom eller inte. Sätt alltid ett värde när en klassning skrivs — aldrig tomt, eftersom tomt är reserverat för "ingen har klassat än". Bär fältet flera värden samtidigt är det felkonfigurerat som flerval; skriv ett värde och säg till.
135
152
 
@@ -156,10 +173,10 @@ _Vad vi föreslår att vi bygger — uppdaterad 2026-08-21_
156
173
 
157
174
  ---
158
175
 
159
- ## Klass och pris
160
- _Omfattning och pris — uppdaterad 2026-08-21_
176
+ ## Lösning och omfattning
177
+ _Vad vi gör, vad det bygger på och vad det kostar — uppdaterad 2026-08-21_
161
178
 
162
- [klass och kundvänlig motivering]
179
+ [kundvänlig motivering, därunder eventuella förutsättningar]
163
180
  ```
164
181
 
165
182
  **Linjen mellan sektionerna** skrivs som `<hr/>`. Asana renderar den i tasks — till skillnad från `<h3>`, som inte finns i beskrivningsfältet. Håll dig därför till `<h2>` för rubrikerna (`##` i markdown), aldrig en nivå djupare.
@@ -172,49 +189,144 @@ Raden är i kundens språk, precis som resten av beskrivningen. Aldrig skillnamn
172
189
 
173
190
  Varje sektion har exakt en källa:
174
191
 
175
- | Sektion | Källa | Skrivs |
176
- | -------------------------- | ------------------------------------------------------------- | --------------------------- |
177
- | `## Ursprunglig förfrågan` | kundens ord, eller utdraget ur mejl/brief | vid skapandet, sedan aldrig |
178
- | `## Specifikation` | `request-write-spec` | vid varje specvarv |
179
- | `## Klass och pris` | `request-classify`: `customer.class` och `customer.rationale` | vid varje klassningsvarv |
192
+ | Sektion | Källa | Skrivs |
193
+ | --------------------------- | --------------------------------------------------------------------------- | ---------------------------- |
194
+ | `## Ursprunglig förfrågan` | kundens ord, eller utdraget ur mejl/brief | vid skapandet, sedan aldrig |
195
+ | `## Specifikation` | `request-write-spec` | vid varje specvarv |
196
+ | `## Lösning och omfattning` | `request-score`: `customer_rationale`, `assumptions_stated`, `alternatives` | vid varje poängsättningsvarv |
180
197
 
181
198
  - **Varje steg uppdaterar bara sin egen sektion och lämnar resten orörd.** Tidigare skrev bara en skill till beskrivningen; nu skriver flera, och ett steg som ersätter hela fältet raderar de andras arbete. Det gäller även datumraden: du uppdaterar din egen sektions rad, aldrig någon annans.
182
199
  - **Ursprunglig förfrågan** är kundens ord ordagrant, eller det relevanta utdraget ur ett längre mejl eller en brief. Skrivs en gång, redigeras aldrig. Fältet `Original request` behålls parallellt som skrivskyddad kopia.
183
- - **Klass och pris** är klassen och den kundvänliga motiveringen. Inga protokolltermer, inga frågerader. Samma text står i `Class rationale`-fältet: fältet finns för att kunna filtreras och läsas i listvyer, sektionen för att kunna läsas i sitt sammanhang.
200
+ - **Lösning och omfattning** är den kundvänliga motiveringen, de förutsättningar posten vilar på, och eventuella alternativa lösningar med var sitt pris. Inga protokolltermer, inga frågerader, **och aldrig poängen** — den är ett internt mått och står bara i `Score`-fältet. Ordningen är motivering, förutsättningar, alternativ. Förutsättningarna godkänner kunden tillsammans med priset; alternativen är andra vägar kunden kan välja i stället, var och en med sitt eget pris.
184
201
 
185
202
  Beskrivningen har ingen sektion för vägval, och ska inte få en. Val som påverkar vad kunden får besvaras i specen — som acceptanskriterium eller under _Ingår inte_ — och är därmed redan en del av `## Specifikation`. Val som bara rör hur vi bygger är interna och ligger i påverkansanalysens subtask.
186
203
 
187
204
  ### Beskrivningen låses vid kundbeslut
188
205
 
189
- Så snart `Decision` har ett värde är beskrivningen det kunden sagt ja till. **Skriv inte om någon sektion efter den punkten.** Det gäller alla tre sektionerna, oavsett vilket läge klassningen kördes i och oavsett vad som ändrats.
206
+ Så snart `Decision` har ett värde är beskrivningen det kunden sagt ja till. **Skriv inte om någon sektion efter den punkten.** Det gäller alla tre sektionerna, oavsett vad som ändrats.
190
207
 
191
208
  Ägartabellen ovan beskriver vad som gäller fram till kundbeslutet.
192
209
 
193
210
  **Läs `Decision` innan varje skrivning till beskrivningen.** Det är en kontroll som ska utföras, inte något du antar utifrån ärendets status — ett ärende kan ha fått ett beslut mellan två av dina körningar.
194
211
 
195
- Blir klassen en annan vid grinden: fyll `Class (revised)`, lägg klassningen som ny subtask, flytta posten till ⚖️ och stanna där. En människa avgör vad kunden får se, och det blir i så fall ett nytt beslut — inte en tyst redigering av ett gammalt.
212
+ Körs poängsättningen om på ett ärende som redan har ett `Decision`: skriv inte över
213
+ `Score`. Lägg poängsättningen som ny subtask, flytta posten till ⚖️ och stanna där. En
214
+ människa avgör vad kunden får se, och det blir i så fall ett nytt beslut — inte en tyst
215
+ redigering av ett gammalt. Kontrollera `Decision` innan du skriver, oavsett varför
216
+ körningen gjordes.
196
217
 
197
- Detsamma gäller om en klassning körs om i prisläget på ett ärende som redan har ett `Decision`. Kontrollera fältet innan du skriver, inte vilket läge du kördes i.
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.
198
219
 
199
- Skälet: en klass som ändras uppåt ä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.
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.
200
221
 
201
- Låset gäller beskrivningen och de fält som bär samma text som den: `Class (offered)` och `Class rationale`. 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 klassning efter kundbeslut landar där som vanligt), `Class (revised)`, `Conditional classification` och räknarna.
222
+ ### Grindläget stöds inte
202
223
 
203
- **Påverkansanalys och klassning läggs som subtasks, inte som kommentarer.**
224
+ `request-score` kan köras som grind på en fullständig implementationsplan, med
225
+ `mode: "guard"`. **Den utdatan skrivs inte till Asana.** Det finns inget fält för en
226
+ reviderad poäng och inget ska skapas förrän någon bestämt hur en avvikelse ska hanteras
227
+ mot kund.
228
+
229
+ Får du en körning med `mode: "guard"`: avbryt, säg att grindläget inte synkas, och
230
+ lämna ärendet orört. Skriv aldrig guard-poängen till `Score` — det fältet bär den
231
+ poäng kunden fick sitt pris på.
232
+
233
+ **Påverkansanalys och poängsättning läggs som subtasks, inte som kommentarer.**
204
234
 
205
235
  ```
206
236
  Ticket
207
237
  ├── beskrivning: de tre sektionerna ovan
208
- ├── Påverkansanalys (subtask)
209
- └── Klassning (subtask)
238
+ └── Analys (subtask, ingen Score)
239
+ ├── Påverkansanalys vN (under Analys, ingen Score)
240
+ └── Poängsättning vN (under Analys, ingen Score)
241
+ ```
242
+
243
+ Är posten uppdelad, eller saknar den berörda ytan tester, tillkommer arbetsrader ovanför
244
+ Analys:
245
+
246
+ ```
247
+ Ticket (ingen egen Score — bär summan av delarna)
248
+ ├── beskrivning: de tre sektionerna ovan
249
+ ├── Testtäckning – <yta> (subtask, egen Score)
250
+ ├── <delmängd A> (subtask, egen Score)
251
+ ├── <delmängd B> (subtask, egen Score)
252
+ └── Analys (subtask, ingen Score)
253
+ ├── Påverkansanalys vN (under Analys, ingen Score)
254
+ └── Poängsättning vN (under Analys, ingen Score)
210
255
  ```
211
256
 
257
+ Se _Analysraderna_ längre ned för vad Analys är och när den skapas.
258
+
212
259
  - Subtasks ärver inte kundbrädan och är därmed interna. Kontrollera att de inte multi-homas dit.
213
- - **Ett nytt varv skapar en ny subtask, den ersätter inte den förra.** Namnge med löpnummer: "Klassning v1", "Klassning v2". Beskrivningen kan inte bära historik, och det är subtaskarna som ger den tillbaka — i läsbar form, till skillnad från en kommentarsström.
214
- - Klassningens subtask innehåller den läsbara tabellen — fråga, svar, belägg — och därunder den råa JSON:en, som behövs för att diffa vid grinden men som ingen läser.
215
- - Finns flera parallella underlag namnges subtaskarna så parningen syns: "Klassning — delad komponent", "Klassning — specialfall".
260
+ - **Ett nytt varv skapar en ny subtask, den ersätter inte den förra.** Namnge med löpnummer: "Poängsättning v1", "Poängsättning v2". Beskrivningen kan inte bära historik, och det är subtaskarna som ger den tillbaka — i läsbar form, till skillnad från en kommentarsström.
261
+ - Poängsättningens subtask innehåller inringningen i läsbar form — vilka referensuppdrag posten vägdes mot, om den är större eller mindre än vart och ett, och på vilken grund — och därunder den råa JSON:en. Inringningen är det granskaren läser: en rimlig poäng med fel jämförelser är en felkalibrering som råkade landa rätt.
262
+ - Finns flera parallella underlag namnges subtaskarna så parningen syns: "Poängsättning — delad komponent", "Poängsättning — specialfall".
263
+
264
+ **Kommentarer används inte längre för påverkansanalys eller poängsättning.** De är kvar för det som faktiskt kommenteras — feedback vid bakåtflytt — och där gäller fortsatt att de är append-only och aldrig redigeras.
265
+
266
+ ## Uppdelade poster
267
+
268
+ Har poängsättningen svarat `DELA_UPP` och analysen pekat ut delarna, skriv dem så här.
269
+
270
+ **Delar som går att leverera för sig blir egna requests** på kundbrädan, alltså vanliga
271
+ tasks och inte subtasks. De har egen spec, eget beslut och egen leverans. Länka dem till
272
+ ursprungsposten.
273
+
274
+ **Delar som inte går att leverera för sig blir subtasks** under posten, en per del, med
275
+ sin poäng i `Score` och en rad överst som säger vilken del av specen delen täcker.
276
+
277
+ **Föräldern får ingen egen `Score`.** Har fältet ett värde sedan tidigare nollas det när
278
+ den första poängsatta subtasken skapas. Asana rullar upp barnens värden till den
279
+ hopfällda föräldern, så ett eget värde där dubbelräknas eller döljer summan.
280
+
281
+ **`Score` skrivs bara på arbetsrader.** `Analys` och raderna under den lämnas tomma.
282
+ Analysraderna blir fler för varje granskningsvarv, och ett värde där skulle räkna
283
+ omarbetning som omfattning.
284
+
285
+ **En del som blev `OVER_SKALAN` får ingen siffra.** Föräldern visar då en summa plus en
286
+ rad utan värde. Det är ett riktigt utfall, inte ett fel — skriv i beskrivningens
287
+ `## Lösning och omfattning` att en del offereras separat, i kundens språk.
288
+
289
+ ### Saknad testtäckning
290
+
291
+ Rapporterar analysen att den berörda ytan saknar automatiska tester: skapa en subtask
292
+ `Testtäckning – <yta>` med egen poäng, och gör implementationens subtask beroende av den
293
+ med `add_dependencies`.
294
+
295
+ Ordningen bär något. Testerna ska beskriva hur ytan fungerar innan ändringen görs;
296
+ skrivna efteråt beskriver de det nya beteendet och bevisar ingenting om vad som gick
297
+ sönder. Testraden hör därför till bygget, inte till förarbetet, trots att den kommer
298
+ först.
299
+
300
+ ### Analysraderna
301
+
302
+ **Alla rader som hör till undersökningen ligger under en subtask som heter `Analys`** –
303
+ påverkansanalys, klassning och poängsättning, ett varv i taget. De ligger alltså ett steg
304
+ längre in än arbetsraderna, inte bredvid dem.
305
+
306
+ Skälet är att analysraderna blir många. Varje granskningsvarv lägger till en, och de
307
+ läses sällan efter att posten är prissatt. Ligger de på samma nivå som arbetet dränker de
308
+ det som faktiskt ska byggas.
309
+
310
+ **En `Analys` per post.** Den skapas första gången en analysrad ska skrivas och
311
+ återanvänds sedan. Ett nytt varv lägger en ny rad under den, aldrig en ny `Analys`.
312
+ Hittar du redan en: använd den, skapa ingen andra.
313
+
314
+ **`Analys` ligger sist i listan**, under arbetsraderna. Arbetet är det man öppnar posten
315
+ för att se.
316
+
317
+ **Varken `Analys` eller dess rader får `Score`.** Asana rullar upp barnens värden till
318
+ föräldern, så ett värde där skulle räknas in i postens summa. Analysraderna blir dessutom
319
+ fler för varje granskningsvarv, och ett värde på dem skulle räkna omarbetning som
320
+ omfattning.
321
+
322
+ **Arbetsraderna ligger kvar direkt under posten.** Testtäckning enligt _Saknad
323
+ testtäckning_ och delarna enligt _Uppdelade poster_ är arbete som ska byggas och bär egen
324
+ `Score`, så de ska synas utan att någon fäller ut något.
216
325
 
217
- **Kommentarer används inte längre för påverkansanalys eller klassning.** De är kvar för det som faktiskt kommenteras — feedback vid bakåtflytt — och där gäller fortsatt att de är append-only och aldrig redigeras.
326
+ Bygg ingen gruppering ovanpå den här. Namnprefix på raderna och Asanas avdelare
327
+ (`is_rendered_as_separator`) användes tidigare för att skilja bygget från förarbetet, och
328
+ båda är borttagna: nästlingen gör samma sak, och avdelarna gick bara att sätta med ett
329
+ direkt anrop mot Asanas API som de flesta sessioner inte kan göra.
218
330
 
219
331
  ## Var en request kommer ifrån
220
332
 
@@ -246,12 +358,12 @@ Blir det flera requests ur samma underlag får var och en **sitt eget relevanta
246
358
 
247
359
  Kundbrädan bär bara det kunden ska se. Interna fält finns enbart på samlingsbrädan, och en task kan inte bära dem förrän den ligger i båda projekten.
248
360
 
249
- | Bara samlingsbrädan | Delade, syns på båda |
250
- | ---------------------------------- | -------------------------------------------------------------- |
251
- | Offer status · Source | Kundstatus |
252
- | Class (revised) | Original request · Customer · Requested by |
253
- | Spec iterations · Class iterations | Request type · Funding |
254
- | Conditional classification | Class (offered) · Class rationale · Decision · Decision reason |
361
+ | Bara samlingsbrädan | Delade, syns på båda |
362
+ | ---------------------------------- | ------------------------------------------ |
363
+ | Offer status · Source | Kundstatus |
364
+ | Spec iterations · Class iterations | Original request · Customer · Requested by |
365
+ | Conditional classification | Request type · Funding |
366
+ | | Score · Decision · Decision reason |
255
367
 
256
368
  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.
257
369
 
@@ -298,13 +410,13 @@ Matcha alltid på gid. `customer_section_name` finns bara för att kunna visas i
298
410
 
299
411
  **Offer status** driver arbetet. Varje läge är en inkorg:
300
412
 
301
- | Status | Väntar på |
302
- | -------------------------------- | -------------------------------------------- |
303
- | 📥 Inkommet | `request-write-spec` |
304
- | 🔍 Spec redo för granskning | människa |
305
- | ✅ Spec ok | `request-assess-impact` + `request-classify` |
306
- | ⚖️ Klassning redo för granskning | människa |
307
- | 📤 Klar – till kund | kundens svar, se `Decision` |
413
+ | Status | Väntar på |
414
+ | ------------------------------------ | ----------------------------------------- |
415
+ | 📥 Inkommet | `request-write-spec` |
416
+ | 🔍 Spec redo för granskning | människa |
417
+ | ✅ Spec ok | `request-assess-impact` + `request-score` |
418
+ | ⚖️ Poängsättning redo för granskning | människa |
419
+ | 📤 Klar – till kund | kundens svar, se `Decision` |
308
420
 
309
421
  **Request status** är kundens vy och härleds av en Asana-regel ur `Offer status`. **Sätt den en enda gång — vid skapandet, innan `Offer status` finns att härleda ur.** Därefter rör du den aldrig; regeln äger den. Startvärdet ska vara exakt det regeln hade härlett, så att fälten aldrig kan säga emot varandra.
310
422
 
@@ -316,9 +428,9 @@ Skälet: `Request status` är det enda i hela flödet en kund ser. Två fält so
316
428
 
317
429
  Feedback är en kommentar plus en flytt bakåt till närmaste AI-läge. Två regler:
318
430
 
319
- **Bumpa räknaren.** Bakåt till 📥 höjer `Spec iterations`. Bakåt till ✅ höjer `Class iterations`. Läs nuvarande värde och skriv värdet plus ett — sätt aldrig ett absolut tal från minnet.
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.
320
432
 
321
- **Ändrad spec ogiltigförklarar klassningen.** Går en post bakåt till 📥 ska den passera ⚖️ igen på vägen tillbaka. Skriv inte över `Class (offered)` med ett nytt värde utan att posten gjort den resan.
433
+ **Ä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.
322
434
 
323
435
  En bakåtflytt producerar en ny spec, och den ska in i beskrivningen — läs därför `Decision` först. Har posten redan ett kundbeslut gäller låsregeln och beskrivningen rörs inte.
324
436
 
@@ -330,7 +442,7 @@ En bakåtflytt producerar en ny spec, och den ska in i beskrivningen — läs d
330
442
 
331
443
  **Skriver till beskrivningen utan att ha läst `Decision`.** Har ärendet ett kundbeslut är beskrivningen låst, oavsett vad som ändrats.
332
444
 
333
- **Rör `Class (offered)` eller `Class rationale` efter att `Decision` är satt.** De bär samma text som beskrivningen och är låsta med den. Blir klassen högre vid grinden är det ett nytt beslut, inte en överskrivning — `Class (revised)` fylls, posten flyttas tillbaka till ⚖️, och människan avgör vad kunden får se.
445
+ **Rör `Score` efter att `Decision` är satt.** De bär samma innehåll som beskrivningen och är låsta med den. Blir poängen högre vid en omkörning är det ett nytt beslut, inte en överskrivning — poängsättningen läggs som subtask, posten flyttas tillbaka till ⚖️, och människan avgör vad kunden får se.
334
446
 
335
447
  **Sätter `Decision` själv.** Det är kundens svar, det kommer utifrån.
336
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "4.4.0",
3
+ "version": "4.6.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "build": "rm -rf dist && esbuild src/cli.ts --bundle --packages=external --platform=node --format=esm --outdir=dist --external:@inquirer/checkbox",
24
24
  "dev": "tsx src/cli.ts",
25
25
  "test": "node scripts/test-fast.mjs",
26
+ "test:quick": "node scripts/test-fast.mjs --quick",
26
27
  "test:full": "node scripts/run-tests.mjs tests/**/*.test.js",
27
28
  "test:coverage": "c8 yarn test:full",
28
29
  "coverage:check": "c8 --check-coverage yarn test:full",
@@ -1,172 +0,0 @@
1
- # project:cloneandsetup procedure
2
-
3
- Clone a project's repos **and** set each one up for local development — node version, dependencies, databases, cross-repo links, and env. This is `project:clone` followed by a per-repo setup pass and a localdev orchestration pass.
4
-
5
- **Always ask before doing work — never assume.** Cloning and setup hit the network, need auth, and can touch databases; confirm before each phase, and respect repos the user already has.
6
-
7
- ## Step 1 — Clone
8
-
9
- Run the full `project:clone` flow by following `references/clone.md` end to end (name → one repo; no name → workspace repos from `repos.manifest.json`). Carry the resulting repo list into Step 2.
10
-
11
- ## Step 2 — Prerequisite gate (one consolidated check)
12
-
13
- Before any setup work, probe **everything** the workspace will need and surface **all** gaps in a single prompt — never discover them piecemeal mid-flow. For the repos being set up, check:
14
-
15
- - **Auth tokens** — `NODE_AUTH_TOKEN` exported (private `@`-scoped npm packages fail without it); `~/.composer/auth.json` (or a repo-local `auth.json`) present if any repo has private composer deps.
16
- - **Docker daemon** — running (`docker info`); required for any repo's `needs:` services.
17
- - **PHP environment** — `valet`, `herd`, `ddev`, or `php` on PATH, if any repo is a PHP/WordPress site.
18
- - **WP-CLI** — `wp --version`, if any repo's `seed:` pulls a WordPress DB.
19
- - **Node versions** — the versions named in each repo's `.nvmrc` / `engines.node` are installable via `nvm`.
20
- - **Data-access preflight** — for every data load the workspace will run (each repo's `seed` **and** the workspace `seeds`), check up front that the user can actually reach the source, so nobody ends up with a half-set-up environment that only fails at the seed step. See below.
21
-
22
- ### Data-access preflight
23
-
24
- The single most common way cloneAndSetup leaves a **half-completed environment** is discovering at the `seed:` step — after cloning, installing, and bringing up services — that the user lacks the AWS/SSH/etc. access to pull the data. Catch it **here**, before any heavy work, and make the data outcome a deliberate choice.
25
-
26
- 1. **Collect every seed:** each repo's `seed` plus the workspace `seeds`. For each, determine its required access:
27
- - If `seed` is an **object with `access`**, run each entry's `check` (a read-only probe; exit 0 = reachable).
28
- - If `seed` is a **bare string**, infer the channel and probe generically: `aws …`/SSM → `aws sts get-caller-identity`; `dep db:pull …` / `ssh`/`scp`/`rsync <user@host>` → `ssh -o BatchMode=yes -o ConnectTimeout=5 <host> true`; `northflank …` → `northflank whoami`. A purely local seed (no remote) needs nothing.
29
- 2. **Report one table** alongside the rest of the gate — per data source: its `label`, the access it needs, and ✓ reachable / ✗ + the `login` remedy (verbatim).
30
- 3. **Resolve every ✗ with one consolidated choice**, then remember it for the run:
31
- - **Set up access now** → print the `login` text, let the user do it, **re-check**, then seed normally.
32
- - **Create that DB EMPTY for now** (only when `empty-ok` is true — the default) → the datastore comes up empty (schema via migrations/`needs:`), the app still boots, and the user can run that one seed later. Name the exact command to fill it.
33
- - **Abort.**
34
- 4. **Only a gap on a seed with `empty-ok: false` is a HARD STOP** (the repo is genuinely broken without data — log in or abort). Every other gap offers the empty path; one source's missing access never blocks another's.
35
-
36
- This is the one prompt the "no half-completed environment" goal justifies. The two valid end-states are **seeded** or **deliberately empty** — never a third "did the expensive work then died at seed."
37
-
38
- Then list the repos and what each will run (node, deps, localdev steps, **data sources + the chosen seed/empty plan**), **report every gap at once**, and get a single go-ahead. For **reused** local clones, ask whether to re-run setup. For each gap, name what it blocks; the user decides whether to fix it now (**ask before any global/system install**) or proceed and skip the affected steps. Don't begin per-repo work until this gate is acknowledged.
39
-
40
- ## Step 3 — Per-repo dependency pass
41
-
42
- For each repo, in its own directory, detect and install from the repo's own files (read its `docs/setup.md` / `CLAUDE.md` / `README.md` first — they win). Select node from `.nvmrc`/`engines.node` (`nvm install`), enable the pinned package manager (`corepack enable`), install JS deps (`yarn`/`pnpm`/`npm` by lockfile), composer deps if `composer.json` + `composer` present. Run each repo's steps in one login shell so the node version stays active. Per-repo failure is reported and skipped, not fatal.
43
-
44
- **Leave `.env` to Step 4 → "Env".** The dependency pass installs only; the env phase writes each repo's `.env` deterministically once services, links, and values are in place.
45
-
46
- ## Step 4 — Local-dev orchestration
47
-
48
- This is the phase that takes the workspace from "installed" to "ready to run." It is driven by `localdev.yml` files; a repo with none is set up by Step 3 only.
49
-
50
- ### House conventions (the "how", so repos specify only the "what")
51
-
52
- Specify the least. Infer the rest from the repo's stack + these conventions, and work out the concrete commands at run time:
53
-
54
- - **IMPORTANT — never install anything without asking first; global/system tools especially** (Homebrew, Docker, Laravel Herd, global `npm`/`composer` packages). Detect what's already present; if something required is missing, name it, say why it's needed, and get an explicit **yes** before installing. Prefer the least-invasive option, and don't switch or override tools the dev already has.
55
- - **PHP / WordPress sites are served by the developer's own PHP environment.** If they already have one — detect `valet`, `herd`, `ddev`, or `php` on PATH — **use it**: just satisfy the env contract (docroot → the repo's `web/`, HTTPS) and report the URL; never override what they already run. **Only when no local PHP environment exists** (a completely fresh machine) suggest installing **[Laravel Herd](https://herd.laravel.com)** as the default (asking first) and point its docroot at `web/` + `herd secure`.
56
- - **Databases and other services (a repo's `needs:`) run in Docker as a shared, workspace-owned `docker-compose` project** — grouped and self-contained (`up`/`down` together, named volumes), namespaced per workspace instance via `COMPOSE_PROJECT_NAME` + its own host ports so the workspace can run multiple times side by side. Image/port/env per the house convention (`mysql:8`, `postgres:16`, …). Don't reuse a repo's own compose that bind-mounts repo-relative init files (`seed.sql`) — author the service in the workspace compose. Services come up **empty** (seeding is the separate `seed:` step); record chosen host ports in the workspace `localdev.yml` `env` map.
57
- - **Dependencies** install from the lockfile (Step 3).
58
- - A repo's `localdev.yml` carries **only what can't be inferred** — its `needs`, repo-specific `build`/`seed` commands, env keys, and URL. Detect the stack (e.g. Bedrock = `composer.json` + `web/` docroot + `wp-cli.yml`; Vendure/Node = `docker-compose.yml` + `@vendure/*`) and apply the matching convention.
59
-
60
- ### The `localdev.yml` format
61
-
62
- **Per-repo — `<repo>/.haus-workflow/localdev.yml`** (how to set up THAT repo alone; sibling-agnostic):
63
-
64
- All fields optional. **Prefer intent (`needs`/`build`/`seed`/`serve`) over explicit `steps`** —
65
- the conventions above turn intent into commands.
66
-
67
- ```yaml
68
- needs: [mysql] # services this repo requires; provisioned in Docker, env wired to them
69
- needsEnv: [WP_HOME, DB_NAME] # env keys that must be present to run/serve
70
- build: 'yarn build' # optional: the build command (run after deps)
71
- seed: 'dep db:pull staging-oderland' # optional: how to load data; remote/destructive → confirm first
72
- # `seed` may also be an OBJECT when the load needs external access (AWS, SSH, …) so the
73
- # Step-2 data-access preflight can check it up front and offer an empty-DB fallback:
74
- seed:
75
- run: './scripts/pull-aws-db.sh --yes' # the seed command
76
- empty-ok: true # default true: skipping yields a coherent EMPTY DB (schema via migrations).
77
- # # false = the repo is broken without data → a missing-access gap HARD-STOPS.
78
- access: # what the user must have to run `run`; each entry probed in the Step-2 preflight
79
- - label: 'AWS CLI credentials — eu-north-1 / 842675986884'
80
- check: 'aws sts get-caller-identity --region eu-north-1' # read-only probe; exit 0 = OK
81
- login: 'Set up the AWS CLI (brew install awscli; aws configure …) — or pick empty DB.'
82
- serve: # optional: how it runs (printed as a next-step, never auto-started)
83
- via: herd # herd | valet | docker | command — for PHP envs, bring-your-own (don't install)
84
- url: 'https://example.test'
85
- start: 'yarn dev'
86
- steps: # optional escape hatch: explicit ordered shell steps when intent isn't enough
87
- - run: 'composer install'
88
- node: 10 # nvm version for THIS step
89
- remote: true # SSH → confirm first
90
- destructive: true # overwrites data → confirm first
91
- optional: true # failure is non-fatal, continue
92
- ```
93
-
94
- **Workspace — `<workspace>/.haus-workflow/localdev.yml`** (the glue BETWEEN repos):
95
-
96
- The workspace `env` map is the **single source of truth for cross-repo values** — DB names, ports, host URLs are chosen once and recorded here as literals. The env phase reads values **from here** and writes them into each repo's `.env`. Recording them here (not only in a repo's `.env`) means a later `seed:` / `db:pull` step can find them without depending on env-file load order.
97
-
98
- ```yaml
99
- order: [repo-a, repo-b] # setup/startup order, by manifest id
100
- links:
101
- - { type: symlink, from: <repo-folder>, to: <repo-folder>/path/to/link }
102
- - { type: composer-path, in: <repo>, dep: <sibling-repo> }
103
- - { type: yarn-link, in: [<repo>, ...], dep: <sibling-package-repo> }
104
- env:
105
- - value: 'app_local' # a chosen literal — the recorded source of truth, OR …
106
- # source: { repo: <repo>, provides: '<value>' } # … a value produced by another repo
107
- sinks:
108
- - { repo: <repo>, key: DB_NAME } # written under `key` into each sink's .env
109
- seeds: # optional: data sources NOT owned by a manifest repo (e.g. a shared WordPress host
110
- # pulled by a workspace script). Same shape as a per-repo `seed` object; included in
111
- # the Step-2 data-access preflight.
112
- - label: 'WordPress host (example.se)'
113
- run: './scripts/pull-wp-example.sh'
114
- empty-ok: true
115
- access:
116
- - label: 'SSH user@host'
117
- check: 'ssh -o BatchMode=yes -o ConnectTimeout=5 user@host true'
118
- login: 'Add your SSH key to the host (ssh-add …) / request access — or skip the pull.'
119
- ```
120
-
121
- ### Run order
122
-
123
- 1. **Discover** `.haus-workflow/localdev.yml` in the workspace root and each repo.
124
- 2. **Resolve order** from the workspace `order` (repos not listed run last, in manifest order). No workspace file → manifest order.
125
- 3. **Per repo, in order**, apply intent via the conventions:
126
- - **`needs`** → add the service to the **workspace compose project** and `docker compose up -d` if not already running (namespaced by `COMPOSE_PROJECT_NAME`, free host ports per instance; not the repo's own bind-mounting compose). Comes up **empty**; record host ports in the workspace `localdev.yml` `env` map — data lands in `seed:`.
127
- - **`build`** → run it (honor any node version the repo's docs note).
128
- - **`serve`** → for `via: herd` / PHP envs, install nothing — verify the dev's environment serves `web/`, and report the URL.
129
- - **`seed`** → populate the empty datastore (`seed.run`, or the bare-string form). **Always a distinct, confirm-gated step, separate from `needs:` bring-up** (don't conflate "DB up" with "DB has data"). **Honor the data plan chosen in the Step-2 data-access preflight** — don't re-discover access here: if the user chose _empty_ for this source, skip it (the DB stays empty, schema via migrations) and print the command to fill it later; if they chose _seed_, run it. Still verify the run-time prerequisites the preflight couldn't (the target repo's **`.env` written** — done in the Env step above; WP-CLI present for `wp`/`db:pull` seeds) and **confirm first** for every remote (SSH/AWS) or destructive (overwrites data) seed — every run, even on re-run. Missing prerequisite → skip with a clear message, don't guess.
130
- - **`steps`** (escape hatch) → run in order, selecting `node:` per step, `optional:` failures continue, **confirming before any `remote:`/`destructive:` step** — every run, even on re-run.
131
- 4. **Links** (workspace-owned, performed generically — do NOT call a repo's own `setup-dev-mode.sh`, which is deprecated):
132
- - `symlink` → `ln -s <from> <to>`; replace an existing symlink, but never clobber a real directory without confirmation.
133
- - `composer-path` → in `in`'s `composer.json`, set the `dep`'s require to `{ "type": "path", "url": "../<dep-folder>", "options": { "symlink": true } }`, then `composer update <vendor/dep>`.
134
- - `yarn-link` → `yarn link` in the `dep` repo, then `yarn link <pkg-name>` in each `in` repo (read `<pkg-name>` from the dep's `package.json`).
135
- 5. **Env (deterministic):** write each repo's `.env` from known values — no improvising.
136
- 1. **Compute** each repo's values, in this precedence: the workspace `localdev.yml` `env` map (chosen literals + cross-repo `source` values — the single source of truth) → generated secrets (DB passwords/tokens minted this run) → **a value already present locally** in a sibling repo's `.env.example` / `.env` (matched by key, or by the same value under a framework-prefix swap `MIX_` ↔ `VITE_` ↔ `NEXT_PUBLIC_` ↔ `REACT_APP_`) — reuse it: it is already on the machine, so copying it into this repo's local (gitignored) `.env` adds nothing new to git → the **dev-defaults table** below for anything still unset.
137
- 2. **Write** them into each repo's `.env` (create it if absent; **upsert** keys, never clobbering a value the user already set). These are local dev files — write them directly.
138
- 3. **Secrets that exist nowhere locally** (third-party API keys, prod credentials, license keys the machine doesn't already have — §1 reuse only applies when the value is present locally) can't be invented, so the flow **explicitly tells the user it needs them** — never silently leaving a blank and proceeding as if set up. Put each in as a clearly-marked `KEY=` blank and report it, naming **which key**, **what it unlocks** (the step or feature that fails without it), and **how to get it manually**: from the provider's own account/dashboard (e.g. the Laravel Nova licence page, the Mediaflow API account), by copying it from the deployed environment's env vars (the server / hosting config), or by asking a colleague who already has it. **NEVER write a secret value into a git-tracked file:** `.env` writes are local and gitignored (fine — reuse freely), but the workspace `localdev.yml` `env` map is committed, so it carries non-secret cross-repo literals only (DB names, ports, host URLs), never credentials. Report all blanks together as one actionable list, not piecemeal.
139
-
140
- **Dev-defaults table** — used only when neither `localdev.yml` nor a generated secret supplies the value:
141
-
142
- | Key | Default |
143
- | -------------------------------- | ------------------------------------------------------------------- |
144
- | `DB_HOST` | `127.0.0.1` |
145
- | `DB_PORT` | `3306` (mysql) / `5432` (postgres) — or the port chosen on conflict |
146
- | `DB_USER` / `DB_PASSWORD` | `root` / a generated secret |
147
- | `WP_HOME`, `WP_SITEURL`, `*_URL` | the repo's `serve.url` |
148
- | `WP_ENV` / `APP_ENV` | `development` |
149
-
150
- 6. **Report, then offer to start.** Give the per-repo summary, then **ask the user whether to start everything now.**
151
- - **Yes** → start each repo in the workspace `order` (its `serve.start`, e.g. `yarn dev`; bring up any remaining foreground services), run any follow-ups (e.g. `wp sync-products sync`), then **print the live URLs** (each repo's `serve.url`).
152
- - **No** → just print the ordered start commands + follow-ups as next steps; start nothing.
153
-
154
- **Default is "ready to run" (D2):** the preparation — datastores up (workspace `docker compose` project), seeds applied (confirm-gated), links, builds, each repo's `.env` written — always happens. Starting the **foreground** dev servers and the initial product sync happen **only if the user says yes** to the start prompt above; otherwise they're printed, not run.
155
-
156
- ## Step 5 — Report and define "done"
157
-
158
- **"Done" is an explicit terminal state**, not "looks set up". The preparation reaches:
159
-
160
- - datastores up (workspace `docker compose` project),
161
- - dependencies installed and builds green,
162
- - links wired,
163
- - each repo's `.env` written from known values.
164
-
165
- From there, **live URLs are reachable only if** every required secret is satisfiable **and** the user starts the servers. If a secret can't be minted (a third-party key, a prod credential), say exactly which keys are still blank and what the user must fill — don't imply links a missing secret will quietly break.
166
-
167
- Summarise per repo (node, deps, localdev steps, links, env). Then **ask whether to start everything now**:
168
-
169
- - **Yes** → start the dev servers in workspace `order`, run any follow-ups, and **print the live URLs** so the user can open the running app.
170
- - **No** → print the ordered start commands + follow-ups instead, and start nothing.
171
-
172
- Never start the app without that explicit yes.