@techgoblin/gobstack 0.4.4-beta.6 → 0.4.4-beta.8

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
@@ -50,14 +50,17 @@ After installing, in this order:
50
50
 
51
51
  cd <target> && git add -A && git commit # the install is a change like any other
52
52
  gob verify # or .goblin/bin/goblin-verify, inside the target
53
- hermes skills trust <target> # one-time, Hermes users, so project-tier skills load
53
+ gob emit --platform <p> # optional, per platform: the agent skills are an opt-in
54
54
  gob audit # once, deliberately: the ONLY network step (SC-07)
55
55
 
56
- **A class-A install verifies green — `43 passed, 0 failed, 11 advisory, 28 skipped`, exit 0 — once
56
+ **A default class-A install (no agent skills — those are `gob emit`'s job) verifies green —
57
+ `38 passed, 0 failed, 11 advisory, 33 skipped`, exit 0 — once
57
58
  `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;
59
+ the one expected red: `37 passed, 1 failed`, `HP-05`. Both numbers measured at W6 (neutral-first);
60
+ the run and the fix are step 2 of `docs/GUIDE.md`.**
61
+ Thirty-three rows skip — the five skill rows (`SK-01`..`SK-04`, `AU-04`) skip on the `playbooks`
62
+ opt-out a skills-free install records, then the not-yet rows (`HS-02` has no pinned pre-change
63
+ commit yet, so the REPLAY is not provable;
61
64
  `AU-02` and `AU-03` have no report to audit; `SC-06`, `SC-07` and `SC-08` have no dependency
62
65
  manifest, no lockfile and no audit record to read; `PF-01` has no measured perf baseline;
63
66
  `BN-01`/`BN-02`/`BN-05` have no `src/` for a ban to read, and `BN-03` plus the four electron bans
@@ -83,7 +86,7 @@ The measurement and the vacuous-pass reading are in `docs/CONTRACTS.md`.
83
86
  | `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
87
  | `gob bans` | run the ban list (per-pattern red lines over the source tree) |
85
88
  | `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 |
89
+ | `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
90
  | `gob uninstall` | remove everything an install wrote, byte-exactly (`gob install --target <dir> --uninstall` is the same job) |
88
91
  | `gob upgrade` | migrate a repo to the shared global engine at `~/.goblin/engine` — 8 steps, two commits, one report |
89
92
  | `gob doctor` | one run across the platforms below: DETECTED / NOT-DETECTED / DRIFT per platform |
@@ -111,6 +114,13 @@ One run of `gob emit --platform <p> --scope project` writes the skills and the c
111
114
  session of that platform reads; `--scope global` writes to the machine-level anchor. `--dry-run`
112
115
  prints the full write plan first.
113
116
 
117
+ **Agent skills are opt-in.** A `gob install` writes the neutral harness only — `.goblin/`,
118
+ `HANDOFF.md`, `AGENTS.md`, the checks and the `.gitignore` block; no skills directory, and no
119
+ files belonging to any coding agent. The guided path is `gob init`'s emit screen; the one-shot
120
+ path is `gob emit --platform <p>` after installing. Repos whose install predates the opt-in
121
+ default keep their skills through `gob upgrade` (the install record names them; only an explicit
122
+ `--skills no`, or `--uninstall`, removes them).
123
+
114
124
  ## What it is not
115
125
 
116
126
  Not a rules document (every rule carries a runnable check or is explicitly counted as
package/bin/goblin-init CHANGED
@@ -5,10 +5,13 @@
5
5
  # [--branch <name>] [--email <addr>] [--gate <cmd>]
6
6
  # [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
7
7
  #
8
- # One screen per question, answered steps collapsing into the ✔/◆/○ rail above. Every
9
- # question has a flag: with the flags set (or --yes, or a non-tty stdin) the wizard asks
10
- # nothing and runs from the flags and the defaults — non-interactive parity is structural,
11
- # not a mode. Over a pipe the ANSI redraw degrades to a numbered cascade (rustup shape).
8
+ # One screen per question, answered steps collapsing into the ✔/◆/○ rail above. On a tty
9
+ # the whole wizard frame (logotype, header, answered rows, rail) is redrawn in place from
10
+ # a saved buffer of the wizard's own lines — never by counting printed output, which the
11
+ # installer/emit engines would break. Every question has a flag: with the flags set (or
12
+ # --yes, or a non-tty stdin) the wizard asks nothing and runs from the flags and the
13
+ # defaults — non-interactive parity is structural, not a mode. Over a pipe the redraw
14
+ # degrades to a numbered cascade (rustup shape) with zero cursor motion.
12
15
  #
13
16
  # The wizard is a FRONT END over the existing engines, never a second installer:
14
17
  # .goblin/ + the payload -> bin/goblin-install (its refusal contract — an existing
@@ -47,9 +50,11 @@ VERSION=$(cat "$SRC/VERSION" 2>/dev/null || printf 'unknown')
47
50
  # nearest 16-colour slot. NO_COLOR or a piped stdout turns colour off entirely.
48
51
  TTY_OUT=0; [ -t 1 ] && TTY_OUT=1
49
52
  TTY_IN=0; [ -t 0 ] && TTY_IN=1
50
- C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET="" C_BOLD=""
53
+ C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET=""
54
+ # Single-accent discipline: one accent (#F54E00) + green + text + muted + dim. No bold
55
+ # is ever emitted — bold was a second emphasis voice and is gone from every screen.
51
56
  if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
52
- C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
57
+ C_RESET=$'\033[0m'
53
58
  case "${COLORTERM:-}" in
54
59
  truecolor|24bit)
55
60
  C_ACCENT=$'\033[38;2;245;78;0m'; C_GREEN=$'\033[38;2;74;222;128m'
@@ -64,41 +69,64 @@ if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
64
69
  fi
65
70
 
66
71
  # ---------------------------------------------------------------- the rail ----
67
- # 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty (cursor-up +
68
- # clear-below, no library); over a pipe each screen degrades to one numbered line.
72
+ # 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty from a SAVED
73
+ # BUFFER of the wizard's own lines (RAIL_BUF) — never by counting the output between
74
+ # draws, because the installer/emit engines print freely and break naive accounting.
75
+ # Over a pipe nothing is saved and nothing rewinds: each screen is one numbered line.
69
76
  STEP_LABELS=("platforms" "class" "branch/email" "first gate" "emit" "done")
70
77
  STEP_VAL=("" "" "" "" "" "")
71
- RAIL_LINES=0
72
- Q_LINES=0
73
78
  CURRENT=1
74
79
 
75
- q() { # one printed line of the current screen (counted so the redraw can rewind it)
80
+ # rail_say: one line of wizard output on a tty — stored in the redraw buffer AND
81
+ # printed with a VT100 clear-line prefix (the redraw may overwrite a longer old line).
82
+ # Over a pipe this is exactly printf '%s\n' and nothing is stored.
83
+ rail_say() {
84
+ if [ "$TTY_OUT" -eq 1 ]; then
85
+ RAIL_BUF+=("$*")
86
+ printf '\033[2K%s\n' "$*"
87
+ else
88
+ printf '%s\n' "$*"
89
+ fi
90
+ }
91
+ # rail_seed: the pre-draw frame (logo + header). On a tty it only FILLS the buffer —
92
+ # rail_draw prints it; over a pipe nothing is buffered and it prints directly.
93
+ rail_seed() {
94
+ if [ "$TTY_OUT" -eq 1 ]; then
95
+ RAIL_BUF+=("$*")
96
+ else
97
+ printf '%s\n' "$*"
98
+ fi
99
+ }
100
+ # rail_note: a line that prints now but never joins the redraw buffer (engine output,
101
+ # transcripts, the verify echo): the next redraw wipes it with its clear-below.
102
+ rail_note() {
76
103
  printf '%s\n' "$*"
77
- Q_LINES=$((Q_LINES + 1))
78
104
  }
79
105
  rail_rewind() {
80
- if [ "$TTY_OUT" -eq 1 ] && [ $((RAIL_LINES + Q_LINES)) -gt 0 ]; then
81
- printf '\033[%dA\033[J' "$((RAIL_LINES + Q_LINES))"
106
+ if [ "$TTY_OUT" -eq 1 ]; then
107
+ printf '\033[%dA\033[J' "${#RAIL_BUF[@]}"
82
108
  fi
83
- RAIL_LINES=0; Q_LINES=0
84
109
  }
85
- rail_print() { # the summary column: ✔ answered (with its value) · ◆ asking · ○ pending
86
- local i glyph col label
110
+ rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
111
+ local i line glyph col label
112
+ if [ "$TTY_OUT" -eq 0 ]; then return 0; fi
113
+ for line in "${RAIL_BUF[@]}"; do
114
+ printf '\033[2K%s\n' "$line"
115
+ done
87
116
  for i in 1 2 3 4 5 6; do
88
117
  label="${STEP_LABELS[$((i - 1))]}"
89
118
  if [ "$i" -lt "$CURRENT" ]; then
90
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" "$i" "$label" "$C_TEXT" "${STEP_VAL[$((i - 1))]}" "$C_RESET"
119
+ glyph="✔"; col="$C_GREEN"
91
120
  elif [ "$i" -eq "$CURRENT" ]; then
92
- printf ' %s◆%s %d. %-13s' "$C_ACCENT" "$C_RESET" "$i" "$label"
93
- RAIL_LINES=$((RAIL_LINES + 1))
94
- return 0 # the asking row is printed by the screen itself, after its question
121
+ glyph="◆"; col="$C_ACCENT"
95
122
  else
96
- printf ' %s○%s %d. %-13s\n' "$C_DIM" "$C_RESET" "$i" "$label"
123
+ glyph="○"; col="$C_DIM"
97
124
  fi
98
- RAIL_LINES=$((RAIL_LINES + 1))
125
+ printf '\033[2K %s%s %d. %-13s%s%s%s\n' "${col}${glyph}" "$C_RESET" "$i" "$label" "$C_TEXT" "${STEP_VAL[$((i - 1))]}" "$C_RESET"
99
126
  done
100
127
  }
101
- cascade() { # the pipe-degraded shape: one numbered line per screen
128
+ rail_reset() { RAIL_BUF=(); }
129
+ cascade() { # the pipe-degraded shape: one numbered line per screen (zero cursor motion)
102
130
  printf 'gob init [%d/6] %s\n' "$CURRENT" "$1"
103
131
  }
104
132
 
@@ -108,7 +136,7 @@ ask_value() {
108
136
  if [ "$TTY_IN" -eq 0 ]; then REPLY="$dflt"; return 0; fi
109
137
  printf ' %s%s%s %s─ %s' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt"
110
138
  read -r REPLY
111
- printf '\n'; Q_LINES=$((Q_LINES + 1))
139
+ rail_say ""
112
140
  [ -n "$REPLY" ] || REPLY="$dflt"
113
141
  return 0
114
142
  }
@@ -146,18 +174,25 @@ TARGET=$(g_expand_tilde "$TARGET")
146
174
  TARGET=$(cd "$TARGET" && pwd)
147
175
 
148
176
  # ------------------------------------------------------------ screen 1: welcome
149
- printf '%s\n' "${C_ACCENT}▄███▄ ▄██▄ ██▄ █ ▄█▄ █ █ ▄██▀ █████ ▄███▄ ▄███ █ █${C_RESET}"
150
- printf '%s\n' "${C_ACCENT}█ █ █ █ █▄▀█ █ █ █▄ █ ▀██▄ █ █ █ █ █ █${C_RESET}"
151
- printf '%s\n' "${C_ACCENT}█ ▄██ █ █ █▄▀█ █ █ █ ▄█ ▄█▀ █ █████ █ ██${C_RESET}"
152
- printf '%s\n' "${C_ACCENT}▀██▀▀ ▀██▀ ██▀ ▀▀▀▀ ▀█▀ █ █ ██▀▀ ▀ █ █ ▀██▀ █ █${C_RESET}"
153
- printf '%s\n' "${C_DIM} discipline around whatever executes${C_RESET}"
154
- printf '\n'
155
- printf '%s\n' "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (gobstack v$VERSION)${C_RESET}"
156
- printf '%s\n' " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
157
- printf '\n'
177
+ # The logotype and the header ride the redraw buffer (rail_say): every later screen
178
+ # redraws logo + header + rail as one stable frame. Over a pipe rail_say is plain
179
+ # printf and nothing is buffered — the cascade keeps zero cursor motion.
158
180
  CURRENT=1
159
- if [ "$TTY_OUT" -eq 1 ]; then rail_print
160
- else cascade "welcome: gobstack v$VERSION — the guided first run"; fi
181
+ rail_reset
182
+ rail_seed "${C_ACCENT}▄███▄ ▄██▄ ██▄ █ ▄█▄ █ █ ▄██▀ █████ ▄███▄ ▄███ █ █${C_RESET}"
183
+ rail_seed "${C_ACCENT}█ █ █ █ █▄▀█ █ █ █▄ █ ▀██▄ █ █ █ █ █ █${C_RESET}"
184
+ rail_seed "${C_ACCENT}█ ▄██ █ █ █▄▀█ █ █ █ ▄█ ▄█▀ █ █████ █ ██${C_RESET}"
185
+ rail_seed "${C_ACCENT}▀██▀▀ ▀██▀ ██▀ ▀▀▀▀ ▀█▀ █ █ ██▀▀ ▀ █ █ ▀██▀ █ █${C_RESET}"
186
+ rail_seed " ${C_MUTED}discipline around whatever executes${C_RESET}"
187
+ rail_seed ""
188
+ rail_seed "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (gobstack v$VERSION)${C_RESET}"
189
+ rail_seed " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
190
+ rail_seed ""
191
+ if [ "$TTY_OUT" -eq 1 ]; then
192
+ rail_draw
193
+ else
194
+ cascade "welcome: gobstack v$VERSION — the guided first run"
195
+ fi
161
196
 
162
197
  # Platform enum assembled from fragments (the MD_SLUG precedent): a literal tool name in
163
198
  # bin/ trips MD-01's model-name pattern, and run-tests.sh greps this directory.
@@ -170,7 +205,7 @@ PLATFORMS="$PLA hermes copilot cursor opencode codex $GEM"
170
205
  # ---------------------------------------------------------- screen 2: detect --
171
206
  CURRENT=2
172
207
  rail_rewind
173
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
208
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
174
209
  DET_ROWS=""
175
210
  DET_N=0
176
211
  for p in $PLATFORMS; do
@@ -194,19 +229,17 @@ for p in $PLATFORMS; do
194
229
  DET_N=$((DET_N + 1))
195
230
  fi
196
231
  done
197
- STEP_VAL[0]=" $C_DIM$DET_N detected$C_RESET"
232
+ STEP_VAL[0]="$C_DIM$DET_N detected$C_RESET"
198
233
  if [ "$TTY_OUT" -eq 1 ]; then
199
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 2 "platforms" "$C_TEXT" "$DET_N detected" "$C_RESET"
200
- RAIL_LINES=$((RAIL_LINES + 1))
234
+ rail_say ""
235
+ rail_say " ✔ 1. platforms $C_TEXT$DET_N detected$C_RESET"
201
236
  if [ "$DET_N" -eq 0 ]; then
202
- printf ' %sno platform detected here — the wizard will still install the harness; emit later with gob emit%s\n' "$C_YELLOW" "$C_RESET"
203
- RAIL_LINES=$((RAIL_LINES + 1))
237
+ rail_say " ${C_YELLOW}no platform detected here — the wizard will still install the harness; emit later with gob emit$C_RESET"
204
238
  else
205
239
  while IFS=$'\t' read -r p proj glob hit; do
206
240
  [ -n "$p" ] || continue
207
241
  case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
208
- printf ' %s✔%s %-9s %s %s(%s)%s\n' "$C_GREEN" "$C_RESET" "$p" "$glob" "$C_DIM" "$hit" "$C_RESET"
209
- RAIL_LINES=$((RAIL_LINES + 1))
242
+ rail_say " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
210
243
  done <<< "$DET_ROWS"
211
244
  fi
212
245
  else
@@ -216,23 +249,23 @@ fi
216
249
  # ------------------------------------------------------------ screen 3: class -
217
250
  CURRENT=3
218
251
  rail_rewind
219
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
252
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
220
253
  CLASS_DEFAULT=1
221
254
  if [ -z "$CLASS" ]; then
222
255
  if [ "$TTY_IN" -eq 1 ]; then
223
- q ""
224
- q " ${C_MUTED}what kind of work does this repo do?$C_RESET"
225
- q " $C_TEXT 1 ▸ app shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
226
- q " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
227
- q " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
228
- q " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
229
- q " $C_TEXT 5 agent fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
230
- q " $C_TEXT 6 desktop the shell you live in, perf budget ${C_DIM}(class F)$C_RESET"
231
- q " ${C_MUTED}↑ number + Enter · Enter = app · the class letter works too (A-F)$C_RESET"
256
+ rail_say ""
257
+ 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"
259
+ rail_say " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
260
+ rail_say " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
261
+ 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"
232
265
  printf ' '
233
266
  read -r REPLY_CLASS
234
267
  ASKED_ANY=1
235
- printf '\n'; Q_LINES=$((Q_LINES + 1))
268
+ rail_say ""
236
269
  else
237
270
  REPLY_CLASS="1"
238
271
  cascade "class: app (default; --class app|service|game|research|agent|desktop or A-F)"
@@ -258,10 +291,9 @@ case "$CLASS" in
258
291
  exit 2 ;;
259
292
  esac
260
293
  CLASS_NAME=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
261
- STEP_VAL[1]=" $C_TEXT$CLASS ($([ -n "$CLASS_NAME" ] && basename "$CLASS_NAME" .yaml | cut -d- -f2-))$C_RESET"
294
+ STEP_VAL[1]="$C_TEXT$CLASS ($([ -n "$CLASS_NAME" ] && basename "$CLASS_NAME" .yaml | cut -d- -f2-))$C_RESET"
262
295
  if [ "$TTY_OUT" -eq 1 ]; then
263
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 3 "class" "$C_TEXT" "$CLASS$([ -n "$CLASS_NAME" ] && printf ' — %s' "$(basename "$CLASS_NAME" .yaml | cut -d- -f2-)")$C_RESET"
264
- RAIL_LINES=$((RAIL_LINES + 1))
296
+ rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$([ -n "$CLASS_NAME" ] && printf ' — %s' "$(basename "$CLASS_NAME" .yaml | cut -d- -f2-)")$C_RESET"
265
297
  else
266
298
  cascade "class: $CLASS"
267
299
  fi
@@ -269,15 +301,15 @@ fi
269
301
  # ----------------------------------------------------- screen 4: branch/email --
270
302
  CURRENT=4
271
303
  rail_rewind
272
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
304
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
273
305
  DEF_BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
274
306
  case "$DEF_BRANCH" in ""|HEAD) DEF_BRANCH=main ;; esac
275
307
  DEF_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
276
308
  [ -n "$DEF_EMAIL" ] || DEF_EMAIL="you@example.com"
277
309
  if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
278
310
  if [ "$TTY_IN" -eq 1 ]; then
279
- q ""
280
- q " ${C_MUTED}the forge contract — the identity this repo commits with (CM-01)$C_RESET"
311
+ rail_say ""
312
+ rail_say " ${C_MUTED}the forge contract — the identity this repo commits with (CM-01)$C_RESET"
281
313
  ask_value "default branch" "$DEF_BRANCH"; BRANCH="${BRANCH:-$REPLY}"
282
314
  ask_value "owner email" "$DEF_EMAIL"; EMAIL="${EMAIL:-$REPLY}"
283
315
  else
@@ -288,29 +320,27 @@ if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
288
320
  fi
289
321
  BRANCH="${BRANCH:-$DEF_BRANCH}"
290
322
  EMAIL="${EMAIL:-$DEF_EMAIL}"
291
- STEP_VAL[2]=" $C_TEXT$BRANCH · $EMAIL$C_RESET"
323
+ STEP_VAL[2]="$C_TEXT$BRANCH · $EMAIL$C_RESET"
292
324
  if [ "$TTY_OUT" -eq 1 ]; then
293
- printf ' %s✔%s %d. %-13s%s%s · %s%s\n' "$C_GREEN" "$C_RESET" 4 "branch/email" "$C_TEXT" "$BRANCH" "$EMAIL" "$C_RESET"
294
- RAIL_LINES=$((RAIL_LINES + 1))
325
+ rail_say " ✔ $((CURRENT - 1)). branch/email $C_TEXT$BRANCH · $EMAIL$C_RESET"
295
326
  fi
296
327
 
297
328
  # -------------------------------------------------------- screen 5: first gate -
298
329
  CURRENT=5
299
330
  rail_rewind
300
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
331
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
301
332
  GATE_DEFAULT="bash tests/run-tests.sh"
302
333
  if [ -z "$GATE" ]; then
303
334
  if [ "$TTY_IN" -eq 1 ]; then
304
- q ""
305
- q " ${C_MUTED}one real command that must stay green — suggested for a bash suite:$C_RESET"
335
+ rail_say ""
336
+ rail_say " ${C_MUTED}one real command that must stay green — suggested for a bash suite:$C_RESET"
306
337
  if [ -f "$TARGET/tests/run-tests.sh" ]; then
307
- q " $C_DIM$GATE_DEFAULT$C_RESET"
338
+ rail_say " $C_DIM$GATE_DEFAULT$C_RESET"
308
339
  ask_value "gate command" "$GATE_DEFAULT"; GATE="$REPLY"
309
340
  else
310
341
  ask_value "gate command" "true"; GATE="$REPLY"
311
342
  if [ "$GATE" = "true" ]; then
312
- q " ${C_YELLOW}no tests/run-tests.sh found — a placeholder gate is allowed but it proves nothing; replace it in .goblin/goblin.yaml$C_RESET"
313
- Q_LINES=$((Q_LINES + 1))
343
+ rail_say " ${C_YELLOW}no tests/run-tests.sh found — a placeholder gate is allowed but it proves nothing; replace it in .goblin/goblin.yaml$C_RESET"
314
344
  fi
315
345
  fi
316
346
  else
@@ -318,16 +348,15 @@ if [ -z "$GATE" ]; then
318
348
  cascade "gate: $GATE (default)"
319
349
  fi
320
350
  fi
321
- STEP_VAL[3]=" $C_TEXT$GATE$C_RESET"
351
+ STEP_VAL[3]="$C_TEXT$GATE$C_RESET"
322
352
  if [ "$TTY_OUT" -eq 1 ]; then
323
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 5 "first gate" "$C_TEXT" "$GATE" "$C_RESET"
324
- RAIL_LINES=$((RAIL_LINES + 1))
353
+ rail_say " ✔ $((CURRENT - 1)). first gate $C_TEXT$GATE$C_RESET"
325
354
  fi
326
355
 
327
356
  # --------------------------------------------------------- screen 6: the plan --
328
357
  CURRENT=6
329
358
  rail_rewind
330
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
359
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
331
360
 
332
361
  # The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
333
362
  EMIT_SELECTED=""
@@ -346,29 +375,37 @@ if [ -n "$EMIT" ]; then
346
375
  [ -n "$ok" ] || { g_err "--emit: unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
347
376
  done
348
377
  else
378
+ # neutral-first: a flags-only run with no --emit emits NOTHING. The detected rows are
379
+ # shown on the emit screen as pre-ticked SUGGESTIONS for the interactive path; in
380
+ # non-interactive mode silence means the neutral harness only.
349
381
  EMIT_SELECTED=""
350
- while IFS=$'\t' read -r p _proj _glob _hit; do
351
- [ -n "$p" ] || continue
352
- EMIT_SELECTED="$EMIT_SELECTED $p"
353
- done <<< "$DET_ROWS"
382
+ if [ "$YES" -eq 0 ] && [ "$TTY_IN" -eq 1 ]; then
383
+ while IFS=$'\t' read -r p _proj _glob _hit; do
384
+ [ -n "$p" ] || continue
385
+ EMIT_SELECTED="$EMIT_SELECTED $p"
386
+ done <<< "$DET_ROWS"
387
+ fi
354
388
  fi
355
389
  EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
356
390
 
357
391
  if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
358
- q ""
359
- q " ${C_MUTED}emit now — the skills go into:$C_RESET"
392
+ rail_say ""
393
+ rail_say " ${C_MUTED}agent skills are opt-in, per platform — emit them into:$C_RESET"
360
394
  for p in $PLATFORMS; do
361
395
  hit=""
362
396
  while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
363
397
  [ -n "$hit" ] || continue
364
- q " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
398
+ rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
365
399
  done
366
- q " ${C_DIM}Enter = these; or type platform names to override$C_RESET"
400
+ rail_say " ${C_MUTED}the install itself writes none — the harness stays neutral$C_RESET"
401
+ rail_say " ${C_MUTED}Enter = these; or type platform names to override (none: type x)$C_RESET"
367
402
  printf ' '
368
403
  read -r REPLY_EMIT
369
404
  ASKED_ANY=1
370
- printf '\n'; Q_LINES=$((Q_LINES + 1))
371
- if [ -n "$REPLY_EMIT" ]; then
405
+ rail_say ""
406
+ if [ "$REPLY_EMIT" = "x" ] || [ "$REPLY_EMIT" = "none" ]; then
407
+ EMIT_SELECTED=""
408
+ elif [ -n "$REPLY_EMIT" ]; then
372
409
  EMIT_SELECTED=""
373
410
  for w in $REPLY_EMIT; do
374
411
  ok=""
@@ -377,16 +414,18 @@ if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
377
414
  done
378
415
  fi
379
416
  fi
380
- STEP_VAL[4]=" $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
417
+ STEP_VAL[4]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
381
418
  if [ "$TTY_OUT" -eq 1 ]; then
382
- printf ' %s✔%s %d. %-13s%s%d platform(s) · scope %s%s\n' "$C_GREEN" "$C_RESET" 6 "emit" "$C_TEXT" "$EMIT_N" "$SCOPE" "$C_RESET"
383
- RAIL_LINES=$((RAIL_LINES + 1))
419
+ rail_say " ✔ $((CURRENT - 1)). emit $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
384
420
  fi
385
421
 
386
422
  # ---- the plan box: the wizard never writes until this has been on the screen ----
423
+ # Spacing: one blank line separates the rail from the box (the spacing pass); the box
424
+ # carries two-space inner padding on every row so the columns align across rows.
387
425
  BOX=""
388
426
  add() { BOX="$BOX$1
389
427
  "; }
428
+ add ""
390
429
  add " ${C_DIM}╭──────────────────────────────────────────────────────╮${C_RESET}"
391
430
  add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}install${C_RESET} $C_DIM.goblin/ + the payload into$C_RESET $C_TEXT$TARGET$C_RESET"
392
431
  add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}class${C_RESET} $C_TEXT$CLASS$C_RESET ${C_DIM}· branch $C_TEXT$BRANCH$C_RESET ${C_DIM}· email$C_RESET $C_TEXT$EMAIL$C_RESET"
@@ -400,10 +439,11 @@ if [ "$DRYRUN" -eq 1 ]; then
400
439
  fi
401
440
  # A question the wizard genuinely asked (a screen that waited on read) buys the confirm;
402
441
  # flag/non-tty runs already stated their answer as a flag — proceeding is the contract.
442
+ # Single-accent: the confirm prompt is muted + dim, never bold, never a second accent.
403
443
  if [ "$YES" -eq 0 ] && [ "${ASKED_ANY:-0}" -eq 1 ]; then
404
444
  printf '%s\n' "$BOX"
405
445
  if [ "$TTY_IN" -eq 1 ]; then
406
- printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_BOLD" "$C_RESET" "$C_DIM" "$C_RESET"
446
+ printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_MUTED" "$C_RESET" "$C_DIM" "$C_RESET"
407
447
  read -r REPLY_GO
408
448
  case "$REPLY_GO" in n*|N*) g_info "aborted — nothing was written"; exit 0 ;; esac
409
449
  else
@@ -416,8 +456,15 @@ fi
416
456
  # carry: the declared identity (branch/owner_email) and the first gate, patched into
417
457
  # .goblin/goblin.yaml right after install renders it. The patch is fail-closed — the
418
458
  # gate must read back through the engine's own parser or the run stops here.
419
- if [ "$TTY_OUT" -eq 0 ]; then printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"; fi
420
- bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
459
+ # rail_note, not rail_say: engine output below is never redrawn over, and the wizard
460
+ # never rewinds past its own buffer by counting lines it did not print.
461
+ if [ "$TTY_OUT" -eq 1 ]; then
462
+ rail_note ""
463
+ rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS${C_RESET}"
464
+ else
465
+ printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
466
+ fi
467
+ bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --skills no --yes
421
468
  EXIT_INSTALL=$?
422
469
  if [ "$EXIT_INSTALL" -ne 0 ]; then
423
470
  g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
@@ -453,7 +500,11 @@ fi
453
500
 
454
501
  EMITTED=0
455
502
  for p in $EMIT_SELECTED; do
456
- [ "$TTY_OUT" -eq 0 ] && printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
503
+ if [ "$TTY_OUT" -eq 0 ]; then
504
+ printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
505
+ else
506
+ rail_note " ${C_MUTED}running: goblin-emit --platform $p --scope $SCOPE${C_RESET}"
507
+ fi
457
508
  bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
458
509
  EXIT_EMIT=$?
459
510
  if [ "$EXIT_EMIT" -ne 0 ]; then
@@ -464,9 +515,9 @@ for p in $EMIT_SELECTED; do
464
515
  done
465
516
 
466
517
  # --------------------------------------------------- screen 6b: done + verify --
467
- if [ "$TTY_OUT" -eq 1 ]; then
468
- printf '\n'
469
- else
518
+ # Spacing pass: exactly one blank line before each done-screen section; the engine's
519
+ # verify output and this screen's own summary stay visually separated.
520
+ if [ "$TTY_OUT" -eq 0 ]; then
470
521
  printf 'gob init [verify] .goblin/bin/goblin-verify\n'
471
522
  fi
472
523
  if [ "$DRYRUN" -eq 0 ]; then
@@ -494,16 +545,20 @@ if [ "$DRYRUN" -eq 0 ]; then
494
545
  else
495
546
  printf ' %sgob verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
496
547
  fi
497
- printf '\n'
498
548
  else
499
549
  g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
500
550
  fi
501
551
 
502
- printf ' %snext:%s\n' "$C_BOLD" "$C_RESET"
552
+ printf '\n'
553
+ printf ' %snext:%s\n' "$C_MUTED" "$C_RESET"
503
554
  printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
504
555
  printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
505
556
  printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
506
- printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
557
+ if [ "$EMIT_N" -gt 0 ]; then
558
+ printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
559
+ else
560
+ printf ' 4. %sagent skills are opt-in per platform: gob emit --platform <p>%s\n' "$C_TEXT" "$C_RESET"
561
+ fi
507
562
  printf '\n'
508
563
  if [ "$VERIFY_RC" -eq 0 ]; then
509
564
  printf ' %s▙ the goblin sees you. keep the gate green.%s\n' "$C_ACCENT" "$C_RESET"
@@ -8,7 +8,8 @@
8
8
  # --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md); a named path that is absent is reported, never silently dropped
9
9
  # --parts <list> comma list to install; default = every part the class requires
10
10
  # --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)
11
+ # --skills yes|no install agent skills under .hermes/skills (default no; opt in per
12
+ # platform with: gob emit --platform <p>)
12
13
  # --dry-run print the plan; write nothing
13
14
  # --upgrade re-install at the current version; report created/updated/unchanged/skipped
14
15
  # --opt-out <part> record the part in disabled: so its required checks are skipped
@@ -40,7 +41,16 @@ PRACTICE="${GOBLIN_PRACTICE:-$HOME/projects/PROJECT-PRACTICE.md}"
40
41
  PRACTICE_ARG=""
41
42
  PARTS=""
42
43
  ARCHIVE="false"
43
- SKILLS="yes"
44
+ # W6 neutral-first: a default install is a NEUTRAL harness (manifest + verify + HANDOFF +
45
+ # .goblin) with NO agent skills. Agent skills are an explicit opt-in (--skills yes, or the
46
+ # per-platform gob emit path the wizard's emit screen drives). The old default (yes) was the
47
+ # house's own setup, not a product default: a non-agent user got a dead .hermes/ tree, and a
48
+ # user of another agent harness got nothing at all.
49
+ SKILLS="no"
50
+ # Whether --skills was named on the command line: an OMITTED flag on a re-install means
51
+ # "whatever the record says" (the migration contract below); a NAMED --skills no is an
52
+ # explicit opt-out and is honoured verbatim.
53
+ SKILLS_NAMED=0
44
54
  DRY_RUN=0
45
55
  UPGRADE=0
46
56
  UNINSTALL=0
@@ -63,7 +73,7 @@ while [ $# -gt 0 ]; do
63
73
  --practice) PRACTICE="${2:-}"; PRACTICE_ARG="$PRACTICE"; shift 2 ;;
64
74
  --parts) PARTS="${2:-}"; shift 2 ;;
65
75
  --archive) ARCHIVE="true"; shift ;;
66
- --skills) SKILLS="${2:-}"; shift 2 ;;
76
+ --skills) SKILLS="${2:-}"; SKILLS_NAMED=1; shift 2 ;;
67
77
  --dry-run) DRY_RUN=1; shift ;;
68
78
  --upgrade) UPGRADE=1; shift ;;
69
79
  --uninstall) UNINSTALL=1; shift ;;
@@ -233,6 +243,20 @@ fi
233
243
  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
234
244
  case "$SKILLS" in yes|no) ;; *) g_err "--skills must be yes or no"; exit 2 ;; esac
235
245
 
246
+ # W6 migration safety: on a --upgrade (or any re-install) of a repo whose record shows skills
247
+ # were installed, an OMITTED --skills flag must READ that state, not silently strip it. The
248
+ # idempotence contract (created/updated/unchanged) holds only if the effective skills choice
249
+ # for a flag-less re-install is `the record's choice`, so previously-installed skills survive
250
+ # every upgrade unchanged. An EXPLICIT --skills no is honoured verbatim (the switch stays
251
+ # real), and --uninstall still removes exactly what the record lists.
252
+ if [ -f "$INSTALLED" ] && [ "$SKILLS_NAMED" -eq 0 ]; then
253
+ PREV_SKILLS=$(g_installed_scalar_options "$INSTALLED" skills)
254
+ if [ "$PREV_SKILLS" = "yes" ]; then
255
+ SKILLS="yes"
256
+ g_info "skills: this repo's install record has agent skills installed - they are kept (remove them with an explicit --skills no, or --uninstall)"
257
+ fi
258
+ fi
259
+
236
260
  PRESET=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
237
261
  [ -n "$PRESET" ] || { g_err "no preset for class $CLASS"; exit 2; }
238
262
 
@@ -572,6 +596,30 @@ if [ "$SKILLS" = "yes" ] && part_wanted playbooks; then
572
596
  done
573
597
  fi
574
598
 
599
+ # W6 neutral-first: an EXPLICIT --skills no re-install of a repo that HAS recorded skills
600
+ # removes them — the switch stays real in both directions. The paths come from the previous
601
+ # record, so exactly what was installed is what goes; a skill file the project edited by hand
602
+ # is refused by put's own contract (never clobbered silently), reported in the summary.
603
+ if [ "$SKILLS_NAMED" -eq 1 ] && [ "$SKILLS" = "no" ] && [ -n "$PREV_FILES" ]; then
604
+ while IFS= read -r rel; do
605
+ case "$rel" in .hermes/skills/*) ;; *) continue ;; esac
606
+ dst="$TARGET/$rel"
607
+ [ -f "$dst" ] || { rm -f "$TARGET/.goblin/$(basename "$rel")" 2>/dev/null; continue; }
608
+ if [ "$DRY_RUN" -eq 1 ]; then
609
+ g_info "plan: remove $rel (explicit --skills no)"
610
+ else
611
+ rm -f "$dst"
612
+ g_info "removed $rel (explicit --skills no)"
613
+ fi
614
+ done <<EOF
615
+ $PREV_FILES
616
+ EOF
617
+ # the emptied .hermes/skills/*/<name> and .hermes/ directories go too (deepest first)
618
+ if [ "$DRY_RUN" -eq 0 ]; then
619
+ find "$TARGET/.hermes" -depth -type d -exec rmdir {} + 2>/dev/null
620
+ fi
621
+ fi
622
+
575
623
  # The automation producers. They land in the target so the target is self-contained and
576
624
  # IN-02/SK-02 hash them like every other installed file; the cron copy step then reads from
577
625
  # here (see the "next:" block and automations/README.md).
@@ -667,12 +715,20 @@ fi
667
715
  } > "$TMP/installed.json"
668
716
 
669
717
  PREV_VERSION=""
670
- if [ -f "$INSTALLED" ]; then PREV_VERSION=$(g_installed_scalar "$INSTALLED" version); fi
718
+ RECORD_SKILLS_EFFECTIVE=""
719
+ if [ -f "$INSTALLED" ]; then
720
+ PREV_VERSION=$(g_installed_scalar "$INSTALLED" version)
721
+ # The record's own skills choice (after the migration read above): a flip of the effective
722
+ # choice IS a change even when no file hash moved — the explicit --skills no re-install must
723
+ # rewrite the record (and drop the files), never report no-op over a state change.
724
+ RECORD_SKILLS_EFFECTIVE=$(g_installed_scalar_options "$INSTALLED" skills)
725
+ fi
671
726
 
672
727
 
673
728
  if [ "$DRY_RUN" -eq 0 ]; then
674
729
  mkdir -p "$TARGET/.goblin"
675
- if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ]; then
730
+ if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ] \
731
+ && [ "$(printf '%s' "$RECORD_SKILLS_EFFECTIVE" )" = "$(printf '%s' "$SKILLS")" ]; then
676
732
  g_info "no-op: $UNCHANGED files unchanged (v$VERSION already installed)"
677
733
  else
678
734
  cp "$TMP/installed.json" "$INSTALLED"
@@ -699,6 +755,8 @@ if [ "$DRY_RUN" -eq 0 ]; then
699
755
  g_info " 3. edit .goblin/goblin.yaml: replace the default gate with your real commands (P8 step 3)"
700
756
  if [ "$SKILLS" = "yes" ]; then
701
757
  g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
758
+ else
759
+ g_info " 4. agent skills are opt-in: gob emit --platform <p> # run gob doctor for the platform list"
702
760
  fi
703
761
  g_info ""
704
762
  g_info "automations (optional; neither writes outside this repo, and A-02 has no agent in it):"