@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.
- package/CHANGELOG.md +12 -0
- package/library/global/agents/request-assessor.md +13 -5
- package/library/global/agents/request-scorer.md +3 -1
- package/library/global/skills/request/SKILL.md +148 -27
- package/library/global/skills/request/scripts/archive.mjs +192 -8
- package/library/global/skills/request/scripts/contract-check.mjs +36 -2
- package/library/global/skills/request/scripts/kontroll.mjs +24 -6
- package/library/global/skills/request/scripts/korning.mjs +183 -50
- package/library/global/skills/request/scripts/lib.mjs +77 -13
- package/library/global/skills/request/scripts/precheck.mjs +16 -0
- package/library/global/skills/request/scripts/report.mjs +37 -2
- package/library/global/skills/request/scripts/svep.mjs +145 -48
- package/library/global/skills/request-assess-impact/SKILL.md +13 -0
- package/library/global/skills/request-score/SKILL.md +6 -0
- package/library/global/skills/request-sync/SKILL.md +11 -12
- package/library/global/skills/request-write-spec/SKILL.md +5 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
12
|
-
*
|
|
13
|
-
* (beslut 2026-09-02
|
|
14
|
-
*
|
|
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] [--
|
|
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 = [
|
|
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}] [--
|
|
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 = {
|
|
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
|
|
185
|
-
*
|
|
186
|
-
* (
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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) =>
|
|
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`, `
|
|
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 |
|
|
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
|
-
**
|
|
143
|
-
|
|
144
|
-
en
|
|
145
|
-
|
|
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
|
-
`
|
|
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), `
|
|
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 ·
|
|
365
|
-
|
|
|
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 `
|
|
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."_
|