@haus-tech/haus-workflow 5.12.1 → 6.0.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 +55 -0
- package/README.md +7 -4
- package/dist/cli.js +7183 -4310
- package/library/catalog/manifest.json +126 -9
- package/library/global/agents/request-design-reviewer.md +54 -0
- package/library/global/agents/request-designer.md +113 -0
- package/library/global/skills/haus-workflow/SKILL.md +7 -3
- package/library/global/skills/haus-workflow/references/init.md +7 -4
- package/library/global/skills/request/SKILL.md +487 -104
- package/library/global/skills/request/scripts/archive.mjs +697 -48
- package/library/global/skills/request/scripts/bedom.mjs +651 -0
- package/library/global/skills/request/scripts/korning.mjs +911 -70
- package/library/global/skills/request/scripts/lib.mjs +328 -3
- package/library/global/skills/request/scripts/precheck.mjs +134 -34
- package/library/global/skills/request/scripts/svep.mjs +44 -14
- package/library/global/skills/request-assess-impact/SKILL.md +1 -4
- package/library/global/skills/request-sync/SKILL.md +8 -2
- package/package.json +1 -1
|
@@ -19,19 +19,41 @@
|
|
|
19
19
|
* --request-id <gid eller slug> --modell <modell|default>
|
|
20
20
|
* [--claude <kommando>] [--delar-parallellt 2]
|
|
21
21
|
* [--steg-timeout 1800000] [--effort <low|medium|high|xhigh|max|default>]
|
|
22
|
+
* [--sprak <svenska|engelska>] [--design-mode <draft|intervju>]
|
|
23
|
+
* node korning.mjs --katalog <abs körningskatalog> --post <abs postkatalog> --repo-rot <abs>
|
|
24
|
+
* --modell <modell|default> [--startare <pid>] [samma valfria flaggor]
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
* gissas aldrig), `<exp>/onskemal.md`, `<exp>/kallmanifest.md`
|
|
25
|
-
* (valfri). Skriver `steg/*.json`, spec/impact/prisfiler i
|
|
26
|
-
* och `korning.json`;
|
|
26
|
+
* Experimentläget (`--exp`) läser `<exp>/experiment.json` (`sprak` OBLIGATORISK,
|
|
27
|
+
* ADR-0114: språket gissas aldrig), `<exp>/onskemal.md`, `<exp>/kallmanifest.md`
|
|
28
|
+
* och `<exp>/design.md` (valfri). Skriver `steg/*.json`, spec/impact/prisfiler i
|
|
29
|
+
* körningskatalogen och `korning.json`;
|
|
27
30
|
* `experiment.json` och `state.json` rörs BARA via `archive.mjs`.
|
|
28
31
|
*
|
|
29
|
-
*
|
|
32
|
+
* Produktionsläget (`--post`, ADR-0155) prissätter en godkänd, designad post ur
|
|
33
|
+
* dess egen arkivkatalog: `spec.md`, `granskarblock.md`, `kallmanifest.md`
|
|
34
|
+
* (`SPRÅK:` obligatorisk, eller `--sprak`), `design.md` och `state.json` med
|
|
35
|
+
* `approval`. Ingen specifier körs; analysen körs bara när den saknas eller
|
|
36
|
+
* gjordes mot en annan spec eller design. `--katalog` är körningskatalogen
|
|
37
|
+
* (`<post>/korningar/<stämpel>/`) med `steg/` och `korning.json`; id:t kommer
|
|
38
|
+
* ur postens state.json, så `--request-id` ges aldrig med `--post`.
|
|
39
|
+
*
|
|
40
|
+
* Draft-läget (ADR-0156, `--design-mode draft` eller `experiment.json`
|
|
41
|
+
* `design_mode: draft`; utan båda `intervju`): saknas designen skriver
|
|
42
|
+
* request-designer ett utkast och request-design-reviewer läser det före
|
|
43
|
+
* assessorn, i båda kanalerna. Produktionen bokför utkastet via
|
|
44
|
+
* `archive.mjs add-design` och granskningen via `add-design-review`;
|
|
45
|
+
* experimentkanalen skriver `design.md` och `design-review.md` i delen.
|
|
46
|
+
*
|
|
47
|
+
* stdout används inte i experimentläget (svep sparar stderr som `stderr.log`);
|
|
48
|
+
* produktionsläget skriver en enda rad `STATUS: <…>` för en människa. Exit 0 = körningen
|
|
30
49
|
* gick igenom (status `klar` sätts av svep), 1 = minst en del blev ofärdig,
|
|
31
50
|
* 2 = användningsfel, 130 = avbruten med SIGTERM eller SIGINT.
|
|
32
51
|
*
|
|
33
52
|
* SIGTERM och SIGINT dödar varje pågående `claude`-process, skriver
|
|
34
|
-
* `korning.json` ur stegen så långt och lämnar körningen som `ofärdig`.
|
|
53
|
+
* `korning.json` ur stegen så långt och lämnar körningen som `ofärdig`.
|
|
54
|
+
* `--startare <pid>` (bara med `--post`) är bedom.mjs:s pid: var 30:e sekund
|
|
55
|
+
* prövas den, och en död startare avbryter körningen på samma sätt, så en
|
|
56
|
+
* startare dödad med SIGKILL inte lämnar en körning som ingen äger. Varje
|
|
35
57
|
* agentsteg har dessutom ett väggtidstak (`--steg-timeout`, 30 minuter): ett
|
|
36
58
|
* steg som hänger utan att svara kostar inga turer men blockerar körningen.
|
|
37
59
|
*/
|
|
@@ -43,6 +65,7 @@ import {
|
|
|
43
65
|
renameSync,
|
|
44
66
|
rmSync,
|
|
45
67
|
realpathSync,
|
|
68
|
+
readdirSync,
|
|
46
69
|
} from 'node:fs'
|
|
47
70
|
import { spawn, spawnSync } from 'node:child_process'
|
|
48
71
|
import path from 'node:path'
|
|
@@ -53,10 +76,15 @@ import {
|
|
|
53
76
|
delaSpecifierSvar,
|
|
54
77
|
matcharModell,
|
|
55
78
|
avgorDeps,
|
|
79
|
+
katalogId,
|
|
80
|
+
sprakUrManifest,
|
|
56
81
|
schemalagg,
|
|
57
82
|
markerStrippedSha256,
|
|
58
83
|
claudeDir,
|
|
59
84
|
canonicalModel,
|
|
85
|
+
designMode as designModeFor,
|
|
86
|
+
specConflicts,
|
|
87
|
+
sectionLines,
|
|
60
88
|
SPEC_HEADINGS,
|
|
61
89
|
IMPACT_HEADINGS,
|
|
62
90
|
SCORE_OUTCOMES,
|
|
@@ -67,7 +95,13 @@ const ARCHIVE = path.join(HAR, 'archive.mjs')
|
|
|
67
95
|
|
|
68
96
|
/** Turtak per agent. Taken är generösa men ändliga: en agent som snurrar
|
|
69
97
|
* kostar pengar tills den dör, och en körning utan tak har inget slut. */
|
|
70
|
-
export const MAX_TURER = {
|
|
98
|
+
export const MAX_TURER = {
|
|
99
|
+
specifier: 120,
|
|
100
|
+
assessor: 200,
|
|
101
|
+
scorer: 80,
|
|
102
|
+
designer: 200,
|
|
103
|
+
designgranskare: 120,
|
|
104
|
+
}
|
|
71
105
|
|
|
72
106
|
/** Modellen som skriver om kundtextfält i kontrollkedjan (kontroll.mjs
|
|
73
107
|
* --fixa-kundtext). Pinnad där, upprepad här bara som stegets modell_begard. */
|
|
@@ -98,13 +132,29 @@ const VARDEFLAGGOR = [
|
|
|
98
132
|
'delar-parallellt',
|
|
99
133
|
'steg-timeout',
|
|
100
134
|
'effort',
|
|
135
|
+
'post',
|
|
136
|
+
'sprak',
|
|
137
|
+
'startare',
|
|
138
|
+
'design-mode',
|
|
101
139
|
]
|
|
140
|
+
const SPRAKEN = ['svenska', 'engelska']
|
|
141
|
+
/** Designlägena (ADR-0156): `draft` låter designern skriva ett saknat
|
|
142
|
+
* designdokument, `intervju` kräver ett ur designa som förut. */
|
|
143
|
+
const DESIGNLAGEN = ['draft', 'intervju']
|
|
144
|
+
const STANDARD_DESIGNLAGE = 'intervju'
|
|
102
145
|
|
|
103
146
|
export const ANVANDNING = [
|
|
104
147
|
'användning: korning.mjs --katalog <abs k<N>> --exp <abs experimentkatalog> --repo-rot <abs>',
|
|
105
148
|
' --request-id <gid eller slug> --modell <modell|default>',
|
|
106
149
|
` [--claude <kommando>] [--delar-parallellt ${STANDARD_DELAR_PARALLELLT}]`,
|
|
107
150
|
` [--steg-timeout ${STANDARD_STEG_TIMEOUT}] [--effort <${EFFORTNIVAER.join('|')}>]`,
|
|
151
|
+
` [--sprak <${SPRAKEN.join('|')}>]`,
|
|
152
|
+
' korning.mjs --katalog <abs körningskatalog> --post <abs postkatalog> --repo-rot <abs>',
|
|
153
|
+
' --modell <modell|default> [--startare <pid>] [samma valfria flaggor] (produktion, ADR-0155)',
|
|
154
|
+
'(--startare ges bara med --post: startarens pid, en död startare avbryter körningen inom 30 s)',
|
|
155
|
+
'(--request-id hör till --exp och ges aldrig med --post, id:t kommer ur postens state.json)',
|
|
156
|
+
'--sprak skriver över språket ur experiment.json eller kallmanifest.md.',
|
|
157
|
+
`[--design-mode <${DESIGNLAGEN.join('|')}>] skriver över design_mode ur experiment.json; utan båda ${STANDARD_DESIGNLAGE} (ADR-0156).`,
|
|
108
158
|
].join('\n')
|
|
109
159
|
|
|
110
160
|
// ---------------------------------------------------------------------------
|
|
@@ -132,6 +182,17 @@ export const ANVANDNING = [
|
|
|
132
182
|
* `Bash(echo:*)`: ett fritt echo är ett skrivkommando i förklädnad.
|
|
133
183
|
*/
|
|
134
184
|
const ASSESSOR_GIT = ['ls-files', 'check-ignore', 'log', 'grep', 'status', 'rev-parse']
|
|
185
|
+
const DESIGN_VERKTYG = [
|
|
186
|
+
'Read',
|
|
187
|
+
'Grep',
|
|
188
|
+
'Glob',
|
|
189
|
+
...ASSESSOR_GIT.map((sub) => `Bash(git ${sub}:*)`),
|
|
190
|
+
...ASSESSOR_GIT.map((sub) => `Bash(rtk git ${sub}:*)`),
|
|
191
|
+
'Bash(head:*)',
|
|
192
|
+
'Bash(grep:*)',
|
|
193
|
+
'Bash(wc:*)',
|
|
194
|
+
'Bash(ls:*)',
|
|
195
|
+
]
|
|
135
196
|
export const AGENT_VERKTYG = {
|
|
136
197
|
'request-scorer': ['Read', 'Skill'],
|
|
137
198
|
'request-specifier': ['Read', 'Grep', 'Glob', 'Skill'],
|
|
@@ -148,6 +209,9 @@ export const AGENT_VERKTYG = {
|
|
|
148
209
|
'Bash(wc:*)',
|
|
149
210
|
'Bash(ls:*)',
|
|
150
211
|
],
|
|
212
|
+
// ADR-0156: assessorns läsande Bash, utan Skill (ingen av dem laddar en skill).
|
|
213
|
+
'request-designer': DESIGN_VERKTYG,
|
|
214
|
+
'request-design-reviewer': DESIGN_VERKTYG,
|
|
151
215
|
}
|
|
152
216
|
|
|
153
217
|
/** Den pinnade headless-invokationen per agentsteg: agentdefinitionen ur
|
|
@@ -286,6 +350,88 @@ export function scorerPrompt({ requestId, delpost, sprak, specPath, impactPath }
|
|
|
286
350
|
].join('\n')
|
|
287
351
|
}
|
|
288
352
|
|
|
353
|
+
/** Designerns prompt (ADR-0156), etiketterna i exakt den ordning
|
|
354
|
+
* request-designer.md listar dem. Den ursprungliga förfrågan finns inte
|
|
355
|
+
* alltid i arkivet; då står det så, och designern läser aldrig tavlan.
|
|
356
|
+
* KONFIG är sökvägen även när filen saknas: agenten läser den om den finns. */
|
|
357
|
+
export function designerPrompt({
|
|
358
|
+
requestId,
|
|
359
|
+
specSha,
|
|
360
|
+
datum,
|
|
361
|
+
onskemal,
|
|
362
|
+
spec,
|
|
363
|
+
granskarblock,
|
|
364
|
+
manifest,
|
|
365
|
+
repoRot,
|
|
366
|
+
deps,
|
|
367
|
+
sprak,
|
|
368
|
+
konfigPath,
|
|
369
|
+
}) {
|
|
370
|
+
const sprakRad = kravSprak(sprak)
|
|
371
|
+
return [
|
|
372
|
+
'SPEC:',
|
|
373
|
+
spec.trim(),
|
|
374
|
+
'',
|
|
375
|
+
'GRANSKARBLOCK:',
|
|
376
|
+
granskarblock.trim(),
|
|
377
|
+
'',
|
|
378
|
+
'URSPRUNGLIG FÖRFRÅGAN:',
|
|
379
|
+
onskemal == null || onskemal.trim() === '' ? '(saknas i arkivet)' : onskemal.trim(),
|
|
380
|
+
'',
|
|
381
|
+
`REPO-ROT: ${repoRot}`,
|
|
382
|
+
`DEPS: ${deps}`,
|
|
383
|
+
sprakRad,
|
|
384
|
+
'KÄLLOR:',
|
|
385
|
+
manifest.trim(),
|
|
386
|
+
'',
|
|
387
|
+
`KONFIG: ${konfigPath}`,
|
|
388
|
+
`ID: ${requestId}`,
|
|
389
|
+
`SPEC-SHA256: ${specSha}`,
|
|
390
|
+
`DATUM: ${datum}`,
|
|
391
|
+
'',
|
|
392
|
+
].join('\n')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Designgranskarens prompt (ADR-0156 beslut 3), etiketterna i
|
|
396
|
+
* request-design-reviewer.md:s ordning. */
|
|
397
|
+
export function designReviewerPrompt({ spec, granskarblock, design, repoRot, deps, sprak }) {
|
|
398
|
+
const sprakRad = kravSprak(sprak)
|
|
399
|
+
return [
|
|
400
|
+
'SPEC:',
|
|
401
|
+
spec.trim(),
|
|
402
|
+
'',
|
|
403
|
+
'GRANSKARBLOCK:',
|
|
404
|
+
granskarblock.trim(),
|
|
405
|
+
'',
|
|
406
|
+
'DESIGN:',
|
|
407
|
+
design.trim(),
|
|
408
|
+
'',
|
|
409
|
+
`REPO-ROT: ${repoRot}`,
|
|
410
|
+
`DEPS: ${deps}`,
|
|
411
|
+
sprakRad,
|
|
412
|
+
'',
|
|
413
|
+
].join('\n')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** En kebab av en rubrik: diakriter strippade (å blir a), gemener, allt som
|
|
417
|
+
* inte är a-z eller 0-9 blir ett bindestreck, ihopslaget och trimmat, högst
|
|
418
|
+
* 60 tecken. En rubrik utan ett enda tecken kvar blir `design`. */
|
|
419
|
+
export const slugAv = (titel) => {
|
|
420
|
+
const slug = String(titel)
|
|
421
|
+
.normalize('NFD')
|
|
422
|
+
.replace(/\p{M}+/gu, '')
|
|
423
|
+
.toLowerCase()
|
|
424
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
425
|
+
.replace(/^-+|-+$/g, '')
|
|
426
|
+
.slice(0, 60)
|
|
427
|
+
.replace(/-+$/, '')
|
|
428
|
+
return slug === '' ? 'design' : slug
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Designdokumentets sökväg relativt repo-roten, samma som designa steg 4. */
|
|
432
|
+
export const designFilnamn = ({ datum, requestId, titel }) =>
|
|
433
|
+
`docs/superpowers/specs/${datum}-${requestId}-${slugAv(titel)}-design.md`
|
|
434
|
+
|
|
289
435
|
/** Omdispatchen: HELA ursprungsprompten oförändrad plus felraderna ordagrant.
|
|
290
436
|
* En ny subagent minns inget, enbart felrader utan underlag är obrukbart. */
|
|
291
437
|
export const omdispatchPrompt = (original, felrader, { endastSpec = null } = {}) =>
|
|
@@ -320,8 +466,8 @@ const raderEfter = (stdout, rubrikRe) => {
|
|
|
320
466
|
return i === -1 ? [] : bulletRader(rader.slice(i + 1).join('\n'))
|
|
321
467
|
}
|
|
322
468
|
|
|
323
|
-
/** precheck.mjs stdout: KANDIDAT, AVVIKELSE,
|
|
324
|
-
* BRISTER (n) + "- "-rader. */
|
|
469
|
+
/** precheck.mjs stdout: KANDIDAT, AVVIKELSE (+ AVVIKELSE-TEXT), DESIGN,
|
|
470
|
+
* VARNING, sedan OK eller BRISTER (n) + "- "-rader. */
|
|
325
471
|
export function tolkaPrecheck(stdout, status) {
|
|
326
472
|
const kandidater = raderMed(stdout, 'KANDIDAT: ')
|
|
327
473
|
const avvikelser = raderMed(stdout, 'AVVIKELSE: ')
|
|
@@ -329,6 +475,8 @@ export function tolkaPrecheck(stdout, status) {
|
|
|
329
475
|
ok: status === 0,
|
|
330
476
|
kandidat: kandidater[0] ?? null,
|
|
331
477
|
avvikelse: avvikelser[0] ?? null,
|
|
478
|
+
avvikelseText: raderMed(stdout, 'AVVIKELSE-TEXT: ')[0] ?? null,
|
|
479
|
+
design: raderMed(stdout, 'DESIGN: ')[0] ?? null,
|
|
332
480
|
brister: raderEfter(stdout, /^BRISTER \(\d+\):/),
|
|
333
481
|
varningar: raderMed(stdout, 'VARNING: '),
|
|
334
482
|
prisavgorande: raderMed(stdout, 'PRISAVGORANDE: ')[0] ?? null,
|
|
@@ -538,13 +686,24 @@ export const behoverDubbelkorning = (doc) =>
|
|
|
538
686
|
const logg = (rad) =>
|
|
539
687
|
process.stderr.write(`[korning] ${new Date().toISOString().slice(11, 19)} ${rad}\n`)
|
|
540
688
|
|
|
541
|
-
const
|
|
689
|
+
const arkivSpawn = (args, ctx) =>
|
|
542
690
|
spawnSync(process.execPath, [ARCHIVE, ...args], { encoding: 'utf8', cwd: ctx.repoRot })
|
|
543
|
-
const
|
|
691
|
+
const scriptSpawn = (namn, args, ctx) =>
|
|
544
692
|
spawnSync(process.execPath, [path.join(HAR, namn), ...args], {
|
|
545
693
|
encoding: 'utf8',
|
|
546
694
|
cwd: ctx.repoRot,
|
|
547
695
|
})
|
|
696
|
+
/** Varje archive.mjs- och scriptanrop går genom de här två, så ett test kan
|
|
697
|
+
* byta ut körningen på ett ställe (`ctx.arkivKor`, `ctx.scriptKor`); main
|
|
698
|
+
* sätter dem aldrig. */
|
|
699
|
+
const arkiv = (args, ctx) => {
|
|
700
|
+
const kor = ctx.arkivKor ?? arkivSpawn
|
|
701
|
+
return kor(args, ctx)
|
|
702
|
+
}
|
|
703
|
+
const script = (namn, args, ctx) => {
|
|
704
|
+
const kor = ctx.scriptKor ?? scriptSpawn
|
|
705
|
+
return kor(namn, args, ctx)
|
|
706
|
+
}
|
|
548
707
|
const felText = (r) => (r.stderr || r.stdout || '').trim() || `exit ${r.status}`
|
|
549
708
|
const idag = () => new Date().toISOString().slice(0, 10)
|
|
550
709
|
const lasFil = (p) => {
|
|
@@ -570,10 +729,21 @@ export function tolkaArgv(argv) {
|
|
|
570
729
|
i++
|
|
571
730
|
enkla[namn] = v
|
|
572
731
|
}
|
|
573
|
-
|
|
732
|
+
// Läget (ADR-0155): exakt en av --post (produktion) och --exp (experiment).
|
|
733
|
+
// Med --post kommer id:t ur postens state.json, så --request-id vore ett
|
|
734
|
+
// andra svar på samma fråga och vägras i stället för att tyst vinna.
|
|
735
|
+
const produktion = enkla.post !== undefined
|
|
736
|
+
if (produktion && enkla.exp !== undefined) return { fel: '--post och --exp utesluter varandra' }
|
|
737
|
+
if (!produktion && enkla.exp === undefined) return { fel: 'kräver --post eller --exp' }
|
|
738
|
+
if (produktion && enkla['request-id'] !== undefined)
|
|
739
|
+
return { fel: '--request-id ges bara med --exp, med --post kommer id:t ur state.json' }
|
|
740
|
+
if (!produktion && enkla.startare !== undefined) return { fel: '--startare ges bara med --post' }
|
|
741
|
+
const obligatoriska = ['katalog', 'repo-rot', 'modell', ...(produktion ? [] : ['request-id'])]
|
|
574
742
|
const saknas = obligatoriska.filter((n) => enkla[n] === undefined)
|
|
575
743
|
if (saknas.length > 0) return { fel: `saknar ${saknas.map((n) => `--${n}`).join(', ')}` }
|
|
576
|
-
const relativa = ['katalog', 'exp', 'repo-rot'].filter(
|
|
744
|
+
const relativa = ['katalog', produktion ? 'post' : 'exp', 'repo-rot'].filter(
|
|
745
|
+
(n) => !path.isAbsolute(enkla[n]),
|
|
746
|
+
)
|
|
577
747
|
if (relativa.length > 0)
|
|
578
748
|
return { fel: `${relativa.map((n) => `--${n}`).join(', ')} måste vara absoluta sökvägar` }
|
|
579
749
|
const takText = enkla['delar-parallellt'] ?? String(STANDARD_DELAR_PARALLELLT)
|
|
@@ -585,16 +755,33 @@ export function tolkaArgv(argv) {
|
|
|
585
755
|
const effort = enkla.effort ?? STANDARD_EFFORT
|
|
586
756
|
if (!EFFORTNIVAER.includes(effort))
|
|
587
757
|
return { fel: `--effort måste vara en av ${EFFORTNIVAER.join(', ')} (fick "${effort}")` }
|
|
758
|
+
if (enkla.sprak !== undefined && !SPRAKEN.includes(enkla.sprak))
|
|
759
|
+
return { fel: `--sprak måste vara ${SPRAKEN.join(' eller ')} (fick "${enkla.sprak}")` }
|
|
760
|
+
if (enkla['design-mode'] !== undefined && !DESIGNLAGEN.includes(enkla['design-mode']))
|
|
761
|
+
return {
|
|
762
|
+
fel: `--design-mode måste vara ${DESIGNLAGEN.join(' eller ')} (fick "${enkla['design-mode']}")`,
|
|
763
|
+
}
|
|
764
|
+
if (
|
|
765
|
+
enkla.startare !== undefined &&
|
|
766
|
+
(!/^\d+$/.test(enkla.startare) || Number(enkla.startare) <= 0)
|
|
767
|
+
)
|
|
768
|
+
return { fel: `--startare måste vara ett heltal > 0 (fick "${enkla.startare}")` }
|
|
588
769
|
return {
|
|
589
770
|
katalog: enkla.katalog,
|
|
590
|
-
exp: enkla.exp,
|
|
771
|
+
exp: enkla.exp ?? null,
|
|
772
|
+
post: enkla.post ?? null,
|
|
591
773
|
repoRot: enkla['repo-rot'],
|
|
592
|
-
requestId: enkla['request-id'],
|
|
774
|
+
requestId: enkla['request-id'] ?? null,
|
|
593
775
|
modell: enkla.modell,
|
|
776
|
+
sprak: enkla.sprak ?? null,
|
|
777
|
+
// null betyder "ingen flagga": källans eget värde gäller (experiment.json
|
|
778
|
+
// design_mode), annars intervju. Samma regel som --sprak.
|
|
779
|
+
designMode: enkla['design-mode'] ?? null,
|
|
594
780
|
claude: enkla.claude ?? 'claude',
|
|
595
781
|
delarParallellt: Number(takText),
|
|
596
782
|
stegTimeout: Number(timeoutText),
|
|
597
783
|
effort,
|
|
784
|
+
startare: enkla.startare === undefined ? null : Number(enkla.startare),
|
|
598
785
|
}
|
|
599
786
|
}
|
|
600
787
|
|
|
@@ -607,11 +794,16 @@ export function lasUnderlag(val) {
|
|
|
607
794
|
} catch (e) {
|
|
608
795
|
return { fel: `kunde inte läsa ${path.join(val.exp, 'experiment.json')}: ${e.message}` }
|
|
609
796
|
}
|
|
610
|
-
const sprak = experiment?.sprak
|
|
797
|
+
const sprak = val.sprak ?? experiment?.sprak
|
|
611
798
|
if (sprak !== 'svenska' && sprak !== 'engelska')
|
|
612
799
|
return {
|
|
613
800
|
fel: 'sprak saknas i experiment.json, sätt med archive.mjs experiment --satt sprak=svenska|engelska',
|
|
614
801
|
}
|
|
802
|
+
const designMode = val.designMode ?? experiment?.design_mode ?? STANDARD_DESIGNLAGE
|
|
803
|
+
if (!DESIGNLAGEN.includes(designMode))
|
|
804
|
+
return {
|
|
805
|
+
fel: `design_mode i experiment.json måste vara ${DESIGNLAGEN.join(' eller ')} (fick "${designMode}")`,
|
|
806
|
+
}
|
|
615
807
|
const onskemal = lasFil(path.join(val.exp, 'onskemal.md'))
|
|
616
808
|
const manifest = lasFil(path.join(val.exp, 'kallmanifest.md'))
|
|
617
809
|
// Designen är ett valfritt pinnat underlag som manifestet (designa, spec
|
|
@@ -632,6 +824,7 @@ export function lasUnderlag(val) {
|
|
|
632
824
|
manifest,
|
|
633
825
|
design,
|
|
634
826
|
sprak,
|
|
827
|
+
designMode,
|
|
635
828
|
eskalerad: experiment.eskalerad === true,
|
|
636
829
|
eskaleringsorsak:
|
|
637
830
|
typeof experiment.eskaleringsorsak === 'string' ? experiment.eskaleringsorsak : null,
|
|
@@ -639,6 +832,52 @@ export function lasUnderlag(val) {
|
|
|
639
832
|
}
|
|
640
833
|
}
|
|
641
834
|
|
|
835
|
+
/** Underlaget i produktionsläget (ADR-0155 beslut 2), ur postens egen
|
|
836
|
+
* katalog och aldrig ur experiment.json. Posten måste vara godkänd
|
|
837
|
+
* (`approval` satt av bedöm-grinden) och får inte vara en experimentkatalog.
|
|
838
|
+
* Språket kommer ur `--sprak` eller manifestets `SPRÅK:`-rad och gissas
|
|
839
|
+
* aldrig (ADR-0114, #521). `granskarblock.md`, `kallmanifest.md` och
|
|
840
|
+
* `design.md` är valfria; `spec.md` krävs. Designläget är `--design-mode`
|
|
841
|
+
* eller intervju (ADR-0156). Den ursprungliga förfrågan till designern
|
|
842
|
+
* läses bara ur `onskemal.md` i posten, annars null (prompten säger då att
|
|
843
|
+
* den saknas i arkivet). */
|
|
844
|
+
export function lasUnderlagProduktion({ post, repoRot, sprak = null, designMode = null }) {
|
|
845
|
+
const statefil = path.join(post, 'state.json')
|
|
846
|
+
let state
|
|
847
|
+
try {
|
|
848
|
+
state = JSON.parse(readFileSync(statefil, 'utf8'))
|
|
849
|
+
} catch (e) {
|
|
850
|
+
return { fel: `kunde inte läsa ${statefil}: ${e.message}` }
|
|
851
|
+
}
|
|
852
|
+
if (state === null || typeof state !== 'object' || Array.isArray(state))
|
|
853
|
+
return { fel: `${statefil} är inget objekt` }
|
|
854
|
+
if (state.experiment === true) return { fel: 'experimentkatalog: kör utvärdera' }
|
|
855
|
+
if (
|
|
856
|
+
state.approval === null ||
|
|
857
|
+
typeof state.approval !== 'object' ||
|
|
858
|
+
Array.isArray(state.approval)
|
|
859
|
+
)
|
|
860
|
+
return { fel: 'approval saknas: bedöm-grinden har inte körts (archive.mjs approve)' }
|
|
861
|
+
const spec = lasFil(path.join(post, 'spec.md'))
|
|
862
|
+
if (spec === null) return { fel: `${post} saknar spec.md` }
|
|
863
|
+
const manifest = lasFil(path.join(post, 'kallmanifest.md'))
|
|
864
|
+
const sprakVald = sprak ?? (manifest === null ? null : sprakUrManifest(manifest))
|
|
865
|
+
if (sprakVald === null)
|
|
866
|
+
return { fel: 'SPRÅK saknas i kallmanifest.md: sätt raden eller ge --sprak' }
|
|
867
|
+
return {
|
|
868
|
+
state,
|
|
869
|
+
spec,
|
|
870
|
+
granskarblock: lasFil(path.join(post, 'granskarblock.md')) ?? '',
|
|
871
|
+
manifest: manifest ?? 'inga',
|
|
872
|
+
design: lasFil(path.join(post, 'design.md')),
|
|
873
|
+
onskemal: lasFil(path.join(post, 'onskemal.md')),
|
|
874
|
+
designMode: designMode ?? STANDARD_DESIGNLAGE,
|
|
875
|
+
sprak: sprakVald,
|
|
876
|
+
deps: avgorDeps(repoRot),
|
|
877
|
+
requestId: katalogId(state, post),
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
642
881
|
/** Orsaken till att ett agentsteg räknas som fallet, eller null. Ordningen är
|
|
643
882
|
* från grövst till finast: en process som inte startade säger inget om
|
|
644
883
|
* modellen, en tom text säger inget om innehållet. `tidsgransMs` sätts när
|
|
@@ -701,7 +940,13 @@ export function raknaSamtidiga(kor = spawnSync) {
|
|
|
701
940
|
* så signalhanteraren kan döda det: en dödad korning.mjs får aldrig lämna
|
|
702
941
|
* betalda `claude`-processer efter sig. Väggtidstaket (`--steg-timeout`)
|
|
703
942
|
* dödar ett steg som hänger utan att svara. Anroparen avgör vad ett fallet
|
|
704
|
-
* steg betyder.
|
|
943
|
+
* steg betyder.
|
|
944
|
+
*
|
|
945
|
+
* `ctx.korAgent` är testsömmen (ADR-0111 beslut 4: inget fejkat `claude`):
|
|
946
|
+
* den ersätter BARA processen, får stegets argument och returnerar (eller
|
|
947
|
+
* löser) `{ stdout, exitCode }`, processens råa utskrift. Allt efter
|
|
948
|
+
* processen är detsamma som i en riktig körning: steg/-filen, tolkningen,
|
|
949
|
+
* modellverifieringen och stegposten via `ctx.samla`. main sätter den aldrig. */
|
|
705
950
|
export function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
706
951
|
return new Promise((los) => {
|
|
707
952
|
const start = Date.now()
|
|
@@ -740,6 +985,18 @@ export function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
|
740
985
|
if (nekade > 0) logg(`STEG ${fil} ${nekade} nekade verktygsanrop`)
|
|
741
986
|
los({ ...t, exitCode, wall_ms, ok: t.orsak === null })
|
|
742
987
|
}
|
|
988
|
+
if (ctx.korAgent) {
|
|
989
|
+
Promise.resolve()
|
|
990
|
+
.then(() => ctx.korAgent({ agent, modell, prompt, fil, maxTurer, ctx }))
|
|
991
|
+
.then(
|
|
992
|
+
(r) => {
|
|
993
|
+
stdout = String(r?.stdout ?? '')
|
|
994
|
+
avsluta({ exitCode: r?.exitCode ?? 0 })
|
|
995
|
+
},
|
|
996
|
+
(e) => avsluta({ exitCode: null, spawnfel: e?.message ?? String(e) }),
|
|
997
|
+
)
|
|
998
|
+
return
|
|
999
|
+
}
|
|
743
1000
|
let barn
|
|
744
1001
|
try {
|
|
745
1002
|
barn = spawn(ctx.claude, agentArgs({ agent, modell, effort: ctx.effort, maxTurer }), {
|
|
@@ -824,11 +1081,13 @@ export const addRunArgs = (dir, rad, effort = STANDARD_EFFORT) => [
|
|
|
824
1081
|
...(rad.note ? ['--note', rad.note] : []),
|
|
825
1082
|
...(rad.doubleRun ? ['--double-run', rad.doubleRun] : []),
|
|
826
1083
|
...(rad.supersededBy ? ['--superseded-by', rad.supersededBy] : []),
|
|
1084
|
+
...(rad.repoSha ? ['--repo-sha', rad.repoSha] : []),
|
|
827
1085
|
]
|
|
828
1086
|
|
|
829
1087
|
/** En runs-rad. Arkivet är append-only och äger sina egna vägransregler; en
|
|
830
1088
|
* vägrad stämpling loggas men fäller aldrig körningen, kvittot på vad som
|
|
831
|
-
* hände ligger kvar i steg/.
|
|
1089
|
+
* hände ligger kvar i steg/. Anropet går genom `arkiv`, alltså genom
|
|
1090
|
+
* testsömmen `ctx.arkivKor`. */
|
|
832
1091
|
function addRun(dir, rad, ctx) {
|
|
833
1092
|
const r = arkiv(addRunArgs(dir, rad, ctx.effort), ctx)
|
|
834
1093
|
if (r.status !== 0) logg(`VARNING add-run ${rad.fil}: ${felText(r)}`)
|
|
@@ -994,17 +1253,26 @@ function skrivSpec(del, spec, granskarblock, meta, ctx) {
|
|
|
994
1253
|
}
|
|
995
1254
|
|
|
996
1255
|
/** Delens namn i loggen och delraderna: sökvägen relativt körningskatalogen,
|
|
997
|
-
* eller katalognamnet när delen ÄR körningsroten
|
|
998
|
-
|
|
1256
|
+
* eller katalognamnet när delen ÄR körningsroten eller ligger utanför den
|
|
1257
|
+
* (produktionsläget: posten är körningskatalogens förälder, ADR-0155). */
|
|
1258
|
+
const delNamn = (ctx, dir) => {
|
|
1259
|
+
const rel = path.relative(ctx.katalog, dir)
|
|
1260
|
+
return rel && !rel.startsWith('..') ? rel : path.basename(dir)
|
|
1261
|
+
}
|
|
999
1262
|
|
|
1000
1263
|
/** Ett precheck-anrop: varningarna loggas, brister och kandidat tolkas, och en
|
|
1001
1264
|
* exitkod skild från 0 UTAN tolkbart BRISTER-block är ett fel, aldrig ett
|
|
1002
1265
|
* godkänt steg. En grind som failar öppet när den själv kraschar är ingen
|
|
1003
1266
|
* grind. */
|
|
1004
|
-
function korPrecheck(del, ctx, { impact = false } = {}) {
|
|
1005
|
-
const
|
|
1267
|
+
function korPrecheck(del, ctx, { impact = false, design = false, utkast = null } = {}) {
|
|
1268
|
+
const flaggor = [
|
|
1269
|
+
...(impact ? ['--impact'] : []),
|
|
1270
|
+
...(design ? ['--design'] : []),
|
|
1271
|
+
...(utkast ? ['--design-utkast', utkast, '--repo-rot', ctx.repoRot] : []),
|
|
1272
|
+
]
|
|
1273
|
+
const r = script('precheck.mjs', [del.dir, ...flaggor], ctx)
|
|
1006
1274
|
const tolkad = tolkaPrecheck(r.stdout, r.status)
|
|
1007
|
-
const etikett =
|
|
1275
|
+
const etikett = ['precheck', ...flaggor].join(' ')
|
|
1008
1276
|
for (const v of tolkad.varningar) logg(`VARNING ${etikett} ${delNamn(ctx, del.dir)}: ${v}`)
|
|
1009
1277
|
if (tolkad.ok || tolkad.brister.length > 0) return { ...tolkad, fel: null }
|
|
1010
1278
|
const svans = (r.stderr || r.stdout || '').trim().split('\n').slice(-3).join(' | ').slice(0, 200)
|
|
@@ -1113,13 +1381,323 @@ async function specOmdispatch(del, spec, brister, ctx) {
|
|
|
1113
1381
|
/** Designen in i delens katalog som design.md före assessorsteget, så att
|
|
1114
1382
|
* add-run hashar den i impact-radens underlag_sha256 och precheck --impact
|
|
1115
1383
|
* prövar proveniensen (designa, spec 2026-09-24). Byteidentisk kopia av
|
|
1116
|
-
* ctx.design; utan design skrivs inget och false returneras.
|
|
1384
|
+
* ctx.design; utan design skrivs inget och false returneras. I
|
|
1385
|
+
* produktionsläget ÄR ctx.design postens egen design.md, så en fil som
|
|
1386
|
+
* redan bär exakt de bytena skrivs aldrig om. */
|
|
1117
1387
|
export function laggDesignIDel(del, ctx) {
|
|
1118
1388
|
if (ctx.design == null) return false
|
|
1119
|
-
|
|
1389
|
+
const mal = path.join(del.dir, 'design.md')
|
|
1390
|
+
if (lasFil(mal) !== ctx.design) writeFileSync(mal, ctx.design)
|
|
1120
1391
|
return true
|
|
1121
1392
|
}
|
|
1122
1393
|
|
|
1394
|
+
// ---------------------------------------------------------------------------
|
|
1395
|
+
// Draft-läget (ADR-0156): designern skriver, granskaren läser, före assessorn
|
|
1396
|
+
// ---------------------------------------------------------------------------
|
|
1397
|
+
|
|
1398
|
+
/** En splitdel i produktionen ärver förälderns design och designas aldrig. */
|
|
1399
|
+
const arSplitdel = (ctx) => Boolean(ctx.post) && (ctx.state?.split?.parent ?? null) !== null
|
|
1400
|
+
|
|
1401
|
+
/** Designern körs bara i draft-läget, på en del som saknar design: ingen
|
|
1402
|
+
* design i ctx (experimentets pinnade design.md eller postens egen), ingen
|
|
1403
|
+
* design.md i delen och ingen splitdel. Annars är steget en no-op. */
|
|
1404
|
+
const designernGaller = (del, ctx) =>
|
|
1405
|
+
ctx.designMode === 'draft' &&
|
|
1406
|
+
ctx.design == null &&
|
|
1407
|
+
!existsSync(path.join(del.dir, 'design.md')) &&
|
|
1408
|
+
!arSplitdel(ctx)
|
|
1409
|
+
|
|
1410
|
+
/** Specens första rubrik, utan #-tecknen; slugen i designa-sökvägen. */
|
|
1411
|
+
const forstaRubrikI = (text) => {
|
|
1412
|
+
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/mu.exec(String(text))
|
|
1413
|
+
return m ? m[1] : 'design'
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const INTERVJU_RE = /^INTERVJU KRÄVS:\s*(.*)$/u
|
|
1417
|
+
|
|
1418
|
+
/** Designern (ADR-0156 beslut 1 och 6): ett agentsteg som skriver ett utkast
|
|
1419
|
+
* eller svarar `INTERVJU KRÄVS:`. Returnerar `{ status: null }` när ett
|
|
1420
|
+
* utkast bokfördes (eller steget inte gäller), `stopp` på INTERVJU KRÄVS
|
|
1421
|
+
* (inget i repot; produktionen skriver kvittot och ESKALERA-stoppet, se
|
|
1422
|
+
* stoppaIntervju, experimentet bara raden) och på designkonflikter
|
|
1423
|
+
* (kvittot och stoppet som designa steg 5), `ofärdig` på allt annat. */
|
|
1424
|
+
export async function korDesigner(del, ctx) {
|
|
1425
|
+
if (!designernGaller(del, ctx)) return { status: null }
|
|
1426
|
+
const specFil = path.join(del.dir, 'spec.md')
|
|
1427
|
+
const spec = lasFil(specFil)
|
|
1428
|
+
if (spec === null) return { status: 'ofärdig', orsak: 'design: spec.md saknas' }
|
|
1429
|
+
const datum = idag()
|
|
1430
|
+
const prompt = designerPrompt({
|
|
1431
|
+
requestId: ctx.requestId,
|
|
1432
|
+
specSha: markerStrippedSha256(specFil),
|
|
1433
|
+
datum,
|
|
1434
|
+
onskemal: ctx.onskemal ?? null,
|
|
1435
|
+
spec,
|
|
1436
|
+
granskarblock: lasFil(path.join(del.dir, 'granskarblock.md')) ?? 'inget granskarblock',
|
|
1437
|
+
manifest: ctx.manifest,
|
|
1438
|
+
repoRot: ctx.repoRot,
|
|
1439
|
+
deps: ctx.deps,
|
|
1440
|
+
sprak: ctx.sprak,
|
|
1441
|
+
konfigPath: path.join(ctx.repoRot, '.haus-workflow', 'workflow-config.md'),
|
|
1442
|
+
})
|
|
1443
|
+
const res = await korAgent({
|
|
1444
|
+
agent: 'request-designer',
|
|
1445
|
+
modell: ctx.modell,
|
|
1446
|
+
prompt,
|
|
1447
|
+
fil: `steg/1${del.nr}-designer.json`,
|
|
1448
|
+
maxTurer: MAX_TURER.designer,
|
|
1449
|
+
ctx,
|
|
1450
|
+
})
|
|
1451
|
+
if (!res.ok) return { status: 'ofärdig', orsak: `design: ${res.orsak}` }
|
|
1452
|
+
const svar = utanYttreStaket(res.text)
|
|
1453
|
+
const intervju = INTERVJU_RE.exec(svar.trim().split('\n')[0].trim())
|
|
1454
|
+
if (intervju) {
|
|
1455
|
+
const orsak = `intervju krävs: ${intervju[1].trim()}`
|
|
1456
|
+
logg(`STOPP ${delNamn(ctx, del.dir)} ${orsak}`)
|
|
1457
|
+
if (ctx.post) return stoppaIntervju(del, intervju[1].trim(), parseSvansrader(svar).modell, ctx)
|
|
1458
|
+
return { status: 'stopp', orsak }
|
|
1459
|
+
}
|
|
1460
|
+
const dok = parseSvansrader(svar).body.trim() + '\n'
|
|
1461
|
+
return ctx.post ? bokforUtkast(del, dok, datum, spec, ctx) : skrivUtkastIDel(del, dok, ctx)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** Ett yttre kodstaket runt hela svaret (en första rad med tre eller fler
|
|
1465
|
+
* backticks och en valfri språktagg, en sista rad med samma staket) tas
|
|
1466
|
+
* bort, ETT lager; annat lämnas orört. Utan det fick frontmattern null och
|
|
1467
|
+
* add-design vägrade ett annars giltigt utkast. */
|
|
1468
|
+
export const utanYttreStaket = (text) => {
|
|
1469
|
+
const rader = String(text).replace(/\r\n/g, '\n').trim().split('\n')
|
|
1470
|
+
const forsta = /^(`{3,})[\w-]*\s*$/.exec(rader[0] ?? '')
|
|
1471
|
+
if (!forsta || rader.length < 2) return String(text)
|
|
1472
|
+
// Svansraderna (MODELL: m.fl.) kan stå efter staketet; de följer med ut.
|
|
1473
|
+
let slut = rader.length - 1
|
|
1474
|
+
const svans = []
|
|
1475
|
+
while (slut > 0 && /^(MODELL|LÄSTA|AVVIKELSE):|^\s*$/u.test(rader[slut]))
|
|
1476
|
+
svans.unshift(rader[slut--])
|
|
1477
|
+
if (rader[slut].trim() !== forsta[1]) return String(text)
|
|
1478
|
+
return [...rader.slice(1, slut), ...svans].join('\n') + '\n'
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** INTERVJU KRÄVS i produktionen (P4-24): svaret skrivs som kvittot
|
|
1482
|
+
* design-intervju-kravs-<datum>.md (-2 vid kollision, som
|
|
1483
|
+
* design-avvisad-<datum>.md) och posten stoppas som ESKALERA på det via
|
|
1484
|
+
* archive.mjs stopp, så nästa batch inte betalar designern igen. Inget
|
|
1485
|
+
* skrivs i repot och state.design rörs inte; designa löser stoppet. */
|
|
1486
|
+
function stoppaIntervju(del, skal, modell, ctx) {
|
|
1487
|
+
const orsak = `intervju krävs: ${skal}`
|
|
1488
|
+
const kvitto = ledigtNamn(del.dir, `design-intervju-kravs-${idag()}`, '.md')
|
|
1489
|
+
const rader = [`INTERVJU KRÄVS: ${skal}`, ...(modell ? [`MODELL: ${modell}`] : [])]
|
|
1490
|
+
writeFileSync(path.join(del.dir, kvitto), rader.join('\n') + '\n')
|
|
1491
|
+
const s = arkiv(
|
|
1492
|
+
['stopp', del.dir, '--utfall', 'ESKALERA', '--kalla', kvitto, '--orsak', orsak],
|
|
1493
|
+
ctx,
|
|
1494
|
+
)
|
|
1495
|
+
if (s.status !== 0) return { status: 'ofärdig', orsak: `stopp: ${felText(s)}` }
|
|
1496
|
+
logg(`STOPP ${delNamn(ctx, del.dir)} ESKALERA ur ${kvitto}`)
|
|
1497
|
+
return { status: 'stopp', orsak }
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/** Repots formatter före bokföringen (designa steg 5): en format-on-commit
|
|
1501
|
+
* får aldrig ändra bytes efter add-design. Bara när repot har prettier; ett
|
|
1502
|
+
* fel är en varning, add-design och precheck --design prövar sedan bytes. */
|
|
1503
|
+
function formatera(abs, ctx) {
|
|
1504
|
+
const bin = path.join(ctx.repoRot, 'node_modules', '.bin', 'prettier')
|
|
1505
|
+
if (!existsSync(bin)) return
|
|
1506
|
+
const r = spawnSync(bin, ['--write', abs], { cwd: ctx.repoRot, encoding: 'utf8', timeout: 60000 })
|
|
1507
|
+
if (r.error || r.status !== 0) logg(`VARNING prettier ${abs}: ${r.error?.message ?? felText(r)}`)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** Produktionen: dokumentet i repot på designa-sökvägen (ett ledigt namn,
|
|
1511
|
+
* aldrig en överskrivning), formaterat, bokfört med add-design och prövat
|
|
1512
|
+
* med precheck --design. När add-design vägrar (utom konfliktstoppet) tas
|
|
1513
|
+
* exakt den fil körningen skrev bort (och de kataloger den skapade, när de
|
|
1514
|
+
* är tomma), så arbetskopian står som före körningen; det råa svaret under
|
|
1515
|
+
* steg/ är kvittot. Konfliktstoppet behåller filen, som designa steg 5. Ett
|
|
1516
|
+
* bokfört utkast tas aldrig bort: state.design pekar då på filen, och en
|
|
1517
|
+
* BRIST i precheck --design efteråt är ofärdig med filen kvar. */
|
|
1518
|
+
function bokforUtkast(del, dok, datum, spec, ctx) {
|
|
1519
|
+
const rel = designFilnamn({ datum, requestId: ctx.requestId, titel: forstaRubrikI(spec) })
|
|
1520
|
+
const katalog = path.dirname(path.join(ctx.repoRot, rel))
|
|
1521
|
+
const skapad = mkdirSync(katalog, { recursive: true }) ?? null
|
|
1522
|
+
const abs = path.join(katalog, ledigtNamn(katalog, path.basename(rel, '.md'), '.md'))
|
|
1523
|
+
writeFileSync(abs, dok)
|
|
1524
|
+
const ofardig = (orsak) => {
|
|
1525
|
+
stadaUtkast(abs, skapad)
|
|
1526
|
+
return { status: 'ofärdig', orsak }
|
|
1527
|
+
}
|
|
1528
|
+
formatera(abs, ctx)
|
|
1529
|
+
const r = arkiv(['add-design', del.dir, '--file', abs], ctx)
|
|
1530
|
+
for (const v of raderMed(r.stderr ?? '', 'VARNING: '))
|
|
1531
|
+
logg(`VARNING add-design ${delNamn(ctx, del.dir)}: ${v}`)
|
|
1532
|
+
if (r.status !== 0) {
|
|
1533
|
+
const text = lasFil(abs) ?? dok
|
|
1534
|
+
const konflikter = specConflicts(text) ?? []
|
|
1535
|
+
if (konflikter.length > 0 && /"## Spec conflicts" är inte none/u.test(r.stderr ?? ''))
|
|
1536
|
+
return stoppaDesignkonflikt(del, text, konflikter, ctx)
|
|
1537
|
+
return ofardig(`add-design: ${felText(r)}`)
|
|
1538
|
+
}
|
|
1539
|
+
logg(`DESIGN ${delNamn(ctx, del.dir)} utkast bokfört: ${path.relative(ctx.repoRot, abs)}`)
|
|
1540
|
+
const p = korPrecheck(del, ctx, { design: true })
|
|
1541
|
+
if (p.fel) return { status: 'ofärdig', orsak: p.fel }
|
|
1542
|
+
if (p.brister.length > 0) return { status: 'ofärdig', orsak: `design: ${bristText(p.brister)}` }
|
|
1543
|
+
return { status: null }
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** Tar bort ett vägrat utkast: filen på sin exakta sökväg, sedan uppåt varje
|
|
1547
|
+
* katalog som den här körningen skapade (`skapad` är mkdirSync:s första
|
|
1548
|
+
* skapade katalog, null när alla fanns) så länge den är tom. Ett fel loggas,
|
|
1549
|
+
* det fäller aldrig körningen. */
|
|
1550
|
+
function stadaUtkast(abs, skapad) {
|
|
1551
|
+
try {
|
|
1552
|
+
rmSync(abs, { force: true })
|
|
1553
|
+
if (skapad === null) return
|
|
1554
|
+
for (let d = path.dirname(abs); ; d = path.dirname(d)) {
|
|
1555
|
+
if (readdirSync(d).length > 0) break
|
|
1556
|
+
rmSync(d, { recursive: true, force: true })
|
|
1557
|
+
if (d === skapad) break
|
|
1558
|
+
}
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
logg(`VARNING kunde inte ta bort det vägrade utkastet ${abs}: ${e.message}`)
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** Experimentkanalen: utkastet som design.md i delen, prövat med precheck
|
|
1565
|
+
* --design-utkast (sektionerna och symbolerna, symbolerna i målrepot via
|
|
1566
|
+
* --repo-rot: delen ligger i arkivets repo). Ett fallet utkast läggs undan
|
|
1567
|
+
* som design-utkast-avvisad-<datum>.md, så assessorn aldrig läser det. */
|
|
1568
|
+
function skrivUtkastIDel(del, dok, ctx) {
|
|
1569
|
+
const konflikter = specConflicts(dok) ?? []
|
|
1570
|
+
if (konflikter.length > 0) return stoppaDesignkonflikt(del, dok, konflikter, ctx)
|
|
1571
|
+
const fil = path.join(del.dir, 'design.md')
|
|
1572
|
+
writeFileSync(fil, dok)
|
|
1573
|
+
const p = korPrecheck(del, ctx, { utkast: fil })
|
|
1574
|
+
if (!p.fel && p.brister.length === 0) {
|
|
1575
|
+
logg(`DESIGN ${delNamn(ctx, del.dir)} utkast skrivet`)
|
|
1576
|
+
return { status: null }
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
renameSync(
|
|
1580
|
+
fil,
|
|
1581
|
+
path.join(del.dir, ledigtNamn(del.dir, `design-utkast-avvisad-${idag()}`, '.md')),
|
|
1582
|
+
)
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
logg(`VARNING kunde inte lägga undan design.md: ${e.message}`)
|
|
1585
|
+
}
|
|
1586
|
+
return { status: 'ofärdig', orsak: p.fel ?? `design: ${bristText(p.brister)}` }
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** Designkonflikter (designa steg 5): dokumentet kopieras in som kvittot
|
|
1590
|
+
* design-avvisad-<datum>.md (-2 vid kollision) och posten stoppas på det,
|
|
1591
|
+
* TILLBAKA_TILL_SPEC eller ESKALERA efter en respec, med konfliktraderna som
|
|
1592
|
+
* orsak. Dokumentet i repot ligger kvar under sitt eget namn. */
|
|
1593
|
+
function stoppaDesignkonflikt(del, text, konflikter, ctx) {
|
|
1594
|
+
const kvitto = ledigtNamn(del.dir, `design-avvisad-${idag()}`, '.md')
|
|
1595
|
+
writeFileSync(path.join(del.dir, kvitto), text)
|
|
1596
|
+
const state = lasJson(path.join(del.dir, 'state.json'))
|
|
1597
|
+
const utfall = (state?.respec_varv ?? 0) >= 1 ? 'ESKALERA' : 'TILLBAKA_TILL_SPEC'
|
|
1598
|
+
const rader = konflikter.map((k) => k.replace(/^[-*]\s+/, ''))
|
|
1599
|
+
const orsak = rader.join('; ')
|
|
1600
|
+
const s = arkiv(['stopp', del.dir, '--utfall', utfall, '--kalla', kvitto, '--orsak', orsak], ctx)
|
|
1601
|
+
if (s.status !== 0) return { status: 'ofärdig', orsak: `stopp: ${felText(s)}` }
|
|
1602
|
+
logg(`STOPP ${delNamn(ctx, del.dir)} ${utfall} ur ${kvitto}`)
|
|
1603
|
+
return { status: 'stopp', orsak: `designkonflikter (${rader.length}): ${orsak}` }
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** Granskarens första rad, `fortsätt`, `stopp` eller null (ett brott). */
|
|
1607
|
+
export const tolkaVerdict = (text) => {
|
|
1608
|
+
const forsta = String(text).replace(/\r\n/g, '\n').trimStart().split('\n')[0].trim()
|
|
1609
|
+
return /^VERDICT: (fortsätt|stopp)$/u.exec(forsta)?.[1] ?? null
|
|
1610
|
+
}
|
|
1611
|
+
const verdictFel = (text) =>
|
|
1612
|
+
`- första raden måste vara "VERDICT: fortsätt" eller "VERDICT: stopp" (fick "${String(text).trimStart().split('\n')[0].trim().slice(0, 80)}")`
|
|
1613
|
+
|
|
1614
|
+
/** Första fyndet under `## Findings`, utan listmarkör. */
|
|
1615
|
+
const forstaFynd = (text) => {
|
|
1616
|
+
const punkt = (sectionLines(text, 'Findings') ?? [])
|
|
1617
|
+
.map((r) => r.trim())
|
|
1618
|
+
.find((r) => r.startsWith('- ') || r.startsWith('* '))
|
|
1619
|
+
return punkt ? punkt.slice(2).trim() : 'inga fynd angivna'
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** Granskaren körs bara efter ett bokfört utkast: draft-läget, ingen
|
|
1623
|
+
* splitdel, design.md i delen med `mode: draft`, och ingen granskning än
|
|
1624
|
+
* (produktionen: state.design satt utan review; experimentet: ingen
|
|
1625
|
+
* design-review.md i delen). Ett intervjudokument granskas aldrig, och ett
|
|
1626
|
+
* dokument med ett kvitto (granskarens eller människans godkännande via
|
|
1627
|
+
* add-design-review) granskas aldrig igen: ett utslag omdispatchas inte
|
|
1628
|
+
* (P4-23). */
|
|
1629
|
+
const granskarenGaller = (del, ctx) => {
|
|
1630
|
+
if (ctx.designMode !== 'draft' || arSplitdel(ctx)) return false
|
|
1631
|
+
const design = lasFil(path.join(del.dir, 'design.md'))
|
|
1632
|
+
if (design === null || designModeFor(design) !== 'draft') return false
|
|
1633
|
+
if (!ctx.post) return !existsSync(path.join(del.dir, 'design-review.md'))
|
|
1634
|
+
const state = lasJson(path.join(del.dir, 'state.json'))
|
|
1635
|
+
return state?.design != null && state.design.review == null
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** Designgranskaren (ADR-0156 beslut 3): ett agentsteg vars första rad är
|
|
1639
|
+
* `VERDICT: fortsätt|stopp`. Annat är ett scriptbevisat brott: EN
|
|
1640
|
+
* omdispatch med felraden, sedan ofärdig. Svaret utan svansraden skrivs som
|
|
1641
|
+
* design-review.md: produktionen i körningskatalogen och via
|
|
1642
|
+
* add-design-review (MODELL-svansen som --model), experimentet i delen.
|
|
1643
|
+
* `stopp` stoppar raden med det första fyndet. */
|
|
1644
|
+
export async function korDesignReviewer(del, ctx) {
|
|
1645
|
+
if (!granskarenGaller(del, ctx)) return { status: null }
|
|
1646
|
+
const prompt = designReviewerPrompt({
|
|
1647
|
+
spec: lasFil(path.join(del.dir, 'spec.md')) ?? '',
|
|
1648
|
+
granskarblock: lasFil(path.join(del.dir, 'granskarblock.md')) ?? 'inget granskarblock',
|
|
1649
|
+
design: lasFil(path.join(del.dir, 'design.md')),
|
|
1650
|
+
repoRot: ctx.repoRot,
|
|
1651
|
+
deps: ctx.deps,
|
|
1652
|
+
sprak: ctx.sprak,
|
|
1653
|
+
})
|
|
1654
|
+
const bas = `steg/1${del.nr}-design-reviewer`
|
|
1655
|
+
const steg = (p, fil) =>
|
|
1656
|
+
korAgent({
|
|
1657
|
+
agent: 'request-design-reviewer',
|
|
1658
|
+
modell: ctx.modell,
|
|
1659
|
+
prompt: p,
|
|
1660
|
+
fil,
|
|
1661
|
+
maxTurer: MAX_TURER.designgranskare,
|
|
1662
|
+
ctx,
|
|
1663
|
+
})
|
|
1664
|
+
let res = await steg(prompt, `${bas}.json`)
|
|
1665
|
+
if (!res.ok) return { status: 'ofärdig', orsak: `designgranskning: ${res.orsak}` }
|
|
1666
|
+
let verdict = tolkaVerdict(res.text)
|
|
1667
|
+
if (verdict === null) {
|
|
1668
|
+
const felrad = verdictFel(res.text)
|
|
1669
|
+
logg(`OMDISPATCH ${delNamn(ctx, del.dir)} designgranskning: ${felrad.slice(2)}`)
|
|
1670
|
+
res = await steg(omdispatchPrompt(prompt, [felrad]), `${bas}-omdispatch.json`)
|
|
1671
|
+
if (!res.ok) return { status: 'ofärdig', orsak: `designgranskning: ${res.orsak}` }
|
|
1672
|
+
verdict = tolkaVerdict(res.text)
|
|
1673
|
+
if (verdict === null)
|
|
1674
|
+
return { status: 'ofärdig', orsak: `designgranskning: ${verdictFel(res.text).slice(2)}` }
|
|
1675
|
+
}
|
|
1676
|
+
const { body, modell } = parseSvansrader(res.text)
|
|
1677
|
+
const dok = body.trim() + '\n'
|
|
1678
|
+
const mal = path.join(ctx.post ? ctx.katalog : del.dir, 'design-review.md')
|
|
1679
|
+
writeFileSync(mal, dok)
|
|
1680
|
+
if (ctx.post) {
|
|
1681
|
+
const r = arkiv(
|
|
1682
|
+
[
|
|
1683
|
+
'add-design-review',
|
|
1684
|
+
del.dir,
|
|
1685
|
+
'--file',
|
|
1686
|
+
mal,
|
|
1687
|
+
'--verdict',
|
|
1688
|
+
verdict,
|
|
1689
|
+
'--model',
|
|
1690
|
+
modell ?? fakturerad(res.svar, ctx.modell) ?? 'okänd',
|
|
1691
|
+
],
|
|
1692
|
+
ctx,
|
|
1693
|
+
)
|
|
1694
|
+
if (r.status !== 0) return { status: 'ofärdig', orsak: `add-design-review: ${felText(r)}` }
|
|
1695
|
+
}
|
|
1696
|
+
logg(`DESIGNGRANSKNING ${delNamn(ctx, del.dir)} ${verdict}`)
|
|
1697
|
+
if (verdict === 'stopp') return { status: 'stopp', orsak: `designgranskning: ${forstaFynd(dok)}` }
|
|
1698
|
+
return { status: null }
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1123
1701
|
/** Analysen: ett agentsteg, impact.md med runs-rad, formkontroll med EN
|
|
1124
1702
|
* omdispatch, och motsägelse mot specen som stopputfall (ADR-0106). */
|
|
1125
1703
|
async function korAssessor(del, ctx) {
|
|
@@ -1133,7 +1711,9 @@ async function korAssessor(del, ctx) {
|
|
|
1133
1711
|
spec,
|
|
1134
1712
|
granskarblock: lasFil(path.join(del.dir, 'granskarblock.md')) ?? 'inget granskarblock',
|
|
1135
1713
|
manifest: ctx.manifest,
|
|
1136
|
-
|
|
1714
|
+
// Ett utkast från korDesigner ligger i delen men aldrig i ctx.design (ctx
|
|
1715
|
+
// delas av en bunts delar): det läses ur delens egen design.md.
|
|
1716
|
+
design: ctx.design ?? lasFil(path.join(del.dir, 'design.md')),
|
|
1137
1717
|
})
|
|
1138
1718
|
const fil = `steg/1${del.nr}-assessor.json`
|
|
1139
1719
|
const res = await korAgent({
|
|
@@ -1161,17 +1741,37 @@ async function korAssessor(del, ctx) {
|
|
|
1161
1741
|
}
|
|
1162
1742
|
}
|
|
1163
1743
|
}
|
|
1164
|
-
if (p.avvikelse === 'motsägelse')
|
|
1165
|
-
const s = arkiv(
|
|
1166
|
-
['stopp', del.dir, '--utfall', 'TILLBAKA_TILL_SPEC', '--kalla', 'impact.md'],
|
|
1167
|
-
ctx,
|
|
1168
|
-
)
|
|
1169
|
-
if (s.status !== 0) return { status: 'ofärdig', orsak: `stopp: ${felText(s)}` }
|
|
1170
|
-
return { status: 'stopp', orsak: 'TILLBAKA_TILL_SPEC' }
|
|
1171
|
-
}
|
|
1744
|
+
if (p.avvikelse === 'motsägelse') return stoppaMotsagelse(del, p, ctx)
|
|
1172
1745
|
return { status: null }
|
|
1173
1746
|
}
|
|
1174
1747
|
|
|
1748
|
+
/** En analys med etiketten motsägelse stoppar som TILLBAKA_TILL_SPEC ur
|
|
1749
|
+
* impact.md (ADR-0106). Börjar texten efter etiketten med `design conflict:`
|
|
1750
|
+
* bär stoppet den texten som `--orsak` (#596): posten går tillbaka till
|
|
1751
|
+
* designen, inte till specen. Texten läses ur precheck-raden
|
|
1752
|
+
* `AVVIKELSE-TEXT:`, aldrig ur filen. */
|
|
1753
|
+
function stoppaMotsagelse(del, p, ctx) {
|
|
1754
|
+
const orsak =
|
|
1755
|
+
typeof p.avvikelseText === 'string' && /^design conflict:/i.test(p.avvikelseText)
|
|
1756
|
+
? p.avvikelseText
|
|
1757
|
+
: null
|
|
1758
|
+
const s = arkiv(
|
|
1759
|
+
[
|
|
1760
|
+
'stopp',
|
|
1761
|
+
del.dir,
|
|
1762
|
+
'--utfall',
|
|
1763
|
+
'TILLBAKA_TILL_SPEC',
|
|
1764
|
+
'--kalla',
|
|
1765
|
+
'impact.md',
|
|
1766
|
+
...(orsak ? ['--orsak', orsak] : []),
|
|
1767
|
+
],
|
|
1768
|
+
ctx,
|
|
1769
|
+
)
|
|
1770
|
+
if (s.status !== 0) return { status: 'ofärdig', orsak: `stopp: ${felText(s)}` }
|
|
1771
|
+
logg(`STOPP ${delNamn(ctx, del.dir)} TILLBAKA_TILL_SPEC${orsak ? `: ${orsak}` : ''}`)
|
|
1772
|
+
return { status: 'stopp', orsak: orsak ? `TILLBAKA_TILL_SPEC: ${orsak}` : 'TILLBAKA_TILL_SPEC' }
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1175
1775
|
const impactRad = (svansModell, fakt) => ({
|
|
1176
1776
|
skill: 'request-assess-impact',
|
|
1177
1777
|
agent: 'request-assessor',
|
|
@@ -1180,15 +1780,35 @@ const impactRad = (svansModell, fakt) => ({
|
|
|
1180
1780
|
})
|
|
1181
1781
|
|
|
1182
1782
|
/** Analysens kropp (svansraderna strippade) som impact.md, med runs-rad.
|
|
1183
|
-
* Ingen --ref: analysen har ingen referensfil att pinna.
|
|
1184
|
-
|
|
1783
|
+
* Ingen --ref: analysen har ingen referensfil att pinna. Raden bär
|
|
1784
|
+
* `--repo-sha`, checkoutens HEAD som assessorn läste, när den är känd
|
|
1785
|
+
* (ADR-0155). */
|
|
1786
|
+
export function skrivImpact(del, res, ctx) {
|
|
1185
1787
|
const { body, modell: svansModell } = parseSvansrader(res.text)
|
|
1186
1788
|
writeFileSync(path.join(del.dir, 'impact.md'), body.trim() + '\n')
|
|
1187
1789
|
const fakt = fakturerad(res.svar, ctx.modell)
|
|
1188
|
-
|
|
1790
|
+
const repoSha = typeof ctx.repoSha === 'string' ? ctx.repoSha : null
|
|
1791
|
+
addRun(del.dir, { ...impactRad(svansModell, fakt), fil: 'impact.md', repoSha }, ctx)
|
|
1189
1792
|
return { svansModell, fakt }
|
|
1190
1793
|
}
|
|
1191
1794
|
|
|
1795
|
+
/** HEAD för checkouten assessorn läser, 40 hex, eller null vid varje fel:
|
|
1796
|
+
* ingen repo, git saknas, en utskrift som inte är en sha. Kastar aldrig. */
|
|
1797
|
+
export function repoSha(repoRot, { kor = spawnSync } = {}) {
|
|
1798
|
+
try {
|
|
1799
|
+
const r = kor('git', ['rev-parse', 'HEAD'], {
|
|
1800
|
+
cwd: repoRot,
|
|
1801
|
+
encoding: 'utf8',
|
|
1802
|
+
timeout: 5000,
|
|
1803
|
+
})
|
|
1804
|
+
if (r?.status !== 0) return null
|
|
1805
|
+
const sha = String(r.stdout ?? '').trim()
|
|
1806
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null
|
|
1807
|
+
} catch {
|
|
1808
|
+
return null
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1192
1812
|
/** EN omdispatch av assessorn. Den avvisade analysen bokförs före omtaget. */
|
|
1193
1813
|
async function impactOmdispatch(del, prompt, brister, meta, ctx) {
|
|
1194
1814
|
avvisaDokument(del.dir, 'impact', brister, impactRad(meta.svansModell, meta.fakt), ctx)
|
|
@@ -1278,8 +1898,7 @@ function samlaFixsteg(fil, fixPath, ctx) {
|
|
|
1278
1898
|
* BROTT, kedjan igen. Kvarstående brott gör körningen avvisad (aldrig hela
|
|
1279
1899
|
* delen: den andra körningen kan vara giltig). Exit 2 är dispatcherfel. */
|
|
1280
1900
|
async function kontrolleraScorer(del, k, res, prompt, ctx) {
|
|
1281
|
-
const txt =
|
|
1282
|
-
const ren = path.join(del.dir, `tmp-scorer-run${k}.json`)
|
|
1901
|
+
const { txt, ren } = tmpFiler(del, k, ctx)
|
|
1283
1902
|
const bas = `steg/2${del.nr}-scorer-run${k}`
|
|
1284
1903
|
const svarsrad = (kontroll, sista) => {
|
|
1285
1904
|
const doc = lasJson(ren)
|
|
@@ -1364,13 +1983,7 @@ async function korScorers(del, ctx) {
|
|
|
1364
1983
|
for (const [i, res] of svar.entries()) {
|
|
1365
1984
|
const kr = await kontrolleraScorer(del, i + 1, res, prompt, ctx)
|
|
1366
1985
|
if (kr.exit2) {
|
|
1367
|
-
stadaTmp([
|
|
1368
|
-
...korningar,
|
|
1369
|
-
{
|
|
1370
|
-
txt: path.join(del.dir, `tmp-scorer-run${i + 1}.txt`),
|
|
1371
|
-
ren: path.join(del.dir, `tmp-scorer-run${i + 1}.json`),
|
|
1372
|
-
},
|
|
1373
|
-
])
|
|
1986
|
+
stadaTmp([...korningar, tmpFiler(del, i + 1, ctx)])
|
|
1374
1987
|
return { status: 'ofärdig', orsak: `poäng: ${kr.orsak}` }
|
|
1375
1988
|
}
|
|
1376
1989
|
korningar.push(kr)
|
|
@@ -1378,6 +1991,18 @@ async function korScorers(del, ctx) {
|
|
|
1378
1991
|
return { korningar }
|
|
1379
1992
|
}
|
|
1380
1993
|
|
|
1994
|
+
/** Kontrollkedjans arbetsfiler för körning `k`. I experimentläget ligger de
|
|
1995
|
+
* i delens katalog, som är körningens egen; i produktionsläget i
|
|
1996
|
+
* körningskatalogen, aldrig i posten, som bara arkivfiler hör hemma i
|
|
1997
|
+
* (ADR-0155 beslut 2). */
|
|
1998
|
+
const tmpFiler = (del, k, ctx) => {
|
|
1999
|
+
const dir = ctx.post ? ctx.katalog : del.dir
|
|
2000
|
+
return {
|
|
2001
|
+
txt: path.join(dir, `tmp-scorer-run${k}.txt`),
|
|
2002
|
+
ren: path.join(dir, `tmp-scorer-run${k}.json`),
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1381
2006
|
/** Kontrollkedjans arbetsyta, aldrig arkivinnehåll: tmp-filerna tas bort när
|
|
1382
2007
|
* varvet är arkiverat eller avbrutet. */
|
|
1383
2008
|
const stadaTmp = (korningar) => {
|
|
@@ -1517,9 +2142,14 @@ function arkiveraVarv(del, korningar, ctx) {
|
|
|
1517
2142
|
}
|
|
1518
2143
|
}
|
|
1519
2144
|
|
|
1520
|
-
/** En del hela vägen:
|
|
1521
|
-
|
|
2145
|
+
/** En del hela vägen: utkast och granskning i draft-läget (ADR-0156), analys,
|
|
2146
|
+
* två scorerkörningar, arkivering. */
|
|
2147
|
+
export async function korDel(del, ctx) {
|
|
1522
2148
|
const namn = delNamn(ctx, del.dir)
|
|
2149
|
+
const utkast = await korDesigner(del, ctx)
|
|
2150
|
+
if (utkast.status !== null) return { namn, ...utkast }
|
|
2151
|
+
const granskning = await korDesignReviewer(del, ctx)
|
|
2152
|
+
if (granskning.status !== null) return { namn, ...granskning }
|
|
1523
2153
|
const analys = await korAssessor(del, ctx)
|
|
1524
2154
|
if (analys.status !== null) return { namn, ...analys }
|
|
1525
2155
|
const scorers = await korScorers(del, ctx)
|
|
@@ -1538,12 +2168,14 @@ const skrivKorningsjson = (ctx, allaSteg) =>
|
|
|
1538
2168
|
|
|
1539
2169
|
/** Avslutet: korning.json skrivs ALLTID (kostnaden är verklig även när
|
|
1540
2170
|
* körningen föll), status vägs samman och sätts via archive.mjs, och sist
|
|
1541
|
-
* skrivs delraderna och bluf-tabellen på stderr.
|
|
1542
|
-
|
|
2171
|
+
* skrivs delraderna och bluf-tabellen på stderr. Produktionsläget har inget
|
|
2172
|
+
* experiment att skriva till (postens state.json är redan satt av stegen),
|
|
2173
|
+
* tar bluf-tabellen över posten och skriver en `STATUS:`-rad på stdout. */
|
|
2174
|
+
export function avsluta(ctx, allaSteg, rader, nyckel = null) {
|
|
1543
2175
|
let statusfel = null
|
|
1544
2176
|
skrivKorningsjson(ctx, allaSteg)
|
|
1545
2177
|
const status = sammanvagdStatus(rader.map((r) => r.status ?? null))
|
|
1546
|
-
if (status !== null) {
|
|
2178
|
+
if (status !== null && !ctx.post) {
|
|
1547
2179
|
const orsak = rader
|
|
1548
2180
|
.filter((r) => r.status === status && r.orsak)
|
|
1549
2181
|
.map((r) => `${r.namn}: ${r.orsak}`)
|
|
@@ -1569,13 +2201,30 @@ function avsluta(ctx, allaSteg, rader, nyckel) {
|
|
|
1569
2201
|
}
|
|
1570
2202
|
for (const r of rader)
|
|
1571
2203
|
logg(`DEL ${r.namn} ${r.status ?? 'klar'}${r.orsak ? ` (${r.orsak})` : ''}`)
|
|
1572
|
-
const bluf = arkiv(['bluf', ctx.katalog], ctx)
|
|
2204
|
+
const bluf = arkiv(['bluf', ctx.post ?? ctx.katalog], ctx)
|
|
1573
2205
|
process.stderr.write(
|
|
1574
2206
|
(bluf.status === 0 ? bluf.stdout : `bluf: ${felText(bluf)}`).trimEnd() + '\n',
|
|
1575
2207
|
)
|
|
2208
|
+
if (ctx.post) process.stdout.write(`STATUS: ${postStatus(ctx, status)}\n`)
|
|
1576
2209
|
return status === 'ofärdig' || statusfel !== null ? 1 : 0
|
|
1577
2210
|
}
|
|
1578
2211
|
|
|
2212
|
+
/** Postens läge efter körningen, läst ur state.json, för STATUS-raden: ett
|
|
2213
|
+
* stopp först, sedan en ofärdig körning (state.json kan stå kvar på
|
|
2214
|
+
* `designed` eller `assessed` och skulle annars se färdig ut), en spärr som
|
|
2215
|
+
* väntar på dom (`archive.mjs sparr`), och sist statusen när den är
|
|
2216
|
+
* `priced` eller `assessed`. Allt annat är ofärdigt. */
|
|
2217
|
+
function postStatus(ctx, korStatus) {
|
|
2218
|
+
const state = lasJson(path.join(ctx.post, 'state.json'))
|
|
2219
|
+
if (state?.stopp !== null && typeof state?.stopp === 'object') return 'stopp'
|
|
2220
|
+
if (korStatus === 'ofärdig') return 'ofärdig'
|
|
2221
|
+
if (arkiv(['sparr', ctx.post], ctx).status === 1) return 'väntar på dom'
|
|
2222
|
+
if (state?.status === 'priced' || state?.status === 'assessed') return state.status
|
|
2223
|
+
// Ett stopp som inte står i state.json (designerns INTERVJU KRÄVS,
|
|
2224
|
+
// ADR-0156: inget skrivs) är fortfarande ett stopp, aldrig ofärdigt.
|
|
2225
|
+
return korStatus === 'stopp' ? 'stopp' : 'ofärdig'
|
|
2226
|
+
}
|
|
2227
|
+
|
|
1579
2228
|
/** Signalavbrottet: döda barnen, skriv mätningen och lämna körningen som
|
|
1580
2229
|
* `ofärdig` efter sig. Utan den här vägen överlever betalda `claude`-processer
|
|
1581
2230
|
* en dödad korning.mjs, och korning.json skrivs aldrig, så både pengarna och
|
|
@@ -1585,24 +2234,63 @@ function avbryt(ctx, allaSteg, nyckel, signal) {
|
|
|
1585
2234
|
const antal = ctx.barn.size
|
|
1586
2235
|
for (const barn of ctx.barn) barn.kill('SIGTERM')
|
|
1587
2236
|
skrivKorningsjson(ctx, allaSteg)
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2237
|
+
// Produktionsläget har inget experiment; kvittot (state.korning) är
|
|
2238
|
+
// startarens att släppa (ADR-0155 beslut 3).
|
|
2239
|
+
if (!ctx.post) {
|
|
2240
|
+
const r = arkiv(
|
|
2241
|
+
[
|
|
2242
|
+
'experiment',
|
|
2243
|
+
ctx.exp,
|
|
2244
|
+
'--korning',
|
|
2245
|
+
nyckel,
|
|
2246
|
+
'--status',
|
|
2247
|
+
'ofärdig',
|
|
2248
|
+
'--orsak',
|
|
2249
|
+
`avbruten (${signal})`,
|
|
2250
|
+
],
|
|
2251
|
+
ctx,
|
|
2252
|
+
)
|
|
2253
|
+
if (r.status !== 0) logg(`VARNING experiment --status ofärdig: ${felText(r)}`)
|
|
2254
|
+
}
|
|
1602
2255
|
logg(`AVBRUTEN ${signal}: ${antal} barnprocess(er) dödade, korning.json skriven`)
|
|
1603
2256
|
process.exit(SIGNAL_EXIT)
|
|
1604
2257
|
}
|
|
1605
2258
|
|
|
2259
|
+
/** Vaktens intervall: startaren prövas var 30:e sekund. */
|
|
2260
|
+
export const VAKT_INTERVALL_MS = 30000
|
|
2261
|
+
|
|
2262
|
+
/** Lever processen? EPERM betyder att den finns men tillhör någon annan,
|
|
2263
|
+
* samma regel som archive.mjs isAlive. */
|
|
2264
|
+
const arLevande = (pid) => {
|
|
2265
|
+
try {
|
|
2266
|
+
process.kill(pid, 0)
|
|
2267
|
+
return true
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
return e?.code === 'EPERM'
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** Vakten över startaren (ADR-0155): en bedom.mjs dödad med SIGKILL rensar
|
|
2274
|
+
* inget kvitto och dödar inget barn, så körningen prövar själv startarens pid
|
|
2275
|
+
* och avbryter (`vidDod`, i main `avbryt`) när den är borta. `vidDod` anropas
|
|
2276
|
+
* högst en gång; timern är `unref()` och håller aldrig processen vid liv. */
|
|
2277
|
+
export function vaktaStartaren({
|
|
2278
|
+
pid,
|
|
2279
|
+
vidDod,
|
|
2280
|
+
isAlive = arLevande,
|
|
2281
|
+
intervallMs = VAKT_INTERVALL_MS,
|
|
2282
|
+
}) {
|
|
2283
|
+
let utlost = false
|
|
2284
|
+
const timer = setInterval(() => {
|
|
2285
|
+
if (utlost || isAlive(pid)) return
|
|
2286
|
+
utlost = true
|
|
2287
|
+
clearInterval(timer)
|
|
2288
|
+
vidDod(`startaren dog (pid ${pid})`)
|
|
2289
|
+
}, intervallMs)
|
|
2290
|
+
timer.unref()
|
|
2291
|
+
return timer
|
|
2292
|
+
}
|
|
2293
|
+
|
|
1606
2294
|
async function main(argv) {
|
|
1607
2295
|
const val = tolkaArgv(argv)
|
|
1608
2296
|
if (val.fel) {
|
|
@@ -1610,7 +2298,14 @@ async function main(argv) {
|
|
|
1610
2298
|
console.error(ANVANDNING)
|
|
1611
2299
|
return 2
|
|
1612
2300
|
}
|
|
1613
|
-
const underlag =
|
|
2301
|
+
const underlag = val.post
|
|
2302
|
+
? lasUnderlagProduktion({
|
|
2303
|
+
post: val.post,
|
|
2304
|
+
repoRot: val.repoRot,
|
|
2305
|
+
sprak: val.sprak,
|
|
2306
|
+
designMode: val.designMode,
|
|
2307
|
+
})
|
|
2308
|
+
: lasUnderlag(val)
|
|
1614
2309
|
if (underlag.fel) {
|
|
1615
2310
|
console.error(underlag.fel)
|
|
1616
2311
|
return 2
|
|
@@ -1619,21 +2314,28 @@ async function main(argv) {
|
|
|
1619
2314
|
const ctx = {
|
|
1620
2315
|
...val,
|
|
1621
2316
|
...underlag,
|
|
2317
|
+
repoSha: repoSha(val.repoRot),
|
|
1622
2318
|
barn: new Set(),
|
|
1623
2319
|
samla: (steg) => allaSteg.push(steg),
|
|
1624
2320
|
raknaSamtidiga,
|
|
1625
2321
|
}
|
|
1626
2322
|
mkdirSync(path.join(ctx.katalog, 'steg'), { recursive: true })
|
|
1627
|
-
|
|
2323
|
+
// Nyckeln är experimentkanalens `<modell>/k<N>`; i produktionsläget är den
|
|
2324
|
+
// bara postens namn i loggen.
|
|
2325
|
+
const nyckel = ctx.post
|
|
2326
|
+
? path.basename(ctx.post)
|
|
2327
|
+
: `${path.basename(path.dirname(ctx.katalog))}/${path.basename(ctx.katalog)}`
|
|
1628
2328
|
for (const signal of ['SIGTERM', 'SIGINT'])
|
|
1629
2329
|
process.on(signal, () => avbryt(ctx, allaSteg, nyckel, signal))
|
|
2330
|
+
if (ctx.post && ctx.startare !== null)
|
|
2331
|
+
vaktaStartaren({ pid: ctx.startare, vidDod: (orsak) => avbryt(ctx, allaSteg, nyckel, orsak) })
|
|
1630
2332
|
logg(
|
|
1631
2333
|
`START ${nyckel} ${ctx.requestId} ${ctx.modell} (${ctx.sprak}${ctx.eskalerad ? ', eskalerad' : ''})`,
|
|
1632
2334
|
)
|
|
1633
2335
|
|
|
1634
2336
|
let rader
|
|
1635
2337
|
try {
|
|
1636
|
-
rader = await korKorningen(ctx)
|
|
2338
|
+
rader = ctx.post ? await korProduktion(ctx) : await korKorningen(ctx)
|
|
1637
2339
|
} catch (e) {
|
|
1638
2340
|
// korning.json skrivs ÄVEN när något kastar: kostnaden är redan dragen och
|
|
1639
2341
|
// stegen ligger på disk. Ett kast som tar mätningen med sig gör körningen
|
|
@@ -1641,7 +2343,7 @@ async function main(argv) {
|
|
|
1641
2343
|
logg(`FEL oväntat: ${e?.stack ?? e}`)
|
|
1642
2344
|
rader = [
|
|
1643
2345
|
{
|
|
1644
|
-
namn: path.basename(ctx.katalog),
|
|
2346
|
+
namn: path.basename(ctx.post ?? ctx.katalog),
|
|
1645
2347
|
status: 'ofärdig',
|
|
1646
2348
|
orsak: `oväntat fel: ${e?.message ?? e}`,
|
|
1647
2349
|
},
|
|
@@ -1672,6 +2374,145 @@ async function korKorningen(ctx) {
|
|
|
1672
2374
|
return rader
|
|
1673
2375
|
}
|
|
1674
2376
|
|
|
2377
|
+
// ---------------------------------------------------------------------------
|
|
2378
|
+
// Produktionsläget (ADR-0155): en godkänd, designad post prissätts ur sin katalog
|
|
2379
|
+
// ---------------------------------------------------------------------------
|
|
2380
|
+
|
|
2381
|
+
/** Precheck-raderna som bara säger att analysen hör till en annan spec eller
|
|
2382
|
+
* design (precheck.mjs, underlags- och designproveniensen under `--impact`).
|
|
2383
|
+
* Exakt de tre formuleringarna: varje annan brist i analysen är ett formfel
|
|
2384
|
+
* som en ny analys inte självklart rättar, och den ska en människa se. */
|
|
2385
|
+
const PROVENIENS_RADER = [
|
|
2386
|
+
'impact.md: analysen gjordes mot en annan version av spec.md',
|
|
2387
|
+
'impact.md: analysen gjordes utan det designdokument som nu finns',
|
|
2388
|
+
'impact.md: analysen gjordes mot en annan version av design.md',
|
|
2389
|
+
]
|
|
2390
|
+
export const arProveniensBrist = (rad) => {
|
|
2391
|
+
const text = String(rad).replace(/^- /, '')
|
|
2392
|
+
return PROVENIENS_RADER.some((p) => text.startsWith(p))
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
const bristText = (brister) => brister.map((b) => b.replace(/^- /, '')).join('; ')
|
|
2396
|
+
|
|
2397
|
+
/** Delposten scorern ska veta om: en split- eller buntdel bär `part`/`of` i
|
|
2398
|
+
* state.json, allt annat är en hel post. */
|
|
2399
|
+
const delpostUr = (state) => {
|
|
2400
|
+
for (const falt of [state?.split, state?.bundle])
|
|
2401
|
+
if (Number.isInteger(falt?.part) && Number.isInteger(falt?.of))
|
|
2402
|
+
return { part: falt.part, of: falt.of }
|
|
2403
|
+
return null
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
/** Steg 2 och 3: analysen återanvänds när den finns och håller proveniensen,
|
|
2407
|
+
* annars körs assessorn, men först efter att designen prövats (en del av en
|
|
2408
|
+
* split ärver förälderns design och prövas inte här). */
|
|
2409
|
+
async function produktionsAnalys(del, ctx) {
|
|
2410
|
+
let korAssessorn = !existsSync(path.join(del.dir, 'impact.md'))
|
|
2411
|
+
if (!korAssessorn) {
|
|
2412
|
+
const p = korPrecheck(del, ctx, { impact: true })
|
|
2413
|
+
if (p.fel) return { status: 'ofärdig', orsak: p.fel }
|
|
2414
|
+
if (p.brister.length > 0 && !p.brister.every(arProveniensBrist))
|
|
2415
|
+
return { status: 'ofärdig', orsak: `impact: ${bristText(p.brister)}` }
|
|
2416
|
+
if (p.brister.length > 0) {
|
|
2417
|
+
logg(`ANALYS ${del.namn} körs om: ${bristText(p.brister)}`)
|
|
2418
|
+
korAssessorn = true
|
|
2419
|
+
} else {
|
|
2420
|
+
logg(`ANALYS ${del.namn} återanvänds`)
|
|
2421
|
+
// En återanvänd analys som redan säger motsägelse prissätts aldrig:
|
|
2422
|
+
// samma stopp som efter en ny analys (idempotent i archive.mjs).
|
|
2423
|
+
return p.avvikelse === 'motsägelse' ? stoppaMotsagelse(del, p, ctx) : { status: null }
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if ((ctx.state?.split?.parent ?? null) === null) {
|
|
2427
|
+
const d = korPrecheck(del, ctx, { design: true })
|
|
2428
|
+
if (d.fel) return { status: 'ofärdig', orsak: d.fel }
|
|
2429
|
+
if (d.design !== 'ok' || d.brister.length > 0) {
|
|
2430
|
+
const rader = [
|
|
2431
|
+
...(d.design !== 'ok' ? [`DESIGN: ${d.design ?? 'saknas'}`] : []),
|
|
2432
|
+
...d.brister,
|
|
2433
|
+
]
|
|
2434
|
+
return { status: 'ofärdig', orsak: `designa ${ctx.requestId}: ${bristText(rader)}` }
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return korAssessor(del, ctx)
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/** Steg 4, efter ett rent arkiveringsvarv: `priced` sätts bara när inget
|
|
2441
|
+
* stopp står och ingen spärr väntar på dom. archive.mjs prövar sedan själv
|
|
2442
|
+
* den gällande prisfilen (pricedBlockers); en vägran är ofärdig med dess
|
|
2443
|
+
* rader, aldrig en tyst `assessed`. */
|
|
2444
|
+
function sattPriced(del, ctx) {
|
|
2445
|
+
const state = lasJson(path.join(del.dir, 'state.json'))
|
|
2446
|
+
if (state?.stopp !== null && typeof state?.stopp === 'object')
|
|
2447
|
+
return { status: 'stopp', orsak: `${state.stopp.utfall} ur ${state.stopp.kalla}` }
|
|
2448
|
+
const sp = arkiv(['sparr', del.dir], ctx)
|
|
2449
|
+
if (sp.status === 1)
|
|
2450
|
+
return { status: 'väntar på dom', orsak: raderMed(sp.stdout, 'SPÄRRAD: ').join('; ') }
|
|
2451
|
+
if (sp.status !== 0) return { status: 'ofärdig', orsak: `sparr: ${felText(sp)}` }
|
|
2452
|
+
const r = arkiv(['set-status', del.dir, 'priced'], ctx)
|
|
2453
|
+
if (r.status === 0) {
|
|
2454
|
+
logg(`PRISSATT ${del.namn}`)
|
|
2455
|
+
return { status: null }
|
|
2456
|
+
}
|
|
2457
|
+
const vagrar = raderMed(r.stdout, 'VÄGRAR priced: ')
|
|
2458
|
+
return {
|
|
2459
|
+
status: 'ofärdig',
|
|
2460
|
+
orsak:
|
|
2461
|
+
vagrar.length > 0
|
|
2462
|
+
? `VÄGRAR priced: ${vagrar.join('; ')}`
|
|
2463
|
+
: `set-status priced: ${felText(r)}`,
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/** Produktionsläget för EN post (ADR-0155 beslut 1): formkontroll av den
|
|
2468
|
+
* godkända specen, analys vid behov, två scorers, arkivering och `priced`.
|
|
2469
|
+
* Returnerar samma radform som korKorningen. Specen godkändes av en
|
|
2470
|
+
* människa, så en brist i den är ofärdig och omdispatchas aldrig här. */
|
|
2471
|
+
export async function korProduktion(ctx) {
|
|
2472
|
+
const del = {
|
|
2473
|
+
dir: ctx.post,
|
|
2474
|
+
namn: path.basename(ctx.post),
|
|
2475
|
+
nr: 1,
|
|
2476
|
+
delpost: delpostUr(ctx.state),
|
|
2477
|
+
}
|
|
2478
|
+
const rad = (r) => ({ namn: del.namn, status: r.status ?? null, orsak: r.orsak ?? null })
|
|
2479
|
+
|
|
2480
|
+
const p = korPrecheck(del, ctx)
|
|
2481
|
+
if (p.fel) return [rad({ status: 'ofärdig', orsak: p.fel })]
|
|
2482
|
+
if (p.brister.length > 0)
|
|
2483
|
+
return [rad({ status: 'ofärdig', orsak: `spec: ${bristText(p.brister)}` })]
|
|
2484
|
+
if (p.kandidat) {
|
|
2485
|
+
const s = arkiv(['stopp', del.dir, '--utfall', p.kandidat, '--kalla', 'spec.md'], ctx)
|
|
2486
|
+
if (s.status !== 0) return [rad({ status: 'ofärdig', orsak: `stopp: ${felText(s)}` })]
|
|
2487
|
+
logg(`STOPP ${del.namn} ${p.kandidat}`)
|
|
2488
|
+
return [rad({ status: 'stopp', orsak: p.kandidat })]
|
|
2489
|
+
}
|
|
2490
|
+
// Ett stopp som redan står (TILLBAKA_TILL_SPEC eller ESKALERA ur
|
|
2491
|
+
// granskarblock.md, ett designkvitto eller en tidigare analys) köper ingen
|
|
2492
|
+
// analys och inga scorers: posten går till respec eller designa först.
|
|
2493
|
+
const stopp = ctx.state?.stopp
|
|
2494
|
+
if (stopp !== null && typeof stopp === 'object') {
|
|
2495
|
+
logg(`STOPP ${del.namn} står: ${stopp.utfall} (${stopp.kalla})`)
|
|
2496
|
+
return [rad({ status: 'stopp', orsak: `stopp står: ${stopp.utfall} (${stopp.kalla})` })]
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// Draft-läget (ADR-0156 beslut 1 och 3): ett saknat designdokument skrivs
|
|
2500
|
+
// av designern och läses av granskaren innan analysbeslutet.
|
|
2501
|
+
const utkast = await korDesigner(del, ctx)
|
|
2502
|
+
if (utkast.status !== null) return [rad(utkast)]
|
|
2503
|
+
const granskning = await korDesignReviewer(del, ctx)
|
|
2504
|
+
if (granskning.status !== null) return [rad(granskning)]
|
|
2505
|
+
|
|
2506
|
+
const analys = await produktionsAnalys(del, ctx)
|
|
2507
|
+
if (analys.status !== null) return [rad(analys)]
|
|
2508
|
+
|
|
2509
|
+
const scorers = await korScorers(del, ctx)
|
|
2510
|
+
if (scorers.status) return [rad(scorers)]
|
|
2511
|
+
const varv = arkiveraVarv(del, scorers.korningar, ctx)
|
|
2512
|
+
if (varv.status !== null) return [rad(varv)]
|
|
2513
|
+
return [rad(sattPriced(del, ctx))]
|
|
2514
|
+
}
|
|
2515
|
+
|
|
1675
2516
|
/** Huvudloopen körs bara när scriptet startas direkt; en import (testerna)
|
|
1676
2517
|
* exekverar ingenting. */
|
|
1677
2518
|
const arDirektKord = () => {
|