@haus-tech/haus-workflow 6.2.0 → 6.4.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 +36 -0
- package/README.md +1 -0
- package/dist/cli.js +28250 -16572
- package/library/global/settings-fragments/hooks.json +14 -0
- package/library/global/skills/haus-workflow/references/clone.md +136 -0
- package/library/global/skills/request/SKILL.md +50 -18
- package/library/global/skills/request/scripts/archive.mjs +81 -8
- package/library/global/skills/request/scripts/bedom.mjs +3 -3
- package/library/global/skills/request/scripts/korning.mjs +45 -12
- package/library/global/skills/request/scripts/lib.mjs +20 -6
- package/library/global/skills/request/scripts/svep.mjs +4 -4
- package/library/schemas/localdev.v2.schema.json +369 -0
- package/package.json +3 -1
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
"matcher": "Bash",
|
|
23
23
|
"command": "haus decisions guard --from-hook"
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"id": "hook.guard.auto-merge",
|
|
27
|
+
"gate": "keep",
|
|
28
|
+
"event": "PreToolUse",
|
|
29
|
+
"matcher": "mcp__ccd_pr__set_auto_merge",
|
|
30
|
+
"command": "haus guard auto-merge --from-hook"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "hook.pr.after-merge",
|
|
34
|
+
"gate": "keep",
|
|
35
|
+
"event": "PostToolUse",
|
|
36
|
+
"matcher": "Bash",
|
|
37
|
+
"command": "haus pr after-merge --from-hook"
|
|
38
|
+
},
|
|
25
39
|
{
|
|
26
40
|
"id": "hook.decisions.suggest",
|
|
27
41
|
"gate": "keep",
|
|
@@ -202,6 +202,142 @@ seeds: # optional: data sources NOT owned by a manifest repo (e.g. a shared Word
|
|
|
202
202
|
login: 'Add your SSH key to the host (ssh-add …) / request access — or skip the pull.'
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
**Workspace, format v2: the `instance:` section (per-worktree environments, ADR-0163).**
|
|
206
|
+
A workspace that runs features in parallel worktrees adds `version: 2` and an `instance:`
|
|
207
|
+
section, and sets `worktree.env: true` in `haus.workspace.yaml`. Unlike the rest of this
|
|
208
|
+
file, `instance:` is parsed and validated strictly by the CLI (`haus doctor`; schema
|
|
209
|
+
`library/schemas/localdev.v2.schema.json`); unknown keys are errors. A v1 file simply has
|
|
210
|
+
no instance support.
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
version: 2
|
|
214
|
+
instance:
|
|
215
|
+
ports: # name -> main-checkout port; a worktree gets <leased block> + offset, in this order
|
|
216
|
+
vendure: 3000
|
|
217
|
+
admin-ui: 4200
|
|
218
|
+
datastores: # shared engines; a worktree is isolated INSIDE them, never by its own container
|
|
219
|
+
mariadb: { engine: mariadb, compose: { project: shop-workspace, service: mariadb }, port: 3318 }
|
|
220
|
+
# compose.file: the compose file (workspace-relative) when its name is not a default one,
|
|
221
|
+
# e.g. compose: { project: shop-workspace, service: pg, file: docker-compose.local.yml }
|
|
222
|
+
redis: { engine: redis, compose: { project: shop-workspace, service: redis }, port: 6381 } # needs --databases 64
|
|
223
|
+
search:
|
|
224
|
+
{
|
|
225
|
+
engine: elasticsearch,
|
|
226
|
+
compose: { project: shop-workspace, service: es },
|
|
227
|
+
port: 9201,
|
|
228
|
+
prefix: 'vendure-',
|
|
229
|
+
}
|
|
230
|
+
databases: # one database per worktree: <name>_wt_<slug>, cloned from <name>_tpl (or <name>)
|
|
231
|
+
ecom: { on: mariadb, name: shop_ecom, perWorktree: clone } # or perWorktree: empty
|
|
232
|
+
sites: # one Herd site per worktree on a copy-on-write docroot clone
|
|
233
|
+
wordpress:
|
|
234
|
+
kind: herd
|
|
235
|
+
root: shop.se # workspace-relative docroot
|
|
236
|
+
host: '{{slug}}.shop.se' # served at http://<slug>.shop.se.test
|
|
237
|
+
share: [wp-content/uploads] # symlinked to the main docroot, not copied
|
|
238
|
+
plugins: { ecom-widgets: shop-ecom-widgets } # plugin dir -> member repo in this worktree
|
|
239
|
+
config:
|
|
240
|
+
{
|
|
241
|
+
kind: wp-config-override,
|
|
242
|
+
defines: { DB_NAME: '{{db.wp}}', WP_HOME: '{{site.wordpress.url}}' },
|
|
243
|
+
}
|
|
244
|
+
after: ['wp search-replace http://shop.se.test {{site.wordpress.url}} --skip-columns=guid']
|
|
245
|
+
processes: # what `env start <name>` runs, from that member repo, on its leased port
|
|
246
|
+
vendure: { repo: shop-ecom, start: 'yarn dev', port: vendure, health: '/health' }
|
|
247
|
+
copyLocal: # extra machine-local files copied from the main checkout (mode 600)
|
|
248
|
+
- { repo: shop-ecom, path: auth.json }
|
|
249
|
+
teardown: # shell commands `env down --destroy` / `env gc --write` run before dropping databases
|
|
250
|
+
- 'docker compose -p shop-ecom-e2e-{{slug}} down -v --remove-orphans'
|
|
251
|
+
env: # the same map as v1; values may now use templates
|
|
252
|
+
- value: '{{port.vendure}}'
|
|
253
|
+
sinks: [{ repo: shop-ecom, key: API_PORT }]
|
|
254
|
+
- value: '{{db.ecom}}'
|
|
255
|
+
sinks: [{ repo: shop-ecom, key: DB_NAME }]
|
|
256
|
+
- value: '{{redis.redis.db}}'
|
|
257
|
+
sinks: [{ repo: shop-ecom, key: REDIS_DB }]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Templates: `{{slug}}`, `{{port.<name>}}`, `{{db.<name>}}`, `{{redis.<name>.db}}`,
|
|
261
|
+
`{{search.<name>.prefix}}`, `{{site.<name>.url|host|root}}`. In the main checkout they
|
|
262
|
+
resolve to today's values (the main ports and database names, Redis DB 0, the main
|
|
263
|
+
prefix), so one manifest serves both; only a worktree gets new ones. Only keys fed by a
|
|
264
|
+
templated entry are instance-owned; every other key in a repo's `.env` is copied as it is.
|
|
265
|
+
`teardown` (workspace file only) cleans up per-worktree resources the CLI does not track,
|
|
266
|
+
such as a compose project an app's own test harness starts; keep each command idempotent.
|
|
267
|
+
`after` (workspace file only) is its counterpart for `env up`: commands that run once per
|
|
268
|
+
environment in the worktree root, after the `.env` files and `.haus-env.json` exist, such as
|
|
269
|
+
copying the main bucket into a per-worktree one (`mc mirror local/assets
|
|
270
|
+
local/assets-wt-{{slug}}`); a failed one is retried by the next `env up`.
|
|
271
|
+
|
|
272
|
+
**Single-repo projects** (a plain repo with Claude Code worktrees, ADR-0167): the repo's own
|
|
273
|
+
`.haus-workflow/localdev.yml` takes the whole `instance:` section, the flag is
|
|
274
|
+
`"worktree": { "env": true }` in `.haus-workflow/haus.config.json`, and `repo:` in sinks,
|
|
275
|
+
processes and `copyLocal` names a folder inside the repo (`.` for the root, `apps/server`
|
|
276
|
+
for an app).
|
|
277
|
+
|
|
278
|
+
**Other engines and site kinds.** The same section covers the other client stacks:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
instance:
|
|
282
|
+
datastores:
|
|
283
|
+
# An engine on the host (DBngin, Homebrew), not in Docker: haus never starts it and
|
|
284
|
+
# connects with the host's mysql/mariadb client. Credentials: export MYSQL_USER and
|
|
285
|
+
# MYSQL_PWD, or use ~/.my.cnf. `port` defaults to native.port. Supported for mariadb,
|
|
286
|
+
# mysql, elasticsearch and opensearch.
|
|
287
|
+
mysql: { engine: mysql, native: { host: 127.0.0.1, port: 3306 } }
|
|
288
|
+
# native: { host: 127.0.0.1, port: 3306, client: /Users/Shared/DBngin/mysql/8.0.33/bin/mysql }
|
|
289
|
+
# SQL Server: database isolation (BACKUP / RESTORE WITH MOVE into <name>_wt_<slug>).
|
|
290
|
+
# azure-sql-edge has no sqlcmd; haus then runs mcr.microsoft.com/mssql-tools (amd64) on
|
|
291
|
+
# the engine's network. The SA password stays in the engine container's env.
|
|
292
|
+
mssql: { engine: mssql, compose: { project: vafab-workspace, service: mssql }, port: 49760 }
|
|
293
|
+
sites:
|
|
294
|
+
cms: # Bedrock: the root is a member repo, its .env gets the defines (copy: none is the default)
|
|
295
|
+
kind: herd
|
|
296
|
+
root: bad-varme.se
|
|
297
|
+
host: '{{slug}}.bad-varme.se'
|
|
298
|
+
php: '8.2'
|
|
299
|
+
share: [web/app/uploads]
|
|
300
|
+
config:
|
|
301
|
+
kind: bedrock-env
|
|
302
|
+
defines:
|
|
303
|
+
DB_NAME: '{{db.wp}}'
|
|
304
|
+
WP_HOME: '{{site.cms.url}}'
|
|
305
|
+
WP_SITEURL: '{{site.cms.url}}/wp'
|
|
306
|
+
app: # Laravel on Herd: same, with Laravel's keys
|
|
307
|
+
kind: herd
|
|
308
|
+
root: app.example.se
|
|
309
|
+
host: '{{slug}}.app.example.se'
|
|
310
|
+
secure: true
|
|
311
|
+
config:
|
|
312
|
+
kind: laravel-env
|
|
313
|
+
defines: { APP_URL: '{{site.app.url}}', DB_DATABASE: '{{db.api}}' }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`bedrock-env` and `laravel-env` write no PHP file and need no `patch-wp-config`: the
|
|
317
|
+
defines become instance-owned keys of the member's `.env`, next to any `env:` sinks for
|
|
318
|
+
that repo, and the rest of the main checkout's `.env` is copied as it is. Teardown unlinks
|
|
319
|
+
the Herd site and never deletes the member repo.
|
|
320
|
+
|
|
321
|
+
**How this relates to the clone flow.** This procedure sets up the **main checkout**: it
|
|
322
|
+
brings the shared engines up, seeds the main databases and writes each repo's `.env`. It
|
|
323
|
+
never touches `instance:`. A worktree then gets its own environment from that main
|
|
324
|
+
checkout with one command, run inside the worktree:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
haus workspace worktree env snapshot ecom # once, and whenever fresh data is wanted: <name>_tpl from the main DB
|
|
328
|
+
haus workspace worktree env up # lease ports, clone DBs, Redis index, prefix, site, .env files
|
|
329
|
+
haus workspace worktree env start vendure # runs on the worktree's own port
|
|
330
|
+
haus workspace worktree env status # ports, URLs, databases
|
|
331
|
+
haus workspace worktree env down --destroy # or just `worktree remove`, which runs it first
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`env up` copies each repo's `.env` from the main checkout (secrets included, never printed)
|
|
335
|
+
and replaces only the instance-owned keys, so the main checkout's `.env` must exist first:
|
|
336
|
+
that is exactly what this clone flow produces. Agents never read a `.env`; they use
|
|
337
|
+
`haus workspace worktree env get <key>` or `.haus-env.json`. For a WordPress site with
|
|
338
|
+
`config.kind: wp-config-override`, run `haus workspace worktree env patch-wp-config <site>`
|
|
339
|
+
once in the main checkout (it backs the file up and never prints it).
|
|
340
|
+
|
|
205
341
|
#### Run order
|
|
206
342
|
|
|
207
343
|
1. **Discover** `.haus-workflow/localdev.yml` in the workspace root and each repo.
|
|
@@ -429,7 +429,11 @@ Model runs from `utvärdera` live in a tree of their own and are never pricing i
|
|
|
429
429
|
|
|
430
430
|
1. **Source material.** A raw request pasted in, or a gid: then fetch the item via
|
|
431
431
|
request-sync (the main conversation) and use `## Ursprunglig förfrågan`
|
|
432
|
-
verbatim.
|
|
432
|
+
verbatim. For a fetched item, write the fetched `## Ursprunglig förfrågan` text, byte for
|
|
433
|
+
byte, as `<katalog>/onskemal.md` in every directory the spec is archived in (step 5),
|
|
434
|
+
the same file name the experiment channel uses: `korning.mjs --post` reads it into
|
|
435
|
+
draft mode's `URSPRUNGLIG FÖRFRÅGAN:` block, so `request-designer` sees the customer's
|
|
436
|
+
own words, and without it the block says `(saknas i arkivet)` (#745).
|
|
433
437
|
2. **Source manifest.** Isolated subagents cannot "look before they assume" without
|
|
434
438
|
help. Build a manifest before dispatch: relevant grep hits in the repo
|
|
435
439
|
(views/terms the request mentions), earlier specs for the same customer (the archive +
|
|
@@ -632,11 +636,13 @@ to the interview.
|
|
|
632
636
|
in the customer repo.
|
|
633
637
|
- Stop after the doc is written and self-reviewed. Do not invoke `writing-plans`:
|
|
634
638
|
the developer does that when the customer orders.
|
|
635
|
-
- Do not commit. The task stages the file and
|
|
639
|
+
- Do not commit. The task stages the file and handles the commit at its close.
|
|
636
640
|
```
|
|
637
641
|
|
|
638
642
|
A brainstorming session that ends without a file records nothing: `state.json` is
|
|
639
|
-
untouched and `designa` can be rerun.
|
|
643
|
+
untouched and `designa` can be rerun. The customer repo is the main checkout or any
|
|
644
|
+
linked worktree of the same repository, anywhere on disk (`add-design` accepts a doc
|
|
645
|
+
whose `git rev-parse --git-common-dir` is the main checkout's `.git`, #747).
|
|
640
646
|
|
|
641
647
|
5. **Record.** Format the doc with the repo's formatter first (`yarn prettier --write <sökväg>`
|
|
642
648
|
when the repo has prettier, otherwise nothing): a format-on-commit hook must not
|
|
@@ -648,8 +654,9 @@ to the interview.
|
|
|
648
654
|
byte-identically into the directory as `design.md`, writes `state.json.design` and
|
|
649
655
|
sets status `designed`. It writes no runs row (see the `state.json` contract). Then
|
|
650
656
|
run `node <skillkatalog>/scripts/precheck.mjs <katalog> --design` and show any
|
|
651
|
-
BRIST or VARNING lines. Stage the file (`git add <sökväg>`); never commit
|
|
652
|
-
house git rule: commit only on explicit OK)
|
|
657
|
+
BRIST or VARNING lines. Stage the file (`git add <sökväg>`); never commit in this step
|
|
658
|
+
(the house git rule: commit only on explicit OK): the commit is the design commit at
|
|
659
|
+
the close, step 6.
|
|
653
660
|
|
|
654
661
|
**A refused doc with conflicts** (exit 2 on `## Spec conflicts`): copy the refused
|
|
655
662
|
doc into the directory as `design-avvisad-<datum>.md` (`-2` on collision, never
|
|
@@ -669,7 +676,7 @@ to the interview.
|
|
|
669
676
|
item's state (status `priced` or `guard_run`, a standing stop other than a design
|
|
670
677
|
conflict) is not fixed in the doc: `respec`, or decide the stop first.
|
|
671
678
|
|
|
672
|
-
6. **Continue into `bedöm`, or close.** When `add-design` recorded the doc, `precheck --design` wrote no BRIST and `## Spec conflicts` is `none`, run `bedöm <adress>` now, in the same turn, from its step 1 (the gate is reread from the board; the design is internal, so nothing reaches the customer before `bedöm`'s own sync rule, ADR-0153): the turn then ends with `bedöm`'s BLUF and Closing, never with a second block. If `bedöm` stops before its step 8 (its gate, a candidate, a contradiction), its Closing still carries the design doc's path and the commit
|
|
679
|
+
6. **Continue into `bedöm`, or close.** **The design commit**, here and wherever this skill names it, means: commit the design doc and any tracked setup files not committed yet (the board connection and what `haus apply` wrote) per the user's commit rule. With a standing authorization (the user's own instructions let the agent commit and push without asking), commit them, push the branch within that rule's limits and report the commit; without one, it is the commit offer: name the files and ask. The request archive is never part of it: specs, analyses and scores stay in the local archive, which is kept out of git on purpose. When the turn continues into `bedöm`, the design commit runs after `bedom.mjs` has exited, never while it runs (`bedöm`: commit nothing in `--repo-rot` while the process runs). When `add-design` recorded the doc, `precheck --design` wrote no BRIST and `## Spec conflicts` is `none`, run `bedöm <adress>` now, in the same turn, from its step 1 (the gate is reread from the board; the design is internal, so nothing reaches the customer before `bedöm`'s own sync rule, ADR-0153): the turn then ends with `bedöm`'s BLUF and Closing, never with a second block. If `bedöm` stops before its step 8 (its gate, a candidate, a contradiction), its Closing still carries the design doc's path and the design commit. VARNING lines stop nothing: the chain runs. In every other case (a refused doc, a BRIST, an interview that ended without a file), **Reply BLUF:** one line per item (the design's title and path), the `## Spec conflicts` section verbatim when it is not `none`, the design commit, then **Close per `## Closing: where the item stands`** (the table from `archive.mjs nasta-steg`, the board line, the ISÄR check), never without. For a doc refused on conflicts, the Closing's part 4 presents the conflicts as the choice (`## Avgjorda val`, `respec`, `speca <adress>`).
|
|
673
680
|
|
|
674
681
|
## Task: `bedöm`
|
|
675
682
|
|
|
@@ -762,7 +769,7 @@ before the analysis (ADR-0156); the verdict is a judgment and is never redispatc
|
|
|
762
769
|
routes to `designa <adress>`, the interview. Commit nothing in `--repo-rot` while
|
|
763
770
|
the process runs: the impact row records the `HEAD` the assessor read (`repo_sha`).
|
|
764
771
|
|
|
765
|
-
3. **Analysis.** If `impact.md` is missing, or precheck rejects it on
|
|
772
|
+
3. **Analysis (machine step: `korning.mjs` runs steps 3 and 4 in one process).** If `impact.md` is missing, or precheck rejects it on
|
|
766
773
|
source-material provenance (the analysis was made against another spec or another design): this requires a repo in
|
|
767
774
|
the session (otherwise stop: a guessed analysis is forbidden by the skill itself).
|
|
768
775
|
Dispatch `request-assessor` per the prompt template: the spec's reviewer block
|
|
@@ -803,7 +810,7 @@ the process runs: the impact row records the `HEAD` the assessor read (`repo_sha
|
|
|
803
810
|
the rejected draft a receipt exactly as in speca step 6: archive it as
|
|
804
811
|
`impact-avvisad-<datum>.md` and stamp a runs row of its own with the check's
|
|
805
812
|
error lines in `--note` (ADR-0099).
|
|
806
|
-
4. **Pricing.** Dispatch `request-scorer` per the prompt template.
|
|
813
|
+
4. **Pricing (machine step, the same process as step 3).** Dispatch `request-scorer` per the prompt template.
|
|
807
814
|
No price list, no repo, no other items' documents in the prompt.
|
|
808
815
|
5. **Anchor resolution and contract check.** Write the agent's whole reply text (JSON
|
|
809
816
|
plus tail lines) to a file and run
|
|
@@ -884,7 +891,8 @@ the process runs: the impact row records the `HEAD` the assessor read (`repo_sha
|
|
|
884
891
|
quoting the source text where it decides. It was the evidence that decided the
|
|
885
892
|
ruling in every measured genuine disagreement. Never paste the diff rows:
|
|
886
893
|
the diff rows go into run 1's runs row `--note`, so `detaljer <item> state` shows
|
|
887
|
-
them.
|
|
894
|
+
them (`korning.mjs` writes them there too; an archive it wrote before #754 carries
|
|
895
|
+
them on run 2's row, so read both rows). Then refer to `döm <item>` and STOP: no rerun, no escalation, no choice
|
|
888
896
|
of run.
|
|
889
897
|
- **LIKA_TOLERANS** (a score difference ≤ 0.5 with the same outcome, regardless of
|
|
890
898
|
confidence, a confidence difference alone never rejects a pair within
|
|
@@ -949,7 +957,7 @@ ref-hash.mjs på request-score/references/poangskala.md>
|
|
|
949
957
|
handwritten. Under the table, one plain sentence per deviation
|
|
950
958
|
worth seeing (an escalation, a split, missing input, something the scorer
|
|
951
959
|
flagged), per `## Reply wording`. The JSON is not shown in the chat:
|
|
952
|
-
`detaljer <item>` does that. When `bedöm` was entered from `designa` (designa step 6), the BLUF also carries the design doc's path and the commit offer `designa` would have printed. Writing back to the board happens via request-sync
|
|
960
|
+
`detaljer <item>` does that. When `bedöm` was entered from `designa` (designa step 6), the BLUF also carries the design doc's path and the design commit (designa step 6), run now that `bedom.mjs` has exited: with a standing authorization, the files committed and pushed and the commit reported; otherwise the commit offer `designa` would have printed. Writing back to the board happens via request-sync
|
|
953
961
|
in the main conversation, only on explicit request, and it is that move
|
|
954
962
|
that takes the item ✅ Spec ok → ⚖️ Poängsättning redo för granskning per
|
|
955
963
|
the sync's rules. Then record the write: `node <skillkatalog>/scripts/archive.mjs synced <katalog> --offer-status "<the status request-sync set>"` (not on a split part, whose receipt is `set-split-gid`; on the split root, in the run that moves the parent). **Close per `## Closing: where the item stands`**
|
|
@@ -1066,6 +1074,22 @@ get wrong:
|
|
|
1066
1074
|
(🔍 Spec redo för granskning, ⚖️ Poängsättning redo för granskning). If
|
|
1067
1075
|
the item stands anywhere else: show the status and stop instead of guessing.
|
|
1068
1076
|
- **`godkänn <nr|gid>`:** 🔍 → ✅ Spec ok, and ⚖️ → 📤 Klar – till kund respectively.
|
|
1077
|
+
**After a 🔍 → ✅ Spec ok move** that request-sync reports done, on an item with an
|
|
1078
|
+
archive directory and no design yet (`state.json.design` is `null`), the next leg starts
|
|
1079
|
+
in the same turn, chosen by `design_mode` in `.haus-workflow/request.json` in the main
|
|
1080
|
+
checkout (ADR-0156):
|
|
1081
|
+
- `draft`: continue into `bedöm <item>` from its step 1, as designa step 6 does (the gate
|
|
1082
|
+
is reread from the board and recorded there, and `bedom.mjs` drafts and reviews the
|
|
1083
|
+
design). The turn ends with `bedöm`'s BLUF and Closing, once.
|
|
1084
|
+
- `interview`, or the key absent: start `designa <item>` from its step 1 and ask the
|
|
1085
|
+
brainstorming's first question in the same turn. The turn ends with that question; the
|
|
1086
|
+
Closing comes with the turn that ends `designa` (or the `bedöm` it continues into). A
|
|
1087
|
+
`designa` that stops before the brainstorming closes in this turn, per its step 6.
|
|
1088
|
+
- Either way, nothing here publishes a score: the ⚖️ sync still waits for `synka`
|
|
1089
|
+
(bedöm step 8). `godkänn 2 och 4` stays two calls: in draft mode each continues into
|
|
1090
|
+
its own `bedöm`, in interview mode only the first starts its interview in the turn and
|
|
1091
|
+
the other waits for `designa <item>`. The ⚖️ → 📤 move and the manual approval below
|
|
1092
|
+
continue into nothing.
|
|
1069
1093
|
- **`underkänn <nr|gid> <reason>`:** the reason is mandatory: without a reason, ask
|
|
1070
1094
|
for it. request-sync adds the reason as a comment on the item, raises
|
|
1071
1095
|
the iteration counter and moves the status backward per its backward-move rules.
|
|
@@ -1082,7 +1106,8 @@ get wrong:
|
|
|
1082
1106
|
- **Close per `## Closing: where the item stands`** when the item has an
|
|
1083
1107
|
archive directory: the table from `archive.mjs nasta-steg` plus the board line, so
|
|
1084
1108
|
the move is visible together with what it opens up for (`designa`, customer, nothing),
|
|
1085
|
-
and the wrap-up lines at a resting point.
|
|
1109
|
+
and the wrap-up lines at a resting point. A move that continues into `bedöm` or
|
|
1110
|
+
`designa` (above) leaves the block to the turn that ends the continued task.
|
|
1086
1111
|
|
|
1087
1112
|
## Task: `döm`
|
|
1088
1113
|
|
|
@@ -1187,7 +1212,8 @@ facts in the analysis above.
|
|
|
1187
1212
|
## Task: `detaljer`
|
|
1188
1213
|
|
|
1189
1214
|
`detaljer <gid|slug|nr> [del-N] [step]`: print archived files for the item: without
|
|
1190
|
-
a step, a list of what exists
|
|
1215
|
+
a step, a list of what exists (the listing names `onskemal.md`, the customer's original
|
|
1216
|
+
request from `speca` step 1, when present); with a step (`spec`, `granskarblock`, `design`,
|
|
1191
1217
|
`granskning`, `analys`, `poäng`, `state`) the file's content raw (`design` prints `design.md`,
|
|
1192
1218
|
`granskning` prints `design-review.md` when present);
|
|
1193
1219
|
`klass` reads older archives'
|
|
@@ -1707,7 +1733,7 @@ them failed an automatic check and was redone once; the redo agreed with the oth
|
|
|
1707
1733
|
## Closing: where the item stands
|
|
1708
1734
|
|
|
1709
1735
|
The tasks `speca`, `designa`, `bedöm`, `godkänn`, `underkänn`, `döm` and `byt titel` ALWAYS end
|
|
1710
|
-
with this block (#530). A `designa` that continues into `bedöm` (designa step 6) ends the turn with `bedöm`'s block, once: the block is per turn, never per task. A reply that ends without it leaves
|
|
1736
|
+
with this block (#530). A `designa` that continues into `bedöm` (designa step 6) ends the turn with `bedöm`'s block, once, and so does a `godkänn` that continues into `bedöm` in draft mode: the block is per turn, never per task. A `godkänn` that starts the `designa` interview ends its turn with the interview's first question, and the block comes with the turn that ends `designa` (or the `bedöm` it continues into). A reply that ends without it leaves
|
|
1711
1737
|
the user to guess where the item stands, what they should do and which command
|
|
1712
1738
|
runs on, and the archive and the board can then drift apart silently (`spec_draft`
|
|
1713
1739
|
locally, empty Offer status on the board). The block consists of five parts, in order:
|
|
@@ -1782,8 +1808,10 @@ locally, empty Offer status on the board). The block consists of five parts, in
|
|
|
1782
1808
|
terms or archive file names):
|
|
1783
1809
|
- Commit the design document and the setup files (the board connection and what
|
|
1784
1810
|
`haus apply` wrote), if they are not committed yet; in a temporary workspace, get
|
|
1785
|
-
them onto the main branch before the workspace is removed. This line
|
|
1786
|
-
|
|
1811
|
+
them onto the main branch before the workspace is removed. This line is the turn's
|
|
1812
|
+
design commit (designa step 6) and replaces a separate one: with a standing
|
|
1813
|
+
authorization, commit and push the files first and let the line report the commit
|
|
1814
|
+
(what went in, on which branch) instead of asking; without one, it stays the offer.
|
|
1787
1815
|
- Keep the specs, analyses and scores out of the customer's history: they stay in the
|
|
1788
1816
|
local request archive, which is kept out of git on purpose.
|
|
1789
1817
|
- When the customer orders, planning starts from the saved design document; if the
|
|
@@ -2031,14 +2059,17 @@ reference files whose rules it mirrors and WARNS when a catalog bump has changed
|
|
|
2031
2059
|
`bedom.mjs` takes `--design-mode draft|intervju` and passes it to every `korning.mjs` it
|
|
2032
2060
|
starts; without the flag the value is `request.json`'s `design_mode` (`interview` is read
|
|
2033
2061
|
as `intervju`), and without that key `intervju` (ADR-0156).
|
|
2034
|
-
- `scripts/korning.mjs --katalog <abs k<N>> --exp <abs experimentkatalog> --repo-rot <abs> --request-id <gid eller slug> --modell <modell|default> [--claude <kommando>] [--delar-parallellt 2] [--effort <low|medium|high|xhigh|max|default>] [--sprak <svenska|engelska>]`:
|
|
2062
|
+
- `scripts/korning.mjs --katalog <abs k<N>> --exp <abs experimentkatalog> --repo-rot <abs> --request-id <gid eller slug> --modell <modell|default> [--claude <kommando>] [--delar-parallellt 2] [--steg-timeout 1800000] [--effort <low|medium|high|xhigh|max|default>] [--sprak <svenska|engelska>]`:
|
|
2035
2063
|
utvärdera's inner process (see `utvärdera` step 4), a script without
|
|
2036
2064
|
model turns. In production (`bedom.mjs`) `--post <abs postkatalog>` replaces `--exp`
|
|
2037
2065
|
plus `--request-id` (giving `--request-id` with `--post` is a usage error, the id comes
|
|
2038
2066
|
from the item's `state.json`) and runs `bedöm` steps 3 to 7 on the item directory,
|
|
2039
2067
|
printing `STATUS: <priced|assessed|stopp|väntar på dom|ofärdig>` on stdout (ADR-0155).
|
|
2040
2068
|
`--startare <pid>` (production only, a usage error with `--exp`) is the starter's pid,
|
|
2041
|
-
checked every 30 s: a dead starter aborts the run like SIGTERM (exit 130).
|
|
2069
|
+
checked every 30 s: a dead starter aborts the run like SIGTERM (exit 130). A signal
|
|
2070
|
+
abort in production prints `STATUS: ofärdig` before it exits.
|
|
2071
|
+
`--steg-timeout <ms>` is the wall-clock cap on every agent step (default 1800000, 30
|
|
2072
|
+
minutes): a step that hangs without replying is killed and counts as a failed step.
|
|
2042
2073
|
`--sprak` overrides the language in both modes; in production it otherwise comes from
|
|
2043
2074
|
`SPRÅK:` in `kallmanifest.md`, and a missing language is exit 2. In utvärdera it builds
|
|
2044
2075
|
the prompts from `onskemal.md`, `kallmanifest.md` and `experiment.json`, where `sprak` is MANDATORY: if it is missing, an
|
|
@@ -2062,7 +2093,8 @@ reference files whose rules it mirrors and WARNS when a catalog bump has changed
|
|
|
2062
2093
|
with `add-design` (in utvärdera written as the part's `design.md` and checked with
|
|
2063
2094
|
`precheck --design-utkast`), then `request-design-reviewer`, whose reply is recorded with
|
|
2064
2095
|
`add-design-review` (in utvärdera copied as the part's `design-review.md`).
|
|
2065
|
-
`INTERVJU KRÄVS:` gives `stopp` with `intervju krävs: <reason>` and writes nothing
|
|
2096
|
+
`INTERVJU KRÄVS:` gives `stopp` with `intervju krävs: <reason>` and writes nothing (in
|
|
2097
|
+
production an empty reason is a script-proven breach, redispatched ONCE, then `ofärdig`), a
|
|
2066
2098
|
refusal on `## Spec conflicts` gives the receipt and the stop of `designa` step 5, any other
|
|
2067
2099
|
refusal, BRIST or symbol hit gives `ofärdig`, and `VERDICT: stopp` gives `stopp` with
|
|
2068
2100
|
`designgranskning: <first finding>`. A reviewer reply without a `VERDICT:` first line is a
|
|
@@ -1898,6 +1898,7 @@ if (cmd === 'add-run') {
|
|
|
1898
1898
|
const lockPath = acquireLock()
|
|
1899
1899
|
let skrivet = false
|
|
1900
1900
|
let ersatter = false
|
|
1901
|
+
let orsakIfylld = false
|
|
1901
1902
|
try {
|
|
1902
1903
|
const state = readStateOrThrow()
|
|
1903
1904
|
const befintlig = state.stopp ?? null
|
|
@@ -1914,7 +1915,7 @@ if (cmd === 'add-run') {
|
|
|
1914
1915
|
} else if (befintlig !== null) {
|
|
1915
1916
|
// Write-once på utfall+kalla; datumet och orsaken ingår inte, ett
|
|
1916
1917
|
// identiskt omanrop en annan dag (eller med en annan orsakstext) är en
|
|
1917
|
-
// no-op.
|
|
1918
|
+
// no-op, utom när stoppet saknar orsak (#753, nedan).
|
|
1918
1919
|
const begart = { utfall, kalla }
|
|
1919
1920
|
const skiljer = Object.keys(begart).filter((k) => befintlig[k] !== begart[k])
|
|
1920
1921
|
if (skiljer.length > 0) {
|
|
@@ -1923,6 +1924,15 @@ if (cmd === 'add-run') {
|
|
|
1923
1924
|
`från ${utfall} ur ${kalla} (${skiljer.join(', ')}); arkivet är append-only, kör respec för att nolla`,
|
|
1924
1925
|
)
|
|
1925
1926
|
}
|
|
1927
|
+
// #753: ett stopp skrivet utan orsak får den när omanropet bär en (ur
|
|
1928
|
+
// filen eller flaggan). Bara orsaken fylls i; utfall, källa och datum
|
|
1929
|
+
// står kvar, och ett stopp som redan har en orsak rörs aldrig.
|
|
1930
|
+
if (!befintlig.orsak && orsak) {
|
|
1931
|
+
state.stopp = { ...befintlig, orsak }
|
|
1932
|
+
writeStateOrThrow(state)
|
|
1933
|
+
skrivet = true
|
|
1934
|
+
orsakIfylld = true
|
|
1935
|
+
}
|
|
1926
1936
|
} else {
|
|
1927
1937
|
state.stopp = { utfall, kalla, date: today(), ...(orsak ? { orsak } : {}) }
|
|
1928
1938
|
// impact.md: posten gick genom bedöm och är analyserad. spec.md,
|
|
@@ -1945,9 +1955,11 @@ if (cmd === 'add-run') {
|
|
|
1945
1955
|
console.log(
|
|
1946
1956
|
ersatter
|
|
1947
1957
|
? `OK: stopp ${utfall} ur ${kalla} (ersätter designkonflikten ur impact.md)`
|
|
1948
|
-
:
|
|
1949
|
-
? `OK: stopp ${utfall} ur ${kalla}`
|
|
1950
|
-
:
|
|
1958
|
+
: orsakIfylld
|
|
1959
|
+
? `OK: stopp ${utfall} ur ${kalla} oförändrat, orsak ifylld`
|
|
1960
|
+
: skrivet
|
|
1961
|
+
? `OK: stopp ${utfall} ur ${kalla}`
|
|
1962
|
+
: `OK: stopp ${utfall} ur ${kalla} oförändrat`,
|
|
1951
1963
|
)
|
|
1952
1964
|
} else if (cmd === 'respec') {
|
|
1953
1965
|
// Speca steg 5 som kommando: en ny spec över befintlig katalog invaliderar
|
|
@@ -2255,6 +2267,27 @@ if (cmd === 'add-run') {
|
|
|
2255
2267
|
}
|
|
2256
2268
|
const pid = Number(pidTxt)
|
|
2257
2269
|
const katalogKorning = flag('katalog-korning')
|
|
2270
|
+
// #748: kvittots katalog är bedom.mjs:s körningskatalog, redan skapad.
|
|
2271
|
+
if (katalogKorning !== null) {
|
|
2272
|
+
if (!path.isAbsolute(katalogKorning)) {
|
|
2273
|
+
console.error(
|
|
2274
|
+
`korning-start: --katalog-korning måste vara en absolut sökväg (${katalogKorning})`,
|
|
2275
|
+
)
|
|
2276
|
+
process.exit(2)
|
|
2277
|
+
}
|
|
2278
|
+
let arKatalog = false
|
|
2279
|
+
try {
|
|
2280
|
+
arKatalog = statSync(katalogKorning).isDirectory()
|
|
2281
|
+
} catch {
|
|
2282
|
+
arKatalog = false
|
|
2283
|
+
}
|
|
2284
|
+
if (!arKatalog) {
|
|
2285
|
+
console.error(
|
|
2286
|
+
`korning-start: --katalog-korning är ingen katalog som finns (${katalogKorning}); skapa körningskatalogen först`,
|
|
2287
|
+
)
|
|
2288
|
+
process.exit(2)
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2258
2291
|
const lockPath = acquireLock()
|
|
2259
2292
|
let varning = null
|
|
2260
2293
|
try {
|
|
@@ -2290,7 +2323,8 @@ if (cmd === 'add-run') {
|
|
|
2290
2323
|
try {
|
|
2291
2324
|
const state = readStateOrThrow()
|
|
2292
2325
|
if (state.experiment === true) throw new ArkivFel('korning-slut vägrar: experimentkatalog')
|
|
2293
|
-
|
|
2326
|
+
// #748: inget kvitto (null eller frånvarande) är ingen skrivning.
|
|
2327
|
+
if (state.korning != null) writeStateOrThrow({ ...state, korning: null })
|
|
2294
2328
|
} catch (e) {
|
|
2295
2329
|
releaseLock(lockPath)
|
|
2296
2330
|
if (e instanceof ArkivFel) {
|
|
@@ -2665,6 +2699,39 @@ function repoRotFor(d) {
|
|
|
2665
2699
|
* en analys lades undan och varningarna (ADR-0156 beslut 4: utkastsektioner
|
|
2666
2700
|
* på ett intervjudokument, en symbolkontroll som inte kunde köras), som
|
|
2667
2701
|
* anroparen skriver på stderr. */
|
|
2702
|
+
/** #747: katalogens git-common-dir som realpath, eller null när den inte
|
|
2703
|
+
* kan prövas (ingen git, inget repo). Miljöns GIT_DIR, GIT_WORK_TREE och
|
|
2704
|
+
* GIT_COMMON_DIR tas bort: inne i en git-hook pekar de på hookens repo, och
|
|
2705
|
+
* svaret ska gälla katalogen, aldrig det repot (granskning Minor 1). */
|
|
2706
|
+
function gitCommonDirReal(dir) {
|
|
2707
|
+
const env = { ...process.env }
|
|
2708
|
+
for (const k of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']) delete env[k]
|
|
2709
|
+
const r = spawnSync('git', ['-C', dir, 'rev-parse', '--git-common-dir'], {
|
|
2710
|
+
encoding: 'utf8',
|
|
2711
|
+
timeout: 5000,
|
|
2712
|
+
env,
|
|
2713
|
+
})
|
|
2714
|
+
if (r.error || r.status !== 0) return null
|
|
2715
|
+
const ut = r.stdout.trim()
|
|
2716
|
+
if (ut === '') return null
|
|
2717
|
+
try {
|
|
2718
|
+
return realpathSync(path.resolve(dir, ut))
|
|
2719
|
+
} catch {
|
|
2720
|
+
return null
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/** #747: ligger filen i en länkad worktree av samma repo, var som helst på
|
|
2725
|
+
* disken? Sant när filens katalog och huvudutcheckningen har samma
|
|
2726
|
+
* git-common-dir, löst likadant på båda sidor, så en `.git` som är en gitfil
|
|
2727
|
+
* (en submodul, --separate-git-dir) också fungerar (granskning Minor 2).
|
|
2728
|
+
* Allt som inte kan prövas är falskt: vägran står kvar. */
|
|
2729
|
+
function iLankadWorktree(real, rotReal) {
|
|
2730
|
+
const dok = gitCommonDirReal(path.dirname(real))
|
|
2731
|
+
if (dok === null) return false
|
|
2732
|
+
return dok === gitCommonDirReal(rotReal)
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2668
2735
|
function laggTillDesign(state, fil) {
|
|
2669
2736
|
const rot = repoRotFor(katalog)
|
|
2670
2737
|
if (rot === null)
|
|
@@ -2678,7 +2745,7 @@ function laggTillDesign(state, fil) {
|
|
|
2678
2745
|
throw new ArkivFel(`kan inte läsa ${fil}: ${e.message}`)
|
|
2679
2746
|
}
|
|
2680
2747
|
const rotReal = realpathSync(rot)
|
|
2681
|
-
if (!real.startsWith(rotReal + path.sep))
|
|
2748
|
+
if (!real.startsWith(rotReal + path.sep) && !iLankadWorktree(real, rotReal))
|
|
2682
2749
|
throw new ArkivFel(`${real} ligger utanför repo-roten ${rotReal}`)
|
|
2683
2750
|
// Slutgranskning M3: arkivet är aldrig designdokumentets hem. Arkivets egen
|
|
2684
2751
|
// design.md som --file hade lagts undan av laggUndan innan kopian, som då
|
|
@@ -2955,7 +3022,13 @@ function delKlar(state, d) {
|
|
|
2955
3022
|
function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null }) {
|
|
2956
3023
|
const status = state.status
|
|
2957
3024
|
const gid = state.request_id ?? '<gid>'
|
|
2958
|
-
|
|
3025
|
+
// #752: en produktionsdel (split.parent satt, request_id null) nås som
|
|
3026
|
+
// <förälderns gid>/del-N; mappnamnet ensamt löser inte. Experiment- och
|
|
3027
|
+
// buntdelar bär sitt eget id och behåller sin form.
|
|
3028
|
+
const produktionsdel =
|
|
3029
|
+
state.request_id == null && state.split?.parent != null && state.experiment !== true
|
|
3030
|
+
const delAdress = produktionsdel ? `${state.split.parent}/${path.basename(post)}` : null
|
|
3031
|
+
const adress = state.request_id ?? delAdress ?? post
|
|
2959
3032
|
const stopp = state.stopp ?? null
|
|
2960
3033
|
const runs = state.runs ?? []
|
|
2961
3034
|
const nScore = maxN(runs, 'score')
|
|
@@ -3017,7 +3090,7 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
3017
3090
|
if (antalDelar > 0) return rad('se delarna', 'se delarna', `bunt (${antalDelar} delar)`)
|
|
3018
3091
|
if (korningLever)
|
|
3019
3092
|
return rad('processen', 'vänta; bedöm igen när bedom.mjs är klar', `pågår (pid ${korningPid})`)
|
|
3020
|
-
if (vantarDom) return rad('dom (döm)', `döm ${post}`)
|
|
3093
|
+
if (vantarDom) return rad('dom (döm)', `döm ${delAdress ?? post}`)
|
|
3021
3094
|
if (stopp) {
|
|
3022
3095
|
const val = stopp.orsak ?? stopp.kalla
|
|
3023
3096
|
if (stopp.utfall === 'KONCEPT' || stopp.utfall === 'DIAGNOS') return kunddialog()
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* matning.json med signalen, rensar varje kvitto med korning-slut och avslutar
|
|
45
45
|
* med 130. Ett oväntat kast i huvudloopen skrivs på stderr och ger exit 2.
|
|
46
46
|
*/
|
|
47
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync,
|
|
47
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, realpathSync } from 'node:fs'
|
|
48
48
|
import { spawnSync } from 'node:child_process'
|
|
49
49
|
import path from 'node:path'
|
|
50
50
|
import { fileURLToPath } from 'node:url'
|
|
@@ -408,12 +408,12 @@ async function korPost(post, ctx) {
|
|
|
408
408
|
const runDir = korningsKatalog(dir, ctx.now ? ctx.now() : new Date())
|
|
409
409
|
const start = startaKorning(dir, ctx.pid ?? process.pid, runDir, { arkivKor })
|
|
410
410
|
if (start.pagar) {
|
|
411
|
-
|
|
411
|
+
rmSync(runDir, { recursive: true, force: false })
|
|
412
412
|
ut(`PÅGÅR ${post} (pid ${start.pid})`)
|
|
413
413
|
return rad('pågår', { orsak: `pid ${start.pid}` })
|
|
414
414
|
}
|
|
415
415
|
if (start.fel) {
|
|
416
|
-
|
|
416
|
+
rmSync(runDir, { recursive: true, force: false })
|
|
417
417
|
ut(`KLAR ${post} ofärdig`)
|
|
418
418
|
return rad('ofärdig', { orsak: start.fel })
|
|
419
419
|
}
|