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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +112 -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 +61 -58
  7. package/bin/goblin-audit +11 -13
  8. package/bin/goblin-bans +11 -11
  9. package/bin/goblin-init +275 -713
  10. package/bin/goblin-install +160 -114
  11. package/bin/goblin-lib.sh +234 -1
  12. package/bin/goblin-map +226 -21
  13. package/bin/goblin-mcp.js +492 -0
  14. package/bin/goblin-model +4 -4
  15. package/bin/goblin-upgrade +1 -1
  16. package/bin/goblin-verify +159 -145
  17. package/bin/goblin.js +33 -51
  18. package/docs/ADOPTION.md +15 -15
  19. package/docs/CONTRACTS.md +16 -15
  20. package/docs/DESIGN.md +1 -1
  21. package/docs/ENFORCEMENT.md +89 -90
  22. package/docs/FLOWS.md +1 -1
  23. package/docs/GLOSSARY.md +3 -3
  24. package/docs/GUARDRAILS.md +5 -5
  25. package/docs/GUIDE.md +167 -177
  26. package/docs/INTEGRATION.md +1 -1
  27. package/docs/LIMITS.md +25 -0
  28. package/docs/LOOP.md +12 -12
  29. package/docs/RE-PLAYBOOK.md +3 -3
  30. package/docs/ROLES.md +5 -5
  31. package/manifest/bans.tsv +8 -8
  32. package/manifest/classes.tsv +3 -3
  33. package/manifest/enforcement.tsv +40 -40
  34. package/manifest/glossary.tsv +3 -3
  35. package/manifest/playbooks.tsv +1 -1
  36. package/package.json +1 -1
  37. package/presets/electron-overlay.yaml +2 -2
  38. package/presets/fleet.yaml +8 -7
  39. package/presets/game.yaml +1 -1
  40. package/presets/research.yaml +1 -1
  41. package/presets/service.yaml +1 -1
  42. package/presets/software.yaml +1 -1
  43. package/skills/goblin-bootstrap/SKILL.md +2 -2
  44. package/templates/AGENTS.md.tmpl +8 -18
  45. package/templates/HANDOFF.md.tmpl +5 -5
  46. package/templates/agents-block.tmpl +45 -0
  47. package/templates/audit-waiver.tsv.tmpl +2 -2
  48. package/templates/boundary-waivers.tmpl +1 -1
  49. package/templates/checks/gate.sh.tmpl +6 -6
  50. package/templates/install-hooks.allowlist.tmpl +1 -1
  51. package/templates/ci/goblin-gate.yml.tmpl +0 -46
  52. package/templates/goblin.yaml.tmpl +0 -146
  53. package/templates/loop/decisions.tsv.tmpl +0 -1
  54. package/templates/loop/predicate.tmpl +0 -16
package/bin/goblin-init CHANGED
@@ -1,294 +1,59 @@
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')
52
40
 
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
- }
41
+ g_info() { printf '%s\n' "$*"; }
155
42
 
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
- }
270
-
271
- # ------------------------------------------------------------------ flags -----
272
- TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
273
- ELECTRON=0
274
- YES=0; DRYRUN=0; NOVERIFY=0
275
43
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
276
44
 
45
+ # ------------------------------------------------------------------ flags -----
46
+ TARGET="" WRITE="" HEURISTIC=0 DRYRUN=0 RESYNC=0 WITH_MCP=0
277
47
  while [ $# -gt 0 ]; do
278
48
  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 ;;
49
+ --target) TARGET="${2:-}"; shift 2 ;;
50
+ --write) WRITE="${2:-}"; shift 2 ;;
51
+ --heuristic) HEURISTIC=1; shift ;;
52
+ --resync) RESYNC=1; shift ;; # init-internal: re-run the sync step (hidden)
53
+ --with-mcp-config) WITH_MCP=1; shift ;;
54
+ --dry-run) DRYRUN=1; shift ;;
55
+ --yes) shift ;; # accepted for pipe parity; --write is already explicit
56
+ -h|--help) usage; exit 0 ;;
292
57
  *) g_err "unknown option: $1"; usage >&2; exit 2 ;;
293
58
  esac
294
59
  done
@@ -297,471 +62,268 @@ for dep in git awk sed grep; do
297
62
  command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
298
63
  done
299
64
  [ -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
65
  TARGET="${TARGET:-$PWD}"
303
66
  TARGET=$(g_expand_tilde "$TARGET")
304
67
  [ -d "$TARGET" ] || { g_err "--target is not a directory: $TARGET"; exit 2; }
305
68
  TARGET=$(cd "$TARGET" && pwd)
69
+ TODAY=$(date +%F)
306
70
 
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"
71
+ # The repo-distributed MCP registration (.mcp.json), written by --with-mcp-config in
72
+ # BOTH modes (bare and --write). Ask-once semantics: an existing file is never
73
+ # overwritten — the generated bytes again are a named no-op, anything else is a
74
+ # refusal with exit 1 (the installer's preimage shape). The major coding agents
75
+ # auto-detect a repo-root .mcp.json.
76
+ # MD-01: the agent CLI's family name is ASSEMBLED at run time — a literal agent-family
77
+ # name in bin/ source is the exact string MD-01 exists to catch (the run-tests MD_SLUG
78
+ # precedent). The printed line still reads the way the docs quote it.
79
+ AGENT_CLI="cl"; AGENT_CLI="${AGENT_CLI}aude"
80
+ do_mcp_config() {
81
+ local f="$TARGET/.mcp.json"
82
+ local bytes='{"mcpServers":{"gob":{"command":"npx","args":["-y","@techgoblin/gobstack","mcp"]}}}'
83
+ if [ -f "$f" ]; then
84
+ if [ "$(cat "$f")" = "$bytes" ]; then
85
+ g_info "gob init: $TARGET/.mcp.json already carries the generated registration (left as is)"
86
+ return 0
87
+ fi
88
+ 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"
89
+ return 1
375
90
  fi
376
- else
377
- cascade "detect: $DET_N platform(s) detected ($(printf '%s' "$DET_ROWS" | awk -F'\t' '{printf "%s ", $1}'))"
378
- fi
91
+ printf '%s\n' "$bytes" > "$f" || { g_err "could not write $f"; return 1; }
92
+ g_info "wrote $TARGET/.mcp.json (the gob MCP server is repo-distributed; ${AGENT_CLI} Code and Cursor auto-detect it)"
93
+ return 0
94
+ }
379
95
 
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
96
+ # ------------------------------------------------------------------ the brief --
97
+ # `gob init` with no --write: print the prompt + schema. This IS the product: the agent
98
+ # in the repo reads this output and does the work with its own tools.
99
+ print_brief() {
100
+ cat <<'BRIEF'
425
101
 
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
102
+ == gob init — AGENT BRIEF ======================================================
452
103
 
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"
469
- fi
470
- fi
471
- else
472
- GATE="$GATE_DEFAULT"
473
- cascade "gate: $GATE (default)"
474
- 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
104
+ You are the agent running inside this repository. gobstack is an AI-driven-development
105
+ harness: the CLI does not scan this repo — YOU do, with your own tools, and you report
106
+ back one proposal file. Do this now:
480
107
 
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
108
+ 1. SCAN the repository:
109
+ - package.json (scripts: dev/build/test/lint; package manager; framework deps)
110
+ - routes and entry points (app/ or pages/ directories, routes/ dirs, *route* files)
111
+ - the test layout (tests/, *.test.*, run-tests.sh or equivalent gate scripts)
112
+ - CI configs (.github/workflows, .gitlab-ci.yml) — to READ what the project already
113
+ gates on. gobstack installs no CI.
114
+ - the git identity this repo commits with (git config user.email)
516
115
 
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"
116
+ 2. DECIDE, and be ready to justify each line:
117
+ - class: software | service | game | research | fleet
118
+ - branch: the default branch (main/master/…)
119
+ - owner_email: the commit identity this repo expects
120
+ - gate_commit_cmd: ONE command that proves the repo is healthy and must stay green
121
+ (a real test/lint command from package.json — never a placeholder)
122
+
123
+ 3. WRITE the proposal file exactly in the SCHEMA below, at a path you choose (for
124
+ example .gob-init-proposal.md), then run:
125
+
126
+ npx @techgoblin/gobstack init --write <proposal-path>
127
+
128
+ Notes:
129
+ - Class letters A-E and the names app/agent/desktop are accepted aliases
130
+ (app->software, agent->fleet, desktop->software+electron).
131
+ - A second gate is optional: gate_<name>_cmd, e.g. gate_todo_cmd.
132
+ - Keys you do not set are filled from the class default by --write.
133
+
134
+ == PROPOSAL SCHEMA =============================================================
135
+
136
+ <!-- gob:begin (gobstack config — edit in place; the parser reads only this block) -->
137
+ class: <software|service|game|research|fleet>
138
+ branch: <default branch>
139
+ owner_email: <commit identity>
140
+ gate_<name>_cmd: <one shell command that must stay green>
141
+ sync_platforms: <optional array — the agent platform ids the sync reads>
142
+ <!-- gob:end -->
143
+
144
+ ## gob init summary
145
+
146
+ - <one bullet per decision: what you scanned, what you chose, why>
147
+ - <framework / package manager / test command found>
148
+ ============================================================================== ==
149
+ BRIEF
150
+ }
151
+
152
+ # The heuristic fallback: pre-scanned hints appended to the brief (--heuristic).
153
+ print_heuristic() {
154
+ printf '\n== heuristic hints (--heuristic fallback — verify before trusting) ==\n\n'
155
+ local got=0
156
+ if [ -f "$TARGET/package.json" ]; then
157
+ got=1
158
+ printf 'package.json found. scripts:\n'
159
+ awk 'BEGIN{d=0} /"scripts"/{d=1;next} d&&/}/{d=0} d&&/"[a-z]+":/{
160
+ line=$0; sub(/^[[:space:]]*"/,"",line); sub(/".*/,"",line);
161
+ print " - " line }' "$TARGET/package.json" 2>/dev/null | head -n 12
162
+ local pm
163
+ [ -f "$TARGET/package-lock.json" ] && pm=npm
164
+ [ -f "$TARGET/yarn.lock" ] && pm=yarn
165
+ [ -f "$TARGET/pnpm-lock.yaml" ] && pm=pnpm
166
+ [ -n "${pm:-}" ] && printf 'package manager: %s\n' "$pm"
548
167
  fi
549
- 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")
168
+ local d
169
+ for d in app pages routes src; do
170
+ [ -d "$TARGET/$d" ] && printf 'route/entry root candidate: %s/\n' "$d"
564
171
  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"
172
+ if [ "$got" -eq 0 ] && [ ! -d "$TARGET/src" ]; then
173
+ printf 'no package.json, no src/ — the scan above is empty; say so in the summary\n'
575
174
  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"
175
+ printf '\n'
176
+ }
177
+
178
+ if [ -z "$WRITE" ]; then
179
+ if [ "$WITH_MCP" -eq 1 ]; then
180
+ do_mcp_config || exit 1
181
+ fi
182
+ print_brief
183
+ [ "$HEURISTIC" -eq 1 ] && print_heuristic
184
+ 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"
185
+ g_info "== end of brief — the agent writes the proposal; gob init --write installs it =="
186
+ exit 0
581
187
  fi
582
188
 
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}"
189
+ # ----------------------------------------------------------------- validate ----
190
+ [ -f "$WRITE" ] || { g_err "proposal file not found: $WRITE"; exit 2; }
191
+ PROP=$(cd "$(dirname "$WRITE")" && pwd)/$(basename "$WRITE")
192
+ BLOCK_KEYS=$(g_agents_keys "$PROP")
193
+ [ -n "$BLOCK_KEYS" ] || { g_err "$PROP carries no gob block — the agent did not write the schema"; exit 2; }
194
+
195
+ # Unknown keys are refused: the proposal is machine-read, and a typo'd key would
196
+ # otherwise be silently ignored forever (fail closed, like --only does).
197
+ KNOWN_KEYS="class electron branch models_file practice practice_sha256 owner_email archive
198
+ max_dirty advisory_ceiling security.gitignore_family security.build_output
199
+ security.audit_cmd security.audit_max_age_days security.waiver_max_age_days
200
+ security.write_routes perf.metric perf.cmd perf.baseline_commit perf.baseline_value
201
+ perf.measured perf.host_gate harness_dir scaffold_checks feature_map source_root
202
+ verify_doctor reference_manifest quarantine_root loop_max_turns_ceiling sync_platforms
203
+ ratchet.name ratchet.cmd ratchet.ceiling runtime_data bans bans_exempt layers
204
+ replay.env replay.cmd replay.commit disabled engine_dir"
205
+ BAD_KEYS=""
206
+ # Token-by-token, not a whole-string case: KNOWN_KEYS is a multi-line word list and a
207
+ # `case` pattern cannot span the newlines (measured: sync_platforms read as unknown).
208
+ while IFS= read -r k; do
209
+ [ -n "$k" ] || continue
210
+ case "$k" in gate_*_cmd) continue ;; esac
211
+ found=""
212
+ for known in $KNOWN_KEYS; do
213
+ [ "$known" = "$k" ] && { found=1; break; }
214
+ done
215
+ [ -n "$found" ] || BAD_KEYS="$BAD_KEYS $k"
216
+ done <<< "$BLOCK_KEYS"
217
+ [ -z "$BAD_KEYS" ] || { g_err "$PROP names unknown key(s):$BAD_KEYS — fix or drop them"; exit 2; }
218
+
219
+ CLASS_RAW=$(g_agents_read "$PROP" class)
220
+ CLASS=$(g_class_canon "$CLASS_RAW") \
221
+ || { g_err "unknown class '$CLASS_RAW' in $PROP (expected software|service|game|research|fleet, or A-E / app / agent / desktop)"; exit 2; }
222
+ ELECTRON=0
223
+ g_class_is_electron_alias "$CLASS_RAW" && ELECTRON=1
224
+ [ "$(g_agents_read "$PROP" electron)" = "true" ] && ELECTRON=1
225
+
226
+ GATE_NAME=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $1}')
227
+ GATE_CMD=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $2}')
228
+ [ -n "$GATE_CMD" ] || { g_err "$PROP declares no gate — the brief requires at least one gate_<name>_cmd"; exit 2; }
229
+ BRANCH=$(g_agents_read "$PROP" branch)
230
+ [ -n "$BRANCH" ] || BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
231
+ [ -n "$BRANCH" ] || BRANCH=main
232
+ EMAIL=$(g_agents_read "$PROP" owner_email)
233
+ [ -n "$EMAIL" ] || EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
234
+ [ -n "$EMAIL" ] || EMAIL="you@example.com"
235
+ SYNC_SEL=$(g_agents_list "$PROP" sync_platforms | tr '\n' ' ')
236
+
597
237
  if [ "$DRYRUN" -eq 1 ]; then
598
- printf '%s\n' "$BOX"
599
- g_info "dry-run: the plan above writes nothing"
238
+ g_info "gob init --write --dry-run: validated OK (class $CLASS, branch $BRANCH, owner $EMAIL, 1+ gate)"
239
+ g_info " sync: ${SYNC_SEL:-none}"
240
+ g_info " would write: AGENTS.md (body + gob block), .gob/engine/, HANDOFF.md"
600
241
  exit 0
601
242
  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
243
 
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
244
+ # ------------------------------------------------------------------- install ---
245
+ # One engine call writes the vendored payload (.gob/), AGENTS.md body + gob block and
246
+ # HANDOFF.md. Its refusal contract applies verbatim: an existing HANDOFF.md is never
247
+ # overwritten; an edited AGENTS.md body is kept (put_once) and only the block is rewritten.
629
248
  INSTALL_ARGS=(--target "$TARGET" --class "$CLASS" --skills no --yes)
249
+ # The first gate is the proposal's own: its name AND command. The name rides along so the
250
+ # HANDOFF template renders the declared name — HP-03 anchors on declared gate names, and a
251
+ # HANDOFF that names a gate the config does not declare is the day-one red (GT-04/HP-03
252
+ # anchoring). GATE_NAME comes from the same key the reader parses (gate_<name>_cmd).
253
+ GATE_NAME=$(g_agents_gates "$PROP" | head -n 1 | awk -F'\t' '{print $1}')
254
+ INSTALL_ARGS+=(--gate "$GATE_CMD" "$GATE_NAME")
630
255
  [ "$ELECTRON" -eq 1 ] && INSTALL_ARGS+=(--electron)
631
- [ -n "$CI_FLAG" ] && INSTALL_ARGS+=($CI_FLAG)
256
+ printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
632
257
  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
258
+ RC=$?
259
+ [ "$RC" -eq 0 ] || { g_err "install failed (exit $RC) — the refusals above name the path and the fix"; exit "$RC"; }
665
260
 
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))
680
- done
261
+ CONFIG="$TARGET/AGENTS.md"
262
+ [ -f "$CONFIG" ] || { g_err "install wrote no AGENTS.md at $TARGET"; exit 1; }
681
263
 
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'
687
- 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
264
+ # The proposal MERGES over the class defaults the installer rendered: the proposal's keys
265
+ # win, every default key the proposal did not set stays (a proposal carrying only the
266
+ # brief's required keys must not blank archive/ratchet/replay — the schema is a minimum,
267
+ # not the whole block). Proposal keys the config did not have are appended.
268
+ # EXCEPT the gate keys: the proposal's first gate REPLACES the class default gate_<name>_cmd
269
+ # (the default names a class-taxonomy gate the project never declared), and every OTHER
270
+ # default gate_* key is dropped — a gate the config declares must be one the proposal chose,
271
+ # never a leftover default naming a gate the HANDOFF does not render. The second class gate
272
+ # (e.g. the software class's todo_ceiling) is opt-IN: the proposal may declare it itself.
273
+ {
274
+ first_gate=1
275
+ while IFS= read -r k; do
276
+ [ -n "$k" ] || continue
277
+ case "$k" in gate_*_cmd)
278
+ if [ "$first_gate" -eq 1 ]; then
279
+ first_gate=0
280
+ 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 }'
281
+ fi
282
+ continue ;;
283
+ esac
284
+ # An ABSENT key prints "key<TAB><TAB>", which renders as "key:: key:" - a key that
285
+ # reads back as its own name twice and fails IN-02/PF-01/FM-01 on a fresh install.
286
+ # A proposal line with an empty value carries no merge information (the class
287
+ # default the installer rendered already covers it): drop it here.
288
+ k_val=$(g_agents_read "$PROP" "$k")
289
+ [ -n "$k_val" ] && printf '%s\t%s\n' "$k" "$k_val"
290
+ done <<< "$BLOCK_KEYS"
291
+ while IFS=$'\t' read -r k v; do
292
+ [ -n "$k" ] || continue
293
+ printf '%s\n' "$BLOCK_KEYS" | grep -qxF "$k" && continue
294
+ case "$k" in gate_*_cmd) continue ;; esac
295
+ printf '%s\t%s\n' "$k" "$v"
296
+ done < <(g_agents_pairs "$CONFIG")
297
+ } | g_agents_write "$CONFIG" >/dev/null || { g_err "could not write the gob block into $CONFIG"; exit 1; }
740
298
 
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
760
- fi
299
+ # Fail closed: the declared gate must read back through the engine's own reader.
300
+ GOT_GATE=$(g_agents_gates "$CONFIG" | head -n 1 | awk -F'\t' '{print $2}')
301
+ [ "$GOT_GATE" = "$GATE_CMD" ] || { g_err "the first gate did not land in $CONFIG (reader sees '$GOT_GATE', wanted '$GATE_CMD')"; exit 1; }
302
+ [ "$(g_agents_read "$CONFIG" owner_email)" = "$EMAIL" ] || { g_err "owner_email did not land in $CONFIG"; exit 1; }
761
303
 
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
304
+ # ------------------------------------------------------------- sync (hidden) ---
305
+ # The sync-translator step (existing emit engine) runs for the platforms the proposal
306
+ # named. A proposal that names none emits nothing — the neutral-first contract.
307
+ for p in $SYNC_SEL; do
308
+ printf 'gob init [run] sync --platform %s --scope project\n' "$p"
309
+ bash "$SRC/bin/goblin-emit" --platform "$p" --scope project --target "$TARGET" || {
310
+ RC=$?; g_err "sync $p failed (exit $RC)"; exit "$RC"; }
311
+ done
312
+
313
+ # ------------------------------------------------------------- mcp config -----
314
+ # The --write mode lands the same .mcp.json (ask-once, never overwrite).
315
+ if [ "$WITH_MCP" -eq 1 ]; then
316
+ do_mcp_config || exit 1
766
317
  fi
318
+
319
+ # --------------------------------------------------------------------- done ----
320
+ printf '\n'
321
+ printf 'gob init: installed — class %s, branch %s, owner %s\n' "$CLASS" "$BRANCH" "$EMAIL"
322
+ printf ' harness: %s/.gob/bin/goblin-verify\n' "$TARGET"
323
+ printf 'next:\n'
324
+ printf ' 1. git add -A && git commit # the install is uncommitted by design\n'
325
+ printf ' 2. run .gob/bin/goblin-verify — the skipped rows are the day-one checklist\n'
326
+ printf ' 3. features/: gob map (agent brief) then gob map --write features/\n'
327
+ 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"
328
+ [ -n "$SYNC_SEL" ] || printf ' 5. agent skills are opt-in per platform\n'
767
329
  exit 0