@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.
- package/CHANGELOG.md +18 -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 +23 -19
- 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`, `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 |
|
|
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
|
-
**
|
|
143
|
-
|
|
144
|
-
en
|
|
145
|
-
|
|
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
|
-
`
|
|
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), `
|
|
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
|
|
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
|
|
341
|
-
3. Först då skriv om beskrivningen
|
|
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 ·
|
|
365
|
-
|
|
|
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 `
|
|
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."_
|