@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.
@@ -20,13 +20,15 @@
20
20
  * steg som ålägger startaren en skrivning måste peka ut vem som utför den.
21
21
  *
22
22
  * Användning:
23
- * node svep.mjs --repo-rot <abs> --konto <namn> --modell <m> [--modell <m>]...
23
+ * node svep.mjs --repo-rot <abs> [--arkiv <abs>] --konto <namn> --modell <m> [--modell <m>]...
24
24
  * --exp <experiments/<slug>> [--exp ...]...
25
25
  * [--tickets-parallellt 2] [--modeller-parallellt 4]
26
26
  * [--om | --hoppa-över] [--claude <kommando>]
27
27
  *
28
- * Ett relativt --exp löses mot <repo-rot>/.haus-workflow/requests/. Varje
29
- * experimentkatalog måste bära onskemal.md, kallmanifest.md och experiment.json
28
+ * Ett relativt --exp löses mot arkivroten: --arkiv när den ges (ARKIVROT: från
29
+ * `archive.mjs rot`), annars härledd ur --repo-rot:s git common dir, alltså
30
+ * <huvudutcheckning>/.haus-workflow/requests/ (ADR-0154, ADR-0155).
31
+ * Varje experimentkatalog måste bära onskemal.md, kallmanifest.md och experiment.json
30
32
  * med `ticket` (huvudkonversationen förbereder dem, utvärdera steg 1 till 3);
31
33
  * saknas något märks ticketen och hoppas över, svepet fortsätter.
32
34
  *
@@ -44,7 +46,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, realpathSync } fr
44
46
  import { spawn, spawnSync } from 'node:child_process'
45
47
  import path from 'node:path'
46
48
  import { fileURLToPath } from 'node:url'
47
- import { canonicalModel, matcharModell, avgorDeps, schemalagg } from './lib.mjs'
49
+ import { arkivRot, canonicalModel, matcharModell, avgorDeps, schemalagg } from './lib.mjs'
48
50
 
49
51
  // matcharModell, avgorDeps och schemalagg flyttade till lib.mjs (delade
50
52
  // byggstenar för requestmotorns kontrollscript); re-exporteras här så
@@ -68,6 +70,7 @@ const STANDARD_MODELLTAK = 4
68
70
  const SIGINT_EXIT = 130
69
71
  const VARDEFLAGGOR = [
70
72
  'repo-rot',
73
+ 'arkiv',
71
74
  'konto',
72
75
  'modell',
73
76
  'exp',
@@ -78,7 +81,7 @@ const VARDEFLAGGOR = [
78
81
  const BOOLFLAGGOR = ['om', 'hoppa-över']
79
82
 
80
83
  export const ANVANDNING = [
81
- 'användning: svep.mjs --repo-rot <abs> --konto <namn> --modell <m> [--modell <m>]...',
84
+ 'användning: svep.mjs --repo-rot <abs> [--arkiv <abs>] --konto <namn> --modell <m> [--modell <m>]...',
82
85
  ' --exp <experiments/<slug>> [--exp ...]...',
83
86
  ` [--tickets-parallellt ${STANDARD_TAK}] [--modeller-parallellt ${STANDARD_MODELLTAK}]`,
84
87
  ' [--om | --hoppa-över] [--claude <kommando>]',
@@ -119,6 +122,8 @@ export function tolkaArgv(argv) {
119
122
  if (bool.om && bool['hoppa-över']) return { fel: '--om och --hoppa-över utesluter varandra' }
120
123
  if (!path.isAbsolute(enkla['repo-rot']))
121
124
  return { fel: `--repo-rot måste vara en absolut sökväg (fick "${enkla['repo-rot']}")` }
125
+ if (enkla.arkiv !== undefined && !path.isAbsolute(enkla.arkiv))
126
+ return { fel: `--arkiv måste vara en absolut sökväg (fick "${enkla.arkiv}")` }
122
127
  const dubbletter = listor.modell.filter((m, i) => listor.modell.indexOf(m) !== i)
123
128
  if (dubbletter.length > 0)
124
129
  return {
@@ -142,13 +147,22 @@ export function tolkaArgv(argv) {
142
147
  om: bool.om,
143
148
  hoppaOver: bool['hoppa-över'] || !bool.om,
144
149
  claude: enkla.claude ?? 'claude',
150
+ arkiv: enkla.arkiv ?? null,
145
151
  }
146
152
  }
147
153
 
148
154
  /** Ett relativt --exp är arkivrelativt (`experiments/<slug>`), som SKILL.md
149
- * skriver det; ett absolut används som det är. */
150
- export function losExp(exp, repoRot) {
151
- return path.isAbsolute(exp) ? exp : path.join(repoRot, '.haus-workflow', 'requests', exp)
155
+ * skriver det, och löses mot arkivroten; ett absolut används som det är. */
156
+ export function losExp(exp, arkiv) {
157
+ return path.isAbsolute(exp) ? exp : path.join(arkiv, exp)
158
+ }
159
+
160
+ /** Arkivroten när --arkiv saknas: huvudutcheckningens arkiv via git common
161
+ * dir (ADR-0154), så en worktree som --repo-rot aldrig blir arkivets hem.
162
+ * Utan git, eller med en common dir som inte heter .git, faller den
163
+ * tillbaka på <repoRot>/.haus-workflow/requests. */
164
+ export function losArkiv(repoRot) {
165
+ return arkivRot(repoRot)?.arkivRot ?? path.join(repoRot, '.haus-workflow', 'requests')
152
166
  }
153
167
 
154
168
  /** `default` som modellvärde ger katalognamnet `baseline/` (The experiment area: `experiments/`). */
@@ -305,10 +319,13 @@ export function sparrKataloger(dir) {
305
319
  return delar.length === 0 ? [dir] : delar.flatMap(sparrKataloger)
306
320
  }
307
321
 
322
+ /** En USD-cell: `decimaler` decimaler, `-` när beloppet saknas. Delas med
323
+ * bedom.mjs, som visar två decimaler där svepet visar fyra. */
324
+ export const usd = (u, decimaler = 4) => (Number.isFinite(u) ? u.toFixed(decimaler) : '-')
325
+
308
326
  /** Sluttabellen: en rad per (ticket, modell). */
309
327
  export function renderaTabell(rader) {
310
328
  const wall = (ms) => (Number.isFinite(ms) ? `${ms}ms` : '-')
311
- const usd = (u) => (Number.isFinite(u) ? u.toFixed(4) : '-')
312
329
  return [
313
330
  '| ticket | modell | körning | status | wall | USD |',
314
331
  '| --- | --- | --- | --- | --- | --- |',
@@ -330,7 +347,7 @@ const arkiv = (args, cwd) =>
330
347
 
331
348
  const felText = (r) => (r.stderr || r.stdout || '').trim() || `exit ${r.status}`
332
349
 
333
- const skrivMatning = (katalog, matning) =>
350
+ export const skrivMatning = (katalog, matning) =>
334
351
  writeFileSync(path.join(katalog, 'matning.json'), JSON.stringify(matning, null, 2) + '\n')
335
352
 
336
353
  const lasKorningsrad = (expDir, nyckel) => {
@@ -346,8 +363,20 @@ const lasKorningsrad = (expDir, nyckel) => {
346
363
  * runt den. korning.json skrivs av korning.mjs självt (filens ägare); här
347
364
  * sparas bara stderr som stderr.log. matning.json skrivs ÄVEN när processen
348
365
  * faller eller inte går att starta: en kraschad körning har en klocktid.
349
- * `pagaende` är svepets register för Ctrl-C. */
350
- function korInre({ claude, gid, modell, katalog, expDir, repoRot, pagaende, nyckel }) {
366
+ * `pagaende` är svepets register för Ctrl-C. `args` ersätter
367
+ * experimentlägets argument (bedom.mjs skickar produktionslägets `--post`,
368
+ * ADR-0155); utan `args` är anropet svepets som förut. */
369
+ export function korInre({
370
+ claude,
371
+ gid,
372
+ modell,
373
+ katalog,
374
+ expDir,
375
+ repoRot,
376
+ pagaende,
377
+ nyckel,
378
+ args = null,
379
+ }) {
351
380
  return new Promise((resolve) => {
352
381
  const startad = new Date()
353
382
  let stderr = ''
@@ -373,7 +402,7 @@ function korInre({ claude, gid, modell, katalog, expDir, repoRot, pagaende, nyck
373
402
  try {
374
403
  barn = spawn(
375
404
  process.execPath,
376
- korningArgs({ katalog, expDir, repoRot, gid, modell, claude }),
405
+ args ?? korningArgs({ katalog, expDir, repoRot, gid, modell, claude }),
377
406
  {
378
407
  cwd: repoRot,
379
408
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -695,7 +724,8 @@ async function main(argv) {
695
724
  process.exit(SIGINT_EXIT)
696
725
  })
697
726
 
698
- const exps = val.exps.map((e) => losExp(e, val.repoRot))
727
+ const arkivrot = val.arkiv ?? losArkiv(val.repoRot)
728
+ const exps = val.exps.map((e) => losExp(e, arkivrot))
699
729
  const resultat = await schemalagg(exps, val.taket, (exp) => korTicket(exp, val, pagaende, ut))
700
730
 
701
731
  const rader = []
@@ -80,10 +80,7 @@ If the item comes back with the outcome `DELA_UPP`, it could not be bracketed ag
80
80
 
81
81
  The last point decides whether the split is worth anything. If all parts require the same manual walkthrough of the same flows in the same environments, the heaviest work is multiplied by the split instead of divided. In that case, write that out.
82
82
 
83
- **Two kinds of parts, and they end up in different places:**
84
-
85
- - If the part can be delivered on its own, it becomes a **separate request**, with its own spec and its own delivery. Write that, but do not write the spec: `request-write-spec` does that.
86
- - If it cannot be delivered on its own, it becomes a **part of the same item**, delivered together with the rest.
83
+ **Every part becomes a subtask under the same item**, never a separate request, whether or not the part could be delivered on its own. Whether a part could be delivered on its own still matters for the order of the work: write it per part, since it says which part can ship first. It never changes where the part ends up.
87
84
 
88
85
  **The parts inherit the item's spec and this analysis.** Write no separate spec and no separate analysis per part. Each part gets a scope line that says which part of the spec it covers, and is scored against the shared source material. The customer ordered one thing; the split is internal structure.
89
86
 
@@ -25,12 +25,16 @@ Read `.haus-workflow/request.json` in the repo root:
25
25
  "customer_board": "1202738613029434",
26
26
  "customer_section": "1202738613029441",
27
27
  "customer_section_name": "Mina sidor",
28
- "collection_board": "1217668109816859"
28
+ "collection_board": "1217668109816859",
29
+ "design_mode": "interview",
30
+ "auto_sync_spec": true
29
31
  }
30
32
  ```
31
33
 
32
34
  `customer_section` is missing when the board is not divided into sections, and when the setup was run before the section step existed. `customer_section_name` exists only so it can be shown in confirmations: **always match on gid, never on name.**
33
35
 
36
+ `design_mode` is `interview` (default, also when the key is absent) or `draft`. `draft` (ADR-0156) lets `/request bedöm` draft the design of an approved item that has none and have a second machine read it before the item is priced; the setup never asks for it, a human edits the file to opt in. `auto_sync_spec` (`true` by default, also when the key is absent): the request skill places a spec at 🔍 without a "go" unless it is `false` (ADR-0157); the setup writes the default and never asks for it, a human edits the file to opt out.
37
+
34
38
  **If the file is missing: run the setup below.** Never guess from the repo name or the folder name.
35
39
 
36
40
  If the file exists: **verify the board once, before the first write.** Look up `customer_board` by gid and print the name: _"Writing to Vafab Mina sidor, is that right?"_ Putting a customer's request on the wrong customer board is the only error in the entire flow that another customer can see, and it cannot be cleaned up quietly.
@@ -77,7 +81,9 @@ Fetch the fields on both boards and go through three things:
77
81
  "customer_board": "1217634399775198",
78
82
  "customer_section": "1217634399775205",
79
83
  "customer_section_name": "Mina sidor",
80
- "collection_board": "1217668109816859"
84
+ "collection_board": "1217668109816859",
85
+ "design_mode": "interview",
86
+ "auto_sync_spec": true
81
87
  }
82
88
  ```
83
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "5.12.1",
3
+ "version": "6.0.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {