@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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* node korning.mjs --katalog <abs k<N>> --exp <abs experimentkatalog> --repo-rot <abs>
|
|
19
19
|
* --request-id <gid eller slug> --modell <modell|default>
|
|
20
20
|
* [--claude <kommando>] [--delar-parallellt 2]
|
|
21
|
-
* [--steg-timeout 1800000]
|
|
21
|
+
* [--steg-timeout 1800000] [--effort <low|medium|high|xhigh|max|default>]
|
|
22
22
|
*
|
|
23
23
|
* Läser `<exp>/experiment.json` (`sprak` OBLIGATORISK, ADR-0114: språket
|
|
24
24
|
* gissas aldrig), `<exp>/onskemal.md` och `<exp>/kallmanifest.md`. Skriver
|
|
@@ -81,6 +81,11 @@ const STANDARD_DELAR_PARALLELLT = 2
|
|
|
81
81
|
export const STANDARD_STEG_TIMEOUT = 1800000
|
|
82
82
|
/** Exitkoden efter ett signalavbrott, kutym för dödad av SIGINT. */
|
|
83
83
|
const SIGNAL_EXIT = 130
|
|
84
|
+
/** Effortnivåerna `claude --effort` tar, plus `default` som betyder "agentens
|
|
85
|
+
* egen effort ur definitionen" och därför ingen flagga alls, samma regel som
|
|
86
|
+
* `--modell default` (#525, ADR-0120). */
|
|
87
|
+
export const EFFORTNIVAER = ['low', 'medium', 'high', 'xhigh', 'max', 'default']
|
|
88
|
+
const STANDARD_EFFORT = 'default'
|
|
84
89
|
const VARDEFLAGGOR = [
|
|
85
90
|
'katalog',
|
|
86
91
|
'exp',
|
|
@@ -90,13 +95,14 @@ const VARDEFLAGGOR = [
|
|
|
90
95
|
'claude',
|
|
91
96
|
'delar-parallellt',
|
|
92
97
|
'steg-timeout',
|
|
98
|
+
'effort',
|
|
93
99
|
]
|
|
94
100
|
|
|
95
101
|
export const ANVANDNING = [
|
|
96
102
|
'användning: korning.mjs --katalog <abs k<N>> --exp <abs experimentkatalog> --repo-rot <abs>',
|
|
97
103
|
' --request-id <gid eller slug> --modell <modell|default>',
|
|
98
104
|
` [--claude <kommando>] [--delar-parallellt ${STANDARD_DELAR_PARALLELLT}]`,
|
|
99
|
-
` [--steg-timeout ${STANDARD_STEG_TIMEOUT}]`,
|
|
105
|
+
` [--steg-timeout ${STANDARD_STEG_TIMEOUT}] [--effort <${EFFORTNIVAER.join('|')}>]`,
|
|
100
106
|
].join('\n')
|
|
101
107
|
|
|
102
108
|
// ---------------------------------------------------------------------------
|
|
@@ -112,9 +118,18 @@ export const ANVANDNING = [
|
|
|
112
118
|
* poängsätta, precis som dess kontrakt kräver. Listan sätts därför explicit i
|
|
113
119
|
* stället för att slå av behörighetslagret (`bypassPermissions` hade tillåtit
|
|
114
120
|
* allt, inklusive skrivningar in i arkivet som bara archive.mjs får göra).
|
|
115
|
-
* Assessorns "bara läsande git"-regel blir här mekanisk:
|
|
121
|
+
* Assessorns "bara läsande git"-regel blir här mekanisk: prefix, inte en
|
|
116
122
|
* mening i prosa som modellen kan tolka fel.
|
|
123
|
+
*
|
|
124
|
+
* Assessorns lista (#523): sex läsande git-subkommandon, deras
|
|
125
|
+
* `rtk git`-speglingar (den globala CLAUDE.md instruerar agenten att prefixa
|
|
126
|
+
* git med rtk, så `git log` nekades tyst i k2-svepet), och de fyra
|
|
127
|
+
* pipe-målen `head`/`grep`/`wc`/`ls`, som en allow-regel matchar bara som
|
|
128
|
+
* kommandon i kedjan. ALDRIG `Bash(git -C:*)`: prefixet säger inget om vilket
|
|
129
|
+
* subkommando som följer katalogen, så det kan inte matchas säkert. ALDRIG
|
|
130
|
+
* `Bash(echo:*)`: ett fritt echo är ett skrivkommando i förklädnad.
|
|
117
131
|
*/
|
|
132
|
+
const ASSESSOR_GIT = ['ls-files', 'check-ignore', 'log', 'grep', 'status', 'rev-parse']
|
|
118
133
|
export const AGENT_VERKTYG = {
|
|
119
134
|
'request-scorer': ['Read', 'Skill'],
|
|
120
135
|
'request-specifier': ['Read', 'Grep', 'Glob', 'Skill'],
|
|
@@ -124,17 +139,21 @@ export const AGENT_VERKTYG = {
|
|
|
124
139
|
'Grep',
|
|
125
140
|
'Glob',
|
|
126
141
|
'Skill',
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'Bash(
|
|
130
|
-
'Bash(
|
|
142
|
+
...ASSESSOR_GIT.map((sub) => `Bash(git ${sub}:*)`),
|
|
143
|
+
...ASSESSOR_GIT.map((sub) => `Bash(rtk git ${sub}:*)`),
|
|
144
|
+
'Bash(head:*)',
|
|
145
|
+
'Bash(grep:*)',
|
|
146
|
+
'Bash(wc:*)',
|
|
147
|
+
'Bash(ls:*)',
|
|
131
148
|
],
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
/** Den pinnade headless-invokationen per agentsteg: agentdefinitionen ur
|
|
135
|
-
* ~/.claude/agents/ kör med sitt eget verktygsurval och
|
|
136
|
-
* `--
|
|
137
|
-
* modell" och får därför ingen flagga
|
|
152
|
+
* ~/.claude/agents/ kör med sitt eget verktygsurval, `--model` och
|
|
153
|
+
* `--effort` läggs på bara som override. `default` betyder "agentens egen
|
|
154
|
+
* modell" respektive "agentens egen effort" och får därför ingen flagga
|
|
155
|
+
* alls (#525: en effort-override som inte når processen är en proveniens
|
|
156
|
+
* som ljuger). `--add-dir` öppnar BARA
|
|
138
157
|
* skills-katalogen under Claude-konfigkatalogen: det är den agenten läser
|
|
139
158
|
* med Read (skillfiler och referensfiler ligger utanför repo-roten som är
|
|
140
159
|
* cwd), medan agentdefinitionen läses av CLI:n själv och aldrig via Read.
|
|
@@ -143,7 +162,7 @@ export const AGENT_VERKTYG = {
|
|
|
143
162
|
* check-lasta släpper ändå bara igenom request-score-skillens egen katalog.
|
|
144
163
|
* `--allowedTools` ligger SIST: flaggan är variadisk och skulle annars svälja
|
|
145
164
|
* allt som kom efter den. Prompten går på stdin som förut. */
|
|
146
|
-
export const agentArgs = ({ agent, modell, maxTurer }) => [
|
|
165
|
+
export const agentArgs = ({ agent, modell, effort = STANDARD_EFFORT, maxTurer }) => [
|
|
147
166
|
'-p',
|
|
148
167
|
'--agent',
|
|
149
168
|
agent,
|
|
@@ -154,6 +173,7 @@ export const agentArgs = ({ agent, modell, maxTurer }) => [
|
|
|
154
173
|
'--add-dir',
|
|
155
174
|
path.join(claudeDir(), 'skills'),
|
|
156
175
|
...(modell === 'default' ? [] : ['--model', modell]),
|
|
176
|
+
...(effort === STANDARD_EFFORT ? [] : ['--effort', effort]),
|
|
157
177
|
...(AGENT_VERKTYG[agent] === undefined ? [] : ['--allowedTools', ...AGENT_VERKTYG[agent]]),
|
|
158
178
|
]
|
|
159
179
|
|
|
@@ -161,9 +181,17 @@ export const agentArgs = ({ agent, modell, maxTurer }) => [
|
|
|
161
181
|
* (ADR-0114). Tabellen bor i lib.mjs; här väljs bara kolumn. */
|
|
162
182
|
const rubrikrader = (sprak) => SPEC_HEADINGS.map((r) => `**${sprak === 'engelska' ? r.en : r.sv}**`)
|
|
163
183
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
184
|
+
/** Ett okänt språk kastar innan någon process startas: scriptet gissar aldrig
|
|
185
|
+
* språk (ADR-0114; IPC 14001 fällde tio utkast på just den gissningen).
|
|
186
|
+
* Samma spärr för alla tre prompterna, #521: kundtexten skrevs på svenska i
|
|
187
|
+
* ett engelskt ärende när bara specifiern fick raden. */
|
|
188
|
+
const kravSprak = (sprak) => {
|
|
189
|
+
if (sprak !== 'svenska' && sprak !== 'engelska')
|
|
190
|
+
throw new Error(`SPRÅK måste vara svenska eller engelska (fick "${sprak}")`)
|
|
191
|
+
return `SPRÅK: ${sprak}`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Specifierns prompt, dispatch-kontraktets mall ordagrant. */
|
|
167
195
|
export function specifierPrompt({
|
|
168
196
|
onskemal,
|
|
169
197
|
manifest,
|
|
@@ -173,8 +201,7 @@ export function specifierPrompt({
|
|
|
173
201
|
eskalerad,
|
|
174
202
|
eskaleringsorsak = null,
|
|
175
203
|
}) {
|
|
176
|
-
|
|
177
|
-
throw new Error(`SPRÅK måste vara svenska eller engelska (fick "${sprak}")`)
|
|
204
|
+
const sprakRad = kravSprak(sprak)
|
|
178
205
|
return [
|
|
179
206
|
'ÖNSKEMÅL:',
|
|
180
207
|
onskemal.trim(),
|
|
@@ -184,7 +211,7 @@ export function specifierPrompt({
|
|
|
184
211
|
'',
|
|
185
212
|
`REPO-ROT: ${repoRot}`,
|
|
186
213
|
`DEPS: ${deps}`,
|
|
187
|
-
|
|
214
|
+
sprakRad,
|
|
188
215
|
'RUBRIKER:',
|
|
189
216
|
...rubrikrader(sprak),
|
|
190
217
|
`ESKALERAD: ${eskalerad ? (eskaleringsorsak ? `ja, ${eskaleringsorsak}` : 'ja') : 'nej'}`,
|
|
@@ -193,11 +220,14 @@ export function specifierPrompt({
|
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
/** Assessorns prompt. KÄLLOR ligger med (hävstång 5): analysen skrevs förr
|
|
196
|
-
* mot ett repo den fick leta i själv, medan manifestet låg färdigt bredvid.
|
|
197
|
-
|
|
223
|
+
* mot ett repo den fick leta i själv, medan manifestet låg färdigt bredvid.
|
|
224
|
+
* SPRÅK efter DEPS (#521): analysens kundvända delar följer raden. */
|
|
225
|
+
export function assessorPrompt({ repoRot, deps, sprak, spec, granskarblock, manifest }) {
|
|
226
|
+
const sprakRad = kravSprak(sprak)
|
|
198
227
|
return [
|
|
199
228
|
`REPO-ROT: ${repoRot}`,
|
|
200
229
|
`DEPS: ${deps}`,
|
|
230
|
+
sprakRad,
|
|
201
231
|
'KÄLLOR:',
|
|
202
232
|
manifest.trim(),
|
|
203
233
|
'',
|
|
@@ -210,12 +240,15 @@ export function assessorPrompt({ repoRot, deps, spec, granskarblock, manifest })
|
|
|
210
240
|
].join('\n')
|
|
211
241
|
}
|
|
212
242
|
|
|
213
|
-
/** Scorerns prompt: underlaget per sökväg, aldrig inklistrat (ADR-0099).
|
|
214
|
-
|
|
243
|
+
/** Scorerns prompt: underlaget per sökväg, aldrig inklistrat (ADR-0099).
|
|
244
|
+
* SPRÅK efter DELPOST (#521): kundtexten i kuvertet följer raden. */
|
|
245
|
+
export function scorerPrompt({ requestId, delpost, sprak, specPath, impactPath }) {
|
|
246
|
+
const sprakRad = kravSprak(sprak)
|
|
215
247
|
return [
|
|
216
248
|
`REQUEST_ID: ${requestId}`,
|
|
217
249
|
'MODE: pricing',
|
|
218
250
|
`DELPOST: ${delpost ? `ja, del ${delpost.part} av ${delpost.of}` : 'nej'}`,
|
|
251
|
+
sprakRad,
|
|
219
252
|
'',
|
|
220
253
|
`SPEC: ${specPath}`,
|
|
221
254
|
`PÅVERKANSANALYS: ${impactPath}`,
|
|
@@ -385,10 +418,23 @@ export function slaIhopKorning(steg) {
|
|
|
385
418
|
fil: s.fil,
|
|
386
419
|
agent: s.agent,
|
|
387
420
|
modell_begard: s.modell_begard,
|
|
421
|
+
// "begärd", inte verifierad: modellen prövas mot modelUsage (ADR-0099),
|
|
422
|
+
// men faktureringen bär ingen effort, så fältet kan inte kontrolleras
|
|
423
|
+
// mot något och ingen effortverifiering uppfinns (#525, ADR-0120).
|
|
424
|
+
effort_begard: s.effort_begard ?? STANDARD_EFFORT,
|
|
388
425
|
total_cost_usd: s.svar?.total_cost_usd ?? 0,
|
|
389
426
|
usage: summera(USAGE_FALT, null, s.svar?.usage),
|
|
390
427
|
num_turns: s.svar?.num_turns ?? 0,
|
|
391
428
|
wall_ms: s.wall_ms ?? null,
|
|
429
|
+
// Lasten wall_ms mättes under (#526): maskinvid och best effort, null
|
|
430
|
+
// när räknaren föll eller steget är äldre än fältet.
|
|
431
|
+
samtidiga_processer: s.samtidiga_processer ?? null,
|
|
432
|
+
// #523: CLI:ns JSON-svar bär `permission_denials` som lista; antalet
|
|
433
|
+
// bokförs så en tyst nekning syns. Saknas fältet är det null, aldrig
|
|
434
|
+
// noll: en nolla skulle läsas som "inget nekades".
|
|
435
|
+
permission_denials: Array.isArray(s.svar?.permission_denials)
|
|
436
|
+
? s.svar.permission_denials.length
|
|
437
|
+
: null,
|
|
392
438
|
is_error: s.svar ? s.svar.is_error === true : true,
|
|
393
439
|
})),
|
|
394
440
|
}
|
|
@@ -482,7 +528,7 @@ const lasFil = (p) => {
|
|
|
482
528
|
/** Argumenttolkning, fail-closed som svep.mjs och kontroll.mjs: alla saknade
|
|
483
529
|
* flaggor namnges på en gång, en okänd flagga eller ett värdelöst flaggvärde
|
|
484
530
|
* stoppar direkt i stället för att gissa. */
|
|
485
|
-
function tolkaArgv(argv) {
|
|
531
|
+
export function tolkaArgv(argv) {
|
|
486
532
|
const enkla = {}
|
|
487
533
|
for (let i = 0; i < argv.length; i++) {
|
|
488
534
|
const a = argv[i]
|
|
@@ -506,6 +552,9 @@ function tolkaArgv(argv) {
|
|
|
506
552
|
const timeoutText = enkla['steg-timeout'] ?? String(STANDARD_STEG_TIMEOUT)
|
|
507
553
|
if (!/^\d+$/.test(timeoutText) || Number(timeoutText) < 1)
|
|
508
554
|
return { fel: `--steg-timeout måste vara ett heltal >= 1 (fick "${timeoutText}")` }
|
|
555
|
+
const effort = enkla.effort ?? STANDARD_EFFORT
|
|
556
|
+
if (!EFFORTNIVAER.includes(effort))
|
|
557
|
+
return { fel: `--effort måste vara en av ${EFFORTNIVAER.join(', ')} (fick "${effort}")` }
|
|
509
558
|
return {
|
|
510
559
|
katalog: enkla.katalog,
|
|
511
560
|
exp: enkla.exp,
|
|
@@ -515,6 +564,7 @@ function tolkaArgv(argv) {
|
|
|
515
564
|
claude: enkla.claude ?? 'claude',
|
|
516
565
|
delarParallellt: Number(takText),
|
|
517
566
|
stegTimeout: Number(timeoutText),
|
|
567
|
+
effort,
|
|
518
568
|
}
|
|
519
569
|
}
|
|
520
570
|
|
|
@@ -591,13 +641,33 @@ function tolkaSteg({ stdout, stegFil, modell, spawnfel, tidsgransMs, exitCode })
|
|
|
591
641
|
}
|
|
592
642
|
}
|
|
593
643
|
|
|
644
|
+
/** Levande agentprocesser på maskinen vid ett stegs start (#526, ADR-0120):
|
|
645
|
+
* `pgrep -f` över mönstret varje agentsteg bär (`--agent request-`), räknat
|
|
646
|
+
* EFTER att det egna steget startats, så det ingår i talet. Värdet är
|
|
647
|
+
* MASKINVIDT, inte körningens: det fångar även en annan körnings steg och ett
|
|
648
|
+
* annat kontos svep mot samma checkout (ADR-0093), vilket är precis den last
|
|
649
|
+
* wall_ms mättes under utan att den bokfördes. Best effort: exit 1 är
|
|
650
|
+
* "ingen träff" (0), allt annat (pgrep saknas, exit >= 2, ett kast) blir
|
|
651
|
+
* null, aldrig ett kast som fäller steget. `kor` är injicerbar för testerna. */
|
|
652
|
+
export function raknaSamtidiga(kor = spawnSync) {
|
|
653
|
+
try {
|
|
654
|
+
const r = kor('pgrep', ['-f', '--', '--agent request-'], { encoding: 'utf8' })
|
|
655
|
+
if (r.error || (r.status !== 0 && r.status !== 1)) return null
|
|
656
|
+
return String(r.stdout ?? '')
|
|
657
|
+
.split('\n')
|
|
658
|
+
.filter((rad) => rad.trim() !== '').length
|
|
659
|
+
} catch {
|
|
660
|
+
return null
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
594
664
|
/** Ett agentsteg: en egen `claude -p --agent`-process med prompten på stdin,
|
|
595
665
|
* svaret RÅTT i `steg/<fil>` och klockan runt den. Barnet läggs i `ctx.barn`
|
|
596
666
|
* så signalhanteraren kan döda det: en dödad korning.mjs får aldrig lämna
|
|
597
667
|
* betalda `claude`-processer efter sig. Väggtidstaket (`--steg-timeout`)
|
|
598
668
|
* dödar ett steg som hänger utan att svara. Anroparen avgör vad ett fallet
|
|
599
669
|
* steg betyder. */
|
|
600
|
-
function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
670
|
+
export function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
601
671
|
return new Promise((los) => {
|
|
602
672
|
const start = Date.now()
|
|
603
673
|
const stegFil = path.join(ctx.katalog, fil)
|
|
@@ -606,23 +676,38 @@ function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
|
606
676
|
let klar = false
|
|
607
677
|
let tidsgransMs = null
|
|
608
678
|
let timer = null
|
|
679
|
+
let samtidiga = null
|
|
609
680
|
const avsluta = ({ exitCode, spawnfel = null }) => {
|
|
610
681
|
if (klar) return
|
|
611
682
|
klar = true
|
|
612
683
|
if (timer !== null) clearTimeout(timer)
|
|
613
684
|
const wall_ms = Date.now() - start
|
|
614
685
|
const t = tolkaSteg({ stdout, stegFil, modell, spawnfel, tidsgransMs, exitCode })
|
|
615
|
-
ctx.samla({
|
|
686
|
+
ctx.samla({
|
|
687
|
+
fil,
|
|
688
|
+
agent,
|
|
689
|
+
modell_begard: modell,
|
|
690
|
+
effort_begard: ctx.effort ?? STANDARD_EFFORT,
|
|
691
|
+
svar: t.svar,
|
|
692
|
+
wall_ms,
|
|
693
|
+
samtidiga_processer: samtidiga,
|
|
694
|
+
})
|
|
616
695
|
if (t.orsak !== null && stderr.trim() !== '')
|
|
617
696
|
logg(`${fil} stderr: ${stderr.trim().slice(0, 200)}`)
|
|
618
697
|
logg(
|
|
619
698
|
`STEG ${fil} ${agent} ${modell} ${wall_ms}ms ${t.orsak === null ? 'ok' : `fallet: ${t.orsak}`}`,
|
|
620
699
|
)
|
|
700
|
+
// #523: en nekning i headless-läget syns annars ingenstans förrän
|
|
701
|
+
// analysen saknar det den skulle ha verifierat.
|
|
702
|
+
const nekade = Array.isArray(t.svar?.permission_denials)
|
|
703
|
+
? t.svar.permission_denials.length
|
|
704
|
+
: 0
|
|
705
|
+
if (nekade > 0) logg(`STEG ${fil} ${nekade} nekade verktygsanrop`)
|
|
621
706
|
los({ ...t, exitCode, wall_ms, ok: t.orsak === null })
|
|
622
707
|
}
|
|
623
708
|
let barn
|
|
624
709
|
try {
|
|
625
|
-
barn = spawn(ctx.claude, agentArgs({ agent, modell, maxTurer }), {
|
|
710
|
+
barn = spawn(ctx.claude, agentArgs({ agent, modell, effort: ctx.effort, maxTurer }), {
|
|
626
711
|
cwd: ctx.repoRot,
|
|
627
712
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
628
713
|
})
|
|
@@ -631,6 +716,9 @@ function korAgent({ agent, modell, prompt, fil, maxTurer, ctx }) {
|
|
|
631
716
|
return
|
|
632
717
|
}
|
|
633
718
|
ctx.barn.add(barn)
|
|
719
|
+
// Räknas efter spawn, så det egna steget ingår; spawn är synkron i libuv
|
|
720
|
+
// och barnet finns med sin argv när anropet återvänder.
|
|
721
|
+
samtidiga = (ctx.raknaSamtidiga ?? raknaSamtidiga)()
|
|
634
722
|
timer = setTimeout(() => {
|
|
635
723
|
tidsgransMs = ctx.stegTimeout
|
|
636
724
|
logg(`STEG ${fil} dödas: tidsgräns ${ctx.stegTimeout} ms`)
|
|
@@ -679,30 +767,35 @@ const SPEC_REF = () =>
|
|
|
679
767
|
refFlagga('skills/request-write-spec/references/specformat.md', 'references/specformat.md')
|
|
680
768
|
const SCORE_REF = () => refFlagga('skills/request-score/references/poangskala.md', 'poangskala.md')
|
|
681
769
|
|
|
770
|
+
/** Argumenten till `archive.mjs add-run` för en runs-rad. `--effort` skickas
|
|
771
|
+
* exakt när körningen begärt en override: utan flaggan läser add-run
|
|
772
|
+
* `effort:` ur agentdefinitionen (ADR-0107 beslut 3), vilket är rätt för
|
|
773
|
+
* `default` och fel för allt annat (#525: 40 av 44 rader i IPC 14001 bar
|
|
774
|
+
* definitionens värde över en körning som begärt ett annat). */
|
|
775
|
+
export const addRunArgs = (dir, rad, effort = STANDARD_EFFORT) => [
|
|
776
|
+
'add-run',
|
|
777
|
+
dir,
|
|
778
|
+
'--file',
|
|
779
|
+
rad.fil,
|
|
780
|
+
'--skill',
|
|
781
|
+
rad.skill,
|
|
782
|
+
'--agent',
|
|
783
|
+
rad.agent,
|
|
784
|
+
'--model',
|
|
785
|
+
rad.model ?? 'okänd',
|
|
786
|
+
...(effort === STANDARD_EFFORT ? [] : ['--effort', effort]),
|
|
787
|
+
...(rad.fakt ? ['--fakturerad', rad.fakt] : []),
|
|
788
|
+
...(rad.ref ?? []),
|
|
789
|
+
...(rad.note ? ['--note', rad.note] : []),
|
|
790
|
+
...(rad.doubleRun ? ['--double-run', rad.doubleRun] : []),
|
|
791
|
+
...(rad.supersededBy ? ['--superseded-by', rad.supersededBy] : []),
|
|
792
|
+
]
|
|
793
|
+
|
|
682
794
|
/** En runs-rad. Arkivet är append-only och äger sina egna vägransregler; en
|
|
683
795
|
* vägrad stämpling loggas men fäller aldrig körningen, kvittot på vad som
|
|
684
796
|
* hände ligger kvar i steg/. */
|
|
685
797
|
function addRun(dir, rad, ctx) {
|
|
686
|
-
const r = arkiv(
|
|
687
|
-
[
|
|
688
|
-
'add-run',
|
|
689
|
-
dir,
|
|
690
|
-
'--file',
|
|
691
|
-
rad.fil,
|
|
692
|
-
'--skill',
|
|
693
|
-
rad.skill,
|
|
694
|
-
'--agent',
|
|
695
|
-
rad.agent,
|
|
696
|
-
'--model',
|
|
697
|
-
rad.model ?? 'okänd',
|
|
698
|
-
...(rad.fakt ? ['--fakturerad', rad.fakt] : []),
|
|
699
|
-
...(rad.ref ?? []),
|
|
700
|
-
...(rad.note ? ['--note', rad.note] : []),
|
|
701
|
-
...(rad.doubleRun ? ['--double-run', rad.doubleRun] : []),
|
|
702
|
-
...(rad.supersededBy ? ['--superseded-by', rad.supersededBy] : []),
|
|
703
|
-
],
|
|
704
|
-
ctx,
|
|
705
|
-
)
|
|
798
|
+
const r = arkiv(addRunArgs(dir, rad, ctx.effort), ctx)
|
|
706
799
|
if (r.status !== 0) logg(`VARNING add-run ${rad.fil}: ${felText(r)}`)
|
|
707
800
|
return r.status === 0
|
|
708
801
|
}
|
|
@@ -987,6 +1080,7 @@ async function korAssessor(del, ctx) {
|
|
|
987
1080
|
const prompt = assessorPrompt({
|
|
988
1081
|
repoRot: ctx.repoRot,
|
|
989
1082
|
deps: ctx.deps,
|
|
1083
|
+
sprak: ctx.sprak,
|
|
990
1084
|
spec,
|
|
991
1085
|
granskarblock: lasFil(path.join(del.dir, 'granskarblock.md')) ?? 'inget granskarblock',
|
|
992
1086
|
manifest: ctx.manifest,
|
|
@@ -1098,6 +1192,11 @@ function korKontroll(txt, ren, fixFil, del, ctx) {
|
|
|
1098
1192
|
fixPath,
|
|
1099
1193
|
'--state',
|
|
1100
1194
|
path.join(del.dir, 'state.json'),
|
|
1195
|
+
// #521: kundtextens språk till contract-check. Ett språkbrott är inte
|
|
1196
|
+
// kundtextfixbart (fixen byter ord, översätter inte) och går till
|
|
1197
|
+
// omdispatch som varje annat modellfel.
|
|
1198
|
+
'--sprak',
|
|
1199
|
+
ctx.sprak,
|
|
1101
1200
|
],
|
|
1102
1201
|
ctx,
|
|
1103
1202
|
)
|
|
@@ -1114,7 +1213,15 @@ function samlaFixsteg(fil, fixPath, ctx) {
|
|
|
1114
1213
|
const rasvar = lasJson(fixPath)
|
|
1115
1214
|
if (!Array.isArray(rasvar)) return
|
|
1116
1215
|
for (const svar of rasvar)
|
|
1117
|
-
ctx.samla({
|
|
1216
|
+
ctx.samla({
|
|
1217
|
+
fil,
|
|
1218
|
+
agent: 'kundtextfix',
|
|
1219
|
+
modell_begard: FIXMODELL,
|
|
1220
|
+
// kontroll.mjs får ingen effort-override, fixanropet kör CLI:ns default.
|
|
1221
|
+
effort_begard: STANDARD_EFFORT,
|
|
1222
|
+
svar,
|
|
1223
|
+
wall_ms: null,
|
|
1224
|
+
})
|
|
1118
1225
|
}
|
|
1119
1226
|
|
|
1120
1227
|
/** En scorerkörning från agentsvar till dom: kontrollkedjan, EN omdispatch vid
|
|
@@ -1137,6 +1244,9 @@ async function kontrolleraScorer(del, k, res, prompt, ctx) {
|
|
|
1137
1244
|
modellSvans: kontroll.modell ?? parseSvansrader(sista?.text ?? '').modell,
|
|
1138
1245
|
fakt: fakturerad(sista?.svar, ctx.modell),
|
|
1139
1246
|
reparerat: kontroll.reparerat ?? [],
|
|
1247
|
+
// Brotten som tvingade en lyckad omdispatch (#522): kasserades de här
|
|
1248
|
+
// saknade runs-raden spår av varför omkörningen betalades.
|
|
1249
|
+
omdispatchadFor: kontroll.omdispatchadFor ?? [],
|
|
1140
1250
|
}
|
|
1141
1251
|
}
|
|
1142
1252
|
if (!res.ok) {
|
|
@@ -1168,7 +1278,11 @@ async function kontrolleraScorer(del, k, res, prompt, ctx) {
|
|
|
1168
1278
|
writeFileSync(txt, om.text)
|
|
1169
1279
|
const andra = korKontroll(txt, ren, `${bas}-omdispatch-kundtextfix.json`, del, ctx)
|
|
1170
1280
|
if (andra.exit2) return { exit2: true, orsak: `kontroll run${k} (omdispatch): ${andra.orsak}` }
|
|
1171
|
-
kontroll = {
|
|
1281
|
+
kontroll = {
|
|
1282
|
+
...andra,
|
|
1283
|
+
reparerat: [...kontroll.reparerat, ...andra.reparerat],
|
|
1284
|
+
omdispatchadFor: kontroll.brott,
|
|
1285
|
+
}
|
|
1172
1286
|
sista = om
|
|
1173
1287
|
}
|
|
1174
1288
|
return svarsrad(kontroll, sista)
|
|
@@ -1180,6 +1294,7 @@ async function korScorers(del, ctx) {
|
|
|
1180
1294
|
const prompt = scorerPrompt({
|
|
1181
1295
|
requestId: ctx.requestId,
|
|
1182
1296
|
delpost: del.delpost,
|
|
1297
|
+
sprak: ctx.sprak,
|
|
1183
1298
|
specPath: path.join(del.dir, 'spec.md'),
|
|
1184
1299
|
impactPath: path.join(del.dir, 'impact.md'),
|
|
1185
1300
|
})
|
|
@@ -1227,8 +1342,20 @@ function skrivPris(del, namn, kr) {
|
|
|
1227
1342
|
writeFileSync(path.join(del.dir, namn), JSON.stringify(innehall, null, 2) + '\n')
|
|
1228
1343
|
}
|
|
1229
1344
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1345
|
+
/** Runs-radens note: extra först, sedan varför en lyckad omdispatch behövdes
|
|
1346
|
+
* (#522) och sist vad kedjan reparerade. Null när inget finns. */
|
|
1347
|
+
export const scorerNote = (kr, extra = []) => {
|
|
1348
|
+
const omdispatchad = (kr.omdispatchadFor ?? []).map((b) => b.replace(/^- /, ''))
|
|
1349
|
+
return (
|
|
1350
|
+
[
|
|
1351
|
+
...extra,
|
|
1352
|
+
...(omdispatchad.length > 0 ? [`omdispatchad för: ${omdispatchad.join('; ')}`] : []),
|
|
1353
|
+
...kr.reparerat.map((r) => `reparerat: ${r}`),
|
|
1354
|
+
]
|
|
1355
|
+
.filter(Boolean)
|
|
1356
|
+
.join('; ') || null
|
|
1357
|
+
)
|
|
1358
|
+
}
|
|
1232
1359
|
|
|
1233
1360
|
const scorerRad = (kr, fil, extra = {}) => ({
|
|
1234
1361
|
fil,
|
|
@@ -1439,7 +1566,13 @@ async function main(argv) {
|
|
|
1439
1566
|
return 2
|
|
1440
1567
|
}
|
|
1441
1568
|
const allaSteg = []
|
|
1442
|
-
const ctx = {
|
|
1569
|
+
const ctx = {
|
|
1570
|
+
...val,
|
|
1571
|
+
...underlag,
|
|
1572
|
+
barn: new Set(),
|
|
1573
|
+
samla: (steg) => allaSteg.push(steg),
|
|
1574
|
+
raknaSamtidiga,
|
|
1575
|
+
}
|
|
1443
1576
|
mkdirSync(path.join(ctx.katalog, 'steg'), { recursive: true })
|
|
1444
1577
|
const nyckel = `${path.basename(path.dirname(ctx.katalog))}/${path.basename(ctx.katalog)}`
|
|
1445
1578
|
for (const signal of ['SIGTERM', 'SIGINT'])
|
|
@@ -441,6 +441,60 @@ export const prisavgorandeForm = (text) => {
|
|
|
441
441
|
|
|
442
442
|
const INGA_ORD = ['inga', 'none', '-', 'tom']
|
|
443
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Funktionsord per språk för språkgrinden på kundtext (#521). Heuristiken är
|
|
446
|
+
* medvetet smal: minst två OLIKA hela ord ur fel språk fäller, ett enda är
|
|
447
|
+
* ett lån eller ett namn. Inget ord får vara vanligt i det andra språket
|
|
448
|
+
* ("for" är inte svenska, "för" är inte engelska); homograferna som ändå
|
|
449
|
+
* finns (svenskans "is", "and", "not", "for") fångas av tvåordsgränsen.
|
|
450
|
+
* Diakritiska tecken är ingen signal: Göteborg står i engelsk text.
|
|
451
|
+
*/
|
|
452
|
+
export const FUNKTIONSORD = {
|
|
453
|
+
svenska: ['och', 'att', 'inte', 'för', 'som', 'är', 'med', 'till', 'av', 'en', 'ett'],
|
|
454
|
+
engelska: ['the', 'and', 'with', 'that', 'this', 'for', 'is', 'are', 'will', 'not', 'of', 'to'],
|
|
455
|
+
}
|
|
456
|
+
const ANDRA_SPRAKET = { svenska: 'engelska', engelska: 'svenska' }
|
|
457
|
+
|
|
458
|
+
/** De olika funktionsorden ur FEL språk som `text` bär som hela ord,
|
|
459
|
+
* skiftlägesokänsligt, i listans ordning. `sprak` är det väntade språket;
|
|
460
|
+
* ett okänt språk kastar, grinden gissar aldrig (ADR-0114). */
|
|
461
|
+
export const felSprakOrd = (text, sprak) => {
|
|
462
|
+
const fel = ANDRA_SPRAKET[sprak]
|
|
463
|
+
if (fel === undefined)
|
|
464
|
+
throw new Error(`SPRÅK måste vara svenska eller engelska (fick "${sprak}")`)
|
|
465
|
+
return FUNKTIONSORD[fel].filter((ord) =>
|
|
466
|
+
new RegExp(`(?<![\\p{L}\\d_])${escapeRe(ord)}(?![\\p{L}\\d_])`, 'iu').test(String(text)),
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const utfallsvalRadRe = () =>
|
|
471
|
+
new RegExp(
|
|
472
|
+
`^\\*\\*(?:${[UTFALLSVAL_RAD.sv, UTFALLSVAL_RAD.en].map(escapeRe).join('|')}):\\*\\*\\s*(.*)$`,
|
|
473
|
+
'u',
|
|
474
|
+
)
|
|
475
|
+
/** Raderna under raden Obesvarade utfallsval fram till nästa fetstilsrad eller
|
|
476
|
+
* rubrik, plus texten efter kolonet; null när raden saknas. Raden är en
|
|
477
|
+
* fetstilsrad, inte en rubrik, så sektionen slutar vid nästa `**…:**`. */
|
|
478
|
+
const utfallsvalAvsnitt = (text) => {
|
|
479
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
480
|
+
const radRe = utfallsvalRadRe()
|
|
481
|
+
const start = lines.findIndex((l) => radRe.test(l.trim()))
|
|
482
|
+
if (start === -1) return null
|
|
483
|
+
const efterKolon = radRe.exec(lines[start].trim())[1].trim()
|
|
484
|
+
const rader = []
|
|
485
|
+
for (const l of lines.slice(start + 1)) {
|
|
486
|
+
const t = l.trim()
|
|
487
|
+
if (isHeadingLine(t) || /^\*\*[^*]+:\*\*/.test(t)) break
|
|
488
|
+
rader.push(l)
|
|
489
|
+
}
|
|
490
|
+
return { efterKolon, rader }
|
|
491
|
+
}
|
|
492
|
+
const LISTPUNKT_RE = /^(?:[-*+]|\d+[.)])\s+(.*)$/
|
|
493
|
+
/** Ett inga-ord som första ord efter kolonet räknas som inga oavsett svans
|
|
494
|
+
* ("inga, eftersom …"); "ingalunda" börjar bara som ett och räknas inte. */
|
|
495
|
+
const arIngaOrd = (efterKolon) =>
|
|
496
|
+
/^(?:inga|none|tom)(?![\p{L}\d])/iu.test(efterKolon) || /^-\s*\.?$/.test(efterKolon)
|
|
497
|
+
|
|
444
498
|
/**
|
|
445
499
|
* Antalet listade, ostrukna alternativ under raden Obesvarade utfallsval
|
|
446
500
|
* (#507). Raden är en fetstilsrad, inte en rubrik: sektionen är allt till
|
|
@@ -451,24 +505,17 @@ const INGA_ORD = ['inga', 'none', '-', 'tom']
|
|
|
451
505
|
* äger precheck felet).
|
|
452
506
|
*/
|
|
453
507
|
export const utfallsvalAlternativ = (text) => {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
'u',
|
|
458
|
-
)
|
|
459
|
-
const start = lines.findIndex((l) => radRe.test(l.trim()))
|
|
460
|
-
if (start === -1) return null
|
|
461
|
-
const efterKolon = radRe.exec(lines[start].trim())[1].trim().toLowerCase()
|
|
508
|
+
const avsnitt = utfallsvalAvsnitt(text)
|
|
509
|
+
if (avsnitt === null) return null
|
|
510
|
+
const efterKolon = avsnitt.efterKolon.toLowerCase()
|
|
462
511
|
if (INGA_ORD.includes(efterKolon.replace(/[.]$/, ''))) return 0
|
|
463
512
|
let antal = 0
|
|
464
|
-
for (const l of
|
|
465
|
-
const t = l.trim()
|
|
466
|
-
if (isHeadingLine(t) || /^\*\*[^*]+:\*\*/.test(t)) break
|
|
513
|
+
for (const l of avsnitt.rader) {
|
|
467
514
|
// Ett alternativ är ett listobjekt på kolumn 0 av originalraden (#507
|
|
468
515
|
// uppföljning): en indenterad rad under ett objekt är en underrad
|
|
469
516
|
// (delar av samma alternativ, t.ex. en struket-motivering), inte ett
|
|
470
|
-
// eget alternativ. Kravet ligger på `l`, inte på
|
|
471
|
-
const m =
|
|
517
|
+
// eget alternativ. Kravet ligger på `l`, inte på en trimmad rad.
|
|
518
|
+
const m = LISTPUNKT_RE.exec(l)
|
|
472
519
|
if (!m) continue
|
|
473
520
|
// Prefixtestet körs mot normaliserad text: markdown-emfas (**fetstil**,
|
|
474
521
|
// _kursiv_, `kod`) runt "struket:" får inte dölja markören för
|
|
@@ -479,6 +526,23 @@ export const utfallsvalAlternativ = (text) => {
|
|
|
479
526
|
return antal
|
|
480
527
|
}
|
|
481
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Sant när avsnittet under Obesvarade utfallsval bär annan text än en lista
|
|
531
|
+
* eller ett inga-ord (#520): alternativ i löptext ("(a) … (b) …"), inline
|
|
532
|
+
* efter kolonet eller på följande rader. Finns minst en listpunkt på kolumn 0
|
|
533
|
+
* är det aldrig prosa, oavsett text runt om: då räknar utfallsvalAlternativ.
|
|
534
|
+
* Ett inga-ord som första ord räknas som inga oavsett svans ("inga, eftersom
|
|
535
|
+
* …"). null när raden saknas (precheck äger felet). Hjälparen tolkar aldrig
|
|
536
|
+
* vad prosan säger, den ser bara att formen saknas.
|
|
537
|
+
*/
|
|
538
|
+
export const utfallsvalProsa = (text) => {
|
|
539
|
+
const avsnitt = utfallsvalAvsnitt(text)
|
|
540
|
+
if (avsnitt === null) return null
|
|
541
|
+
if (arIngaOrd(avsnitt.efterKolon)) return false
|
|
542
|
+
if (avsnitt.rader.some((l) => LISTPUNKT_RE.test(l))) return false
|
|
543
|
+
return avsnitt.efterKolon !== '' || avsnitt.rader.some((l) => l.trim() !== '')
|
|
544
|
+
}
|
|
545
|
+
|
|
482
546
|
/**
|
|
483
547
|
* Närmaste katalog, från `dir` och uppåt, som innehåller `file`; sökvägen
|
|
484
548
|
* till filen eller null. Stannar vid en katalog med ett namn i `stopNames`
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
avvikelseEtikett,
|
|
29
29
|
AVVIKELSE_ETIKETTER,
|
|
30
30
|
utfallsvalAlternativ,
|
|
31
|
+
utfallsvalProsa,
|
|
31
32
|
prisavgorandeVal,
|
|
32
33
|
prisavgorandeForm,
|
|
33
34
|
} from './lib.mjs'
|
|
@@ -264,6 +265,21 @@ if (requireImpact) {
|
|
|
264
265
|
problems.push(
|
|
265
266
|
`impact.md: motsägelse med ${alternativ} listade alternativ under "Obesvarade utfallsval": välj precisering och låt fyrstegsregeln avgöra, eller stryk alternativen med prefixet "struket:" och skälet`,
|
|
266
267
|
)
|
|
268
|
+
// #520: alternativ i löptext ("(a) … (b) …") räknas inte som listade
|
|
269
|
+
// och passerade grinden ovan som om raden vore "inga". Formen krävs:
|
|
270
|
+
// vid motsägelse är prosa en BRIST (grinden kan annars inte avgöra om
|
|
271
|
+
// etiketten är rätt), vid precisering en VARNING (scorerns
|
|
272
|
+
// fyrstegsregel läser listan, prosa läser den inte). Aldrig en
|
|
273
|
+
// tolkning av vad "(a)" betyder.
|
|
274
|
+
const formkrav =
|
|
275
|
+
'skriv en listpunkt per alternativ på kolumn 0 ("- …"), stryk med prefixet "struket:" och skälet, eller skriv "inga"'
|
|
276
|
+
if (
|
|
277
|
+
(avvikelse === 'motsägelse' || avvikelse === 'precisering') &&
|
|
278
|
+
utfallsvalProsa(impact) === true
|
|
279
|
+
)
|
|
280
|
+
(avvikelse === 'motsägelse' ? problems : warnings).push(
|
|
281
|
+
`impact.md: ${avvikelse} med alternativ i löptext under "Obesvarade utfallsval": ${formkrav}`,
|
|
282
|
+
)
|
|
267
283
|
}
|
|
268
284
|
// Analysen får inte läcka poängsättning eller tid (ADR-0098: kontrollen
|
|
269
285
|
// gällde tidigare klassord; klassaxeln är borttagen och regeln riktas nu
|