@techgoblin/gobstack 0.5.0-beta.1 → 0.5.0-beta.3

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
@@ -56,7 +56,7 @@ After installing, in this order:
56
56
 
57
57
  cd <target> && git add -A && git commit # the install is a change like any other
58
58
  gob verify # or .goblin/bin/goblin-verify, inside the target
59
- gob emit --platform <p> # optional, per platform: the agent skills are an opt-in
59
+ gob sync --platform <p> # optional, per platform: the agent skills are an opt-in (gob emit is the same verb)
60
60
  gob audit # once, deliberately: the ONLY network step (SC-07)
61
61
 
62
62
  **A default software-class install (no agent skills — those are `gob emit`'s job) verifies green —
@@ -92,12 +92,13 @@ The measurement and the vacuous-pass reading are in `docs/CONTRACTS.md`.
92
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 |
93
93
  | `gob bans` | run the ban list (per-pattern red lines over the source tree) |
94
94
  | `gob audit` | check recorded dependency claims against live advisory feeds — the only command that touches the network |
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`) |
95
+ | `gob install` | install the harness into a target repo: manifest, verifier, gates, HANDOFF — no agent skills (those are an opt-in: `gob sync --platform <p>`, or `--skills yes`); `--ci-gate yes|no` opts the CI lane in or out (default: the class decides) |
96
96
  | `gob uninstall` | remove everything an install wrote, byte-exactly (`gob install --target <dir> --uninstall` is the same job) |
97
97
  | `gob upgrade` | migrate a repo to the shared global engine at `~/.goblin/engine` — 8 steps, two commits, one report |
98
98
  | `gob doctor` | one run across the platforms below: DETECTED / NOT-DETECTED / DRIFT per platform |
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 |
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 |
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; `gob sync` is the same command under its friendlier name — both spellings work |
100
+ | `gob sync` | the emit verb, renamed (wizard v2): same engine, same flags, same exit contract; `gob emit --help` and `gob sync --help` are byte-identical apart from the verb name |
101
+ | `gob init` | the first-run wizard: detect → class → identity → health check → ci → sync → done, 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; the ci step defaults to no — nothing under .github/ unless you opt in (`--ci-gate yes|no` overrides); `--dry-run` prints the plan and writes nothing |
101
102
 
102
103
  `goblin` remains as a legacy alias for every command above — existing scripts keep working, but
103
104
  new commands and docs use `gob`.
@@ -122,10 +123,11 @@ prints the full write plan first.
122
123
 
123
124
  **Agent skills are opt-in.** A `gob install` writes the neutral harness only — `.goblin/`,
124
125
  `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).
126
+ files belonging to any coding agent. The guided path is `gob init`'s sync screen; the one-shot
127
+ path is `gob sync --platform <p>` after installing (`gob emit --platform <p>` is the same
128
+ command under its original name — both work byte-identically). Repos whose install predates the
129
+ opt-in default keep their skills through `gob upgrade` (the install record names them; only an
130
+ explicit `--skills no`, or `--uninstall`, removes them).
129
131
 
130
132
  ## What it is not
131
133
 
package/bin/goblin CHANGED
@@ -5,7 +5,7 @@
5
5
  # gob bans [--only <id[,id...]>] [--list]
6
6
  # gob audit [--target <dir>] [--print]
7
7
  # gob doctor [--platform <p>] # W4a
8
- # gob emit --platform <p> [...] # W4a/W4b
8
+ # gob emit --platform <p> [...] # W4a/W4b; `gob sync` is the same verb, renamed
9
9
  # gob init [...] # W6: the first-run wizard
10
10
  # gob upgrade [--target .] [...] # W3
11
11
  # gob --version
@@ -46,7 +46,8 @@ gob — the gobstack command line.
46
46
  gob bans [--only <id[,id...]>] [--list]
47
47
  gob audit [--target <dir>] [--print]
48
48
  gob doctor [--platform <p>] [--target <dir>]
49
- gob emit --platform <p> --scope project|global [...]
49
+ gob emit --platform <p> --scope project|global [...] # alias: gob sync
50
+ gob sync --platform <p> --scope project|global [...] # the same verb, friendlier name
50
51
  gob init [--target <dir>] [--class app|A-F] [--dry-run]
51
52
  gob upgrade [--target .] [--dry-run] [--yes] [--engine-dir <path>]
52
53
  gob --version
@@ -94,6 +95,11 @@ case "$CMD" in
94
95
  # same three values verify's wrapper states, propagated verbatim.
95
96
  exec bash "$SRC/bin/goblin-emit" "$@"
96
97
  ;;
98
+ sync)
99
+ # Wizard v2: the friendlier name for the same emission engine. `emit` stays a
100
+ # first-class verb; this case only adds the alias and propagates identically.
101
+ exec bash "$SRC/bin/goblin-emit" "$@"
102
+ ;;
97
103
  init)
98
104
  # W6: the first-run wizard. It drives install/emit/verify and propagates their
99
105
  # exit contract verbatim, like every other subcommand here.
package/bin/goblin-emit CHANGED
@@ -68,9 +68,9 @@ PREIMG="${GOBLIN_PREIMAGES:-$HOME/.goblin-stack/preimages}"
68
68
 
69
69
  usage() {
70
70
  cat <<USAGE
71
- gob emit — per-platform emission (W4a, the seven platforms since W4b).
71
+ gob sync — per-platform skill sync (W4a, the seven platforms since W4b).
72
72
 
73
- gob emit --platform <$_PLA|hermes|copilot|cursor|opencode|codex|$_G1$_G2>
73
+ gob sync --platform <$_PLA|hermes|copilot|cursor|opencode|codex|$_G1$_G2>
74
74
  --scope project|global
75
75
  [--skills core|all|none] [--target <dir>] [--source <path>]
76
76
  [--uninstall] [--unshadow] [--dry-run] [--strict]
@@ -86,6 +86,8 @@ gob emit — per-platform emission (W4a, the seven platforms since W4b).
86
86
  --dry-run print the full write plan (every path + the priced index size), write nothing
87
87
  --strict make a NOT-DETECTED platform a hard error instead of a project-scope allowance
88
88
 
89
+ `gob emit` is the same command under its original name — both names work, byte-identically.
90
+
89
91
  Exit codes: 0 ok or no-op | 1 refusal (with the path and the fix) | 2 bad input.
90
92
  USAGE
91
93
  }
@@ -143,7 +145,7 @@ case "$PLATFORM" in
143
145
  "") printf 'error: emit: --platform is required\n' >&2; usage >&2; exit 2 ;;
144
146
  $_PLA|hermes|copilot|cursor|opencode|codex|$_G1$_G2) ;;
145
147
  *)
146
- die "unknown platform '$PLATFORM' - gob emit ships $_PLA, hermes, copilot, cursor, opencode, codex, $_G1$_G2" 2
148
+ die "unknown platform '$PLATFORM' - gob sync ships $_PLA, hermes, copilot, cursor, opencode, codex, $_G1$_G2" 2
147
149
  ;;
148
150
  esac
149
151
  TSV="$ADAPTERS_DIR/$PLATFORM/adapter.tsv"
package/bin/goblin-init CHANGED
@@ -3,9 +3,13 @@
3
3
  #
4
4
  # gob init [--target <dir>] [--class <software|service|game|research|fleet|A..E|app|agent|desktop>]
5
5
  # [--electron] [--branch <name>] [--email <addr>] [--gate <cmd>]
6
- # [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
6
+ # [--ci-gate yes|no] [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
7
7
  #
8
- # One screen per question, answered steps collapsing into the ✔/◆/○ rail above. On a tty
8
+ # Seven screens, one per question, answered steps collapsing into the ✔/◆/○ rail above.
9
+ # On a tty the class and ci steps are a KEYBOARD RADIO (arrows move the ▸ pointer, Enter
10
+ # commits; a typed number still works), the emit step is CHECKBOXES (space toggles ☑/☐,
11
+ # Enter commits), and the health check and identity prompts show their accepted default in
12
+ # the [default] form. On a tty
9
13
  # the whole wizard frame (logotype, header, answered rows, rail) is redrawn in place from
10
14
  # a saved buffer of the wizard's own lines — never by counting printed output, which the
11
15
  # installer/emit engines would break. Every question has a flag: with the flags set (or
@@ -16,7 +20,7 @@
16
20
  # The wizard is a FRONT END over the existing engines, never a second installer:
17
21
  # .goblin/ + the payload -> bin/goblin-install (its refusal contract — an existing
18
22
  # HANDOFF.md is never overwritten — applies verbatim)
19
- # skills + context blocks -> bin/goblin-emit --scope project (the same code path)
23
+ # skills + context blocks -> gob sync (= gob emit; the same code path, friendlier name)
20
24
  # the verdict -> bin/goblin-verify
21
25
  #
22
26
  # Exit codes: 0 done | 1 a refusal (named path + fix) | 2 bad input, or verify could
@@ -69,13 +73,14 @@ if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
69
73
  fi
70
74
 
71
75
  # ---------------------------------------------------------------- the rail ----
72
- # 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty from a SAVED
76
+ # 7 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty from a SAVED
73
77
  # BUFFER of the wizard's own lines (RAIL_BUF) — never by counting the output between
74
78
  # draws, because the installer/emit engines print freely and break naive accounting.
75
79
  # Over a pipe nothing is saved and nothing rewinds: each screen is one numbered line.
76
- STEP_LABELS=("platforms" "class" "branch/email" "first gate" "emit" "done")
77
- STEP_VAL=("" "" "" "" "" "")
80
+ STEP_LABELS=("detect" "class" "identity" "health check" "ci" "sync" "done")
81
+ STEP_VAL=("" "" "" "" "" "" "")
78
82
  CURRENT=1
83
+ N_STEPS=7
79
84
 
80
85
  # rail_say: one line of wizard output on a tty — stored in the redraw buffer AND
81
86
  # printed with a VT100 clear-line prefix (the redraw may overwrite a longer old line).
@@ -104,7 +109,13 @@ rail_note() {
104
109
  }
105
110
  rail_rewind() {
106
111
  if [ "$TTY_OUT" -eq 1 ]; then
107
- printf '\033[%dA\033[J' "${#RAIL_BUF[@]}"
112
+ # The drawn frame is the buffer PLUS the N_STEPS rail rows rail_draw prints under
113
+ # it. Rewinding by the buffer count alone left the rail rows painted on screen, so
114
+ # the next draw stacked a second frame under them — the duplicate-banner the client
115
+ # caught on a Mac (2026-10-06): every screen ≥2 grew the stack by one frame. Rewind
116
+ # the full drawn height; the count is N_STEPS, so the rail growing again (wizard v2:
117
+ # 6 steps to 7) cannot reintroduce the stack.
118
+ printf '\033[%dA\033[J' "$(( ${#RAIL_BUF[@]} + N_STEPS ))"
108
119
  fi
109
120
  }
110
121
  rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
@@ -113,7 +124,7 @@ rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
113
124
  for line in "${RAIL_BUF[@]}"; do
114
125
  printf '\033[2K%s\n' "$line"
115
126
  done
116
- for i in 1 2 3 4 5 6; do
127
+ for i in 1 2 3 4 5 6 7; do
117
128
  label="${STEP_LABELS[$((i - 1))]}"
118
129
  if [ "$i" -lt "$CURRENT" ]; then
119
130
  glyph="✔"; col="$C_GREEN"
@@ -127,20 +138,135 @@ rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
127
138
  }
128
139
  rail_reset() { RAIL_BUF=(); }
129
140
  cascade() { # the pipe-degraded shape: one numbered line per screen (zero cursor motion)
130
- printf 'gob init [%d/6] %s\n' "$CURRENT" "$1"
141
+ printf 'gob init [%d/%d] %s\n' "$CURRENT" "$N_STEPS" "$1"
131
142
  }
132
143
 
133
- # ask_value <label> <default> -> REPLY (Enter = default; non-tty stdin = default)
134
144
  ask_value() {
135
145
  local label="$1" dflt="$2"
136
146
  if [ "$TTY_IN" -eq 0 ]; then REPLY="$dflt"; return 0; fi
137
- printf ' %s%s%s %s─ %s' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt"
147
+ # The [default] form: what Enter accepts sits on the line, in brackets, before the cursor.
148
+ printf ' %s%s%s %s[%s]%s ' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt" "$C_RESET"
138
149
  read -r REPLY
139
150
  rail_say ""
140
151
  [ -n "$REPLY" ] || REPLY="$dflt"
141
152
  return 0
142
153
  }
143
154
 
155
+ # ---- the interactive selection primitives (tty only) --------------------------
156
+ # The class and ci steps are a RADIO: one ▸ pointer, arrows move it, Enter commits
157
+ # (the number-then-Enter path keeps working). The emit step is CHECKBOXES: space toggles
158
+ # the pointed row between ☑ and ☐, Enter commits. Raw mode is taken for one key and
159
+ # ALWAYS restored on the same line — a wizard that dies in raw mode bricks the shell.
160
+ KEY_UP=$'\033[A'; KEY_DOWN=$'\033[B'
161
+ tty_key() { # -> REPLY_KEY: UP | DOWN | SPACE | ENTER | <char>
162
+ local k rest
163
+ REPLY_KEY="ENTER"
164
+ [ "$TTY_IN" -eq 1 ] || return 0
165
+ stty raw -echo 2>/dev/null
166
+ k=$(dd bs=1 count=1 2>/dev/null)
167
+ rest=""
168
+ if [ "$k" = "$(printf '\033')" ]; then
169
+ rest=$(dd bs=1 count=2 2>/dev/null)
170
+ fi
171
+ stty sane 2>/dev/null
172
+ case "$k" in
173
+ "$(printf '\033')")
174
+ case "$rest" in
175
+ "[A") REPLY_KEY="UP" ;;
176
+ "[B") REPLY_KEY="DOWN" ;;
177
+ *) REPLY_KEY="OTHER" ;;
178
+ esac ;;
179
+ ""|$(printf '\r')|$(printf '\n')) # Enter: CR raw, LF cooked/ICRNL, LF from script(1)
180
+ REPLY_KEY="ENTER" ;;
181
+ " ") # a literal space byte — dd bs=1 count=1 hands it through unreduced
182
+ REPLY_KEY="SPACE" ;;
183
+ *) REPLY_KEY="$k" ;;
184
+ esac
185
+ return 0
186
+ }
187
+
188
+ # ask_radio <prompt> <var-out> <option-label>... -> sets <var-out> to the CHOSEN LABEL.
189
+ # Space/Enter on the pointed row commits; arrows move; a digit pre-positions then commits.
190
+ ask_radio() {
191
+ local prompt="$1" out="$2"; shift 2
192
+ local opts=("$@") n=$# sel=0 i key redraw
193
+ if [ "$TTY_IN" -ne 1 ]; then sel=0; printf -v "$out" '%s' "${opts[$sel]}"; return 0; fi
194
+ redraw=1
195
+ while :; do
196
+ if [ "$redraw" -eq 1 ]; then
197
+ printf '\033[2K\r'
198
+ printf ' %s\n' "$prompt"
199
+ for i in "${!opts[@]}"; do
200
+ if [ "$i" -eq "$sel" ]; then
201
+ printf ' %s▸ %s%s%s\n' "$C_ACCENT" "$C_TEXT" "${opts[$i]}" "$C_RESET"
202
+ else
203
+ printf ' %s%s%s\n' "$C_DIM" "${opts[$i]}" "$C_RESET"
204
+ fi
205
+ done
206
+ redraw=0
207
+ fi
208
+ tty_key; key="$REPLY_KEY"
209
+ case "$key" in
210
+ UP) [ "$sel" -gt 0 ] && sel=$((sel - 1)) && redraw=1 ;;
211
+ DOWN) [ "$sel" -lt $((n - 1)) ] && sel=$((sel + 1)) && redraw=1 ;;
212
+ ENTER) break ;;
213
+ SPACE) break ;;
214
+ [1-9]) [ "$key" -le "$n" ] 2>/dev/null && { sel=$((key - 1)); break; } ;;
215
+ esac
216
+ if [ "$redraw" -eq 1 ]; then
217
+ # rewind the option rows + the prompt line, then redraw
218
+ printf '\033[%dA' "$(( n + 1 ))"
219
+ fi
220
+ done
221
+ printf -v "$out" '%s' "${opts[$sel]}"
222
+ return 0
223
+ }
224
+
225
+ # ask_check_pre <prompt> <pre-ticked-space-list> <label>... -> CHOOSE_FLAGS parallel to
226
+ # the labels, 1 = on (a label named in the pre-tick list starts ☑). Space toggles the
227
+ # pointed row (☑/☐), arrows move, Enter commits. Non-tty stdin leaves the flags exactly
228
+ # as pre-ticked (the caller already decided them).
229
+ CHOOSE_FLAGS=()
230
+ ask_check_pre() {
231
+ local prompt="$1" pretick="$2"; shift 2
232
+ local opts=("$@") n=$# sel=0 i key redraw w
233
+ CHOOSE_FLAGS=()
234
+ for i in "${!opts[@]}"; do
235
+ CHOOSE_FLAGS[i]=0
236
+ for w in $pretick; do [ "${opts[$i]}" = "$w" ] && CHOOSE_FLAGS[i]=1; done
237
+ done
238
+ if [ "$TTY_IN" -ne 1 ]; then return 0; fi
239
+ redraw=1
240
+ while :; do
241
+ if [ "$redraw" -eq 1 ]; then
242
+ printf '\033[2K\r'
243
+ printf ' %s\n' "$prompt"
244
+ for i in "${!opts[@]}"; do
245
+ local mark pointer
246
+ if [ "${CHOOSE_FLAGS[$i]}" -eq 1 ]; then mark="☑"; else mark="☐"; fi
247
+ if [ "$i" -eq "$sel" ]; then
248
+ printf ' %s▸ %s%s %s%s\n' "$C_ACCENT" "$C_TEXT" "$mark" "${opts[$i]}" "$C_RESET"
249
+ else
250
+ printf ' %s%s %s%s\n' "$C_DIM" "$mark" "${opts[$i]}" "$C_RESET"
251
+ fi
252
+ done
253
+ redraw=0
254
+ fi
255
+ tty_key; key="$REPLY_KEY"
256
+ case "$key" in
257
+ UP) [ "$sel" -gt 0 ] && sel=$((sel - 1)) && redraw=1 ;;
258
+ DOWN) [ "$sel" -lt $((n - 1)) ] && sel=$((sel + 1)) && redraw=1 ;;
259
+ SPACE) CHOOSE_FLAGS[$sel]=$(( 1 - CHOOSE_FLAGS[$sel] )); redraw=1 ;;
260
+ ENTER) break ;;
261
+ [1-9]) [ "$key" -le "$n" ] 2>/dev/null && { sel=$((key - 1)); break; } ;;
262
+ esac
263
+ if [ "$redraw" -eq 1 ]; then
264
+ printf '\033[%dA' "$(( n + 1 ))"
265
+ fi
266
+ done
267
+ return 0
268
+ }
269
+
144
270
  # ------------------------------------------------------------------ flags -----
145
271
  TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
146
272
  ELECTRON=0
@@ -155,6 +281,7 @@ while [ $# -gt 0 ]; do
155
281
  --branch) BRANCH="${2:-}"; shift 2 ;;
156
282
  --email) EMAIL="${2:-}"; shift 2 ;;
157
283
  --gate) GATE="${2:-}"; shift 2 ;;
284
+ --ci-gate) CI_GATE="${2:-}"; shift 2 ;;
158
285
  --emit) EMIT="${2:-}"; shift 2 ;;
159
286
  --scope) SCOPE="${2:-}"; shift 2 ;;
160
287
  --yes) YES=1; shift ;;
@@ -188,7 +315,7 @@ rail_seed "${C_ACCENT}▀██▀▀ ▀██▀ ██▀ ▀▀▀▀ ▀
188
315
  rail_seed " ${C_MUTED}discipline around whatever executes${C_RESET}"
189
316
  rail_seed ""
190
317
  rail_seed "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (gobstack v$VERSION)${C_RESET}"
191
- rail_seed " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
318
+ rail_seed " ${C_MUTED}7 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
192
319
  rail_seed ""
193
320
  if [ "$TTY_OUT" -eq 1 ]; then
194
321
  rail_draw
@@ -234,18 +361,18 @@ done
234
361
  STEP_VAL[0]="$C_DIM$DET_N detected$C_RESET"
235
362
  if [ "$TTY_OUT" -eq 1 ]; then
236
363
  rail_say ""
237
- rail_say " ✔ 1. platforms $C_TEXT$DET_N detected$C_RESET"
364
+ rail_say " ✔ 1. detect $C_TEXT$DET_N platform(s) clean$C_RESET"
238
365
  if [ "$DET_N" -eq 0 ]; then
239
- rail_say " ${C_YELLOW}no platform detected here — the wizard will still install the harness; emit later with gob emit$C_RESET"
366
+ rail_say " ${C_YELLOW}no platform detected here — the wizard will still install the harness; sync later with gob sync$C_RESET"
240
367
  else
241
368
  while IFS=$'\t' read -r p proj glob hit; do
242
369
  [ -n "$p" ] || continue
243
370
  case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
244
- rail_say " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
371
+ rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
245
372
  done <<< "$DET_ROWS"
246
373
  fi
247
374
  else
248
- cascade "platforms: $DET_N detected ($(printf '%s' "$DET_ROWS" | awk -F'\t' '{printf "%s ", $1}'))"
375
+ cascade "detect: $DET_N platform(s) detected ($(printf '%s' "$DET_ROWS" | awk -F'\t' '{printf "%s ", $1}'))"
249
376
  fi
250
377
 
251
378
  # ------------------------------------------------------------ screen 3: class -
@@ -256,27 +383,21 @@ CLASS_DEFAULT=1
256
383
  if [ -z "$CLASS" ]; then
257
384
  if [ "$TTY_IN" -eq 1 ]; then
258
385
  rail_say ""
259
- rail_say " ${C_MUTED}what kind of work does this repo do?$C_RESET"
260
- rail_say " $C_TEXT 1 ▸ software shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
261
- rail_say " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
262
- rail_say " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
263
- rail_say " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$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"
267
- printf ' '
268
- read -r REPLY_CLASS
269
386
  ASKED_ANY=1
387
+ # The keyboard radio: arrows move the ▸ pointer, Enter commits, a typed number
388
+ # pre-positions and commits. Non-tty stdin never reaches this branch.
389
+ ask_radio "what kind of work does this repo do?" REPLY_CLASS \
390
+ "software — shipped features, PRs, review gates" \
391
+ "service — backend jobs, config, unattended runs" \
392
+ "game — playable builds, perf budgets" \
393
+ "research — specs, replays, reference corpora" \
394
+ "fleet — fleets, loops, unattended automation"
395
+ CLASS="${REPLY_CLASS%% —*}"
270
396
  rail_say ""
271
397
  else
272
- REPLY_CLASS="1"
398
+ CLASS="software"
273
399
  cascade "class: software (default; --class software|service|game|research|fleet or A-E)"
274
400
  fi
275
- case "$REPLY_CLASS" in
276
- 1) CLASS="software" ;; 2) CLASS="service" ;; 3) CLASS="game" ;;
277
- 4) CLASS="research" ;; 5) CLASS="fleet" ;;
278
- *) CLASS="${REPLY_CLASS:-software}" ;;
279
- esac
280
401
  fi
281
402
  # Resolve every accepted spelling to the canonical class NAME. The letters A-E and the old
282
403
  # taught names (app/agent/desktop) are read-time aliases; anything else is refused here, with
@@ -335,12 +456,12 @@ GATE_DEFAULT="bash tests/run-tests.sh"
335
456
  if [ -z "$GATE" ]; then
336
457
  if [ "$TTY_IN" -eq 1 ]; then
337
458
  rail_say ""
338
- rail_say " ${C_MUTED}one real command that must stay green — suggested for a bash suite:$C_RESET"
459
+ rail_say " ${C_MUTED}what command proves this repo is healthy? it must stay green:$C_RESET"
339
460
  if [ -f "$TARGET/tests/run-tests.sh" ]; then
340
461
  rail_say " $C_DIM$GATE_DEFAULT$C_RESET"
341
- ask_value "gate command" "$GATE_DEFAULT"; GATE="$REPLY"
462
+ ask_value "health check" "$GATE_DEFAULT"; GATE="$REPLY"
342
463
  else
343
- ask_value "gate command" "true"; GATE="$REPLY"
464
+ ask_value "health check" "true"; GATE="$REPLY"
344
465
  if [ "$GATE" = "true" ]; then
345
466
  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"
346
467
  fi
@@ -352,13 +473,49 @@ if [ -z "$GATE" ]; then
352
473
  fi
353
474
  STEP_VAL[3]="$C_TEXT$GATE$C_RESET"
354
475
  if [ "$TTY_OUT" -eq 1 ]; then
355
- rail_say " ✔ $((CURRENT - 1)). first gate $C_TEXT$GATE$C_RESET"
476
+ rail_say " ✔ $((CURRENT - 1)). health check $C_TEXT$GATE$C_RESET"
356
477
  fi
357
478
 
358
- # --------------------------------------------------------- screen 6: the plan --
479
+ # -------------------------------------------------------- screen 6: ci gate ----
480
+ # The CI opt-in. The class decides by default (ci-gate R/O places the workflow, '-'
481
+ # never does); an explicit no opts the part out through --ci-gate no, which the installer
482
+ # records in disabled: so CL-01 reports the opt-out rather than a silent absence.
359
483
  CURRENT=6
360
484
  rail_rewind
361
485
  if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
486
+ CI_DEFAULT="no — nothing under .github/ (the default)"
487
+ if [ "${CI_GATE:-}" = "" ] && [ "$TTY_IN" -eq 1 ] && [ "$YES" -eq 0 ]; then
488
+ rail_say ""
489
+ ASKED_ANY=1
490
+ ask_radio "install a CI workflow that runs the health check on every pull request?" REPLY_CI \
491
+ "no — nothing under .github/ (the default)" \
492
+ "yes — write the CI workflow" \
493
+ "class decides — follow the class contract"
494
+ case "$REPLY_CI" in
495
+ "yes"*) CI_GATE="yes" ;;
496
+ "no"*) CI_GATE="no" ;;
497
+ *) CI_GATE="" ;;
498
+ esac
499
+ rail_say ""
500
+ fi
501
+ CI_GATE="${CI_GATE:-}" # empty = the class decides; yes/no ride the installer's --ci-gate
502
+ case "$CI_GATE" in
503
+ "") CI_CHOICE="no (the default)"; CI_FLAG="--ci-gate no" ;;
504
+ yes) CI_CHOICE="yes"; CI_FLAG="--ci-gate yes" ;;
505
+ no) CI_CHOICE="no (explicit opt-out)"; CI_FLAG="--ci-gate no" ;;
506
+ *) g_err "--ci-gate must be yes or no, got '$CI_GATE'"; exit 2 ;;
507
+ esac
508
+ STEP_VAL[4]="$C_DIM$CI_CHOICE$C_RESET"
509
+ if [ "$TTY_OUT" -eq 1 ]; then
510
+ rail_say " ✔ $((CURRENT - 1)). ci $C_DIM$CI_CHOICE$C_RESET"
511
+ else
512
+ cascade "ci: $CI_CHOICE"
513
+ fi
514
+
515
+ # -------------------------------------------------------- screen 7: the plan ---
516
+ CURRENT=7
517
+ rail_rewind
518
+ if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
362
519
 
363
520
  # The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
364
521
  EMIT_SELECTED=""
@@ -392,33 +549,33 @@ EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
392
549
 
393
550
  if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
394
551
  rail_say ""
395
- rail_say " ${C_MUTED}agent skills are opt-in, per platform — emit them into:$C_RESET"
552
+ ASKED_ANY=1
553
+ # The checkboxes: the detected platforms pre-tick, space toggles the pointed row between
554
+ # ☑ and ☐, arrows move, Enter commits. The install itself writes none either way — the
555
+ # harness stays neutral; these are the per-platform skills opt-in.
556
+ EMIT_OPTS=()
396
557
  for p in $PLATFORMS; do
397
558
  hit=""
398
- while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
559
+ while IFS=$'\t' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
399
560
  [ -n "$hit" ] || continue
400
- rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
561
+ EMIT_OPTS+=("$p")
401
562
  done
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"
404
- printf ' '
405
- read -r REPLY_EMIT
406
- ASKED_ANY=1
407
- rail_say ""
408
- if [ "$REPLY_EMIT" = "x" ] || [ "$REPLY_EMIT" = "none" ]; then
409
- EMIT_SELECTED=""
410
- elif [ -n "$REPLY_EMIT" ]; then
563
+ if [ "${#EMIT_OPTS[@]}" -gt 0 ]; then
564
+ ask_check_pre "agent skills are opt-in, per platform — sync them into:" "$EMIT_SELECTED" "${EMIT_OPTS[@]}"
411
565
  EMIT_SELECTED=""
412
- for w in $REPLY_EMIT; do
413
- ok=""
414
- for p in $PLATFORMS; do [ "$p" = "$w" ] && ok=1; done
415
- [ -n "$ok" ] && EMIT_SELECTED="$EMIT_SELECTED $w" || { g_err "unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
566
+ i=0
567
+ for p in "${EMIT_OPTS[@]}"; do
568
+ [ "${CHOOSE_FLAGS[$i]}" -eq 1 ] && EMIT_SELECTED="$EMIT_SELECTED $p"
569
+ i=$((i + 1))
416
570
  done
571
+ else
572
+ rail_say " ${C_MUTED}no platform detected — nothing to pre-tick; sync later with gob sync --platform <p>$C_RESET"
417
573
  fi
574
+ rail_say ""
418
575
  fi
419
- STEP_VAL[4]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
576
+ STEP_VAL[5]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
420
577
  if [ "$TTY_OUT" -eq 1 ]; then
421
- rail_say " ✔ $((CURRENT - 1)). emit $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
578
+ rail_say " ✔ $((CURRENT - 1)). sync $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
422
579
  fi
423
580
 
424
581
  # ---- the plan box: the wizard never writes until this has been on the screen ----
@@ -432,7 +589,8 @@ add " ${C_DIM}╭────────────────────
432
589
  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"
433
590
  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"
434
591
  add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}gate${C_RESET} $C_TEXT$GATE$C_RESET"
435
- 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")"
592
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}ci${C_RESET} $C_DIM$CI_CHOICE$C_RESET"
593
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}sync${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")"
436
594
  add " ${C_DIM}╰──────────────────────────────────────────────────────╯${C_RESET}"
437
595
  if [ "$DRYRUN" -eq 1 ]; then
438
596
  printf '%s\n' "$BOX"
@@ -462,15 +620,14 @@ fi
462
620
  # never rewinds past its own buffer by counting lines it did not print.
463
621
  if [ "$TTY_OUT" -eq 1 ]; then
464
622
  rail_note ""
465
- rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS$([ "$ELECTRON" -eq 1 ] && printf ' --electron')${C_RESET}"
466
- else
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
623
+ rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS$([ "$ELECTRON" -eq 1 ] && printf ' --electron')$([ -n "$CI_FLAG" ] && printf ' %s' "$CI_FLAG")${C_RESET}"
471
624
  else
472
- bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --skills no --yes
625
+ printf 'gob init [run] goblin-install --target %s --class %s%s%s\n' "$TARGET" "$CLASS" "$([ "$ELECTRON" -eq 1 ] && printf ' --electron')" "$([ -n "$CI_FLAG" ] && printf ' %s' "$CI_FLAG")"
473
626
  fi
627
+ INSTALL_ARGS=(--target "$TARGET" --class "$CLASS" --skills no --yes)
628
+ [ "$ELECTRON" -eq 1 ] && INSTALL_ARGS+=(--electron)
629
+ [ -n "$CI_FLAG" ] && INSTALL_ARGS+=($CI_FLAG)
630
+ bash "$SRC/bin/goblin-install" "${INSTALL_ARGS[@]}"
474
631
  EXIT_INSTALL=$?
475
632
  if [ "$EXIT_INSTALL" -ne 0 ]; then
476
633
  g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
@@ -507,20 +664,20 @@ fi
507
664
  EMITTED=0
508
665
  for p in $EMIT_SELECTED; do
509
666
  if [ "$TTY_OUT" -eq 0 ]; then
510
- printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
667
+ printf 'gob init [run] gob sync --platform %s --scope %s\n' "$p" "$SCOPE"
511
668
  else
512
- rail_note " ${C_MUTED}running: goblin-emit --platform $p --scope $SCOPE${C_RESET}"
669
+ rail_note " ${C_MUTED}running: gob sync --platform $p --scope $SCOPE${C_RESET}"
513
670
  fi
514
671
  bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
515
672
  EXIT_EMIT=$?
516
673
  if [ "$EXIT_EMIT" -ne 0 ]; then
517
- g_err "emit $p failed (exit $EXIT_EMIT) — run 'gob emit --platform $p --scope $SCOPE' to see the refusal again"
674
+ g_err "sync $p failed (exit $EXIT_EMIT) — run 'gob sync --platform $p --scope $SCOPE' to see the refusal again"
518
675
  exit "$EXIT_EMIT"
519
676
  fi
520
677
  EMITTED=$((EMITTED + 1))
521
678
  done
522
679
 
523
- # --------------------------------------------------- screen 6b: done + verify --
680
+ # --------------------------------------------------- screen 7b: done + verify --
524
681
  # Spacing pass: exactly one blank line before each done-screen section; the engine's
525
682
  # verify output and this screen's own summary stay visually separated.
526
683
  if [ "$TTY_OUT" -eq 0 ]; then
@@ -559,11 +716,11 @@ if [ "$DRYRUN" -eq 0 ]; then
559
716
  printf ' %snext:%s\n' "$C_MUTED" "$C_RESET"
560
717
  printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
561
718
  printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
562
- printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
719
+ printf ' 3. %snot synced here: gob sync --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
563
720
  if [ "$EMIT_N" -gt 0 ]; then
564
721
  printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
565
722
  else
566
- printf ' 4. %sagent skills are opt-in per platform: gob emit --platform <p>%s\n' "$C_TEXT" "$C_RESET"
723
+ printf ' 4. %sagent skills are opt-in per platform: gob sync --platform <p>%s\n' "$C_TEXT" "$C_RESET"
567
724
  fi
568
725
  printf '\n'
569
726
  if [ "$VERIFY_RC" -eq 0 ]; then
@@ -19,6 +19,11 @@
19
19
  # --opt-out <part> record the part in disabled: so its required checks are skipped
20
20
  # --uninstall remove exactly the files in installed.json
21
21
  # --re-pin re-record practice_sha256: for an edited standard; nothing else changes
22
+ # --ci-gate yes|no the CI lane opt-in (default: the class decides - ci-gate R/O places
23
+ # .github/workflows/goblin-gate.yml, ci-gate '-' never does). An
24
+ # explicit no opts the part out (recorded in disabled: so CL-01
25
+ # reports the opt-out, never a silent absence); an explicit yes is
26
+ # refused for a class that forbids the part.
22
27
  # --force allow overwriting a file goblin-stack did not create
23
28
  # --yes non-interactive; take the defaults above
24
29
  #
@@ -61,6 +66,11 @@ UNINSTALL=0
61
66
  RE_PIN=0
62
67
  FORCE=0
63
68
  OPT_OUT=""
69
+ # The CI lane opt-in (wizard v2). Empty = the class decides, exactly as before this flag
70
+ # existed: every pre-existing invocation (and every pipe) keeps its byte-identical shape.
71
+ # "no" is an explicit opt-out of the ci-gate part; "yes" is an explicit opt-in, refused for
72
+ # a class that forbids the part (CL-01 would fail it on the very first verify).
73
+ CI_GATE_OPT=""
64
74
  # The electron opt-in (the merged desktop/F class): renders presets/electron-overlay.yaml over
65
75
  # the software preset. Auto-set when --class resolves via the desktop/F/f alias, so the alias
66
76
  # behaves as the old class F did rather than silently installing weaker software.
@@ -88,6 +98,7 @@ while [ $# -gt 0 ]; do
88
98
  --uninstall) UNINSTALL=1; shift ;;
89
99
  --re-pin) RE_PIN=1; shift ;;
90
100
  --opt-out) OPT_OUT="${2:-}"; shift 2 ;;
101
+ --ci-gate) CI_GATE_OPT="${2:-}"; shift 2 ;;
91
102
  --force) FORCE=1; shift ;;
92
103
  --yes) shift ;;
93
104
  -h|--help) usage; exit 0 ;;
@@ -257,6 +268,7 @@ CLASS_RAW="$CLASS"
257
268
  CLASS=$(g_class_canon "$CLASS") \
258
269
  || { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
259
270
  case "$SKILLS" in yes|no) ;; *) g_err "--skills must be yes or no"; exit 2 ;; esac
271
+ case "$CI_GATE_OPT" in ""|yes|no) ;; *) g_err "--ci-gate must be yes or no, got '$CI_GATE_OPT'"; exit 2 ;; esac
260
272
 
261
273
  # W6 migration safety: on a --upgrade (or any re-install) of a repo whose record shows skills
262
274
  # were installed, an OMITTED --skills flag must READ that state, not silently strip it. The
@@ -354,7 +366,15 @@ WANT_RATCHET=1; [ "$(need ratchet)" = "-" ] && WANT_RATCHET=0
354
366
  # The CI lane (G6 A.6). Installed for a class that requires or permits it (A, C, E, F) and never
355
367
  # for one that forbids it (B, D) - where CL-01 then requires the file to be ABSENT, which is why
356
368
  # class_part_artifact keys off this exact path rather than off .github/workflows/.
369
+ # Wizard v2 adds the explicit opt-in/out: the empty default keeps the class-driven shape
370
+ # byte-identical for every existing invocation; `no` opts the part out (recorded in disabled:
371
+ # so CL-01 reports the opt-out rather than a silent absence, the --skills no precedent); `yes`
372
+ # is refused for a class that forbids the part, because the file could never survive verify.
357
373
  WANT_CI_GATE=1; [ "$(need ci-gate)" = "-" ] && WANT_CI_GATE=0
374
+ case "$CI_GATE_OPT" in
375
+ no) WANT_CI_GATE=0; OPT_OUT="${OPT_OUT:+$OPT_OUT,}ci-gate" ;;
376
+ yes) [ "$WANT_CI_GATE" -eq 1 ] || { g_err "--ci-gate yes: class $CLASS forbids the ci-gate part - CI is not in this class's contract (see docs/CI.md)"; exit 2; } ;;
377
+ esac
358
378
  [ "$ARCHIVE" = "true" ] && { WANT_SPEC=0; WANT_REVIEWS=0; SCAFFOLD=no; }
359
379
 
360
380
  OWNER_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
@@ -731,7 +751,7 @@ fi
731
751
  printf ' "source_sha": "%s",\n' "$( cd "$SRC" && git rev-parse HEAD 2>/dev/null || echo unknown )"
732
752
  printf ' "class": "%s",\n' "$CLASS"
733
753
  printf ' "installed_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
734
- printf ' "options": {"skills": "%s", "archive": %s, "parts": "%s"},\n' "$SKILLS" "$ARCHIVE" "${PARTS:-all}"
754
+ printf ' "options": {"skills": "%s", "archive": %s, "parts": "%s", "ci_gate": "%s"},\n' "$SKILLS" "$ARCHIVE" "${PARTS:-all}" "${CI_GATE_OPT:-class}"
735
755
  printf ' "files": {\n'
736
756
  awk -F'\t' 'NF==2 {printf " \"%s\": \"%s\",\n", $1, $2}' "$TMP/files.tsv" | sed '$ s/,$//'
737
757
  printf ' },\n'
package/bin/goblin.js CHANGED
@@ -44,7 +44,9 @@ if (arg0 === "--version" || arg0 === "-V" || arg0 === "-v") {
44
44
  process.exit(0);
45
45
  }
46
46
 
47
- const SCRIPT = { verify: "goblin-verify", bans: "goblin-bans", audit: "goblin-audit", upgrade: "goblin-upgrade", doctor: "goblin-doctor", emit: "goblin-emit", init: "goblin-init" };
47
+ const SCRIPT = { verify: "goblin-verify", bans: "goblin-bans", audit: "goblin-audit", upgrade: "goblin-upgrade", doctor: "goblin-doctor", emit: "goblin-emit", sync: "goblin-emit", init: "goblin-init" };
48
+ // `sync` is the friendlier name for `emit` (wizard v2): same engine, same flags, same exit
49
+ // contract. `emit` stays a first-class verb - nothing is removed, this row only adds an alias.
48
50
  const [cmd, ...rest] = process.argv.slice(2);
49
51
 
50
52
  // No args, a help flag, or an unrecognized first arg: short usage, exit 2. The one survivor of
@@ -57,13 +59,13 @@ function usage() {
57
59
  [
58
60
  "gob <command>",
59
61
  "",
60
- " gob init start here — the guided first step (detect, class, emit, first verify)",
62
+ " gob init start here — the guided first step (health, class, gate, ci, sync, verify)",
61
63
  " gob verify run the rule matrix against the current repo",
62
64
  " gob bans run the ban list (per-pattern red lines over the source tree)",
63
65
  " gob audit check recorded dependency claims against live advisory feeds",
64
66
  " gob upgrade migrate a repo to the shared global engine at ~/.goblin/engine",
65
67
  " gob doctor one detection/drift run across the agent platforms",
66
- " gob emit write the skills + context block for one platform",
68
+ " gob sync write the skills + context block for one platform (alias: gob emit)",
67
69
  " gob uninstall --target . remove exactly what an install wrote (preimages)",
68
70
  "",
69
71
  "start here: gob init",
package/docs/GUIDE.md CHANGED
@@ -88,9 +88,9 @@ one way this guide installs it.
88
88
  **Do not install into a real project yet.** You want to see what it does before it touches
89
89
  something you care about.
90
90
 
91
- The guided path is `gob init` — one screen per question (class, branch/email, first
92
- gate, which platforms to emit), every question also answerable by flag, `--dry-run` to
93
- see the plan first:
91
+ The guided path is `gob init` — one screen per question (health check, class,
92
+ identity (branch/email), the health check, the CI opt-in, which platforms to sync), every question also
93
+ answerable by flag, `--dry-run` to see the plan first:
94
94
 
95
95
  mkdir -p /tmp/gs-try && cd /tmp/gs-try
96
96
  git init -b main
@@ -100,6 +100,13 @@ see the plan first:
100
100
  gob init --target . --class software --branch main --email "you@example.com" \
101
101
  --gate "bash tests/run-tests.sh" --yes
102
102
 
103
+ The wizard's ci step asks whether the gate should also run in CI
104
+ (`.github/workflows/goblin-gate.yml`). The wizard's default is **no** — nothing lands under `.github/` unless you opt in. (Outside the wizard, `gob install`'s own default is the class decides: a class whose
105
+ contract requires or permits the ci-gate part gets the workflow, one that forbids it never does.)
106
+ `--ci-gate yes|no` overrides: an explicit `no` is recorded as an opt-out (so verify reports the
107
+ opt-out, never a silent absence), and an explicit `yes` is refused for a class that forbids the
108
+ part.
109
+
103
110
  or the plain installer this wizard drives, if you prefer the one-shot shape:
104
111
 
105
112
  gob install --target . --class software
@@ -112,13 +119,14 @@ Expected output (this is a real transcript, trimmed):
112
119
  1. cd /tmp/gs-try && git add -A && git commit # the install is a change like any other
113
120
  2. .goblin/bin/goblin-verify # or add .goblin/bin to PATH
114
121
  3. edit .goblin/goblin.yaml: replace the default gate with your real commands (P8 step 3)
115
- 4. agent skills are opt-in: gob emit --platform <p> # hermes, claude, copilot, cursor, opencode, codex, gemini
122
+ 4. agent skills are opt-in: gob sync --platform <p> # hermes, claude, copilot, cursor, opencode, codex, gemini
116
123
 
117
124
  **`created 25`** is the installer's count of the files it **tracks** — the 16 in its `files`
118
125
  map, the 8 it `owns`, and `.gitignore`. It writes **26**: the 26th is `.goblin/installed.json`,
119
126
  the record it keeps for itself, which it writes but does not count. It has written nothing
120
127
  outside this directory. The default install ships **no agent skills** — the harness is neutral,
121
- and `gob emit --platform <p>` is the per-platform opt-in (the old `--skills yes` default is
128
+ and `gob sync --platform <p>` is the per-platform opt-in (`gob emit` is the same command under
129
+ its original name; the old `--skills yes` default is
122
130
  still there for repos that want the Hermes project tier vendored).
123
131
 
124
132
  ### Why `git init -b main` matters
@@ -448,7 +456,7 @@ with no fallback** — a repo is never judged by an engine it did not declare. A
448
456
  says `mode=global` keeps hashing whatever files it still holds; the engine's own identity prints in
449
457
  every run's footer (`engine: mode=… cli_sha256=… enforcement_tsv_sha256=…`). The same commands are
450
458
  available outside any repo through the npm CLI: `gob verify` / `gob bans` / `gob audit` /
451
- `gob doctor` / `gob emit` / `gob upgrade` / `gob --version`.
459
+ `gob doctor` / `gob sync` / `gob upgrade` / `gob --version`.
452
460
 
453
461
  **Migrating a repo to the global engine (W3):**
454
462
 
@@ -476,7 +484,7 @@ engine and silently de-migrate the record).
476
484
  |---|---|
477
485
  | `goblin-install` | `0` ok · `1` a refusal (with the path and the fix) · `2` bad input |
478
486
  | `goblin-verify` | `0` all checks passed · `1` a check failed · `2` could not run · `3` the manifest itself is broken |
479
- | `goblin` (npm CLI) | propagates the subcommand's codes verbatim — `verify`/`bans`/`audit`/`--version`; `install`/`uninstall`/`re-pin`/`upgrade` route into `goblin-install` (`upgrade` migrates to the global engine: `0` ok · `1` refusal · `2` bad input); `doctor`/`emit` carry the same contract: `doctor` exits `0` every probed platform DETECTED and clean · `1` any DRIFT · `2` nothing to probe, and `emit` exits `0` ok or no-op · `1` refusal (with the path and the fix) · `2` bad input or unknown platform |
487
+ | `goblin` (npm CLI) | propagates the subcommand's codes verbatim — `verify`/`bans`/`audit`/`--version`; `install`/`uninstall`/`re-pin`/`upgrade` route into `goblin-install` (`upgrade` migrates to the global engine: `0` ok · `1` refusal · `2` bad input); `doctor`/`emit` carry the same contract: `doctor` exits `0` every probed platform DETECTED and clean · `1` any DRIFT · `2` nothing to probe, and `emit` exits `0` ok or no-op · `1` refusal (with the path and the fix) · `2` bad input or unknown platform; `sync` is the same verb renamed and propagates identically |
480
488
  | platforms (W4b) | `emit`/`doctor` cover seven: `claude`, `hermes`, `copilot`, `cursor`, `opencode`, `codex`, `gemini` — each detected via its own anchor (`~/.claude`, `~/.hermes`, `~/.copilot`, `~/.cursor`, `~/.config/opencode`, `~/.codex`, `~/.gemini`); codex and gemini carry `partial` command-blocking (see LIMITS #47) |
481
489
 
482
490
  `3` is the one to notice: it means gobstack's own rule table is malformed, not your project.
@@ -583,7 +591,7 @@ with *"prove it was broken first"* — it is the one practice that survives cont
583
591
  # 1. try it somewhere disposable
584
592
  mkdir -p /tmp/gs-try && cd /tmp/gs-try
585
593
  git init -b main
586
- gob install --target . --class software # expect: created 25 (no skills — those are gob emit)
594
+ gob install --target . --class software # expect: created 25 (no skills — those are gob sync)
587
595
 
588
596
  # 2. commit and check
589
597
  git add -A && git commit -m "chore: install gobstack"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techgoblin/gobstack",
3
- "version": "0.5.0-beta.1",
3
+ "version": "0.5.0-beta.3",
4
4
  "description": "Agent-discipline toolkit: one verify command, an enforcement matrix, and LIMITS. bash engine, npm shim.",
5
5
  "bin": {
6
6
  "goblin": "bin/goblin.js",
@@ -22,10 +22,11 @@ not a rule dump — the rules live in one executable place, and facts beat requi
22
22
  harness on purpose, so nothing here assumes which coding agent you use. To install the
23
23
  procedure skills for your tool:
24
24
 
25
- gob emit --platform <p>
25
+ gob sync --platform <p>
26
26
 
27
27
  (`--scope project` writes them inside this repo; `--scope global` writes them for your user.)
28
- Run `gob emit` with no platform to see the list. If skills were installed here, they live
28
+ Run `gob sync` with no platform to see the list (`gob emit` is the same command under its
29
+ original name). If skills were installed here, they live
29
30
  under the platform's own directory, and `gob verify` hashes them.
30
31
 
31
32
  Two things this repo does not do: it does not choose models (a role resolves through the