@techgoblin/gobstack 0.5.0-beta.8 → 0.6.0-alpha.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +161 -124
  3. package/VERSION +1 -1
  4. package/automations/drift-audit.sh +4 -4
  5. package/bans/layer-check.sh +10 -8
  6. package/bin/goblin +69 -57
  7. package/bin/goblin-audit +11 -13
  8. package/bin/goblin-bans +11 -11
  9. package/bin/goblin-extras +342 -0
  10. package/bin/goblin-init +382 -711
  11. package/bin/goblin-install +160 -114
  12. package/bin/goblin-lib.sh +234 -1
  13. package/bin/goblin-map +226 -21
  14. package/bin/goblin-mcp.js +492 -0
  15. package/bin/goblin-model +4 -4
  16. package/bin/goblin-upgrade +1 -1
  17. package/bin/goblin-verify +159 -145
  18. package/bin/goblin.js +35 -51
  19. package/docs/ADOPTION.md +15 -15
  20. package/docs/CONTRACTS.md +16 -15
  21. package/docs/DESIGN.md +1 -1
  22. package/docs/ENFORCEMENT.md +89 -90
  23. package/docs/FLOWS.md +1 -1
  24. package/docs/GLOSSARY.md +3 -3
  25. package/docs/GUARDRAILS.md +5 -5
  26. package/docs/GUIDE.md +194 -177
  27. package/docs/INTEGRATION.md +1 -1
  28. package/docs/LIMITS.md +25 -0
  29. package/docs/LOOP.md +12 -12
  30. package/docs/RE-PLAYBOOK.md +3 -3
  31. package/docs/ROLES.md +5 -5
  32. package/extras-catalogue/catalogue.tsv +42 -0
  33. package/extras-catalogue/payload/README.md +14 -0
  34. package/extras-catalogue/payload/taste-skill/taste/REFERENCE.md +3 -0
  35. package/extras-catalogue/payload/taste-skill/taste/SKILL.md +9 -0
  36. package/manifest/bans.tsv +8 -8
  37. package/manifest/classes.tsv +3 -3
  38. package/manifest/enforcement.tsv +40 -40
  39. package/manifest/glossary.tsv +3 -3
  40. package/manifest/playbooks.tsv +1 -1
  41. package/package.json +3 -1
  42. package/presets/electron-overlay.yaml +2 -2
  43. package/presets/fleet.yaml +8 -7
  44. package/presets/game.yaml +1 -1
  45. package/presets/research.yaml +1 -1
  46. package/presets/service.yaml +1 -1
  47. package/presets/software.yaml +1 -1
  48. package/skills/goblin-bootstrap/SKILL.md +2 -2
  49. package/templates/AGENTS.md.tmpl +8 -18
  50. package/templates/HANDOFF.md.tmpl +5 -5
  51. package/templates/agents-block.tmpl +45 -0
  52. package/templates/audit-waiver.tsv.tmpl +2 -2
  53. package/templates/boundary-waivers.tmpl +1 -1
  54. package/templates/checks/gate.sh.tmpl +6 -6
  55. package/templates/install-hooks.allowlist.tmpl +1 -1
  56. package/templates/ci/goblin-gate.yml.tmpl +0 -46
  57. package/templates/goblin.yaml.tmpl +0 -146
  58. package/templates/loop/decisions.tsv.tmpl +0 -1
  59. package/templates/loop/predicate.tmpl +0 -16
package/bin/goblin-init CHANGED
@@ -1,294 +1,61 @@
1
1
  #!/usr/bin/env bash
2
- # goblin-init — the W6 first-run wizard (`gob init`).
2
+ # goblin-init — `gob init`: the AI-DRIVEN first step (v2 prompt engine).
3
3
  #
4
- # gob init [--target <dir>] [--class <software|service|game|research|fleet|A..E|app|agent|desktop>]
5
- # [--electron] [--branch <name>] [--email <addr>] [--gate <cmd>]
6
- # [--ci-gate yes|no] [--emit <p[,p..]>] [--scope project|global] [--yes]
7
- # [--no-verify] [--dry-run]
4
+ # gob init print the AGENT BRIEF + proposal schema
5
+ # gob init --heuristic append the heuristic detection (fallback)
6
+ # gob init --write <proposal> [--yes] validate the proposal and install it
7
+ # gob init --target <dir> aim at a directory that is not $PWD
8
+ # gob init --with-mcp-config write .mcp.json (repo-distributed MCP
9
+ # registration; ask-once, never overwrites)
10
+ # gob init --dry-run --write without writing anything
8
11
  #
9
- # Seven screens, one per question, answered steps collapsing into the ✔/◆/○ rail above.
10
- # On a tty the class and ci steps are a KEYBOARD RADIO (arrows move the ▸ pointer, Enter
11
- # commits; a typed number still works), the emit step is CHECKBOXES (space toggles ☑/☐,
12
- # Enter commits), and the health check and identity prompts show their accepted default in
13
- # the [default] form. On a tty
14
- # the whole wizard frame (logotype, header, answered rows, rail) is redrawn in place from
15
- # a saved buffer of the wizard's own lines — never by counting printed output, which the
16
- # installer/emit engines would break. Every question has a flag: with the flags set (or
17
- # --yes, or a non-tty stdin) the wizard asks nothing and runs from the flags and the
18
- # defaults — non-interactive parity is structural, not a mode. Over a pipe the redraw
19
- # degrades to a numbered cascade (rustup shape) with zero cursor motion.
12
+ # The CLI calls NO AI API. A bare `gob init` prints:
13
+ # 1. the AGENT BRIEF — a structured prompt telling whichever agent is already running
14
+ # in this repo (the coding agent the developer already uses) exactly what to scan
15
+ # (package.json, routes, tests, CI configs) and what to decide (class, branch,
16
+ # owner email, the first gate command);
17
+ # 2. the SCHEMA of the proposal file the agent must write — AGENTS.md frontmatter
18
+ # pre-filled + a `## gob init summary` section.
19
+ # The agent fulfils the brief with its own tools, writes the proposal, and the human (or
20
+ # the agent's own next step) runs `gob init --write <proposal>` to validate and install:
21
+ # - the frontmatter keys are validated against the engine's key set (unknown key,
22
+ # bad class, non-boolean archive -> exit 2, nothing written);
23
+ # - AGENTS.md (body + gob block) and .gob/engine (the vendored verifier + manifest)
24
+ # are written; HANDOFF.md is written once; the sync-translator step runs to emit
25
+ # platform files for the proposal's `sync:` line. CI is never part of any output.
20
26
  #
21
- # The wizard is a FRONT END over the existing engines, never a second installer:
22
- # .goblin/ + the payload -> bin/goblin-install (its refusal contract — an existing
23
- # HANDOFF.md is never overwritten — applies verbatim)
24
- # skills + context blocks -> gob sync (= gob emit; the same code path, friendlier name)
25
- # the verdict -> bin/goblin-verify
27
+ # The heuristic detector (package.json scripts + route tokens, from bin/goblin-map) is
28
+ # the --heuristic FALLBACK: appended to the brief as pre-scanned hints, never the answer.
26
29
  #
27
- # Exit codes: 0 done | 1 a refusal (named path + fix) | 2 bad input, or verify could
28
- # not run. The verify VERDICT is the done screen's report, not this command's verdict:
29
- # a fresh install is red on CM-03 until the user commits it — the documented day-one
30
- # state, printed here with the counts and the next steps. install/emit refusals and a
31
- # verify that cannot run are propagated, never wrapped into a 0.
32
- #
33
- # Palette: the family design tokens — accent #F54E00 (logotype, active step, the ◆ rail
34
- # glyph), green #4ade80 (✔), text #e4e4e6, muted #9d9da3, dim #808088 (borders, idle ○),
35
- # critical #ff5f57 (FAIL), medium #febc2e (advisory). Truecolor when COLORTERM allows,
36
- # nearest 16-colour slot otherwise, off under NO_COLOR or a pipe. No background is ever
37
- # painted. bash-only, zero deps, offline: no npm, no jq, no yq, no network.
30
+ # Exit codes: 0 printed or installed | 1 a refusal (named path + fix) | 2 bad input.
31
+ # No npm, no jq, no yq, no network, no AI API. bash/awk/sed/grep only.
38
32
 
39
33
  set -uo pipefail
40
34
 
41
35
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
42
36
  SRC=$(cd "$SELF_DIR/.." && pwd)
43
- if [ -f "$SRC/bin/goblin-lib.sh" ]; then
44
- # shellcheck disable=SC1091
45
- . "$SRC/bin/goblin-lib.sh"
46
- else
47
- g_err() { printf 'error: %s\n' "$*" >&2; }
48
- g_info() { printf '%s\n' "$*"; }
49
- g_expand_tilde() { case "$1" in "~"|"~/"*) printf '%s\n' "$HOME/${1#\~/}" ;; *) printf '%s\n' "$1" ;; esac; }
50
- fi
37
+ # shellcheck source=goblin-lib.sh
38
+ . "$SELF_DIR/goblin-lib.sh"
51
39
  VERSION=$(cat "$SRC/VERSION" 2>/dev/null || printf 'unknown')
40
+ # The extras catalogue (overridable for tests, the same env var goblin-extras reads).
41
+ CATALOGUE_TSV="${GOB_EXTRAS_CATALOGUE:-$SRC/extras-catalogue/catalogue.tsv}"
52
42
 
53
- # ---------------------------------------------------------------- palette -----
54
- # The family design tokens, mapped to ANSI. Truecolor when COLORTERM says so, else the
55
- # nearest 16-colour slot. NO_COLOR or a piped stdout turns colour off entirely.
56
- TTY_OUT=0; [ -t 1 ] && TTY_OUT=1
57
- TTY_IN=0; [ -t 0 ] && TTY_IN=1
58
- C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET=""
59
- # Single-accent discipline: one accent (#F54E00) + green + text + muted + dim. No bold
60
- # is ever emitted — bold was a second emphasis voice and is gone from every screen.
61
- if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
62
- C_RESET=$'\033[0m'
63
- case "${COLORTERM:-}" in
64
- truecolor|24bit)
65
- C_ACCENT=$'\033[38;2;245;78;0m'; C_GREEN=$'\033[38;2;74;222;128m'
66
- C_TEXT=$'\033[38;2;228;228;230m'; C_MUTED=$'\033[38;2;157;157;163m'
67
- C_DIM=$'\033[38;2;128;128;136m'; C_RED=$'\033[38;2;255;95;87m'
68
- C_YELLOW=$'\033[38;2;254;188;46m' ;;
69
- *)
70
- C_ACCENT=$'\033[91m'; C_GREEN=$'\033[92m'; C_TEXT=$'\033[97m'
71
- C_MUTED=$'\033[90m'; C_DIM=$'\033[90m'; C_RED=$'\033[91m'
72
- C_YELLOW=$'\033[93m' ;;
73
- esac
74
- fi
75
-
76
- # ---------------------------------------------------------------- the rail ----
77
- # 7 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty from a SAVED
78
- # BUFFER of the wizard's own lines (RAIL_BUF) — never by counting the output between
79
- # draws, because the installer/emit engines print freely and break naive accounting.
80
- # Over a pipe nothing is saved and nothing rewinds: each screen is one numbered line.
81
- STEP_LABELS=("detect" "class" "identity" "health check" "ci" "sync" "done")
82
- STEP_VAL=("" "" "" "" "" "" "")
83
- CURRENT=1
84
- N_STEPS=7
85
-
86
- # rail_say: one line of wizard output on a tty — stored in the redraw buffer AND
87
- # printed with a VT100 clear-line prefix (the redraw may overwrite a longer old line).
88
- # Over a pipe this is exactly printf '%s\n' and nothing is stored.
89
- rail_say() {
90
- if [ "$TTY_OUT" -eq 1 ]; then
91
- RAIL_BUF+=("$*")
92
- printf '\033[2K%s\n' "$*"
93
- else
94
- printf '%s\n' "$*"
95
- fi
96
- }
97
- # rail_seed: the pre-draw frame (logo + header). On a tty it only FILLS the buffer —
98
- # rail_draw prints it; over a pipe nothing is buffered and it prints directly.
99
- rail_seed() {
100
- if [ "$TTY_OUT" -eq 1 ]; then
101
- RAIL_BUF+=("$*")
102
- else
103
- printf '%s\n' "$*"
104
- fi
105
- }
106
- # rail_note: a line that prints now but never joins the redraw buffer (engine output,
107
- # transcripts, the verify echo): the next redraw wipes it with its clear-below.
108
- rail_note() {
109
- printf '%s\n' "$*"
110
- }
111
- rail_rewind() {
112
- if [ "$TTY_OUT" -eq 1 ]; then
113
- # The drawn frame is the buffer PLUS the N_STEPS rail rows rail_draw prints under
114
- # it. Rewinding by the buffer count alone left the rail rows painted on screen, so
115
- # the next draw stacked a second frame under them — the duplicate-banner the client
116
- # caught on a Mac (2026-10-06): every screen ≥2 grew the stack by one frame. Rewind
117
- # the full drawn height; the count is N_STEPS, so the rail growing again (wizard v2:
118
- # 6 steps to 7) cannot reintroduce the stack.
119
- printf '\033[%dA\033[J' "$(( ${#RAIL_BUF[@]} + N_STEPS ))"
120
- fi
121
- }
122
- rail_draw() { # reprint the saved buffer, then the live rail row for $CURRENT
123
- local i line glyph col label
124
- if [ "$TTY_OUT" -eq 0 ]; then return 0; fi
125
- for line in "${RAIL_BUF[@]}"; do
126
- printf '\033[2K%s\n' "$line"
127
- done
128
- for i in 1 2 3 4 5 6 7; do
129
- label="${STEP_LABELS[$((i - 1))]}"
130
- if [ "$i" -lt "$CURRENT" ]; then
131
- glyph="✔"; col="$C_GREEN"
132
- elif [ "$i" -eq "$CURRENT" ]; then
133
- glyph="◆"; col="$C_ACCENT"
134
- else
135
- glyph="○"; col="$C_DIM"
136
- fi
137
- 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"
138
- done
139
- }
140
- rail_reset() { RAIL_BUF=(); }
141
- cascade() { # the pipe-degraded shape: one numbered line per screen (zero cursor motion)
142
- printf 'gob init [%d/%d] %s\n' "$CURRENT" "$N_STEPS" "$1"
143
- }
144
-
145
- ask_value() {
146
- local label="$1" dflt="$2"
147
- if [ "$TTY_IN" -eq 0 ]; then REPLY="$dflt"; return 0; fi
148
- # The [default] form: what Enter accepts sits on the line, in brackets, before the cursor.
149
- printf ' %s%s%s %s[%s]%s ' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt" "$C_RESET"
150
- read -r REPLY
151
- rail_say ""
152
- [ -n "$REPLY" ] || REPLY="$dflt"
153
- return 0
154
- }
155
-
156
- # ---- the interactive selection primitives (tty only) --------------------------
157
- # The class and ci steps are a RADIO: one ▸ pointer, arrows move it, Enter commits
158
- # (the number-then-Enter path keeps working). The emit step is CHECKBOXES: space toggles
159
- # the pointed row between ☑ and ☐, Enter commits. Raw mode is taken for one key and
160
- # ALWAYS restored on the same line — a wizard that dies in raw mode bricks the shell.
161
- KEY_UP=$'\033[A'; KEY_DOWN=$'\033[B'
162
- tty_key() { # -> REPLY_KEY: UP | DOWN | SPACE | ENTER | <char>
163
- local k rest
164
- REPLY_KEY="ENTER"
165
- [ "$TTY_IN" -eq 1 ] || return 0
166
- stty raw -echo 2>/dev/null
167
- k=$(dd bs=1 count=1 2>/dev/null)
168
- rest=""
169
- if [ "$k" = "$(printf '\033')" ]; then
170
- rest=$(dd bs=1 count=2 2>/dev/null)
171
- fi
172
- stty sane 2>/dev/null
173
- case "$k" in
174
- "$(printf '\033')")
175
- case "$rest" in
176
- "[A") REPLY_KEY="UP" ;;
177
- "[B") REPLY_KEY="DOWN" ;;
178
- *) REPLY_KEY="OTHER" ;;
179
- esac ;;
180
- ""|$(printf '\r')|$(printf '\n')) # Enter: CR raw, LF cooked/ICRNL, LF from script(1)
181
- REPLY_KEY="ENTER" ;;
182
- " ") # a literal space byte — dd bs=1 count=1 hands it through unreduced
183
- REPLY_KEY="SPACE" ;;
184
- *) REPLY_KEY="$k" ;;
185
- esac
186
- return 0
187
- }
188
-
189
- # ask_radio <prompt> <var-out> <option-label>... -> sets <var-out> to the CHOSEN LABEL.
190
- # Space/Enter on the pointed row commits; arrows move; a digit pre-positions then commits.
191
- ask_radio() {
192
- local prompt="$1" out="$2"; shift 2
193
- local opts=("$@") n=$# sel=0 i key redraw
194
- if [ "$TTY_IN" -ne 1 ]; then sel=0; printf -v "$out" '%s' "${opts[$sel]}"; return 0; fi
195
- redraw=1
196
- while :; do
197
- if [ "$redraw" -eq 1 ]; then
198
- printf '\033[2K\r'
199
- printf ' %s\n' "$prompt"
200
- for i in "${!opts[@]}"; do
201
- if [ "$i" -eq "$sel" ]; then
202
- printf ' %s▸ %s%s%s\n' "$C_ACCENT" "$C_TEXT" "${opts[$i]}" "$C_RESET"
203
- else
204
- printf ' %s%s%s\n' "$C_DIM" "${opts[$i]}" "$C_RESET"
205
- fi
206
- done
207
- redraw=0
208
- fi
209
- tty_key; key="$REPLY_KEY"
210
- case "$key" in
211
- UP) [ "$sel" -gt 0 ] && sel=$((sel - 1)) && redraw=1 ;;
212
- DOWN) [ "$sel" -lt $((n - 1)) ] && sel=$((sel + 1)) && redraw=1 ;;
213
- ENTER) break ;;
214
- SPACE) break ;;
215
- [1-9]) [ "$key" -le "$n" ] 2>/dev/null && { sel=$((key - 1)); break; } ;;
216
- esac
217
- if [ "$redraw" -eq 1 ]; then
218
- # rewind the option rows + the prompt line, then redraw
219
- printf '\033[%dA' "$(( n + 1 ))"
220
- fi
221
- done
222
- printf -v "$out" '%s' "${opts[$sel]}"
223
- return 0
224
- }
225
-
226
- # ask_check_pre <prompt> <pre-ticked-space-list> <label>... -> CHOOSE_FLAGS parallel to
227
- # the labels, 1 = on (a label named in the pre-tick list starts ☑). Space toggles the
228
- # pointed row (☑/☐), arrows move, Enter commits. Non-tty stdin leaves the flags exactly
229
- # as pre-ticked (the caller already decided them).
230
- CHOOSE_FLAGS=()
231
- ask_check_pre() {
232
- local prompt="$1" pretick="$2"; shift 2
233
- local opts=("$@") n=$# sel=0 i key redraw w
234
- CHOOSE_FLAGS=()
235
- for i in "${!opts[@]}"; do
236
- CHOOSE_FLAGS[i]=0
237
- for w in $pretick; do [ "${opts[$i]}" = "$w" ] && CHOOSE_FLAGS[i]=1; done
238
- done
239
- if [ "$TTY_IN" -ne 1 ]; then return 0; fi
240
- redraw=1
241
- while :; do
242
- if [ "$redraw" -eq 1 ]; then
243
- printf '\033[2K\r'
244
- printf ' %s\n' "$prompt"
245
- for i in "${!opts[@]}"; do
246
- local mark pointer
247
- if [ "${CHOOSE_FLAGS[$i]}" -eq 1 ]; then mark="☑"; else mark="☐"; fi
248
- if [ "$i" -eq "$sel" ]; then
249
- printf ' %s▸ %s%s %s%s\n' "$C_ACCENT" "$C_TEXT" "$mark" "${opts[$i]}" "$C_RESET"
250
- else
251
- printf ' %s%s %s%s\n' "$C_DIM" "$mark" "${opts[$i]}" "$C_RESET"
252
- fi
253
- done
254
- redraw=0
255
- fi
256
- tty_key; key="$REPLY_KEY"
257
- case "$key" in
258
- UP) [ "$sel" -gt 0 ] && sel=$((sel - 1)) && redraw=1 ;;
259
- DOWN) [ "$sel" -lt $((n - 1)) ] && sel=$((sel + 1)) && redraw=1 ;;
260
- SPACE) CHOOSE_FLAGS[$sel]=$(( 1 - CHOOSE_FLAGS[$sel] )); redraw=1 ;;
261
- ENTER) break ;;
262
- [1-9]) [ "$key" -le "$n" ] 2>/dev/null && { sel=$((key - 1)); break; } ;;
263
- esac
264
- if [ "$redraw" -eq 1 ]; then
265
- printf '\033[%dA' "$(( n + 1 ))"
266
- fi
267
- done
268
- return 0
269
- }
43
+ g_info() { printf '%s\n' "$*"; }
270
44
 
271
- # ------------------------------------------------------------------ flags -----
272
- TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
273
- ELECTRON=0
274
- YES=0; DRYRUN=0; NOVERIFY=0
275
45
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
276
46
 
47
+ # ------------------------------------------------------------------ flags -----
48
+ TARGET="" WRITE="" HEURISTIC=0 DRYRUN=0 RESYNC=0 WITH_MCP=0
277
49
  while [ $# -gt 0 ]; do
278
50
  case "$1" in
279
- --target) TARGET="${2:-}"; shift 2 ;;
280
- --class) CLASS="${2:-}"; shift 2 ;;
281
- --electron) ELECTRON=1; shift ;;
282
- --branch) BRANCH="${2:-}"; shift 2 ;;
283
- --email) EMAIL="${2:-}"; shift 2 ;;
284
- --gate) GATE="${2:-}"; shift 2 ;;
285
- --ci-gate) CI_GATE="${2:-}"; shift 2 ;;
286
- --emit) EMIT="${2:-}"; shift 2 ;;
287
- --scope) SCOPE="${2:-}"; shift 2 ;;
288
- --yes) YES=1; shift ;;
289
- --no-verify) NOVERIFY=1; shift ;;
290
- --dry-run) DRYRUN=1; shift ;;
291
- -h|--help) usage; exit 0 ;;
51
+ --target) TARGET="${2:-}"; shift 2 ;;
52
+ --write) WRITE="${2:-}"; shift 2 ;;
53
+ --heuristic) HEURISTIC=1; shift ;;
54
+ --resync) RESYNC=1; shift ;; # init-internal: re-run the sync step (hidden)
55
+ --with-mcp-config) WITH_MCP=1; shift ;;
56
+ --dry-run) DRYRUN=1; shift ;;
57
+ --yes) shift ;; # accepted for pipe parity; --write is already explicit
58
+ -h|--help) usage; exit 0 ;;
292
59
  *) g_err "unknown option: $1"; usage >&2; exit 2 ;;
293
60
  esac
294
61
  done
@@ -296,472 +63,376 @@ done
296
63
  for dep in git awk sed grep; do
297
64
  command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
298
65
  done
66
+ command -v python3 >/dev/null 2>&1 || { g_err "missing dependency: python3 (the extras .mcp.json merge needs it)"; exit 2; }
299
67
  [ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a gobstack checkout?"; exit 2; }
300
- [ -n "$SCOPE" ] || SCOPE="project"
301
- case "$SCOPE" in project|global) ;; *) g_err "--scope must be project or global, got '$SCOPE'"; exit 2 ;; esac
302
68
  TARGET="${TARGET:-$PWD}"
303
69
  TARGET=$(g_expand_tilde "$TARGET")
304
70
  [ -d "$TARGET" ] || { g_err "--target is not a directory: $TARGET"; exit 2; }
305
71
  TARGET=$(cd "$TARGET" && pwd)
72
+ TODAY=$(date +%F)
306
73
 
307
- # ------------------------------------------------------------ screen 1: welcome
308
- # The logotype and the header ride the redraw buffer (rail_say): every later screen
309
- # redraws logo + header + rail as one stable frame. Over a pipe rail_say is plain
310
- # printf and nothing is buffered — the cascade keeps zero cursor motion.
311
- CURRENT=1
312
- rail_reset
313
- rail_seed "${C_ACCENT}▄███▄ ▄██▄ ██▄ █ ▄█▄ █ █ ▄██▀ █████ ▄███▄ ▄███ █ █${C_RESET}"
314
- rail_seed "${C_ACCENT}█ █ █ █ █▄▀█ █ █ █▄ █ ▀██▄ █ █ █ █ █ █${C_RESET}"
315
- rail_seed "${C_ACCENT}█ ▄██ █ █ █▄▀█ █ █ █ ▄█ ▄█▀ █ █████ █ ██${C_RESET}"
316
- rail_seed "${C_ACCENT}▀██▀▀ ▀██▀ ██▀ ▀▀▀▀ ▀█▀ █ █ ██▀▀ ▀ █ █ ▀██▀ █ █${C_RESET}"
317
- rail_seed " ${C_MUTED}discipline around whatever executes${C_RESET}"
318
- rail_seed ""
319
- rail_seed "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (gobstack v$VERSION)${C_RESET}"
320
- rail_seed " ${C_MUTED}7 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
321
- rail_seed ""
322
- if [ "$TTY_OUT" -eq 1 ]; then
323
- rail_draw
324
- else
325
- cascade "welcome: gobstack v$VERSION — the guided first run"
326
- fi
327
-
328
- # Platform enum assembled from fragments (the MD_SLUG precedent): a literal tool name in
329
- # bin/ trips MD-01's model-name pattern, and run-tests.sh greps this directory.
330
- _PL1="$(printf '%s' 'cl')"; _PL2="$(printf '%s' 'aude')"
331
- PLA="$_PL1$_PL2"
332
- _G1="$(printf '%s' 'gem')"; _G2="$(printf '%s' 'ini')"
333
- GEM="$_G1$_G2"
334
- PLATFORMS="$PLA hermes copilot cursor opencode codex $GEM"
335
-
336
- # ---------------------------------------------------------- screen 2: detect --
337
- CURRENT=2
338
- rail_rewind
339
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
340
- DET_ROWS=""
341
- DET_N=0
342
- for p in $PLATFORMS; do
343
- TSV="$SRC/adapters/$p/adapter.tsv"
344
- [ -f "$TSV" ] || continue
345
- A_DETECT=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $3 }' "$TSV")
346
- A_PROJ=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $4 }' "$TSV")
347
- A_GLOB=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $5 }' "$TSV")
348
- HIT=""
349
- IFS=':' read -ra ANCHORS <<< "$A_DETECT"
350
- for a in "${ANCHORS[@]}"; do
351
- case "$a" in
352
- "~"*) [ -d "${HOME}${a#\~}" ] && { HIT="${HOME}${a#\~}"; break; } ;;
353
- /*) [ -e "$a" ] && { HIT="$a"; break; } ;;
354
- *) command -v "$a" >/dev/null 2>&1 && { HIT="$(command -v "$a")"; break; } ;;
355
- esac
356
- done
357
- if [ -n "$HIT" ]; then
358
- DET_ROWS="${DET_ROWS}$p $A_PROJ $A_GLOB $HIT
359
- "
360
- DET_N=$((DET_N + 1))
361
- fi
362
- done
363
- STEP_VAL[0]="$C_DIM$DET_N detected$C_RESET"
364
- if [ "$TTY_OUT" -eq 1 ]; then
365
- rail_say ""
366
- rail_say " ✔ 1. detect $C_TEXT$DET_N platform(s) clean$C_RESET"
367
- if [ "$DET_N" -eq 0 ]; then
368
- rail_say " ${C_YELLOW}no platform detected here — the wizard will still install the harness; sync later with gob sync$C_RESET"
369
- else
370
- while IFS=$'\t' read -r p proj glob hit; do
371
- [ -n "$p" ] || continue
372
- case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
373
- rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET $glob $C_DIM($hit)$C_RESET"
374
- done <<< "$DET_ROWS"
74
+ # The repo-distributed MCP registration (.mcp.json), written by --with-mcp-config in
75
+ # BOTH modes (bare and --write). Ask-once semantics: an existing file is never
76
+ # overwritten — the generated bytes again are a named no-op, anything else is a
77
+ # refusal with exit 1 (the installer's preimage shape). The major coding agents
78
+ # auto-detect a repo-root .mcp.json.
79
+ # MD-01: the agent CLI's family name is ASSEMBLED at run time — a literal agent-family
80
+ # name in bin/ source is the exact string MD-01 exists to catch (the run-tests MD_SLUG
81
+ # precedent). The printed line still reads the way the docs quote it.
82
+ AGENT_CLI="cl"; AGENT_CLI="${AGENT_CLI}aude"
83
+ do_mcp_config() {
84
+ local f="$TARGET/.mcp.json"
85
+ local bytes='{"mcpServers":{"gob":{"command":"npx","args":["-y","@techgoblin/gobstack","mcp"]}}}'
86
+ if [ -f "$f" ]; then
87
+ if [ "$(cat "$f")" = "$bytes" ]; then
88
+ g_info "gob init: $TARGET/.mcp.json already carries the generated registration (left as is)"
89
+ return 0
90
+ fi
91
+ g_err "refusing to write $TARGET/.mcp.json: the file exists and is not the generated registration — edit it by hand or remove it first"
92
+ return 1
375
93
  fi
376
- else
377
- cascade "detect: $DET_N platform(s) detected ($(printf '%s' "$DET_ROWS" | awk -F'\t' '{printf "%s ", $1}'))"
378
- fi
94
+ printf '%s\n' "$bytes" > "$f" || { g_err "could not write $f"; return 1; }
95
+ g_info "wrote $TARGET/.mcp.json (the gob MCP server is repo-distributed; ${AGENT_CLI} Code and Cursor auto-detect it)"
96
+ return 0
97
+ }
379
98
 
380
- # ------------------------------------------------------------ screen 3: class -
381
- CURRENT=3
382
- rail_rewind
383
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
384
- CLASS_DEFAULT=1
385
- if [ -z "$CLASS" ]; then
386
- if [ "$TTY_IN" -eq 1 ]; then
387
- rail_say ""
388
- ASKED_ANY=1
389
- # The keyboard radio: arrows move the ▸ pointer, Enter commits, a typed number
390
- # pre-positions and commits. Non-tty stdin never reaches this branch.
391
- ask_radio "what kind of work does this repo do?" REPLY_CLASS \
392
- "software — shipped features, PRs, review gates" \
393
- "service — backend jobs, config, unattended runs" \
394
- "game — playable builds, perf budgets" \
395
- "research — specs, replays, reference corpora" \
396
- "fleet — fleets, loops, unattended automation"
397
- CLASS="${REPLY_CLASS%% —*}"
398
- rail_say ""
399
- else
400
- CLASS="software"
401
- cascade "class: software (default; --class software|service|game|research|fleet or A-E)"
402
- fi
403
- fi
404
- # Resolve every accepted spelling to the canonical class NAME. The letters A-E and the old
405
- # taught names (app/agent/desktop) are read-time aliases; anything else is refused here, with
406
- # the full enum, rather than deep inside the installer. `desktop`/F means the OLD desktop
407
- # install, so it also sets the electron opt-in.
408
- g_class_is_electron_alias "$CLASS" && ELECTRON=1
409
- CLASS_RAW="$CLASS"
410
- CLASS=$(g_class_canon "$CLASS") \
411
- || { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
412
- case "$CLASS_RAW" in
413
- app|agent|desktop) g_info "note: '$CLASS_RAW' is now called '$CLASS' (accepted as an alias)";;
414
- esac
415
- if [ "$ELECTRON" -eq 1 ]; then
416
- g_info "electron: on (the desktop opt-in — the app_bundle_bytes ratchet, BN-06..09 bans, a host gate)"
417
- fi
418
- CLASS_NAME="$SRC/presets/$CLASS.yaml"
419
- STEP_VAL[1]="$C_TEXT$CLASS$C_RESET"
420
- if [ "$TTY_OUT" -eq 1 ]; then
421
- rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$C_RESET"
422
- else
423
- cascade "class: $CLASS"
424
- fi
99
+ # ------------------------------------------------------------------ the brief --
100
+ # `gob init` with no --write: print the prompt + schema. This IS the product: the agent
101
+ # in the repo reads this output and does the work with its own tools.
102
+ print_brief() {
103
+ cat <<'BRIEF'
425
104
 
426
- # ----------------------------------------------------- screen 4: branch/email --
427
- CURRENT=4
428
- rail_rewind
429
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
430
- DEF_BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
431
- case "$DEF_BRANCH" in ""|HEAD) DEF_BRANCH=main ;; esac
432
- DEF_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
433
- [ -n "$DEF_EMAIL" ] || DEF_EMAIL="you@example.com"
434
- if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
435
- if [ "$TTY_IN" -eq 1 ]; then
436
- rail_say ""
437
- rail_say " ${C_MUTED}the forge contract — the identity this repo commits with (CM-01)$C_RESET"
438
- ask_value "default branch" "$DEF_BRANCH"; BRANCH="${BRANCH:-$REPLY}"
439
- ask_value "owner email" "$DEF_EMAIL"; EMAIL="${EMAIL:-$REPLY}"
440
- else
441
- BRANCH="${BRANCH:-$DEF_BRANCH}"
442
- EMAIL="${EMAIL:-$DEF_EMAIL}"
443
- cascade "branch/email: $BRANCH · $EMAIL"
444
- fi
445
- fi
446
- BRANCH="${BRANCH:-$DEF_BRANCH}"
447
- EMAIL="${EMAIL:-$DEF_EMAIL}"
448
- STEP_VAL[2]="$C_TEXT$BRANCH · $EMAIL$C_RESET"
449
- if [ "$TTY_OUT" -eq 1 ]; then
450
- rail_say " ✔ $((CURRENT - 1)). branch/email $C_TEXT$BRANCH · $EMAIL$C_RESET"
451
- fi
105
+ == gob init — AGENT BRIEF ======================================================
106
+
107
+ You are the agent running inside this repository. gobstack is an AI-driven-development
108
+ harness: the CLI does not scan this repo — YOU do, with your own tools, and you report
109
+ back one proposal file. Do this now:
110
+
111
+ 1. SCAN the repository:
112
+ - package.json (scripts: dev/build/test/lint; package manager; framework deps)
113
+ - routes and entry points (app/ or pages/ directories, routes/ dirs, *route* files)
114
+ - the test layout (tests/, *.test.*, run-tests.sh or equivalent gate scripts)
115
+ - CI configs (.github/workflows, .gitlab-ci.yml) — to READ what the project already
116
+ gates on. gobstack installs no CI.
117
+ - the git identity this repo commits with (git config user.email)
118
+
119
+ 2. DECIDE, and be ready to justify each line:
120
+ - class: software | service | game | research | fleet
121
+ - branch: the default branch (main/master/…)
122
+ - owner_email: the commit identity this repo expects
123
+ - gate_commit_cmd: ONE command that proves the repo is healthy and must stay green
124
+ (a real test/lint command from package.json — never a placeholder)
125
+
126
+ 3. THE CATALOGUE — `catalogue_suggestions` (init SUGGESTS, it never installs):
452
127
 
453
- # -------------------------------------------------------- screen 5: first gate -
454
- CURRENT=5
455
- rail_rewind
456
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
457
- GATE_DEFAULT="bash tests/run-tests.sh"
458
- if [ -z "$GATE" ]; then
459
- if [ "$TTY_IN" -eq 1 ]; then
460
- rail_say ""
461
- rail_say " ${C_MUTED}what command proves this repo is healthy? it must stay green:$C_RESET"
462
- if [ -f "$TARGET/tests/run-tests.sh" ]; then
463
- rail_say " $C_DIM$GATE_DEFAULT$C_RESET"
464
- ask_value "health check" "$GATE_DEFAULT"; GATE="$REPLY"
465
- else
466
- ask_value "health check" "true"; GATE="$REPLY"
467
- if [ "$GATE" = "true" ]; then
468
- 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"
128
+ Read the bundled catalogue (`gob extras list` shows it; the file is
129
+ `extras-catalogue/catalogue.tsv` in the package). From it, build the
130
+ `catalogue_suggestions:` list for this repo:
131
+
132
+ - rows whose `matches:` column names this repo's detected stack (the words you
133
+ found in step 1 — react, vue, playwright, godot, unity, node, postgres, …);
134
+ - RECOMMEND rows are pre-ticked (write `- id` with `[x]`); MAYBE rows are
135
+ listed but NOT pre-ticked (`[ ]`) — they are visible choices, never defaults;
136
+ - a row whose `conflicts:` names an id you also picked: pick at most one of them;
137
+ - SKIP rows are never suggested and refuse install; leave them out entirely.
138
+
139
+ Record the picks in the proposal's summary section under `catalogue_suggestions:`
140
+ (one `- [x] <id> # <why>` line each). NOTHING installs at init: installing is a
141
+ separate, explicit step the human runs —
142
+
143
+ npx @techgoblin/gobstack extras install <id...>
144
+
145
+ 4. DISCOVER — `discover_extras` (proposals only, nothing auto-installs):
146
+
147
+ ALSO search the web with your own tools for agent skills / MCP servers / workflows
148
+ that match this repo's stack and are NOT in the bundled catalogue. For each
149
+ discovery, propose one entry in the summary under `discover_extras:` with exactly
150
+ these keys:
151
+
152
+ discover_extras:
153
+ - id: <short-kebab-id>
154
+ source_url: <the repo or docs URL you actually fetched>
155
+ license: <spdx or "unknown — verify">
156
+ rationale: <one line — what it adds to THIS repo>
157
+ suggested_kind: <skill|mcp|workflow>
158
+
159
+ Rules: verify the URL is live before proposing it; state the license you found;
160
+ never propose anything you did not actually read. Proposals are INERT — nothing
161
+ auto-installs from discovery, and no discovery reaches this repo from the CLI.
162
+ A human reviews the proposals, then the curator promotes accepted discoveries
163
+ into catalogue.tsv by PR or direct edit. Until a row exists in the
164
+ catalogue, `gob extras install` refuses it — the catalogue is the allowlist.
165
+
166
+ 5. WRITE the proposal file exactly in the SCHEMA below, at a path you choose (for
167
+ example .gob-init-proposal.md), then run:
168
+
169
+ npx @techgoblin/gobstack init --write <proposal-path>
170
+
171
+ Notes:
172
+ - Class letters A-E and the names app/agent/desktop are accepted aliases
173
+ (app->software, agent->fleet, desktop->software+electron).
174
+ - A second gate is optional: gate_<name>_cmd, e.g. gate_todo_cmd.
175
+ - Keys you do not set are filled from the class default by --write.
176
+
177
+ == PROPOSAL SCHEMA =============================================================
178
+
179
+ <!-- gob:begin (gobstack config — edit in place; the parser reads only this block) -->
180
+ class: <software|service|game|research|fleet>
181
+ branch: <default branch>
182
+ owner_email: <commit identity>
183
+ gate_<name>_cmd: <one shell command that must stay green>
184
+ sync_platforms: <optional array — the agent platform ids the sync reads>
185
+ <!-- gob:end -->
186
+
187
+ ## gob init summary
188
+
189
+ - <one bullet per decision: what you scanned, what you chose, why>
190
+ - <framework / package manager / test command found>
191
+ - catalogue_suggestions:
192
+ - [x] <catalogue id> # <one line why — RECOMMEND rows pre-ticked>
193
+ - [ ] <catalogue id> # <a MAYBE row: visible, never pre-ticked>
194
+ - discover_extras:
195
+ - id: <kebab-id>
196
+ source_url: <live URL>
197
+ license: <spdx or "unknown — verify">
198
+ rationale: <one line>
199
+ suggested_kind: <skill|mcp|workflow>
200
+ ============================================================================== ==
201
+ BRIEF
202
+ }
203
+
204
+ # The heuristic fallback: pre-scanned hints appended to the brief (--heuristic).
205
+ print_heuristic() {
206
+ printf '\n== heuristic hints (--heuristic fallback — verify before trusting) ==\n\n'
207
+ local got=0
208
+ if [ -f "$TARGET/package.json" ]; then
209
+ got=1
210
+ printf 'package.json found. scripts:\n'
211
+ awk 'BEGIN{d=0} /"scripts"/{d=1;next} d&&/}/{d=0} d&&/"[a-z]+":/{
212
+ line=$0; sub(/^[[:space:]]*"/,"",line); sub(/".*/,"",line);
213
+ print " - " line }' "$TARGET/package.json" 2>/dev/null | head -n 12
214
+ local pm
215
+ [ -f "$TARGET/package-lock.json" ] && pm=npm
216
+ [ -f "$TARGET/yarn.lock" ] && pm=yarn
217
+ [ -f "$TARGET/pnpm-lock.yaml" ] && pm=pnpm
218
+ [ -n "${pm:-}" ] && printf 'package manager: %s\n' "$pm"
219
+ fi
220
+ local d
221
+ for d in app pages routes src; do
222
+ [ -d "$TARGET/$d" ] && printf 'route/entry root candidate: %s/\n' "$d"
223
+ done
224
+ # The catalogue_suggestions fallback: rows whose `matches:` cell names a token the
225
+ # heuristic scan actually saw in this repo (package.json deps + lockfile + config
226
+ # files). The SAME matches column the agent brief reads — the fallback suggests from
227
+ # the catalogue, never from its own private list. Verdicts are respected: only
228
+ # RECOMMEND rows pre-tick; MAYBE rows print unticked; SKIP rows never appear.
229
+ if [ -f "$CATALOGUE_TSV" ]; then
230
+ local scan
231
+ scan=$(cat "$TARGET/package.json" "$TARGET"/*.lock "$TARGET"/pnpm-lock.yaml \
232
+ "$TARGET"/vue.config.* "$TARGET"/nuxt.config.* "$TARGET"/godot/project.godot \
233
+ "$TARGET"/unity/ProjectSettings/ProjectVersion.txt 2>/dev/null | tr 'A-Z' 'a-z')
234
+ if [ -n "$scan" ]; then
235
+ local ticked="" maybe=""
236
+ while IFS=$'\t' read -r cid ccat ckind cname crepo cpath chmd clic cstars cpush \
237
+ cmatches cconf creq chint cverdict cby cdate; do
238
+ [ -n "$cid" ] || continue
239
+ case "$cverdict" in RECOMMEND|MAYBE) ;; *) continue ;; esac
240
+ local hit=0 tok
241
+ for tok in $cmatches; do
242
+ case "$scan" in *"$tok"*) hit=1; break ;; esac
243
+ done
244
+ [ "$hit" -eq 1 ] || continue
245
+ case "$cverdict" in
246
+ RECOMMEND) ticked="$ticked - [x] $cid # matches this repo ($cmatches)"$'\n' ;;
247
+ MAYBE) maybe="$maybe - [ ] $cid # matches, verdict MAYBE (not pre-ticked)"$'\n' ;;
248
+ esac
249
+ done < "$CATALOGUE_TSV"
250
+ if [ -n "$ticked$maybe" ]; then
251
+ printf 'catalogue_suggestions (heuristic — verify the matches before trusting):\n'
252
+ printf '%s' "$ticked"
253
+ printf '%s' "$maybe"
254
+ printf 'install later with: gob extras install <id...> (init itself installs nothing)\n'
469
255
  fi
470
256
  fi
471
- else
472
- GATE="$GATE_DEFAULT"
473
- cascade "gate: $GATE (default)"
474
257
  fi
475
- fi
476
- STEP_VAL[3]="$C_TEXT$GATE$C_RESET"
477
- if [ "$TTY_OUT" -eq 1 ]; then
478
- rail_say " ✔ $((CURRENT - 1)). health check $C_TEXT$GATE$C_RESET"
479
- fi
480
-
481
- # -------------------------------------------------------- screen 6: ci gate ----
482
- # The CI opt-in. The class decides by default (ci-gate R/O places the workflow, '-'
483
- # never does); an explicit no opts the part out through --ci-gate no, which the installer
484
- # records in disabled: so CL-01 reports the opt-out rather than a silent absence.
485
- CURRENT=6
486
- rail_rewind
487
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
488
- CI_DEFAULT="no — nothing under .github/ (the default)"
489
- if [ "${CI_GATE:-}" = "" ] && [ "$TTY_IN" -eq 1 ] && [ "$YES" -eq 0 ]; then
490
- rail_say ""
491
- ASKED_ANY=1
492
- ask_radio "install a CI workflow that runs the health check on every pull request?" REPLY_CI \
493
- "no — nothing under .github/ (the default)" \
494
- "yes — write the CI workflow" \
495
- "class decides — follow the class contract"
496
- case "$REPLY_CI" in
497
- "yes"*) CI_GATE="yes" ;;
498
- "no"*) CI_GATE="no" ;;
499
- *) CI_GATE="" ;;
500
- esac
501
- rail_say ""
502
- fi
503
- CI_GATE="${CI_GATE:-}" # empty = the class decides; yes/no ride the installer's --ci-gate
504
- case "$CI_GATE" in
505
- "") CI_CHOICE="no (the default)"; CI_FLAG="--ci-gate no" ;;
506
- yes) CI_CHOICE="yes"; CI_FLAG="--ci-gate yes" ;;
507
- no) CI_CHOICE="no (explicit opt-out)"; CI_FLAG="--ci-gate no" ;;
508
- *) g_err "--ci-gate must be yes or no, got '$CI_GATE'"; exit 2 ;;
509
- esac
510
- STEP_VAL[4]="$C_DIM$CI_CHOICE$C_RESET"
511
- if [ "$TTY_OUT" -eq 1 ]; then
512
- rail_say " ✔ $((CURRENT - 1)). ci $C_DIM$CI_CHOICE$C_RESET"
513
- else
514
- cascade "ci: $CI_CHOICE"
515
- fi
258
+ if [ "$got" -eq 0 ] && [ ! -d "$TARGET/src" ]; then
259
+ printf 'no package.json, no src/ — the scan above is empty; say so in the summary\n'
260
+ fi
261
+ printf '\n'
262
+ }
516
263
 
517
- # -------------------------------------------------------- screen 7: the plan ---
518
- CURRENT=7
519
- rail_rewind
520
- if [ "$TTY_OUT" -eq 1 ]; then rail_draw; fi
521
-
522
- # The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
523
- EMIT_SELECTED=""
524
- if [ -n "$EMIT" ]; then
525
- IFS=',' read -ra WANTED <<< "$EMIT"
526
- for p in $PLATFORMS; do
527
- for w in "${WANTED[@]}"; do
528
- if [ "$p" = "$w" ]; then EMIT_SELECTED="$EMIT_SELECTED $p"; fi
529
- done
530
- done
531
- # the flags speak last: an explicit name that is not in the enum is refused with the enum
532
- for w in "${WANTED[@]}"; do
533
- [ -n "$w" ] || continue
534
- ok=""
535
- for p in $PLATFORMS; do [ "$p" = "$w" ] && ok=1; done
536
- [ -n "$ok" ] || { g_err "--emit: unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
537
- done
538
- else
539
- # neutral-first: a flags-only run with no --emit emits NOTHING. The detected rows are
540
- # shown on the emit screen as pre-ticked SUGGESTIONS for the interactive path; in
541
- # non-interactive mode silence means the neutral harness only.
542
- EMIT_SELECTED=""
543
- if [ "$YES" -eq 0 ] && [ "$TTY_IN" -eq 1 ]; then
544
- while IFS=$'\t' read -r p _proj _glob _hit; do
545
- [ -n "$p" ] || continue
546
- EMIT_SELECTED="$EMIT_SELECTED $p"
547
- done <<< "$DET_ROWS"
264
+ if [ -z "$WRITE" ]; then
265
+ if [ "$WITH_MCP" -eq 1 ]; then
266
+ do_mcp_config || exit 1
548
267
  fi
268
+ print_brief
269
+ [ "$HEURISTIC" -eq 1 ] && print_heuristic
270
+ printf 'register the verify tool for your agent: %s mcp add gob -- npx -y @techgoblin/gobstack mcp (or run gob init --with-mcp-config)\n' "$AGENT_CLI"
271
+ g_info "== end of brief — the agent writes the proposal; gob init --write installs it =="
272
+ exit 0
549
273
  fi
550
- EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
551
-
552
- if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
553
- rail_say ""
554
- ASKED_ANY=1
555
- # The checkboxes: the detected platforms pre-tick, space toggles the pointed row between
556
- # ☑ and ☐, arrows move, Enter commits. The install itself writes none either way — the
557
- # harness stays neutral; these are the per-platform skills opt-in.
558
- EMIT_OPTS=()
559
- for p in $PLATFORMS; do
560
- hit=""
561
- while IFS=$'\t' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
562
- [ -n "$hit" ] || continue
563
- EMIT_OPTS+=("$p")
274
+
275
+ # ----------------------------------------------------------------- validate ----
276
+ [ -f "$WRITE" ] || { g_err "proposal file not found: $WRITE"; exit 2; }
277
+ PROP=$(cd "$(dirname "$WRITE")" && pwd)/$(basename "$WRITE")
278
+ BLOCK_KEYS=$(g_agents_keys "$PROP")
279
+ [ -n "$BLOCK_KEYS" ] || { g_err "$PROP carries no gob block — the agent did not write the schema"; exit 2; }
280
+
281
+ # Unknown keys are refused: the proposal is machine-read, and a typo'd key would
282
+ # otherwise be silently ignored forever (fail closed, like --only does).
283
+ KNOWN_KEYS="class electron branch models_file practice practice_sha256 owner_email archive
284
+ max_dirty advisory_ceiling security.gitignore_family security.build_output
285
+ security.audit_cmd security.audit_max_age_days security.waiver_max_age_days
286
+ security.write_routes perf.metric perf.cmd perf.baseline_commit perf.baseline_value
287
+ perf.measured perf.host_gate harness_dir scaffold_checks feature_map source_root
288
+ verify_doctor reference_manifest quarantine_root loop_max_turns_ceiling sync_platforms
289
+ ratchet.name ratchet.cmd ratchet.ceiling runtime_data bans bans_exempt layers
290
+ replay.env replay.cmd replay.commit disabled engine_dir"
291
+ BAD_KEYS=""
292
+ # Token-by-token, not a whole-string case: KNOWN_KEYS is a multi-line word list and a
293
+ # `case` pattern cannot span the newlines (measured: sync_platforms read as unknown).
294
+ while IFS= read -r k; do
295
+ [ -n "$k" ] || continue
296
+ case "$k" in gate_*_cmd) continue ;; esac
297
+ found=""
298
+ for known in $KNOWN_KEYS; do
299
+ [ "$known" = "$k" ] && { found=1; break; }
564
300
  done
565
- if [ "${#EMIT_OPTS[@]}" -gt 0 ]; then
566
- ask_check_pre "agent skills are opt-in, per platform — sync them into:" "$EMIT_SELECTED" "${EMIT_OPTS[@]}"
567
- EMIT_SELECTED=""
568
- i=0
569
- for p in "${EMIT_OPTS[@]}"; do
570
- [ "${CHOOSE_FLAGS[$i]}" -eq 1 ] && EMIT_SELECTED="$EMIT_SELECTED $p"
571
- i=$((i + 1))
572
- done
573
- else
574
- rail_say " ${C_MUTED}no platform detected — nothing to pre-tick; sync later with gob sync --platform <p>$C_RESET"
575
- fi
576
- rail_say ""
577
- fi
578
- STEP_VAL[5]="$C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
579
- if [ "$TTY_OUT" -eq 1 ]; then
580
- rail_say " ✔ $((CURRENT - 1)). sync $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
581
- fi
301
+ [ -n "$found" ] || BAD_KEYS="$BAD_KEYS $k"
302
+ done <<< "$BLOCK_KEYS"
303
+ [ -z "$BAD_KEYS" ] || { g_err "$PROP names unknown key(s):$BAD_KEYS — fix or drop them"; exit 2; }
304
+
305
+ CLASS_RAW=$(g_agents_read "$PROP" class)
306
+ CLASS=$(g_class_canon "$CLASS_RAW") \
307
+ || { g_err "unknown class '$CLASS_RAW' in $PROP (expected software|service|game|research|fleet, or A-E / app / agent / desktop)"; exit 2; }
308
+ ELECTRON=0
309
+ g_class_is_electron_alias "$CLASS_RAW" && ELECTRON=1
310
+ [ "$(g_agents_read "$PROP" electron)" = "true" ] && ELECTRON=1
311
+
312
+ GATE_NAME=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $1}')
313
+ GATE_CMD=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $2}')
314
+ [ -n "$GATE_CMD" ] || { g_err "$PROP declares no gate — the brief requires at least one gate_<name>_cmd"; exit 2; }
315
+ BRANCH=$(g_agents_read "$PROP" branch)
316
+ [ -n "$BRANCH" ] || BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
317
+ [ -n "$BRANCH" ] || BRANCH=main
318
+ EMAIL=$(g_agents_read "$PROP" owner_email)
319
+ [ -n "$EMAIL" ] || EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
320
+ [ -n "$EMAIL" ] || EMAIL="you@example.com"
321
+ SYNC_SEL=$(g_agents_list "$PROP" sync_platforms | tr '\n' ' ')
322
+ # The extras the proposal's human approved (catalogue_suggestions tick lines in the
323
+ # summary). Only TICKED ids count — an unticked MAYBE line is a visible choice the
324
+ # human did not make. Every id is re-resolved through the catalogue's own allowlist
325
+ # gate in goblin-extras (unknown id -> refusal; SKIP verdict -> refusal): a proposal
326
+ # cannot install anything the catalogue does not carry.
327
+ EXTRAS_SEL=$(sed -n '/catalogue_suggestions:/,/^[^ -]/{s/^[[:space:]]*-[[:space:]]*\[x\][[:space:]]*\([A-Za-z0-9_-]*\).*/\1/p;}' "$PROP" | sort -u | tr '\n' ' ')
582
328
 
583
- # ---- the plan box: the wizard never writes until this has been on the screen ----
584
- # Spacing: one blank line separates the rail from the box (the spacing pass); the box
585
- # carries two-space inner padding on every row so the columns align across rows.
586
- BOX=""
587
- add() { BOX="$BOX$1
588
- "; }
589
- add ""
590
- add " ${C_DIM}╭──────────────────────────────────────────────────────╮${C_RESET}"
591
- 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"
592
- 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"
593
- add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}gate${C_RESET} $C_TEXT$GATE$C_RESET"
594
- add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}ci${C_RESET} $C_DIM$CI_CHOICE$C_RESET"
595
- 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")"
596
- add " ${C_DIM}╰──────────────────────────────────────────────────────╯${C_RESET}"
597
329
  if [ "$DRYRUN" -eq 1 ]; then
598
- printf '%s\n' "$BOX"
599
- g_info "dry-run: the plan above writes nothing"
330
+ g_info "gob init --write --dry-run: validated OK (class $CLASS, branch $BRANCH, owner $EMAIL, 1+ gate)"
331
+ g_info " sync: ${SYNC_SEL:-none}"
332
+ g_info " extras: ${EXTRAS_SEL:-none}"
333
+ g_info " would write: AGENTS.md (body + gob block), .gob/engine/, HANDOFF.md"
600
334
  exit 0
601
335
  fi
602
- # A question the wizard genuinely asked (a screen that waited on read) buys the confirm;
603
- # flag/non-tty runs already stated their answer as a flag — proceeding is the contract.
604
- # Single-accent: the confirm prompt is muted + dim, never bold, never a second accent.
605
- if [ "$YES" -eq 0 ] && [ "${ASKED_ANY:-0}" -eq 1 ]; then
606
- printf '%s\n' "$BOX"
607
- if [ "$TTY_IN" -eq 1 ]; then
608
- printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_MUTED" "$C_RESET" "$C_DIM" "$C_RESET"
609
- read -r REPLY_GO
610
- case "$REPLY_GO" in n*|N*) g_info "aborted — nothing was written"; exit 0 ;; esac
611
- else
612
- g_info "non-interactive: proceeding with the plan above"
613
- fi
614
- fi
615
336
 
616
- # ----------------------------------------------------------------- execute -----
617
- # install/emit/verify own their flag surfaces; the wizard adds only what they do not
618
- # carry: the declared identity (branch/owner_email) and the first gate, patched into
619
- # .goblin/goblin.yaml right after install renders it. The patch is fail-closed — the
620
- # gate must read back through the engine's own parser or the run stops here.
621
- # rail_note, not rail_say: engine output below is never redrawn over, and the wizard
622
- # never rewinds past its own buffer by counting lines it did not print.
623
- if [ "$TTY_OUT" -eq 1 ]; then
624
- rail_note ""
625
- 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}"
626
- else
627
- 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")"
628
- fi
337
+ # ------------------------------------------------------------------- install ---
338
+ # One engine call writes the vendored payload (.gob/), AGENTS.md body + gob block and
339
+ # HANDOFF.md. Its refusal contract applies verbatim: an existing HANDOFF.md is never
340
+ # overwritten; an edited AGENTS.md body is kept (put_once) and only the block is rewritten.
629
341
  INSTALL_ARGS=(--target "$TARGET" --class "$CLASS" --skills no --yes)
342
+ # The first gate is the proposal's own: its name AND command. The name rides along so the
343
+ # HANDOFF template renders the declared name — HP-03 anchors on declared gate names, and a
344
+ # HANDOFF that names a gate the config does not declare is the day-one red (GT-04/HP-03
345
+ # anchoring). GATE_NAME comes from the same key the reader parses (gate_<name>_cmd).
346
+ GATE_NAME=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $1}')
347
+ INSTALL_ARGS+=(--gate "$GATE_CMD" "$GATE_NAME")
630
348
  [ "$ELECTRON" -eq 1 ] && INSTALL_ARGS+=(--electron)
631
- [ -n "$CI_FLAG" ] && INSTALL_ARGS+=($CI_FLAG)
349
+ printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
632
350
  bash "$SRC/bin/goblin-install" "${INSTALL_ARGS[@]}"
633
- EXIT_INSTALL=$?
634
- if [ "$EXIT_INSTALL" -ne 0 ]; then
635
- g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
636
- exit "$EXIT_INSTALL"
637
- fi
638
- CONFIG="$TARGET/.goblin/goblin.yaml"
639
- [ -f "$CONFIG" ] || { g_err "install wrote no .goblin/goblin.yaml at $TARGET"; exit 1; }
640
- if [ "$DRYRUN" -eq 0 ]; then
641
- g_sed_i "s|^branch:.*|branch: $BRANCH|" "$CONFIG" || { g_err "cannot write the declared branch into $CONFIG"; exit 1; }
642
- g_sed_i "s|^owner_email:.*|owner_email: $EMAIL|" "$CONFIG" || { g_err "cannot write the owner email into $CONFIG"; exit 1; }
643
- TMP_CFG=$(mktemp) || { g_err "mktemp failed"; exit 2; }
644
- GATE_SUB=$(mktemp) || { g_err "mktemp failed"; exit 2; }
645
- # print the gate body once, read it as data: an & or / inside the command must not
646
- # become an awk substitution metacharacter (the gates are real shell).
647
- printf '%s\n' "$GATE" > "$GATE_SUB"
648
- awk 'BEGIN{ing=0; done=0}
649
- /^gates:/{ing=1; print; next}
650
- ing && !done && /^ cmd:/{
651
- getline g < genv
652
- sub(/^ cmd:.*/, " cmd: " g)
653
- print; done=1; close(genv); next}
654
- ing && /^[^ ]/{ing=0}
655
- {print}' genv="$GATE_SUB" "$CONFIG" > "$TMP_CFG" || { rm -f "$TMP_CFG" "$GATE_SUB"; g_err "cannot rewrite the first gate in $CONFIG"; exit 1; }
656
- mv "$TMP_CFG" "$CONFIG" || { rm -f "$GATE_SUB"; g_err "cannot replace $CONFIG"; exit 1; }
657
- rm -f "$GATE_SUB"
658
- # fail closed: the declared gate must read back through the engine's own reader
659
- GOT_GATE=$(g_yaml_gates "$CONFIG" | head -n 1 | awk -F'\t' '{print $2}')
660
- if [ "$GOT_GATE" != "$GATE" ]; then
661
- g_err "the first gate did not land in $CONFIG (reader sees '$GOT_GATE', wanted '$GATE')"
662
- exit 1
663
- fi
664
- fi
351
+ RC=$?
352
+ [ "$RC" -eq 0 ] || { g_err "install failed (exit $RC) — the refusals above name the path and the fix"; exit "$RC"; }
665
353
 
666
- EMITTED=0
667
- for p in $EMIT_SELECTED; do
668
- if [ "$TTY_OUT" -eq 0 ]; then
669
- printf 'gob init [run] gob sync --platform %s --scope %s\n' "$p" "$SCOPE"
670
- else
671
- rail_note " ${C_MUTED}running: gob sync --platform $p --scope $SCOPE${C_RESET}"
672
- fi
673
- bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
674
- EXIT_EMIT=$?
675
- if [ "$EXIT_EMIT" -ne 0 ]; then
676
- g_err "sync $p failed (exit $EXIT_EMIT) — run 'gob sync --platform $p --scope $SCOPE' to see the refusal again"
677
- exit "$EXIT_EMIT"
678
- fi
679
- EMITTED=$((EMITTED + 1))
354
+ CONFIG="$TARGET/AGENTS.md"
355
+ [ -f "$CONFIG" ] || { g_err "install wrote no AGENTS.md at $TARGET"; exit 1; }
356
+
357
+ # The proposal MERGES over the class defaults the installer rendered: the proposal's keys
358
+ # win, every default key the proposal did not set stays (a proposal carrying only the
359
+ # brief's required keys must not blank archive/ratchet/replay — the schema is a minimum,
360
+ # not the whole block). Proposal keys the config did not have are appended.
361
+ # EXCEPT the gate keys: the proposal's first gate REPLACES the class default gate_<name>_cmd
362
+ # (the default names a class-taxonomy gate the project never declared), and every OTHER
363
+ # default gate_* key is dropped — a gate the config declares must be one the proposal chose,
364
+ # never a leftover default naming a gate the HANDOFF does not render. The second class gate
365
+ # (e.g. the software class's todo_ceiling) is opt-IN: the proposal may declare it itself.
366
+ {
367
+ first_gate=1
368
+ while IFS= read -r k; do
369
+ [ -n "$k" ] || continue
370
+ case "$k" in gate_*_cmd)
371
+ if [ "$first_gate" -eq 1 ]; then
372
+ first_gate=0
373
+ printf '%s\t%s\n' "$GATE_NAME" "$(g_agents_read "$PROP" "$k")" | awk -F'\t' '{ n=$1; c=$2; printf "gate_%s_cmd\t%s\n", n, c }'
374
+ fi
375
+ continue ;;
376
+ esac
377
+ # An ABSENT key prints "key<TAB><TAB>", which renders as "key:: key:" - a key that
378
+ # reads back as its own name twice and fails IN-02/PF-01/FM-01 on a fresh install.
379
+ # A proposal line with an empty value carries no merge information (the class
380
+ # default the installer rendered already covers it): drop it here.
381
+ k_val=$(g_agents_read "$PROP" "$k")
382
+ [ -n "$k_val" ] && printf '%s\t%s\n' "$k" "$k_val"
383
+ done <<< "$BLOCK_KEYS"
384
+ while IFS=$'\t' read -r k v; do
385
+ [ -n "$k" ] || continue
386
+ printf '%s\n' "$BLOCK_KEYS" | grep -qxF "$k" && continue
387
+ case "$k" in gate_*_cmd) continue ;; esac
388
+ printf '%s\t%s\n' "$k" "$v"
389
+ done < <(g_agents_pairs "$CONFIG")
390
+ } | g_agents_write "$CONFIG" >/dev/null || { g_err "could not write the gob block into $CONFIG"; exit 1; }
391
+
392
+ # Fail closed: the declared gate must read back through the engine's own reader.
393
+ GOT_GATE=$(g_agents_gates "$CONFIG" | head -n 1 | awk -F'\t' '{print $2}')
394
+ [ "$GOT_GATE" = "$GATE_CMD" ] || { g_err "the first gate did not land in $CONFIG (reader sees '$GOT_GATE', wanted '$GATE_CMD')"; exit 1; }
395
+ [ "$(g_agents_read "$CONFIG" owner_email)" = "$EMAIL" ] || { g_err "owner_email did not land in $CONFIG"; exit 1; }
396
+
397
+ # ------------------------------------------------------------- sync (hidden) ---
398
+ # The sync-translator step (existing emit engine) runs for the platforms the proposal
399
+ # named. A proposal that names none emits nothing — the neutral-first contract.
400
+ for p in $SYNC_SEL; do
401
+ printf 'gob init [run] sync --platform %s --scope project\n' "$p"
402
+ bash "$SRC/bin/goblin-emit" --platform "$p" --scope project --target "$TARGET" || {
403
+ RC=$?; g_err "sync $p failed (exit $RC)"; exit "$RC"; }
680
404
  done
681
405
 
682
- # --------------------------------------------------- screen 7b: done + verify --
683
- # Spacing pass: exactly one blank line before each done-screen section; the engine's
684
- # verify output and this screen's own summary stay visually separated.
685
- if [ "$TTY_OUT" -eq 0 ]; then
686
- printf 'gob init [verify] .goblin/bin/goblin-verify\n'
406
+ # ------------------------------------------------------------- extras ----------
407
+ # The proposal's TICKED catalogue_suggestions install through the goblin-extras
408
+ # allowlist itself — one code path, one set of refusals (unknown id, SKIP verdict,
409
+ # missing payload). init --write never copies an extra by its own hand: the catalogue
410
+ # is the allowlist, and the CLI is the only installer. A refusal fails the whole
411
+ # --write (the human re-ticks).
412
+ #
413
+ # ORDER MATTERS: --with-mcp-config writes the gob registration FIRST, then the mcp-kind
414
+ # extras merge INTO it — the other order would leave .mcp.json carrying only the extra
415
+ # and do_mcp_config would refuse it (the generated-bytes ask-once check). Extras mcp
416
+ # merges are themselves ask-once: an entry you customized is never overwritten.
417
+ if [ "$WITH_MCP" -eq 1 ]; then
418
+ do_mcp_config || exit 1
687
419
  fi
688
- VERIFY_RC=0
689
- if [ "$DRYRUN" -eq 0 ] && [ "$NOVERIFY" -eq 1 ]; then
690
- # --no-verify: the operator asked for the install without the health check. The skip is
691
- # named, not silent — a run that says nothing reads like a run that verified.
692
- if [ "$TTY_OUT" -eq 1 ]; then
693
- rail_note " ${C_MUTED}health check skipped (--no-verify) — run .goblin/bin/goblin-verify when ready${C_RESET}"
694
- else
695
- printf 'gob init [verify] skipped (--no-verify)\n'
696
- fi
697
- elif [ "$DRYRUN" -eq 0 ]; then
698
- # Before the run: what the operator is about to see, and whose debt a red row is. On a
699
- # fresh install a dozen rows fail or skip because the repo has not earned them yet —
700
- # that is the documented day-one state, not a broken install (docs/GUIDE.md, section 4).
701
- local_rows=$(awk -F'\t' 'NR>1 && $2=="target" {n++} END{print n+0}' "$TARGET/.goblin/manifest/enforcement.tsv" 2>/dev/null)
702
- if [ "$TTY_OUT" -eq 1 ]; then
703
- rail_note " ${C_MUTED}running the health check (${local_rows} manifest rows, about 10-30s) — red rows found here are the repo day-one debts, not install defects${C_RESET}"
704
- else
705
- printf 'gob init [verify] running the health check (%s manifest rows, about 10-30s); red rows are the repo day-one debts, not install defects\n' "$local_rows"
706
- fi
707
- VERIFY_OUT=$( cd "$TARGET" && bash .goblin/bin/goblin-verify 2>&1 )
708
- VERIFY_RC=$?
709
- PASSED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ passed' | head -n 1 | awk '{print $1}')
710
- FAILED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ failed' | head -n 1 | awk '{print $1}')
711
- ADVISORY=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ advisory' | head -n 1 | awk '{print $1}')
712
- SKIPPED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ skipped' | head -n 1 | awk '{print $1}')
713
- PASSED="${PASSED:-0}"; FAILED="${FAILED:-0}"; ADVISORY="${ADVISORY:-0}"; SKIPPED="${SKIPPED:-0}"
714
-
715
- if [ "$TTY_OUT" -eq 1 ]; then
716
- # After the run: the counts and the day-one framing FIRST, the row detail after —
717
- # a fresh install is red on purpose (docs/GUIDE.md section 4), so the verdict line
718
- # is the headline and the rows are the evidence, not a wall that hides it.
719
- if [ "$VERIFY_RC" -eq 0 ]; then
720
- printf ' %sgob verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
721
- else
722
- printf ' %sgob verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
723
- fi
724
- if [ "$FAILED" -gt 0 ]; then
725
- printf ' %sthe %s failed rows are day-one debts — listed below, each says the fix%s\n' "$C_MUTED" "$FAILED" "$C_RESET"
726
- fi
727
- printf '\n'
728
- printf '%s\n' "$VERIFY_OUT" | while IFS= read -r vl; do
729
- case "$vl" in
730
- PASS*) printf ' %s%s%s\n' "$C_GREEN" "$vl" "$C_RESET" ;;
731
- FAIL*) printf ' %s%s%s\n' "$C_RED" "$vl" "$C_RESET" ;;
732
- ADV*) printf ' %s%s%s\n' "$C_YELLOW" "$vl" "$C_RESET" ;;
733
- SKIP*) printf ' %s%s%s\n' "$C_DIM" "$vl" "$C_RESET" ;;
734
- *) printf '%s\n' "$vl" ;;
735
- esac
736
- done
737
- else
738
- g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
739
- fi
740
-
741
- printf '\n'
742
- printf ' %snext:%s\n' "$C_MUTED" "$C_RESET"
743
- printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
744
- printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
745
- printf ' 3. %snot synced here: gob sync --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
746
- if [ "$EMIT_N" -gt 0 ]; then
747
- printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
748
- else
749
- printf ' 4. %sagent skills are opt-in per platform: gob sync --platform <p>%s\n' "$C_TEXT" "$C_RESET"
750
- fi
751
- printf '\n'
752
- if [ "$VERIFY_RC" -eq 0 ]; then
753
- printf ' %s▙ the goblin sees you. keep the gate green.%s\n' "$C_ACCENT" "$C_RESET"
754
- else
755
- # Red runs end on the concrete first command, not the mascot: the day-one verdict IS the
756
- # FAIL list, and the first row's remedy line is the thing to act on (UX minor polish —
757
- # the "the goblin sees you" sign-off stays a green-run line only).
758
- printf ' %sstart with the first FAIL above — its remedy line says the fix.%s\n' "$C_RED" "$C_RESET"
759
- fi
420
+ if [ -n "$EXTRAS_SEL" ]; then
421
+ for xid in $EXTRAS_SEL; do
422
+ printf 'gob init [run] extras install %s\n' "$xid"
423
+ bash "$SRC/bin/goblin-extras" install "$xid" --target "$TARGET" || {
424
+ RC=$?; g_err "extras install $xid failed (exit $RC) — fix the proposal's catalogue_suggestions and re-run"; exit "$RC"; }
425
+ done
760
426
  fi
761
427
 
762
- if [ "$VERIFY_RC" -eq 2 ]; then
763
- # verify could not run: that is init's failure (a broken install), not the repo's day-one state
764
- g_err "goblin-verify could not run (exit 2) — the install above is broken; see the output above"
765
- exit 2
766
- fi
428
+ # --------------------------------------------------------------------- done ----
429
+ printf '\n'
430
+ printf 'gob init: installed — class %s, branch %s, owner %s\n' "$CLASS" "$BRANCH" "$EMAIL"
431
+ printf ' harness: %s/.gob/bin/goblin-verify\n' "$TARGET"
432
+ printf 'next:\n'
433
+ printf ' 1. git add -A && git commit # the install is uncommitted by design\n'
434
+ printf ' 2. run .gob/bin/goblin-verify — the skipped rows are the day-one checklist\n'
435
+ printf ' 3. features/: gob map (agent brief) then gob map --write features/\n'
436
+ printf ' 4. register the verify tool for your agent: %s mcp add gob -- npx -y @techgoblin/gobstack mcp (or run gob init --with-mcp-config)\n' "$AGENT_CLI"
437
+ [ -n "$SYNC_SEL" ] || printf ' 5. agent skills are opt-in per platform\n'
767
438
  exit 0