@techgoblin/gobstack 0.4.4-beta.7 → 0.5.0-beta.1

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/README.md CHANGED
@@ -28,7 +28,7 @@ dependencies disagree with npm's. If you see `ERESOLVE` after a local install, r
28
28
  dependency from `package.json` and install globally instead.
29
29
 
30
30
  **The two-layer model.** The global install gives you the CLI only. `gob init` (or
31
- `gob install --target <dir> --class A`) then vendors a self-contained engine into the target
31
+ `gob install --target <dir> --class software`) then vendors a self-contained engine into the target
32
32
  repo under `.goblin/` — verifier, manifest, ban probes, skills, all of it. That second layer is
33
33
  why an initialized repo keeps working on machines with **no gobstack installed at all**: the
34
34
  engine lives in the repo, not in your `node_modules`, and `bash .goblin/bin/goblin-verify` (or a
@@ -36,7 +36,7 @@ plain `git` + `bash` box) is the only runtime the repo's gate needs.
36
36
 
37
37
  Then, from any project:
38
38
 
39
- gob install --target /path/to/repo --class A
39
+ gob install --target /path/to/repo --class software
40
40
 
41
41
  The installer writes only paths it records, hash-compares before writing, and prints `no-op` on a
42
42
  second run with the same arguments. It never overwrites `HANDOFF.md`, `AGENTS.md`, a `*-SPEC.md`,
@@ -46,18 +46,27 @@ of file it manages: `docs/CONTRACTS.md`.
46
46
  A repo that already has its own `HANDOFF.md` exits 1 on the refusal. That is the contract, not a
47
47
  failure: reconcile the file rather than forcing over it — `docs/ADOPTION.md`.
48
48
 
49
+ `--class` picks the preset: **software** (the default — shipped features, PRs, review gates),
50
+ **service** (backend jobs, config, unattended runs), **game** (playable builds, perf budgets),
51
+ **research** (specs, replays, reference corpora) and **fleet** (config-of-the-agent repos).
52
+ The single letters `A`–`E` are accepted aliases. What each preset turns on is the matrix in
53
+ `docs/ADOPTION.md`; the older names still resolve (see `docs/CONTRACTS.md`).
54
+
49
55
  After installing, in this order:
50
56
 
51
57
  cd <target> && git add -A && git commit # the install is a change like any other
52
58
  gob verify # or .goblin/bin/goblin-verify, inside the target
53
- hermes skills trust <target> # one-time, Hermes users, so project-tier skills load
59
+ gob emit --platform <p> # optional, per platform: the agent skills are an opt-in
54
60
  gob audit # once, deliberately: the ONLY network step (SC-07)
55
61
 
56
- **A class-A install verifies green — `43 passed, 0 failed, 11 advisory, 28 skipped`, exit 0 — once
62
+ **A default software-class install (no agent skills — those are `gob emit`'s job) verifies green —
63
+ `38 passed, 0 failed, 11 advisory, 33 skipped`, exit 0 — once
57
64
  `HANDOFF.md` names a commit that exists. Before that edit the scaffold's `0000000` placeholder is
58
- the one expected red: `42 passed, 1 failed`, `HP-05`. Both numbers measured 2026-09-25; the run and
59
- the fix are step 2 of `docs/GUIDE.md`.**
60
- Twenty-eight rows skip (`HS-02` has no pinned pre-change commit yet, so the REPLAY is not provable;
65
+ the one expected red: `37 passed, 1 failed`, `HP-05`. Both numbers measured at W6 (neutral-first);
66
+ the run and the fix are step 2 of `docs/GUIDE.md`.**
67
+ Thirty-three rows skip — the five skill rows (`SK-01`..`SK-04`, `AU-04`) skip on the `playbooks`
68
+ opt-out a skills-free install records, then the not-yet rows (`HS-02` has no pinned pre-change
69
+ commit yet, so the REPLAY is not provable;
61
70
  `AU-02` and `AU-03` have no report to audit; `SC-06`, `SC-07` and `SC-08` have no dependency
62
71
  manifest, no lockfile and no audit record to read; `PF-01` has no measured perf baseline;
63
72
  `BN-01`/`BN-02`/`BN-05` have no `src/` for a ban to read, and `BN-03` plus the four electron bans
@@ -83,12 +92,12 @@ The measurement and the vacuous-pass reading are in `docs/CONTRACTS.md`.
83
92
  | `gob verify` | run the rule matrix against the current repo — `PASS`/`FAIL`/`SKIP` per row, exit 0 pass · 1 a check failed · 2 could not run · 3 the manifest is broken |
84
93
  | `gob bans` | run the ban list (per-pattern red lines over the source tree) |
85
94
  | `gob audit` | check recorded dependency claims against live advisory feeds — the only command that touches the network |
86
- | `gob install` | install the manifest, skills and verifier into a target repo |
95
+ | `gob install` | install the harness into a target repo: manifest, verifier, gates, HANDOFF — no agent skills (those are an opt-in: `gob emit --platform <p>`, or `--skills yes`) |
87
96
  | `gob uninstall` | remove everything an install wrote, byte-exactly (`gob install --target <dir> --uninstall` is the same job) |
88
97
  | `gob upgrade` | migrate a repo to the shared global engine at `~/.goblin/engine` — 8 steps, two commits, one report |
89
98
  | `gob doctor` | one run across the platforms below: DETECTED / NOT-DETECTED / DRIFT per platform |
90
99
  | `gob emit` | write the skills + context block for one platform (`--scope project` or `global`); `--unshadow` removes a hermes project skill whose hash equals the source |
91
- | `gob init` | the first-run wizard: detect → class → branch/email → first gate → emit → verify, one screen per question; every question has a flag (`--class app --branch main --email a@b.c --gate 'cmd' --emit hermes`), so CI runs it with zero prompts; `--dry-run` prints the plan and writes nothing |
100
+ | `gob init` | the first-run wizard: detect → class → branch/email → first gate → emit → verify, one screen per question; every question has a flag (`--class software --branch main --email a@b.c --gate 'cmd' --emit hermes`), so CI runs it with zero prompts; `--dry-run` prints the plan and writes nothing |
92
101
 
93
102
  `goblin` remains as a legacy alias for every command above — existing scripts keep working, but
94
103
  new commands and docs use `gob`.
@@ -111,6 +120,13 @@ One run of `gob emit --platform <p> --scope project` writes the skills and the c
111
120
  session of that platform reads; `--scope global` writes to the machine-level anchor. `--dry-run`
112
121
  prints the full write plan first.
113
122
 
123
+ **Agent skills are opt-in.** A `gob install` writes the neutral harness only — `.goblin/`,
124
+ `HANDOFF.md`, `AGENTS.md`, the checks and the `.gitignore` block; no skills directory, and no
125
+ files belonging to any coding agent. The guided path is `gob init`'s emit screen; the one-shot
126
+ path is `gob emit --platform <p>` after installing. Repos whose install predates the opt-in
127
+ default keep their skills through `gob upgrade` (the install record names them; only an explicit
128
+ `--skills no`, or `--uninstall`, removes them).
129
+
114
130
  ## What it is not
115
131
 
116
132
  Not a rules document (every rule carries a runnable check or is explicitly counted as
@@ -135,9 +151,9 @@ below is the reference material the guide points into, so the two do not compete
135
151
  | `docs/CONTRACTS.md` | the installer/verifier interface, exit codes, idempotency, uninstall |
136
152
  | `docs/INTEGRATION.md` | the board, cron, the skills precedence order, the referenced standard |
137
153
  | `docs/RISKS.md` | the risk register, the advisory rows named, the non-goals |
138
- | `docs/CI.md` | the CI lane: what makes a workflow a gate, the four settings a repository cannot set, and the desktop-shell class |
154
+ | `docs/CI.md` | the CI lane: what makes a workflow a gate, the four settings a repository cannot set, and the electron opt-in |
139
155
  | `docs/LOOP.md` | the judge role and the loop contract: what a goal-mode loop actually does, the record, and what neither can see |
140
- | `docs/ADOPTION.md` | the six classes, the preset matrix, the adoption order |
156
+ | `docs/ADOPTION.md` | the five classes, the preset matrix, the adoption order |
141
157
  | `docs/LIMITS.md` | where this is weaker than its sources, and what is unproven |
142
158
 
143
159
  `manifest/enforcement.tsv` is the source of truth for rules; `manifest/classes.tsv` for what a
@@ -156,7 +172,7 @@ run · `3` the manifest is broken. Every run prints what it cannot see.
156
172
  bash tests/run-tests.sh
157
173
 
158
174
  Runs the source-scope rules (PR-01..PR-05) and the test scripts, including `t-verify-red.sh` —
159
- one control per target-scope row (167 over 82 target rows), each required to go RED and then
175
+ one control per target-scope row (168 over 82 target rows), each required to go RED and then
160
176
  restored, plus `t-audit.sh` for the SC-07 producer. **A verifier that only ever prints GREEN is a
161
177
  failure**, so that file is the one that matters most.
162
178
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.4
1
+ 0.5.0
package/bin/goblin-audit CHANGED
@@ -23,7 +23,7 @@
23
23
  # because an empty record reads to SC-07 as "clean" and that would be a fabricated pass
24
24
  set -uo pipefail
25
25
 
26
- GOBLIN_AUDIT_VERSION="0.4.4"
26
+ GOBLIN_AUDIT_VERSION="0.5.0"
27
27
 
28
28
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
29
29
 
package/bin/goblin-bans CHANGED
@@ -28,7 +28,7 @@
28
28
 
29
29
  set -uo pipefail
30
30
 
31
- GOBLIN_BANS_VERSION="0.4.4"
31
+ GOBLIN_BANS_VERSION="0.5.0"
32
32
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
33
33
  # shellcheck source=goblin-lib.sh
34
34
  . "$SELF_DIR/goblin-lib.sh"
@@ -88,6 +88,14 @@ if [ -f "$CONFIG" ]; then
88
88
  ENABLED=$(g_yaml_scalar "$CONFIG" bans)
89
89
  ENABLED=${ENABLED#[}; ENABLED=${ENABLED%]}
90
90
  ENABLED=$(printf '%s' "$ENABLED" | tr ',' ' ' | tr -s ' ' ' ')
91
+ # The electron opt-in (software class): `electron: true` turns the electron ban set ON even
92
+ # when a hand-edited bans: list omits it — the declaration is the contract, and the engine
93
+ # (not the LLM) is what makes a listed ban non-skippable. A repo without the key is unaffected.
94
+ if [ "$(g_yaml_scalar "$CONFIG" electron)" = "true" ]; then
95
+ for e in BN-06 BN-07 BN-08 BN-09; do
96
+ case " $ENABLED " in *" $e "*) ;; *) ENABLED="$ENABLED $e" ;; esac
97
+ done
98
+ fi
91
99
  fi
92
100
  enabled() {
93
101
  [ -z "$ENABLED" ] && return 1
package/bin/goblin-init CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # goblin-init — the W6 first-run wizard (`gob init`).
3
3
  #
4
- # gob init [--target <dir>] [--class <app|service|game|research|agent|desktop|A..F>]
5
- # [--branch <name>] [--email <addr>] [--gate <cmd>]
4
+ # gob init [--target <dir>] [--class <software|service|game|research|fleet|A..E|app|agent|desktop>]
5
+ # [--electron] [--branch <name>] [--email <addr>] [--gate <cmd>]
6
6
  # [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
7
7
  #
8
8
  # One screen per question, answered steps collapsing into the ✔/◆/○ rail above. On a tty
@@ -143,6 +143,7 @@ ask_value() {
143
143
 
144
144
  # ------------------------------------------------------------------ flags -----
145
145
  TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
146
+ ELECTRON=0
146
147
  YES=0; DRYRUN=0
147
148
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
148
149
 
@@ -150,6 +151,7 @@ while [ $# -gt 0 ]; do
150
151
  case "$1" in
151
152
  --target) TARGET="${2:-}"; shift 2 ;;
152
153
  --class) CLASS="${2:-}"; shift 2 ;;
154
+ --electron) ELECTRON=1; shift ;;
153
155
  --branch) BRANCH="${2:-}"; shift 2 ;;
154
156
  --email) EMAIL="${2:-}"; shift 2 ;;
155
157
  --gate) GATE="${2:-}"; shift 2 ;;
@@ -255,45 +257,45 @@ if [ -z "$CLASS" ]; then
255
257
  if [ "$TTY_IN" -eq 1 ]; then
256
258
  rail_say ""
257
259
  rail_say " ${C_MUTED}what kind of work does this repo do?$C_RESET"
258
- rail_say " $C_TEXT 1 ▸ app shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
260
+ rail_say " $C_TEXT 1 ▸ software shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
259
261
  rail_say " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
260
262
  rail_say " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
261
263
  rail_say " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
262
- rail_say " $C_TEXT 5 agent fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
263
- rail_say " $C_TEXT 6 desktop the shell you live in, perf budget ${C_DIM}(class F)$C_RESET"
264
- rail_say " ${C_MUTED}↑ number + Enter · Enter = app · the class letter works too (A-F)$C_RESET"
264
+ rail_say " $C_TEXT 5 fleet fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
265
+ rail_say " ${C_MUTED}↑ number + Enter · Enter = software · a class letter also works (A-E)$C_RESET"
266
+ rail_say " ${C_DIM}an Electron desktop shell: pick software, then --electron$C_RESET"
265
267
  printf ' '
266
268
  read -r REPLY_CLASS
267
269
  ASKED_ANY=1
268
270
  rail_say ""
269
271
  else
270
272
  REPLY_CLASS="1"
271
- cascade "class: app (default; --class app|service|game|research|agent|desktop or A-F)"
273
+ cascade "class: software (default; --class software|service|game|research|fleet or A-E)"
272
274
  fi
273
275
  case "$REPLY_CLASS" in
274
- 1) CLASS="app" ;; 2) CLASS="service" ;; 3) CLASS="game" ;;
275
- 4) CLASS="research" ;; 5) CLASS="agent" ;; 6) CLASS="desktop" ;;
276
- *) CLASS="${REPLY_CLASS:-app}" ;;
276
+ 1) CLASS="software" ;; 2) CLASS="service" ;; 3) CLASS="game" ;;
277
+ 4) CLASS="research" ;; 5) CLASS="fleet" ;;
278
+ *) CLASS="${REPLY_CLASS:-software}" ;;
277
279
  esac
278
280
  fi
279
- # Normalise every accepted spelling to the letter goblin-install requires. Domain names,
280
- # the class letter, and a lowercase letter all resolve; anything else is refused here,
281
- # with the full enum, rather than deep inside the installer.
282
- case "$CLASS" in
283
- app|A|a) CLASS="A" ;;
284
- service|B|b) CLASS="B" ;;
285
- game|C|c) CLASS="C" ;;
286
- research|D|d) CLASS="D" ;;
287
- agent|E|e) CLASS="E" ;;
288
- desktop|F|f) CLASS="F" ;;
289
- *)
290
- g_err "--class must be app|service|game|research|agent|desktop (or A-F), got '$CLASS'"
291
- exit 2 ;;
281
+ # Resolve every accepted spelling to the canonical class NAME. The letters A-E and the old
282
+ # taught names (app/agent/desktop) are read-time aliases; anything else is refused here, with
283
+ # the full enum, rather than deep inside the installer. `desktop`/F means the OLD desktop
284
+ # install, so it also sets the electron opt-in.
285
+ g_class_is_electron_alias "$CLASS" && ELECTRON=1
286
+ CLASS_RAW="$CLASS"
287
+ CLASS=$(g_class_canon "$CLASS") \
288
+ || { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
289
+ case "$CLASS_RAW" in
290
+ app|agent|desktop) g_info "note: '$CLASS_RAW' is now called '$CLASS' (accepted as an alias)";;
292
291
  esac
293
- CLASS_NAME=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
294
- STEP_VAL[1]="$C_TEXT$CLASS ($([ -n "$CLASS_NAME" ] && basename "$CLASS_NAME" .yaml | cut -d- -f2-))$C_RESET"
292
+ if [ "$ELECTRON" -eq 1 ]; then
293
+ g_info "electron: on (the desktop opt-in — the app_bundle_bytes ratchet, BN-06..09 bans, a host gate)"
294
+ fi
295
+ CLASS_NAME="$SRC/presets/$CLASS.yaml"
296
+ STEP_VAL[1]="$C_TEXT$CLASS$C_RESET"
295
297
  if [ "$TTY_OUT" -eq 1 ]; then
296
- rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$([ -n "$CLASS_NAME" ] && printf ' — %s' "$(basename "$CLASS_NAME" .yaml | cut -d- -f2-)")$C_RESET"
298
+ rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$C_RESET"
297
299
  else
298
300
  cascade "class: $CLASS"
299
301
  fi
@@ -375,29 +377,37 @@ if [ -n "$EMIT" ]; then
375
377
  [ -n "$ok" ] || { g_err "--emit: unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
376
378
  done
377
379
  else
380
+ # neutral-first: a flags-only run with no --emit emits NOTHING. The detected rows are
381
+ # shown on the emit screen as pre-ticked SUGGESTIONS for the interactive path; in
382
+ # non-interactive mode silence means the neutral harness only.
378
383
  EMIT_SELECTED=""
379
- while IFS=$'\t' read -r p _proj _glob _hit; do
380
- [ -n "$p" ] || continue
381
- EMIT_SELECTED="$EMIT_SELECTED $p"
382
- done <<< "$DET_ROWS"
384
+ if [ "$YES" -eq 0 ] && [ "$TTY_IN" -eq 1 ]; then
385
+ while IFS=$'\t' read -r p _proj _glob _hit; do
386
+ [ -n "$p" ] || continue
387
+ EMIT_SELECTED="$EMIT_SELECTED $p"
388
+ done <<< "$DET_ROWS"
389
+ fi
383
390
  fi
384
391
  EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
385
392
 
386
393
  if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
387
394
  rail_say ""
388
- rail_say " ${C_MUTED}emit now — the skills go into:$C_RESET"
395
+ rail_say " ${C_MUTED}agent skills are opt-in, per platform — emit them into:$C_RESET"
389
396
  for p in $PLATFORMS; do
390
397
  hit=""
391
398
  while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
392
399
  [ -n "$hit" ] || continue
393
400
  rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
394
401
  done
395
- rail_say " ${C_MUTED}Enter = these; or type platform names to override$C_RESET"
402
+ rail_say " ${C_MUTED}the install itself writes none — the harness stays neutral$C_RESET"
403
+ rail_say " ${C_MUTED}Enter = these; or type platform names to override (none: type x)$C_RESET"
396
404
  printf ' '
397
405
  read -r REPLY_EMIT
398
406
  ASKED_ANY=1
399
407
  rail_say ""
400
- if [ -n "$REPLY_EMIT" ]; then
408
+ if [ "$REPLY_EMIT" = "x" ] || [ "$REPLY_EMIT" = "none" ]; then
409
+ EMIT_SELECTED=""
410
+ elif [ -n "$REPLY_EMIT" ]; then
401
411
  EMIT_SELECTED=""
402
412
  for w in $REPLY_EMIT; do
403
413
  ok=""
@@ -452,11 +462,15 @@ fi
452
462
  # never rewinds past its own buffer by counting lines it did not print.
453
463
  if [ "$TTY_OUT" -eq 1 ]; then
454
464
  rail_note ""
455
- rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS${C_RESET}"
465
+ rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS$([ "$ELECTRON" -eq 1 ] && printf ' --electron')${C_RESET}"
456
466
  else
457
- printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
467
+ printf 'gob init [run] goblin-install --target %s --class %s%s\n' "$TARGET" "$CLASS" "$([ "$ELECTRON" -eq 1 ] && printf ' --electron')"
468
+ fi
469
+ if [ "$ELECTRON" -eq 1 ]; then
470
+ bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --electron --skills no --yes
471
+ else
472
+ bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --skills no --yes
458
473
  fi
459
- bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
460
474
  EXIT_INSTALL=$?
461
475
  if [ "$EXIT_INSTALL" -ne 0 ]; then
462
476
  g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
@@ -546,7 +560,11 @@ if [ "$DRYRUN" -eq 0 ]; then
546
560
  printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
547
561
  printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
548
562
  printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
549
- printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
563
+ if [ "$EMIT_N" -gt 0 ]; then
564
+ printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
565
+ else
566
+ printf ' 4. %sagent skills are opt-in per platform: gob emit --platform <p>%s\n' "$C_TEXT" "$C_RESET"
567
+ fi
550
568
  printf '\n'
551
569
  if [ "$VERIFY_RC" -eq 0 ]; then
552
570
  printf ' %s▙ the goblin sees you. keep the gate green.%s\n' "$C_ACCENT" "$C_RESET"
@@ -3,12 +3,17 @@
3
3
  #
4
4
  # Usage: gob install --target <dir> [options] (script: bin/goblin-install)
5
5
  # --target <dir> required; the repo root to install into
6
- # --class A|B|C|D|E|F required unless --uninstall or --re-pin; anything else needs an explicit class
6
+ # --class <name> required unless --uninstall or --re-pin; one of
7
+ # software|service|game|research|fleet, or the aliases A-E / app / agent
8
+ # / desktop (desktop/F => software + --electron); anything else is refused
9
+ # --electron the electron opt-in overlay over the software class (electron: true):
10
+ # the app_bundle_bytes ratchet, BN-06..09 bans, the host-gate declaration
7
11
  # --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
8
12
  # --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md); a named path that is absent is reported, never silently dropped
9
13
  # --parts <list> comma list to install; default = every part the class requires
10
14
  # --archive mark the project archive: verify requires no HANDOFF and no gates
11
- # --skills yes|no install .hermes/skills (default yes; needs the one-time skills trust step)
15
+ # --skills yes|no install agent skills under .hermes/skills (default no; opt in per
16
+ # platform with: gob emit --platform <p>)
12
17
  # --dry-run print the plan; write nothing
13
18
  # --upgrade re-install at the current version; report created/updated/unchanged/skipped
14
19
  # --opt-out <part> record the part in disabled: so its required checks are skipped
@@ -22,7 +27,7 @@
22
27
 
23
28
  set -uo pipefail
24
29
 
25
- GOBLIN_INSTALL_VERSION="0.4.4"
30
+ GOBLIN_INSTALL_VERSION="0.5.0"
26
31
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
27
32
  SRC=$(cd "$SELF_DIR/.." && pwd)
28
33
  # shellcheck source=goblin-lib.sh
@@ -40,13 +45,26 @@ PRACTICE="${GOBLIN_PRACTICE:-$HOME/projects/PROJECT-PRACTICE.md}"
40
45
  PRACTICE_ARG=""
41
46
  PARTS=""
42
47
  ARCHIVE="false"
43
- SKILLS="yes"
48
+ # W6 neutral-first: a default install is a NEUTRAL harness (manifest + verify + HANDOFF +
49
+ # .goblin) with NO agent skills. Agent skills are an explicit opt-in (--skills yes, or the
50
+ # per-platform gob emit path the wizard's emit screen drives). The old default (yes) was the
51
+ # house's own setup, not a product default: a non-agent user got a dead .hermes/ tree, and a
52
+ # user of another agent harness got nothing at all.
53
+ SKILLS="no"
54
+ # Whether --skills was named on the command line: an OMITTED flag on a re-install means
55
+ # "whatever the record says" (the migration contract below); a NAMED --skills no is an
56
+ # explicit opt-out and is honoured verbatim.
57
+ SKILLS_NAMED=0
44
58
  DRY_RUN=0
45
59
  UPGRADE=0
46
60
  UNINSTALL=0
47
61
  RE_PIN=0
48
62
  FORCE=0
49
63
  OPT_OUT=""
64
+ # The electron opt-in (the merged desktop/F class): renders presets/electron-overlay.yaml over
65
+ # the software preset. Auto-set when --class resolves via the desktop/F/f alias, so the alias
66
+ # behaves as the old class F did rather than silently installing weaker software.
67
+ ELECTRON=0
50
68
 
51
69
  usage() {
52
70
  # The header comment runs from line 2 to the first blank line, so adding an option cannot
@@ -63,7 +81,8 @@ while [ $# -gt 0 ]; do
63
81
  --practice) PRACTICE="${2:-}"; PRACTICE_ARG="$PRACTICE"; shift 2 ;;
64
82
  --parts) PARTS="${2:-}"; shift 2 ;;
65
83
  --archive) ARCHIVE="true"; shift ;;
66
- --skills) SKILLS="${2:-}"; shift 2 ;;
84
+ --electron) ELECTRON=1; shift ;;
85
+ --skills) SKILLS="${2:-}"; SKILLS_NAMED=1; shift 2 ;;
67
86
  --dry-run) DRY_RUN=1; shift ;;
68
87
  --upgrade) UPGRADE=1; shift ;;
69
88
  --uninstall) UNINSTALL=1; shift ;;
@@ -229,14 +248,48 @@ if [ "$RE_PIN" -eq 1 ]; then
229
248
  exit 0
230
249
  fi
231
250
 
232
- [ -n "$CLASS" ] || { g_err "--class is required (A|B|C|D|E|F)"; exit 2; }
233
- case "$CLASS" in A|B|C|D|E|F) ;; *) g_err "--class must be one of A B C D E F, got '$CLASS'"; exit 2 ;; esac
251
+ [ -n "$CLASS" ] || { g_err "--class is required (software|service|game|research|fleet, or A-E / app / agent / desktop)"; exit 2; }
252
+ # Resolve every accepted spelling to the canonical class NAME before anything reads it. The
253
+ # letters A-E and the old names app/agent/desktop are read-time aliases; desktop/F/f also sets
254
+ # the electron opt-in so the merged class behaves as the old class F did.
255
+ g_class_is_electron_alias "$CLASS" && ELECTRON=1
256
+ CLASS_RAW="$CLASS"
257
+ CLASS=$(g_class_canon "$CLASS") \
258
+ || { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
234
259
  case "$SKILLS" in yes|no) ;; *) g_err "--skills must be yes or no"; exit 2 ;; esac
235
260
 
236
- PRESET=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
237
- [ -n "$PRESET" ] || { g_err "no preset for class $CLASS"; exit 2; }
261
+ # W6 migration safety: on a --upgrade (or any re-install) of a repo whose record shows skills
262
+ # were installed, an OMITTED --skills flag must READ that state, not silently strip it. The
263
+ # idempotence contract (created/updated/unchanged) holds only if the effective skills choice
264
+ # for a flag-less re-install is `the record's choice`, so previously-installed skills survive
265
+ # every upgrade unchanged. An EXPLICIT --skills no is honoured verbatim (the switch stays
266
+ # real), and --uninstall still removes exactly what the record lists.
267
+ if [ -f "$INSTALLED" ] && [ "$SKILLS_NAMED" -eq 0 ]; then
268
+ PREV_SKILLS=$(g_installed_scalar_options "$INSTALLED" skills)
269
+ if [ "$PREV_SKILLS" = "yes" ]; then
270
+ SKILLS="yes"
271
+ g_info "skills: this repo's install record has agent skills installed - they are kept (remove them with an explicit --skills no, or --uninstall)"
272
+ fi
273
+ fi
274
+
275
+ PRESET="$SRC/presets/$CLASS.yaml"
276
+ [ -f "$PRESET" ] || { g_err "no preset for class $CLASS ($PRESET)"; exit 2; }
277
+
278
+ # The electron overlay (the merged desktop/F class): every key it declares — including an empty
279
+ # one like sec_write_routes: "" — wins over the class preset; an absent key falls through. This
280
+ # is config, not a second code path: the same preset() reader, one layer up.
281
+ OVERLAY=""
282
+ if [ "$ELECTRON" -eq 1 ]; then
283
+ OVERLAY="$SRC/presets/electron-overlay.yaml"
284
+ [ -f "$OVERLAY" ] || { g_err "no electron overlay at $OVERLAY"; exit 2; }
285
+ fi
286
+ overlay_has() { [ -n "$OVERLAY" ] && grep -qE "^$1:" "$OVERLAY"; }
238
287
 
239
- preset() { local v; v=$(g_yaml_scalar "$PRESET" "$1"); printf '%s' "${v%\"}" | sed 's/^"//'; }
288
+ preset() {
289
+ local v
290
+ if overlay_has "$1"; then v=$(g_yaml_scalar "$OVERLAY" "$1"); else v=$(g_yaml_scalar "$PRESET" "$1"); fi
291
+ printf '%s' "${v%\"}" | sed 's/^"//'
292
+ }
240
293
 
241
294
  HARNESS_DIR=$(preset harness_dir); [ -n "$HARNESS_DIR" ] || HARNESS_DIR=checks
242
295
  SCAFFOLD=$(preset scaffold_checks); [ -n "$SCAFFOLD" ] || SCAFFOLD=no
@@ -464,6 +517,7 @@ render_config() {
464
517
  runtime=" - $RUNTIME_DATA"
465
518
  render "$SRC/templates/goblin.yaml.tmpl" \
466
519
  CLASS "$CLASS" \
520
+ ELECTRON "$( [ "$ELECTRON" -eq 1 ] && printf 'true' || printf 'false' )" \
467
521
  BRANCH "$BRANCH" \
468
522
  MODELS_FILE "$MODELS" \
469
523
  PRACTICE "$PRACTICE" \
@@ -572,6 +626,30 @@ if [ "$SKILLS" = "yes" ] && part_wanted playbooks; then
572
626
  done
573
627
  fi
574
628
 
629
+ # W6 neutral-first: an EXPLICIT --skills no re-install of a repo that HAS recorded skills
630
+ # removes them — the switch stays real in both directions. The paths come from the previous
631
+ # record, so exactly what was installed is what goes; a skill file the project edited by hand
632
+ # is refused by put's own contract (never clobbered silently), reported in the summary.
633
+ if [ "$SKILLS_NAMED" -eq 1 ] && [ "$SKILLS" = "no" ] && [ -n "$PREV_FILES" ]; then
634
+ while IFS= read -r rel; do
635
+ case "$rel" in .hermes/skills/*) ;; *) continue ;; esac
636
+ dst="$TARGET/$rel"
637
+ [ -f "$dst" ] || { rm -f "$TARGET/.goblin/$(basename "$rel")" 2>/dev/null; continue; }
638
+ if [ "$DRY_RUN" -eq 1 ]; then
639
+ g_info "plan: remove $rel (explicit --skills no)"
640
+ else
641
+ rm -f "$dst"
642
+ g_info "removed $rel (explicit --skills no)"
643
+ fi
644
+ done <<EOF
645
+ $PREV_FILES
646
+ EOF
647
+ # the emptied .hermes/skills/*/<name> and .hermes/ directories go too (deepest first)
648
+ if [ "$DRY_RUN" -eq 0 ]; then
649
+ find "$TARGET/.hermes" -depth -type d -exec rmdir {} + 2>/dev/null
650
+ fi
651
+ fi
652
+
575
653
  # The automation producers. They land in the target so the target is self-contained and
576
654
  # IN-02/SK-02 hash them like every other installed file; the cron copy step then reads from
577
655
  # here (see the "next:" block and automations/README.md).
@@ -667,12 +745,20 @@ fi
667
745
  } > "$TMP/installed.json"
668
746
 
669
747
  PREV_VERSION=""
670
- if [ -f "$INSTALLED" ]; then PREV_VERSION=$(g_installed_scalar "$INSTALLED" version); fi
748
+ RECORD_SKILLS_EFFECTIVE=""
749
+ if [ -f "$INSTALLED" ]; then
750
+ PREV_VERSION=$(g_installed_scalar "$INSTALLED" version)
751
+ # The record's own skills choice (after the migration read above): a flip of the effective
752
+ # choice IS a change even when no file hash moved — the explicit --skills no re-install must
753
+ # rewrite the record (and drop the files), never report no-op over a state change.
754
+ RECORD_SKILLS_EFFECTIVE=$(g_installed_scalar_options "$INSTALLED" skills)
755
+ fi
671
756
 
672
757
 
673
758
  if [ "$DRY_RUN" -eq 0 ]; then
674
759
  mkdir -p "$TARGET/.goblin"
675
- if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ]; then
760
+ if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ] \
761
+ && [ "$(printf '%s' "$RECORD_SKILLS_EFFECTIVE" )" = "$(printf '%s' "$SKILLS")" ]; then
676
762
  g_info "no-op: $UNCHANGED files unchanged (v$VERSION already installed)"
677
763
  else
678
764
  cp "$TMP/installed.json" "$INSTALLED"
@@ -699,6 +785,8 @@ if [ "$DRY_RUN" -eq 0 ]; then
699
785
  g_info " 3. edit .goblin/goblin.yaml: replace the default gate with your real commands (P8 step 3)"
700
786
  if [ "$SKILLS" = "yes" ]; then
701
787
  g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
788
+ else
789
+ g_info " 4. agent skills are opt-in: gob emit --platform <p> # run gob doctor for the platform list"
702
790
  fi
703
791
  g_info ""
704
792
  g_info "automations (optional; neither writes outside this repo, and A-02 has no agent in it):"
package/bin/goblin-lib.sh CHANGED
@@ -16,7 +16,7 @@
16
16
  # - name: typecheck four-space-indented second member of a list entry
17
17
  # cmd: npx tsc --noEmit
18
18
 
19
- GOBLIN_LIB_VERSION="0.4.4"
19
+ GOBLIN_LIB_VERSION="0.5.0"
20
20
 
21
21
  # ---------------------------------------------------------------- output -----
22
22
  g_pass() { printf 'PASS %-6s %s\n' "$1" "$2"; }
@@ -185,6 +185,33 @@ g_part_disabled() {
185
185
  }
186
186
 
187
187
  # ------------------------------------------------------------- class data ----
188
+ # g_class_canon <spelling> -> the canonical class NAME
189
+ # software | service | game | research | fleet
190
+ # The taxonomy is five domain-named classes. The letters A-E and the older taught domain names
191
+ # (app, agent, desktop) stay as READ-TIME aliases so every existing installed.json / goblin.yaml
192
+ # - which record a letter or an old name - keeps verifying with no rewrite. `desktop` / `F` / `f`
193
+ # resolve to `software`: F was merged into A (their classes.tsv need columns are identical), and
194
+ # what made a desktop shell different is the `electron:` opt-in + ban list, config keys the repo
195
+ # already carries. Unknown spelling -> empty output; the caller refuses with the enum.
196
+ g_class_canon() {
197
+ case "$1" in
198
+ software|A|a|app) printf 'software' ;;
199
+ service|B|b) printf 'service' ;;
200
+ game|C|c) printf 'game' ;;
201
+ research|D|d) printf 'research' ;;
202
+ fleet|E|e|agent) printf 'fleet' ;;
203
+ desktop|F|f) printf 'software' ;;
204
+ *) return 1 ;;
205
+ esac
206
+ }
207
+
208
+ # g_class_is_electron_alias <spelling> -> 0 when the spelling is the merged desktop/F spelling.
209
+ # `--class desktop` (or F/f) must mean the OLD desktop install, not a silently weaker software
210
+ # one: the installer auto-sets electron: true so the alias behaves as F did.
211
+ g_class_is_electron_alias() {
212
+ case "$1" in desktop|F|f) return 0 ;; *) return 1 ;; esac
213
+ }
214
+
188
215
  # g_class_need <classes.tsv> <class> <part> -> R | O | -
189
216
  g_class_need() {
190
217
  awk -F'\t' -v c="$2" -v p="$3" '
@@ -216,6 +243,37 @@ g_installed_scalar() {
216
243
  sed -n "s/^[[:space:]]*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}$/\1/p" "$1" | head -n 1
217
244
  }
218
245
 
246
+ # g_installed_scalar_options <file> <key> — the value of one key inside the "options" object
247
+ # of an install record (e.g. skills). The installer writes options on ONE line
248
+ # (`"options": {"skills": "yes", ...}`), so the awk reads that line's shape directly; a
249
+ # multi-line variant is read the same way the other installed_* readers read their block.
250
+ g_installed_scalar_options() {
251
+ awk -v k="$2" '
252
+ /^[[:space:]]*"options"[[:space:]]*:[[:space:]]*\{/ && index($0, "\"" k "\"") {
253
+ # single-line form: pull the value out between the key and the next , or }
254
+ line = $0
255
+ sub(/^[^{]*\{/, "", line) # everything up to and including the opening brace
256
+ n = split(line, pair, ",")
257
+ for (i = 1; i <= n; i++) {
258
+ p = pair[i]
259
+ sub(/^[[:space:]]*/, "", p); sub(/[[:space:]]*$/, "", p)
260
+ key = p; sub(/[[:space:]]*:.*/, "", key); gsub(/"/, "", key)
261
+ if (key == k) {
262
+ v = p; sub(/^[^:]*:[[:space:]]*/, "", v); gsub(/"/, "", v); sub(/\}[[:space:]]*$/, "", v)
263
+ print v; exit
264
+ }
265
+ }
266
+ exit
267
+ }
268
+ /^[[:space:]]*"options"[[:space:]]*:[[:space:]]*\{/ { inf = 1; next }
269
+ inf && /^[[:space:]]*\}/ { inf = 0 }
270
+ inf && index($0, "\"" k "\"") {
271
+ v = $0; sub(/^[^:]*:[[:space:]]*/, "", v); sub(/,?[[:space:]]*$/, "", v); gsub(/"/, "", v)
272
+ print v; exit
273
+ }
274
+ ' "$1"
275
+ }
276
+
219
277
  # ------------------------------------------------------------- self-test -----
220
278
  # Proves the parser actually parses. Every assertion is a real comparison against a
221
279
  # value written to a temp file in this function — blank the awk in g_yaml_scalar and
@@ -30,7 +30,7 @@
30
30
  # `git revert` of this command's own two commits (W3-SPEC §5).
31
31
  set -uo pipefail
32
32
 
33
- GOBLIN_UPGRADE_VERSION="0.4.4"
33
+ GOBLIN_UPGRADE_VERSION="0.5.0"
34
34
 
35
35
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
36
36