@techgoblin/gobstack 0.4.4-beta.4 → 0.4.4-beta.7

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/bin/goblin-init CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env bash
2
2
  # goblin-init — the W6 first-run wizard (`gob init`).
3
3
  #
4
- # goblin init [--target <dir>] [--class <app|service|game|research|agent|desktop|A..F>]
4
+ # gob init [--target <dir>] [--class <app|service|game|research|agent|desktop|A..F>]
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
  }
@@ -137,7 +165,7 @@ done
137
165
  for dep in git awk sed grep; do
138
166
  command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
139
167
  done
140
- [ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a goblin-stack checkout?"; exit 2; }
168
+ [ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a gobstack checkout?"; exit 2; }
141
169
  [ -n "$SCOPE" ] || SCOPE="project"
142
170
  case "$SCOPE" in project|global) ;; *) g_err "--scope must be project or global, got '$SCOPE'"; exit 2 ;; esac
143
171
  TARGET="${TARGET:-$PWD}"
@@ -146,17 +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_DIM} discipline around whatever executes${C_RESET}"
153
- printf '\n'
154
- printf '%s\n' "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (goblin-stack v$VERSION)${C_RESET}"
155
- printf '%s\n' " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
156
- 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.
157
180
  CURRENT=1
158
- if [ "$TTY_OUT" -eq 1 ]; then rail_print
159
- else cascade "welcome: goblin-stack 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
160
196
 
161
197
  # Platform enum assembled from fragments (the MD_SLUG precedent): a literal tool name in
162
198
  # bin/ trips MD-01's model-name pattern, and run-tests.sh greps this directory.
@@ -169,7 +205,7 @@ PLATFORMS="$PLA hermes copilot cursor opencode codex $GEM"
169
205
  # ---------------------------------------------------------- screen 2: detect --
170
206
  CURRENT=2
171
207
  rail_rewind
172
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
208
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
173
209
  DET_ROWS=""
174
210
  DET_N=0
175
211
  for p in $PLATFORMS; do
@@ -193,19 +229,17 @@ for p in $PLATFORMS; do
193
229
  DET_N=$((DET_N + 1))
194
230
  fi
195
231
  done
196
- STEP_VAL[0]=" $C_DIM$DET_N detected$C_RESET"
232
+ STEP_VAL[0]="$C_DIM$DET_N detected$C_RESET"
197
233
  if [ "$TTY_OUT" -eq 1 ]; then
198
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 2 "platforms" "$C_TEXT" "$DET_N detected" "$C_RESET"
199
- RAIL_LINES=$((RAIL_LINES + 1))
234
+ rail_say ""
235
+ rail_say " ✔ 1. platforms $C_TEXT$DET_N detected$C_RESET"
200
236
  if [ "$DET_N" -eq 0 ]; then
201
- printf ' %sno platform detected here — the wizard will still install the harness; emit later with gob emit%s\n' "$C_YELLOW" "$C_RESET"
202
- 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"
203
238
  else
204
239
  while IFS=$'\t' read -r p proj glob hit; do
205
240
  [ -n "$p" ] || continue
206
241
  case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
207
- printf ' %s✔%s %-9s %s %s(%s)%s\n' "$C_GREEN" "$C_RESET" "$p" "$glob" "$C_DIM" "$hit" "$C_RESET"
208
- RAIL_LINES=$((RAIL_LINES + 1))
242
+ rail_say " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
209
243
  done <<< "$DET_ROWS"
210
244
  fi
211
245
  else
@@ -215,23 +249,23 @@ fi
215
249
  # ------------------------------------------------------------ screen 3: class -
216
250
  CURRENT=3
217
251
  rail_rewind
218
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
252
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
219
253
  CLASS_DEFAULT=1
220
254
  if [ -z "$CLASS" ]; then
221
255
  if [ "$TTY_IN" -eq 1 ]; then
222
- q ""
223
- q " ${C_MUTED}what kind of work does this repo do?$C_RESET"
224
- q " $C_TEXT 1 ▸ app shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
225
- q " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
226
- q " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
227
- q " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
228
- q " $C_TEXT 5 agent fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
229
- q " $C_TEXT 6 desktop the shell you live in, perf budget ${C_DIM}(class F)$C_RESET"
230
- 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"
231
265
  printf ' '
232
266
  read -r REPLY_CLASS
233
267
  ASKED_ANY=1
234
- printf '\n'; Q_LINES=$((Q_LINES + 1))
268
+ rail_say ""
235
269
  else
236
270
  REPLY_CLASS="1"
237
271
  cascade "class: app (default; --class app|service|game|research|agent|desktop or A-F)"
@@ -257,10 +291,9 @@ case "$CLASS" in
257
291
  exit 2 ;;
258
292
  esac
259
293
  CLASS_NAME=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
260
- 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"
261
295
  if [ "$TTY_OUT" -eq 1 ]; then
262
- 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"
263
- 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"
264
297
  else
265
298
  cascade "class: $CLASS"
266
299
  fi
@@ -268,15 +301,15 @@ fi
268
301
  # ----------------------------------------------------- screen 4: branch/email --
269
302
  CURRENT=4
270
303
  rail_rewind
271
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
304
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
272
305
  DEF_BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
273
306
  case "$DEF_BRANCH" in ""|HEAD) DEF_BRANCH=main ;; esac
274
307
  DEF_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
275
308
  [ -n "$DEF_EMAIL" ] || DEF_EMAIL="you@example.com"
276
309
  if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
277
310
  if [ "$TTY_IN" -eq 1 ]; then
278
- q ""
279
- 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"
280
313
  ask_value "default branch" "$DEF_BRANCH"; BRANCH="${BRANCH:-$REPLY}"
281
314
  ask_value "owner email" "$DEF_EMAIL"; EMAIL="${EMAIL:-$REPLY}"
282
315
  else
@@ -287,29 +320,27 @@ if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
287
320
  fi
288
321
  BRANCH="${BRANCH:-$DEF_BRANCH}"
289
322
  EMAIL="${EMAIL:-$DEF_EMAIL}"
290
- STEP_VAL[2]=" $C_TEXT$BRANCH · $EMAIL$C_RESET"
323
+ STEP_VAL[2]="$C_TEXT$BRANCH · $EMAIL$C_RESET"
291
324
  if [ "$TTY_OUT" -eq 1 ]; then
292
- printf ' %s✔%s %d. %-13s%s%s · %s%s\n' "$C_GREEN" "$C_RESET" 4 "branch/email" "$C_TEXT" "$BRANCH" "$EMAIL" "$C_RESET"
293
- RAIL_LINES=$((RAIL_LINES + 1))
325
+ rail_say " ✔ $((CURRENT - 1)). branch/email $C_TEXT$BRANCH · $EMAIL$C_RESET"
294
326
  fi
295
327
 
296
328
  # -------------------------------------------------------- screen 5: first gate -
297
329
  CURRENT=5
298
330
  rail_rewind
299
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
331
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
300
332
  GATE_DEFAULT="bash tests/run-tests.sh"
301
333
  if [ -z "$GATE" ]; then
302
334
  if [ "$TTY_IN" -eq 1 ]; then
303
- q ""
304
- 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"
305
337
  if [ -f "$TARGET/tests/run-tests.sh" ]; then
306
- q " $C_DIM$GATE_DEFAULT$C_RESET"
338
+ rail_say " $C_DIM$GATE_DEFAULT$C_RESET"
307
339
  ask_value "gate command" "$GATE_DEFAULT"; GATE="$REPLY"
308
340
  else
309
341
  ask_value "gate command" "true"; GATE="$REPLY"
310
342
  if [ "$GATE" = "true" ]; then
311
- 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"
312
- 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"
313
344
  fi
314
345
  fi
315
346
  else
@@ -317,16 +348,15 @@ if [ -z "$GATE" ]; then
317
348
  cascade "gate: $GATE (default)"
318
349
  fi
319
350
  fi
320
- STEP_VAL[3]=" $C_TEXT$GATE$C_RESET"
351
+ STEP_VAL[3]="$C_TEXT$GATE$C_RESET"
321
352
  if [ "$TTY_OUT" -eq 1 ]; then
322
- printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 5 "first gate" "$C_TEXT" "$GATE" "$C_RESET"
323
- RAIL_LINES=$((RAIL_LINES + 1))
353
+ rail_say " ✔ $((CURRENT - 1)). first gate $C_TEXT$GATE$C_RESET"
324
354
  fi
325
355
 
326
356
  # --------------------------------------------------------- screen 6: the plan --
327
357
  CURRENT=6
328
358
  rail_rewind
329
- if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
359
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
330
360
 
331
361
  # The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
332
362
  EMIT_SELECTED=""
@@ -354,19 +384,19 @@ fi
354
384
  EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
355
385
 
356
386
  if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
357
- q ""
358
- q " ${C_MUTED}emit now — the skills go into:$C_RESET"
387
+ rail_say ""
388
+ rail_say " ${C_MUTED}emit now — the skills go into:$C_RESET"
359
389
  for p in $PLATFORMS; do
360
390
  hit=""
361
391
  while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
362
392
  [ -n "$hit" ] || continue
363
- q " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET"
393
+ rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
364
394
  done
365
- q " ${C_DIM}Enter = these; or type platform names to override$C_RESET"
395
+ rail_say " ${C_MUTED}Enter = these; or type platform names to override$C_RESET"
366
396
  printf ' '
367
397
  read -r REPLY_EMIT
368
398
  ASKED_ANY=1
369
- printf '\n'; Q_LINES=$((Q_LINES + 1))
399
+ rail_say ""
370
400
  if [ -n "$REPLY_EMIT" ]; then
371
401
  EMIT_SELECTED=""
372
402
  for w in $REPLY_EMIT; do
@@ -376,19 +406,21 @@ if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
376
406
  done
377
407
  fi
378
408
  fi
379
- STEP_VAL[4]=" $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
409
+ STEP_VAL[4]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
380
410
  if [ "$TTY_OUT" -eq 1 ]; then
381
- 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"
382
- RAIL_LINES=$((RAIL_LINES + 1))
411
+ rail_say " ✔ $((CURRENT - 1)). emit $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
383
412
  fi
384
413
 
385
414
  # ---- the plan box: the wizard never writes until this has been on the screen ----
415
+ # Spacing: one blank line separates the rail from the box (the spacing pass); the box
416
+ # carries two-space inner padding on every row so the columns align across rows.
386
417
  BOX=""
387
418
  add() { BOX="$BOX$1
388
419
  "; }
420
+ add ""
389
421
  add " ${C_DIM}╭──────────────────────────────────────────────────────╮${C_RESET}"
390
422
  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"
391
- 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"
423
+ 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"
392
424
  add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}gate${C_RESET} $C_TEXT$GATE$C_RESET"
393
425
  add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}emit${C_RESET} $C_TEXT$EMIT_N platform(s), scope $SCOPE$C_RESET$([ "$EMIT_N" -gt 0 ] && printf '%s' " $C_DIM($(printf '%s' "$EMIT_SELECTED" | awk '{$1=$1};1'))$C_RESET")"
394
426
  add " ${C_DIM}╰──────────────────────────────────────────────────────╯${C_RESET}"
@@ -399,10 +431,11 @@ if [ "$DRYRUN" -eq 1 ]; then
399
431
  fi
400
432
  # A question the wizard genuinely asked (a screen that waited on read) buys the confirm;
401
433
  # flag/non-tty runs already stated their answer as a flag — proceeding is the contract.
434
+ # Single-accent: the confirm prompt is muted + dim, never bold, never a second accent.
402
435
  if [ "$YES" -eq 0 ] && [ "${ASKED_ANY:-0}" -eq 1 ]; then
403
436
  printf '%s\n' "$BOX"
404
437
  if [ "$TTY_IN" -eq 1 ]; then
405
- printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_BOLD" "$C_RESET" "$C_DIM" "$C_RESET"
438
+ printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_MUTED" "$C_RESET" "$C_DIM" "$C_RESET"
406
439
  read -r REPLY_GO
407
440
  case "$REPLY_GO" in n*|N*) g_info "aborted — nothing was written"; exit 0 ;; esac
408
441
  else
@@ -415,7 +448,14 @@ fi
415
448
  # carry: the declared identity (branch/owner_email) and the first gate, patched into
416
449
  # .goblin/goblin.yaml right after install renders it. The patch is fail-closed — the
417
450
  # gate must read back through the engine's own parser or the run stops here.
418
- if [ "$TTY_OUT" -eq 0 ]; then printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"; fi
451
+ # rail_note, not rail_say: engine output below is never redrawn over, and the wizard
452
+ # never rewinds past its own buffer by counting lines it did not print.
453
+ if [ "$TTY_OUT" -eq 1 ]; then
454
+ rail_note ""
455
+ rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS${C_RESET}"
456
+ else
457
+ printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
458
+ fi
419
459
  bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
420
460
  EXIT_INSTALL=$?
421
461
  if [ "$EXIT_INSTALL" -ne 0 ]; then
@@ -452,20 +492,24 @@ fi
452
492
 
453
493
  EMITTED=0
454
494
  for p in $EMIT_SELECTED; do
455
- [ "$TTY_OUT" -eq 0 ] && printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
495
+ if [ "$TTY_OUT" -eq 0 ]; then
496
+ printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
497
+ else
498
+ rail_note " ${C_MUTED}running: goblin-emit --platform $p --scope $SCOPE${C_RESET}"
499
+ fi
456
500
  bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
457
501
  EXIT_EMIT=$?
458
502
  if [ "$EXIT_EMIT" -ne 0 ]; then
459
- g_err "emit $p failed (exit $EXIT_EMIT) — run 'goblin emit --platform $p --scope $SCOPE' to see the refusal again"
503
+ g_err "emit $p failed (exit $EXIT_EMIT) — run 'gob emit --platform $p --scope $SCOPE' to see the refusal again"
460
504
  exit "$EXIT_EMIT"
461
505
  fi
462
506
  EMITTED=$((EMITTED + 1))
463
507
  done
464
508
 
465
509
  # --------------------------------------------------- screen 6b: done + verify --
466
- if [ "$TTY_OUT" -eq 1 ]; then
467
- printf '\n'
468
- else
510
+ # Spacing pass: exactly one blank line before each done-screen section; the engine's
511
+ # verify output and this screen's own summary stay visually separated.
512
+ if [ "$TTY_OUT" -eq 0 ]; then
469
513
  printf 'gob init [verify] .goblin/bin/goblin-verify\n'
470
514
  fi
471
515
  if [ "$DRYRUN" -eq 0 ]; then
@@ -489,16 +533,16 @@ if [ "$DRYRUN" -eq 0 ]; then
489
533
  done
490
534
  printf '\n'
491
535
  if [ "$VERIFY_RC" -eq 0 ]; then
492
- printf ' %sgoblin verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
536
+ printf ' %sgob verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
493
537
  else
494
- printf ' %sgoblin verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
538
+ printf ' %sgob verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
495
539
  fi
496
- printf '\n'
497
540
  else
498
541
  g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
499
542
  fi
500
543
 
501
- printf ' %snext:%s\n' "$C_BOLD" "$C_RESET"
544
+ printf '\n'
545
+ printf ' %snext:%s\n' "$C_MUTED" "$C_RESET"
502
546
  printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
503
547
  printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
504
548
  printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # goblin-install — drop the harness into a target repo, idempotently.
3
3
  #
4
- # Usage: goblin-install --target <dir> [options]
4
+ # Usage: gob install --target <dir> [options] (script: bin/goblin-install)
5
5
  # --target <dir> required; the repo root to install into
6
6
  # --class A|B|C|D|E|F required unless --uninstall or --re-pin; anything else needs an explicit class
7
7
  # --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # goblin-upgrade — the 0.4.4 per-repo → global migration (W3, W3-SPEC §2).
2
+ # goblin-upgrade — the 0.4.4 per-repo → global engine migration (W3).
3
3
  #
4
- # goblin upgrade [--target <dir>] [--dry-run] [--yes] [--engine-dir <path>]
4
+ # gob upgrade [--target <dir>] [--dry-run] [--yes] [--engine-dir <path>]
5
5
  #
6
6
  # What it does (the corrected 7-step sequence, W3-SPEC §2 — the record is rewritten
7
7
  # BEFORE the deletion, and every verify runs against a committed tree, CM-03):
@@ -230,7 +230,7 @@ if [ "${RECORD_OK:-0}" -eq 1 ] && [ -z "${MODE:-}" ]; then
230
230
  R8_MSG="upgrade: partial migration detected - the record says global but $PAYLOAD_N engine files are still here. fix: resume by removing them (the engine lives at ${BLOCK_DIR:-$ED}), or roll back: git revert <commit A>"
231
231
  elif [ "$BLOCK_MODE" != "global" ] && [ "$RECORD_ENGINE_N" -lt 18 ]; then
232
232
  MODE="refuse-R9" # hand-migration: entries PARTIALLY or fully dropped, no block
233
- R9_MSG="upgrade: $REPO's record is missing its engine: block and carries only $RECORD_ENGINE_N of the 18 engine entries - dropped $((18 - RECORD_ENGINE_N)) by hand, a state goblin upgrade did not produce. fix: restore the record (git checkout .goblin/installed.json) and re-run"
233
+ R9_MSG="upgrade: $REPO's record is missing its engine: block and carries only $RECORD_ENGINE_N of the 18 engine entries - dropped $((18 - RECORD_ENGINE_N)) by hand, a state gob upgrade did not produce. fix: restore the record (git checkout .goblin/installed.json) and re-run"
234
234
  elif [ "$PAYLOAD_N" -eq 0 ]; then
235
235
  MODE="refuse-R5" # fresh-shape record but no payload to lift
236
236
  R5_MSG="upgrade: no engine at $ED and no vendored payload to lift it from - install the engine first (goblin-install a fresh repo, or set --engine-dir)"
@@ -244,7 +244,7 @@ fi
244
244
 
245
245
  # ---- dry-run report -------------------------------------------------------------
246
246
  if [ "$DRY_RUN" -eq 1 ]; then
247
- printf 'goblin upgrade --dry-run — target %s\n' "$REPO"
247
+ printf 'gob upgrade --dry-run — target %s\n' "$REPO"
248
248
  printf ' engine-dir: %s\n' "$ED"
249
249
  case "$MODE" in
250
250
  migrate)
@@ -447,7 +447,7 @@ if [ -f "$GATE" ] && grep -q '\.goblin/bin/goblin-lib.sh' "$GATE"; then
447
447
  awk '
448
448
  /^# shellcheck source=\/dev\/null$/ && !done {
449
449
  print "# shellcheck source=/dev/null"
450
- print "# The engine went global (goblin upgrade): resolve it exactly as"
450
+ print "# The engine went global (gob upgrade): resolve it exactly as"
451
451
  print "# goblin-verify does - engine_dir:, then the machine default."
452
452
  print "CONFIG=\"$ROOT/.goblin/goblin.yaml\""
453
453
  print "ENGINE_DIR=$(sed -n \"s/^engine_dir:[[:space:]]*//p\" \"$CONFIG\" | head -n 1)"
package/bin/goblin-verify CHANGED
@@ -1363,7 +1363,7 @@ check_sc_07() {
1363
1363
  local rec="$ROOT/.goblin/audit.tsv" wfile="$ROOT/.goblin/audit-waiver.tsv"
1364
1364
  if [ ! -f "$rec" ]; then
1365
1365
  if [ "$ENGINE_MODE" = "global" ]; then
1366
- printf 'no audit record yet. Run `goblin audit` once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
1366
+ printf 'no audit record yet. Run `gob audit` once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
1367
1367
  else
1368
1368
  printf 'no audit record yet. Run .goblin/bin/goblin-audit once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
1369
1369
  fi
package/bin/goblin.js CHANGED
@@ -6,15 +6,15 @@
6
6
  // and a marketplace packager stripping the executable bit breaks the symlink, not
7
7
  // the spawnSync path below.
8
8
  //
9
- // goblin verify [--only <id,...>] ... -> bin/goblin-verify
10
- // goblin bans [...] -> bin/goblin-bans
11
- // goblin audit [...] -> bin/goblin-audit
12
- // goblin upgrade [...] -> bin/goblin-upgrade (W3)
13
- // goblin doctor [...] -> bin/goblin-doctor (W4a)
14
- // goblin emit [...] -> bin/goblin-emit (W4a)
15
- // goblin init [...] -> bin/goblin-init (W6, the first-run wizard)
16
- // goblin uninstall [--target <dir>] -> bin/goblin-install --uninstall
17
- // goblin install [...] -> bin/goblin-install (the one legacy fallback)
9
+ // gob verify [--only <id,...>] ... -> bin/goblin-verify
10
+ // gob bans [...] -> bin/goblin-bans
11
+ // gob audit [...] -> bin/goblin-audit
12
+ // gob upgrade [...] -> bin/goblin-upgrade (W3)
13
+ // gob doctor [...] -> bin/goblin-doctor (W4a)
14
+ // gob emit [...] -> bin/goblin-emit (W4a)
15
+ // gob init [...] -> bin/goblin-init (W6, the first-run wizard)
16
+ // gob uninstall [--target <dir>] -> bin/goblin-install --uninstall
17
+ // gob install [...] -> bin/goblin-install (the one legacy fallback)
18
18
  // no args | -h/--help | any other unrecognized first arg
19
19
  // -> this file's short usage, exit 2. A bare `goblin`
20
20
  // used to fall through into the installer; a typo
@@ -50,23 +50,23 @@ const [cmd, ...rest] = process.argv.slice(2);
50
50
  // No args, a help flag, or an unrecognized first arg: short usage, exit 2. The one survivor of
51
51
  // the old catch-all fallback is the literal `install` first arg — bare `goblin` mapped to the
52
52
  // installer through npm's bin default, and a typo (`goblin inti`) silently installed into
53
- // whatever directory the shell sat in. A bare subcommand-less `goblin install ...` keeps the
53
+ // whatever directory the shell sat in. A bare subcommand-less `gob install ...` keeps the
54
54
  // installer; everything else stops here and names the word it did not know.
55
55
  function usage() {
56
56
  process.stderr.write(
57
57
  [
58
- "goblin <command>",
58
+ "gob <command>",
59
59
  "",
60
- " goblin init start here — the guided first step (detect, class, emit, first verify)",
61
- " goblin verify run the rule matrix against the current repo",
62
- " goblin bans run the ban list (per-pattern red lines over the source tree)",
63
- " goblin audit check recorded dependency claims against live advisory feeds",
64
- " goblin upgrade migrate a repo to the shared global engine at ~/.goblin/engine",
65
- " goblin doctor one detection/drift run across the agent platforms",
66
- " goblin emit write the skills + context block for one platform",
67
- " goblin uninstall --target . remove exactly what an install wrote (preimages)",
60
+ " gob init start here — the guided first step (detect, class, emit, first verify)",
61
+ " gob verify run the rule matrix against the current repo",
62
+ " gob bans run the ban list (per-pattern red lines over the source tree)",
63
+ " gob audit check recorded dependency claims against live advisory feeds",
64
+ " gob upgrade migrate a repo to the shared global engine at ~/.goblin/engine",
65
+ " gob doctor one detection/drift run across the agent platforms",
66
+ " gob emit write the skills + context block for one platform",
67
+ " gob uninstall --target . remove exactly what an install wrote (preimages)",
68
68
  "",
69
- "start here: goblin init",
69
+ "start here: gob init",
70
70
  "uninstall: npm uninstall -g @techgoblin/gobstack",
71
71
  "",
72
72
  ].join("\n"));
@@ -77,7 +77,7 @@ if (cmd === undefined || cmd.startsWith("-")) {
77
77
  process.exit(2);
78
78
  }
79
79
  if (!SCRIPT[cmd] && cmd !== "install" && cmd !== "uninstall") {
80
- process.stderr.write(`goblin: unrecognized command: ${cmd}\n\n`);
80
+ process.stderr.write(`gob: unrecognized command: ${cmd}\n\n`);
81
81
  usage();
82
82
  process.exit(2);
83
83
  }
@@ -87,7 +87,7 @@ let extra = [];
87
87
  if (cmd === "install") {
88
88
  target = "goblin-install"; // the one legacy fallback, kept verbatim
89
89
  } else if (cmd === "uninstall") {
90
- // `goblin uninstall --target <dir>` routes into the installer's uninstall job — the shape
90
+ // `gob uninstall --target <dir>` routes into the installer's uninstall job — the shape
91
91
  // docs/GUIDE.md and README already promise. `--uninstall` is appended FIRST so the user's
92
92
  // own `--target <dir>` and options still parse, and a stray literal `--uninstall` cannot
93
93
  // appear twice.