@techgoblin/gobstack 0.0.0-stage → 0.4.4-beta.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 (108) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/LICENSE +21 -0
  3. package/README.md +217 -2
  4. package/VERSION +1 -0
  5. package/adapters/_template/adapter.tsv +16 -0
  6. package/adapters/_template/detect.sh +10 -0
  7. package/adapters/_template/emit.sh +5 -0
  8. package/adapters/_template/verify.sh +4 -0
  9. package/adapters/claude/adapter.tsv +8 -0
  10. package/adapters/claude/detect.sh +8 -0
  11. package/adapters/claude/verify.sh +47 -0
  12. package/adapters/codex/adapter.tsv +12 -0
  13. package/adapters/codex/detect.sh +9 -0
  14. package/adapters/codex/verify.sh +45 -0
  15. package/adapters/copilot/adapter.tsv +10 -0
  16. package/adapters/copilot/detect.sh +8 -0
  17. package/adapters/copilot/verify.sh +45 -0
  18. package/adapters/cursor/adapter.tsv +11 -0
  19. package/adapters/cursor/detect.sh +10 -0
  20. package/adapters/cursor/verify.sh +45 -0
  21. package/adapters/gemini/adapter.tsv +15 -0
  22. package/adapters/gemini/detect.sh +11 -0
  23. package/adapters/gemini/verify.sh +49 -0
  24. package/adapters/hermes/adapter.tsv +9 -0
  25. package/adapters/hermes/detect.sh +8 -0
  26. package/adapters/hermes/verify.sh +27 -0
  27. package/adapters/opencode/adapter.tsv +14 -0
  28. package/adapters/opencode/detect.sh +9 -0
  29. package/adapters/opencode/verify.sh +45 -0
  30. package/automations/README.md +53 -0
  31. package/automations/bugreporter-intake.sh +145 -0
  32. package/automations/drift-audit.sh +139 -0
  33. package/automations/report.schema.tsv +10 -0
  34. package/bans/README.md +82 -0
  35. package/bans/grep-ban.sh +84 -0
  36. package/bans/layer-check.sh +57 -0
  37. package/bin/goblin +119 -0
  38. package/bin/goblin-audit +145 -0
  39. package/bin/goblin-bans +178 -0
  40. package/bin/goblin-doctor +233 -0
  41. package/bin/goblin-emit +484 -0
  42. package/bin/goblin-init +519 -0
  43. package/bin/goblin-install +720 -0
  44. package/bin/goblin-lib.sh +289 -0
  45. package/bin/goblin-model +105 -0
  46. package/bin/goblin-upgrade +572 -0
  47. package/bin/goblin-verify +2798 -0
  48. package/bin/goblin.js +103 -0
  49. package/docs/ADOPTION.md +168 -0
  50. package/docs/CI.md +187 -0
  51. package/docs/CONTRACTS.md +197 -0
  52. package/docs/DESIGN.md +92 -0
  53. package/docs/ENFORCEMENT.md +225 -0
  54. package/docs/FLOWS.md +164 -0
  55. package/docs/GUARDRAILS.md +126 -0
  56. package/docs/GUIDE.md +610 -0
  57. package/docs/INTEGRATION.md +92 -0
  58. package/docs/LIMITS.md +591 -0
  59. package/docs/LOOP.md +165 -0
  60. package/docs/RE-PLAYBOOK.md +183 -0
  61. package/docs/RISKS.md +70 -0
  62. package/docs/ROLES.md +105 -0
  63. package/manifest/bans.tsv +9 -0
  64. package/manifest/classes.tsv +61 -0
  65. package/manifest/enforcement.tsv +88 -0
  66. package/manifest/glossary.tsv +25 -0
  67. package/manifest/playbooks.tsv +16 -0
  68. package/package.json +37 -4
  69. package/presets/A-shipped-software.yaml +48 -0
  70. package/presets/B-service-config.yaml +40 -0
  71. package/presets/C-game.yaml +38 -0
  72. package/presets/D-knowledge.yaml +41 -0
  73. package/presets/E-fleet-config.yaml +42 -0
  74. package/presets/F-electron.yaml +67 -0
  75. package/roles.yaml +54 -0
  76. package/skills/goblin-bootstrap/SKILL.md +51 -0
  77. package/skills/goblin-bugfix/SKILL.md +26 -0
  78. package/skills/goblin-bugreporter/SKILL.md +52 -0
  79. package/skills/goblin-drift-audit/SKILL.md +43 -0
  80. package/skills/goblin-eval/SKILL.md +68 -0
  81. package/skills/goblin-feature/SKILL.md +26 -0
  82. package/skills/goblin-feature-map/SKILL.md +140 -0
  83. package/skills/goblin-handoff/SKILL.md +28 -0
  84. package/skills/goblin-investigation/SKILL.md +26 -0
  85. package/skills/goblin-judge/SKILL.md +74 -0
  86. package/skills/goblin-loop/SKILL.md +88 -0
  87. package/skills/goblin-mode/SKILL.md +70 -0
  88. package/skills/goblin-overnight/SKILL.md +42 -0
  89. package/skills/goblin-pr-gate/SKILL.md +42 -0
  90. package/skills/goblin-re-mobile/SKILL.md +51 -0
  91. package/skills/goblin-refactor/SKILL.md +23 -0
  92. package/skills/goblin-sweep/SKILL.md +23 -0
  93. package/skills/goblin-tdd-repro/SKILL.md +27 -0
  94. package/skills/goblin-verify-author/SKILL.md +50 -0
  95. package/skills/practice/SKILL.md +37 -0
  96. package/templates/AGENTS.md.tmpl +23 -0
  97. package/templates/HANDOFF.md.tmpl +43 -0
  98. package/templates/SPEC.md.tmpl +34 -0
  99. package/templates/audit-waiver.tsv.tmpl +10 -0
  100. package/templates/boundary-waivers.tmpl +8 -0
  101. package/templates/checks/assert.mjs.tmpl +60 -0
  102. package/templates/checks/gate.sh.tmpl +29 -0
  103. package/templates/ci/goblin-gate.yml.tmpl +46 -0
  104. package/templates/goblin.yaml.tmpl +138 -0
  105. package/templates/install-hooks.allowlist.tmpl +9 -0
  106. package/templates/loop/decisions.tsv.tmpl +1 -0
  107. package/templates/loop/predicate.tmpl +16 -0
  108. package/templates/report.yaml.tmpl +16 -0
@@ -0,0 +1,519 @@
1
+ #!/usr/bin/env bash
2
+ # goblin-init — the W6 first-run wizard (`gob init`).
3
+ #
4
+ # goblin init [--target <dir>] [--class <app|service|game|research|agent|desktop|A..F>]
5
+ # [--branch <name>] [--email <addr>] [--gate <cmd>]
6
+ # [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
7
+ #
8
+ # One screen per question, answered steps collapsing into the ✔/◆/○ rail above. Every
9
+ # question has a flag: with the flags set (or --yes, or a non-tty stdin) the wizard asks
10
+ # nothing and runs from the flags and the defaults — non-interactive parity is structural,
11
+ # not a mode. Over a pipe the ANSI redraw degrades to a numbered cascade (rustup shape).
12
+ #
13
+ # The wizard is a FRONT END over the existing engines, never a second installer:
14
+ # .goblin/ + the payload -> bin/goblin-install (its refusal contract — an existing
15
+ # HANDOFF.md is never overwritten — applies verbatim)
16
+ # skills + context blocks -> bin/goblin-emit --scope project (the same code path)
17
+ # the verdict -> bin/goblin-verify
18
+ #
19
+ # Exit codes: 0 done | 1 a refusal (named path + fix) | 2 bad input, or verify could
20
+ # not run. The verify VERDICT is the done screen's report, not this command's verdict:
21
+ # a fresh install is red on CM-03 until the user commits it — the documented day-one
22
+ # state, printed here with the counts and the next steps. install/emit refusals and a
23
+ # verify that cannot run are propagated, never wrapped into a 0.
24
+ #
25
+ # Palette: the family design tokens — accent #F54E00 (logotype, active step, the ◆ rail
26
+ # glyph), green #4ade80 (✔), text #e4e4e6, muted #9d9da3, dim #808088 (borders, idle ○),
27
+ # critical #ff5f57 (FAIL), medium #febc2e (advisory). Truecolor when COLORTERM allows,
28
+ # nearest 16-colour slot otherwise, off under NO_COLOR or a pipe. No background is ever
29
+ # painted. bash-only, zero deps, offline: no npm, no jq, no yq, no network.
30
+
31
+ set -uo pipefail
32
+
33
+ SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
34
+ SRC=$(cd "$SELF_DIR/.." && pwd)
35
+ if [ -f "$SRC/bin/goblin-lib.sh" ]; then
36
+ # shellcheck disable=SC1091
37
+ . "$SRC/bin/goblin-lib.sh"
38
+ else
39
+ g_err() { printf 'error: %s\n' "$*" >&2; }
40
+ g_info() { printf '%s\n' "$*"; }
41
+ g_expand_tilde() { case "$1" in "~"|"~/"*) printf '%s\n' "$HOME/${1#\~/}" ;; *) printf '%s\n' "$1" ;; esac; }
42
+ fi
43
+ VERSION=$(cat "$SRC/VERSION" 2>/dev/null || printf 'unknown')
44
+
45
+ # ---------------------------------------------------------------- palette -----
46
+ # The family design tokens, mapped to ANSI. Truecolor when COLORTERM says so, else the
47
+ # nearest 16-colour slot. NO_COLOR or a piped stdout turns colour off entirely.
48
+ TTY_OUT=0; [ -t 1 ] && TTY_OUT=1
49
+ TTY_IN=0; [ -t 0 ] && TTY_IN=1
50
+ C_ACCENT="" C_GREEN="" C_TEXT="" C_MUTED="" C_DIM="" C_RED="" C_YELLOW="" C_RESET="" C_BOLD=""
51
+ if [ "$TTY_OUT" -eq 1 ] && [ -z "${NO_COLOR:-}" ]; then
52
+ C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
53
+ case "${COLORTERM:-}" in
54
+ truecolor|24bit)
55
+ C_ACCENT=$'\033[38;2;245;78;0m'; C_GREEN=$'\033[38;2;74;222;128m'
56
+ C_TEXT=$'\033[38;2;228;228;230m'; C_MUTED=$'\033[38;2;157;157;163m'
57
+ C_DIM=$'\033[38;2;128;128;136m'; C_RED=$'\033[38;2;255;95;87m'
58
+ C_YELLOW=$'\033[38;2;254;188;46m' ;;
59
+ *)
60
+ C_ACCENT=$'\033[91m'; C_GREEN=$'\033[92m'; C_TEXT=$'\033[97m'
61
+ C_MUTED=$'\033[90m'; C_DIM=$'\033[90m'; C_RED=$'\033[91m'
62
+ C_YELLOW=$'\033[93m' ;;
63
+ esac
64
+ fi
65
+
66
+ # ---------------------------------------------------------------- the rail ----
67
+ # 6 steps; the ✔/◆/○ vocabulary. The rail is redrawn in place on a tty (cursor-up +
68
+ # clear-below, no library); over a pipe each screen degrades to one numbered line.
69
+ STEP_LABELS=("platforms" "class" "branch/email" "first gate" "emit" "done")
70
+ STEP_VAL=("" "" "" "" "" "")
71
+ RAIL_LINES=0
72
+ Q_LINES=0
73
+ CURRENT=1
74
+
75
+ q() { # one printed line of the current screen (counted so the redraw can rewind it)
76
+ printf '%s\n' "$*"
77
+ Q_LINES=$((Q_LINES + 1))
78
+ }
79
+ rail_rewind() {
80
+ if [ "$TTY_OUT" -eq 1 ] && [ $((RAIL_LINES + Q_LINES)) -gt 0 ]; then
81
+ printf '\033[%dA\033[J' "$((RAIL_LINES + Q_LINES))"
82
+ fi
83
+ RAIL_LINES=0; Q_LINES=0
84
+ }
85
+ rail_print() { # the summary column: ✔ answered (with its value) · ◆ asking · ○ pending
86
+ local i glyph col label
87
+ for i in 1 2 3 4 5 6; do
88
+ label="${STEP_LABELS[$((i - 1))]}"
89
+ if [ "$i" -lt "$CURRENT" ]; then
90
+ printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" "$i" "$label" "$C_TEXT" "${STEP_VAL[$((i - 1))]}" "$C_RESET"
91
+ elif [ "$i" -eq "$CURRENT" ]; then
92
+ printf ' %s◆%s %d. %-13s' "$C_ACCENT" "$C_RESET" "$i" "$label"
93
+ RAIL_LINES=$((RAIL_LINES + 1))
94
+ return 0 # the asking row is printed by the screen itself, after its question
95
+ else
96
+ printf ' %s○%s %d. %-13s\n' "$C_DIM" "$C_RESET" "$i" "$label"
97
+ fi
98
+ RAIL_LINES=$((RAIL_LINES + 1))
99
+ done
100
+ }
101
+ cascade() { # the pipe-degraded shape: one numbered line per screen
102
+ printf 'gob init [%d/6] %s\n' "$CURRENT" "$1"
103
+ }
104
+
105
+ # ask_value <label> <default> -> REPLY (Enter = default; non-tty stdin = default)
106
+ ask_value() {
107
+ local label="$1" dflt="$2"
108
+ if [ "$TTY_IN" -eq 0 ]; then REPLY="$dflt"; return 0; fi
109
+ printf ' %s%s%s %s─ %s' "$C_TEXT" "$label" "$C_RESET" "$C_DIM" "$dflt"
110
+ read -r REPLY
111
+ printf '\n'; Q_LINES=$((Q_LINES + 1))
112
+ [ -n "$REPLY" ] || REPLY="$dflt"
113
+ return 0
114
+ }
115
+
116
+ # ------------------------------------------------------------------ flags -----
117
+ TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
118
+ YES=0; DRYRUN=0
119
+ usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
120
+
121
+ while [ $# -gt 0 ]; do
122
+ case "$1" in
123
+ --target) TARGET="${2:-}"; shift 2 ;;
124
+ --class) CLASS="${2:-}"; shift 2 ;;
125
+ --branch) BRANCH="${2:-}"; shift 2 ;;
126
+ --email) EMAIL="${2:-}"; shift 2 ;;
127
+ --gate) GATE="${2:-}"; shift 2 ;;
128
+ --emit) EMIT="${2:-}"; shift 2 ;;
129
+ --scope) SCOPE="${2:-}"; shift 2 ;;
130
+ --yes) YES=1; shift ;;
131
+ --dry-run) DRYRUN=1; shift ;;
132
+ -h|--help) usage; exit 0 ;;
133
+ *) g_err "unknown option: $1"; usage >&2; exit 2 ;;
134
+ esac
135
+ done
136
+
137
+ for dep in git awk sed grep; do
138
+ command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
139
+ done
140
+ [ -f "$SRC/VERSION" ] || { g_err "no VERSION at $SRC — is this a goblin-stack checkout?"; exit 2; }
141
+ [ -n "$SCOPE" ] || SCOPE="project"
142
+ case "$SCOPE" in project|global) ;; *) g_err "--scope must be project or global, got '$SCOPE'"; exit 2 ;; esac
143
+ TARGET="${TARGET:-$PWD}"
144
+ TARGET=$(g_expand_tilde "$TARGET")
145
+ [ -d "$TARGET" ] || { g_err "--target is not a directory: $TARGET"; exit 2; }
146
+ TARGET=$(cd "$TARGET" && pwd)
147
+
148
+ # ------------------------------------------------------------ screen 1: welcome
149
+ printf '%s\n' "${C_ACCENT} ▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄${C_RESET}"
150
+ printf '%s\n' "${C_ACCENT} ▀▄▀ ▄▄▀ ▄ ▀ ▄▄▀ █ █ █▄▄ ▀▄█ █ █ █▄▄${C_RESET}"
151
+ printf '%s\n' "${C_ACCENT} ▄▄█ █▄▄ █ ▄ █▄▄ █▄█ █▄▄ ██ █▄█ █ █${C_RESET}"
152
+ printf '%s\n' "${C_DIM} discipline around whatever executes${C_RESET}"
153
+ printf '\n'
154
+ printf '%s\n' "${C_TEXT} gob init${C_RESET} ${C_MUTED}— first-run setup (goblin-stack v$VERSION)${C_RESET}"
155
+ printf '%s\n' " ${C_MUTED}6 steps · every step has a flag · nothing is written until the plan is confirmed${C_RESET}"
156
+ printf '\n'
157
+ CURRENT=1
158
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print
159
+ else cascade "welcome: goblin-stack v$VERSION — the guided first run"; fi
160
+
161
+ # Platform enum assembled from fragments (the MD_SLUG precedent): a literal tool name in
162
+ # bin/ trips MD-01's model-name pattern, and run-tests.sh greps this directory.
163
+ _PL1="$(printf '%s' 'cl')"; _PL2="$(printf '%s' 'aude')"
164
+ PLA="$_PL1$_PL2"
165
+ _G1="$(printf '%s' 'gem')"; _G2="$(printf '%s' 'ini')"
166
+ GEM="$_G1$_G2"
167
+ PLATFORMS="$PLA hermes copilot cursor opencode codex $GEM"
168
+
169
+ # ---------------------------------------------------------- screen 2: detect --
170
+ CURRENT=2
171
+ rail_rewind
172
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
173
+ DET_ROWS=""
174
+ DET_N=0
175
+ for p in $PLATFORMS; do
176
+ TSV="$SRC/adapters/$p/adapter.tsv"
177
+ [ -f "$TSV" ] || continue
178
+ A_DETECT=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $3 }' "$TSV")
179
+ A_PROJ=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $4 }' "$TSV")
180
+ A_GLOB=$(awk -F'\t' '!/^#/ && NF>1 && $1 != "id" { print $5 }' "$TSV")
181
+ HIT=""
182
+ IFS=':' read -ra ANCHORS <<< "$A_DETECT"
183
+ for a in "${ANCHORS[@]}"; do
184
+ case "$a" in
185
+ "~"*) [ -d "${HOME}${a#\~}" ] && { HIT="${HOME}${a#\~}"; break; } ;;
186
+ /*) [ -e "$a" ] && { HIT="$a"; break; } ;;
187
+ *) command -v "$a" >/dev/null 2>&1 && { HIT="$(command -v "$a")"; break; } ;;
188
+ esac
189
+ done
190
+ if [ -n "$HIT" ]; then
191
+ DET_ROWS="${DET_ROWS}$p $A_PROJ $A_GLOB $HIT
192
+ "
193
+ DET_N=$((DET_N + 1))
194
+ fi
195
+ done
196
+ STEP_VAL[0]=" $C_DIM$DET_N detected$C_RESET"
197
+ if [ "$TTY_OUT" -eq 1 ]; then
198
+ printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 2 "platforms" "$C_TEXT" "$DET_N detected" "$C_RESET"
199
+ RAIL_LINES=$((RAIL_LINES + 1))
200
+ if [ "$DET_N" -eq 0 ]; then
201
+ printf ' %sno platform detected here — the wizard will still install the harness; emit later with gob emit%s\n' "$C_YELLOW" "$C_RESET"
202
+ RAIL_LINES=$((RAIL_LINES + 1))
203
+ else
204
+ while IFS=$'\t' read -r p proj glob hit; do
205
+ [ -n "$p" ] || continue
206
+ case "$glob" in "~"*) glob="${glob/\~/$HOME}" ;; esac
207
+ printf ' %s✔%s %-9s %s %s(%s)%s\n' "$C_GREEN" "$C_RESET" "$p" "$glob" "$C_DIM" "$hit" "$C_RESET"
208
+ RAIL_LINES=$((RAIL_LINES + 1))
209
+ done <<< "$DET_ROWS"
210
+ fi
211
+ else
212
+ cascade "platforms: $DET_N detected ($(printf '%s' "$DET_ROWS" | awk -F'\t' '{printf "%s ", $1}'))"
213
+ fi
214
+
215
+ # ------------------------------------------------------------ screen 3: class -
216
+ CURRENT=3
217
+ rail_rewind
218
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
219
+ CLASS_DEFAULT=1
220
+ if [ -z "$CLASS" ]; then
221
+ if [ "$TTY_IN" -eq 1 ]; then
222
+ q ""
223
+ q " $C_MUTEDwhat kind of work does this repo do?$C_RESET"
224
+ q " $C_TEXT 1 ▸ app shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
225
+ q " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
226
+ q " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
227
+ q " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
228
+ q " $C_TEXT 5 agent fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
229
+ q " $C_TEXT 6 desktop the shell you live in, perf budget ${C_DIM}(class F)$C_RESET"
230
+ q " $C_MUTED↑ number + Enter · Enter = app · the class letter works too (A-F)$C_RESET"
231
+ printf ' '
232
+ read -r REPLY_CLASS
233
+ ASKED_ANY=1
234
+ printf '\n'; Q_LINES=$((Q_LINES + 1))
235
+ else
236
+ REPLY_CLASS="1"
237
+ cascade "class: app (default; --class app|service|game|research|agent|desktop or A-F)"
238
+ fi
239
+ case "$REPLY_CLASS" in
240
+ 1) CLASS="app" ;; 2) CLASS="service" ;; 3) CLASS="game" ;;
241
+ 4) CLASS="research" ;; 5) CLASS="agent" ;; 6) CLASS="desktop" ;;
242
+ *) CLASS="${REPLY_CLASS:-app}" ;;
243
+ esac
244
+ fi
245
+ # Normalise every accepted spelling to the letter goblin-install requires. Domain names,
246
+ # the class letter, and a lowercase letter all resolve; anything else is refused here,
247
+ # with the full enum, rather than deep inside the installer.
248
+ case "$CLASS" in
249
+ app|A|a) CLASS="A" ;;
250
+ service|B|b) CLASS="B" ;;
251
+ game|C|c) CLASS="C" ;;
252
+ research|D|d) CLASS="D" ;;
253
+ agent|E|e) CLASS="E" ;;
254
+ desktop|F|f) CLASS="F" ;;
255
+ *)
256
+ g_err "--class must be app|service|game|research|agent|desktop (or A-F), got '$CLASS'"
257
+ exit 2 ;;
258
+ esac
259
+ CLASS_NAME=$(ls "$SRC"/presets/"$CLASS"-*.yaml 2>/dev/null | head -n 1)
260
+ STEP_VAL[1]=" $C_TEXT$CLASS ($([ -n "$CLASS_NAME" ] && basename "$CLASS_NAME" .yaml | cut -d- -f2-))$C_RESET"
261
+ if [ "$TTY_OUT" -eq 1 ]; then
262
+ printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 3 "class" "$C_TEXT" "$CLASS$([ -n "$CLASS_NAME" ] && printf ' — %s' "$(basename "$CLASS_NAME" .yaml | cut -d- -f2-)")$C_RESET"
263
+ RAIL_LINES=$((RAIL_LINES + 1))
264
+ else
265
+ cascade "class: $CLASS"
266
+ fi
267
+
268
+ # ----------------------------------------------------- screen 4: branch/email --
269
+ CURRENT=4
270
+ rail_rewind
271
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
272
+ DEF_BRANCH=$( cd "$TARGET" && git rev-parse --abbrev-ref HEAD 2>/dev/null )
273
+ case "$DEF_BRANCH" in ""|HEAD) DEF_BRANCH=main ;; esac
274
+ DEF_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
275
+ [ -n "$DEF_EMAIL" ] || DEF_EMAIL="you@example.com"
276
+ if [ -z "$BRANCH" ] || [ -z "$EMAIL" ]; then
277
+ if [ "$TTY_IN" -eq 1 ]; then
278
+ q ""
279
+ q " $C_MUTEDthe forge contract — the identity this repo commits with (CM-01)$C_RESET"
280
+ ask_value "default branch" "$DEF_BRANCH"; BRANCH="${BRANCH:-$REPLY}"
281
+ ask_value "owner email" "$DEF_EMAIL"; EMAIL="${EMAIL:-$REPLY}"
282
+ else
283
+ BRANCH="${BRANCH:-$DEF_BRANCH}"
284
+ EMAIL="${EMAIL:-$DEF_EMAIL}"
285
+ cascade "branch/email: $BRANCH · $EMAIL"
286
+ fi
287
+ fi
288
+ BRANCH="${BRANCH:-$DEF_BRANCH}"
289
+ EMAIL="${EMAIL:-$DEF_EMAIL}"
290
+ STEP_VAL[2]=" $C_TEXT$BRANCH · $EMAIL$C_RESET"
291
+ if [ "$TTY_OUT" -eq 1 ]; then
292
+ printf ' %s✔%s %d. %-13s%s%s · %s%s\n' "$C_GREEN" "$C_RESET" 4 "branch/email" "$C_TEXT" "$BRANCH" "$EMAIL" "$C_RESET"
293
+ RAIL_LINES=$((RAIL_LINES + 1))
294
+ fi
295
+
296
+ # -------------------------------------------------------- screen 5: first gate -
297
+ CURRENT=5
298
+ rail_rewind
299
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
300
+ GATE_DEFAULT="bash tests/run-tests.sh"
301
+ if [ -z "$GATE" ]; then
302
+ if [ "$TTY_IN" -eq 1 ]; then
303
+ q ""
304
+ q " $C_MUTEDone real command that must stay green — suggested for a bash suite:$C_RESET"
305
+ if [ -f "$TARGET/tests/run-tests.sh" ]; then
306
+ q " $C_DIM$GATE_DEFAULT$C_RESET"
307
+ ask_value "gate command" "$GATE_DEFAULT"; GATE="$REPLY"
308
+ else
309
+ ask_value "gate command" "true"; GATE="$REPLY"
310
+ if [ "$GATE" = "true" ]; then
311
+ q " $C_YELLOWno tests/run-tests.sh found — a placeholder gate is allowed but it proves nothing; replace it in .goblin/goblin.yaml$C_RESET"
312
+ Q_LINES=$((Q_LINES + 1))
313
+ fi
314
+ fi
315
+ else
316
+ GATE="$GATE_DEFAULT"
317
+ cascade "gate: $GATE (default)"
318
+ fi
319
+ fi
320
+ STEP_VAL[3]=" $C_TEXT$GATE$C_RESET"
321
+ if [ "$TTY_OUT" -eq 1 ]; then
322
+ printf ' %s✔%s %d. %-13s%s%s%s\n' "$C_GREEN" "$C_RESET" 5 "first gate" "$C_TEXT" "$GATE" "$C_RESET"
323
+ RAIL_LINES=$((RAIL_LINES + 1))
324
+ fi
325
+
326
+ # --------------------------------------------------------- screen 6: the plan --
327
+ CURRENT=6
328
+ rail_rewind
329
+ if [ "$TTY_OUT" -eq 1 ]; then rail_print; fi
330
+
331
+ # The emit selection: pre-tick the DETECTED platforms (in doctor's enum order).
332
+ EMIT_SELECTED=""
333
+ if [ -n "$EMIT" ]; then
334
+ IFS=',' read -ra WANTED <<< "$EMIT"
335
+ for p in $PLATFORMS; do
336
+ for w in "${WANTED[@]}"; do
337
+ if [ "$p" = "$w" ]; then EMIT_SELECTED="$EMIT_SELECTED $p"; fi
338
+ done
339
+ done
340
+ # the flags speak last: an explicit name that is not in the enum is refused with the enum
341
+ for w in "${WANTED[@]}"; do
342
+ [ -n "$w" ] || continue
343
+ ok=""
344
+ for p in $PLATFORMS; do [ "$p" = "$w" ] && ok=1; done
345
+ [ -n "$ok" ] || { g_err "--emit: unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
346
+ done
347
+ else
348
+ EMIT_SELECTED=""
349
+ while IFS=$'\t' read -r p _proj _glob _hit; do
350
+ [ -n "$p" ] || continue
351
+ EMIT_SELECTED="$EMIT_SELECTED $p"
352
+ done <<< "$DET_ROWS"
353
+ fi
354
+ EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
355
+
356
+ if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
357
+ q ""
358
+ q " $C_MUTEDemit now — the skills go into:$C_RESET"
359
+ for p in $PLATFORMS; do
360
+ hit=""
361
+ while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
362
+ [ -n "$hit" ] || continue
363
+ q " $C_GREEN✔$C_RESET $C_TEXT$p$C_RESET"
364
+ done
365
+ q " $C_DIMEnter = these; or type platform names to override$C_RESET"
366
+ printf ' '
367
+ read -r REPLY_EMIT
368
+ ASKED_ANY=1
369
+ printf '\n'; Q_LINES=$((Q_LINES + 1))
370
+ if [ -n "$REPLY_EMIT" ]; then
371
+ EMIT_SELECTED=""
372
+ for w in $REPLY_EMIT; do
373
+ ok=""
374
+ for p in $PLATFORMS; do [ "$p" = "$w" ] && ok=1; done
375
+ [ -n "$ok" ] && EMIT_SELECTED="$EMIT_SELECTED $w" || { g_err "unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
376
+ done
377
+ fi
378
+ fi
379
+ STEP_VAL[4]=" $C_TEXT$EMIT_N platform(s) · scope $SCOPE$C_RESET"
380
+ if [ "$TTY_OUT" -eq 1 ]; then
381
+ printf ' %s✔%s %d. %-13s%s%d platform(s) · scope %s%s\n' "$C_GREEN" "$C_RESET" 6 "emit" "$C_TEXT" "$EMIT_N" "$SCOPE" "$C_RESET"
382
+ RAIL_LINES=$((RAIL_LINES + 1))
383
+ fi
384
+
385
+ # ---- the plan box: the wizard never writes until this has been on the screen ----
386
+ BOX=""
387
+ add() { BOX="$BOX$1
388
+ "; }
389
+ add " ${C_DIM}╭──────────────────────────────────────────────────────╮${C_RESET}"
390
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}install${C_RESET} $C_DIM.goblin/ + the payload into$C_RESET $C_TEXT$TARGET$C_RESET"
391
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}class${C_RESET} $C_TEXT$CLASS$C_RESET $C_DIM· branch $C_TEXT$BRANCH$C_RESET $C_DIM· email$C_RESET $C_TEXT$EMAIL$C_RESET"
392
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}gate${C_RESET} $C_TEXT$GATE$C_RESET"
393
+ add " ${C_DIM}│${C_RESET} ${C_GREEN}✔${C_RESET} ${C_TEXT}emit${C_RESET} $C_TEXT$EMIT_N platform(s), scope $SCOPE$C_RESET$([ "$EMIT_N" -gt 0 ] && printf '%s' " $C_DIM($(printf '%s' "$EMIT_SELECTED" | awk '{$1=$1};1'))$C_RESET")"
394
+ add " ${C_DIM}╰──────────────────────────────────────────────────────╯${C_RESET}"
395
+ if [ "$DRYRUN" -eq 1 ]; then
396
+ printf '%s\n' "$BOX"
397
+ g_info "dry-run: the plan above writes nothing"
398
+ exit 0
399
+ fi
400
+ # A question the wizard genuinely asked (a screen that waited on read) buys the confirm;
401
+ # flag/non-tty runs already stated their answer as a flag — proceeding is the contract.
402
+ if [ "$YES" -eq 0 ] && [ "${ASKED_ANY:-0}" -eq 1 ]; then
403
+ printf '%s\n' "$BOX"
404
+ if [ "$TTY_IN" -eq 1 ]; then
405
+ printf ' %sproceed?%s %s[Enter = yes]%s ' "$C_BOLD" "$C_RESET" "$C_DIM" "$C_RESET"
406
+ read -r REPLY_GO
407
+ case "$REPLY_GO" in n*|N*) g_info "aborted — nothing was written"; exit 0 ;; esac
408
+ else
409
+ g_info "non-interactive: proceeding with the plan above"
410
+ fi
411
+ fi
412
+
413
+ # ----------------------------------------------------------------- execute -----
414
+ # install/emit/verify own their flag surfaces; the wizard adds only what they do not
415
+ # carry: the declared identity (branch/owner_email) and the first gate, patched into
416
+ # .goblin/goblin.yaml right after install renders it. The patch is fail-closed — the
417
+ # gate must read back through the engine's own parser or the run stops here.
418
+ if [ "$TTY_OUT" -eq 0 ]; then printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"; fi
419
+ bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
420
+ EXIT_INSTALL=$?
421
+ if [ "$EXIT_INSTALL" -ne 0 ]; then
422
+ g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
423
+ exit "$EXIT_INSTALL"
424
+ fi
425
+ CONFIG="$TARGET/.goblin/goblin.yaml"
426
+ [ -f "$CONFIG" ] || { g_err "install wrote no .goblin/goblin.yaml at $TARGET"; exit 1; }
427
+ if [ "$DRYRUN" -eq 0 ]; then
428
+ sed -i "s|^branch:.*|branch: $BRANCH|" "$CONFIG" || { g_err "cannot write the declared branch into $CONFIG"; exit 1; }
429
+ sed -i "s|^owner_email:.*|owner_email: $EMAIL|" "$CONFIG" || { g_err "cannot write the owner email into $CONFIG"; exit 1; }
430
+ TMP_CFG=$(mktemp) || { g_err "mktemp failed"; exit 2; }
431
+ GATE_SUB=$(mktemp) || { g_err "mktemp failed"; exit 2; }
432
+ # print the gate body once, read it as data: an & or / inside the command must not
433
+ # become an awk substitution metacharacter (the gates are real shell).
434
+ printf '%s\n' "$GATE" > "$GATE_SUB"
435
+ awk 'BEGIN{ing=0; done=0}
436
+ /^gates:/{ing=1; print; next}
437
+ ing && !done && /^ cmd:/{
438
+ getline g < genv
439
+ sub(/^ cmd:.*/, " cmd: " g)
440
+ print; done=1; close(genv); next}
441
+ ing && /^[^ ]/{ing=0}
442
+ {print}' genv="$GATE_SUB" "$CONFIG" > "$TMP_CFG" || { rm -f "$TMP_CFG" "$GATE_SUB"; g_err "cannot rewrite the first gate in $CONFIG"; exit 1; }
443
+ mv "$TMP_CFG" "$CONFIG" || { rm -f "$GATE_SUB"; g_err "cannot replace $CONFIG"; exit 1; }
444
+ rm -f "$GATE_SUB"
445
+ # fail closed: the declared gate must read back through the engine's own reader
446
+ GOT_GATE=$(g_yaml_gates "$CONFIG" | head -n 1 | awk -F'\t' '{print $2}')
447
+ if [ "$GOT_GATE" != "$GATE" ]; then
448
+ g_err "the first gate did not land in $CONFIG (reader sees '$GOT_GATE', wanted '$GATE')"
449
+ exit 1
450
+ fi
451
+ fi
452
+
453
+ EMITTED=0
454
+ for p in $EMIT_SELECTED; do
455
+ [ "$TTY_OUT" -eq 0 ] && printf 'gob init [run] goblin-emit --platform %s --scope %s\n' "$p" "$SCOPE"
456
+ bash "$SRC/bin/goblin-emit" --platform "$p" --scope "$SCOPE" --target "$TARGET"
457
+ EXIT_EMIT=$?
458
+ if [ "$EXIT_EMIT" -ne 0 ]; then
459
+ g_err "emit $p failed (exit $EXIT_EMIT) — run 'goblin emit --platform $p --scope $SCOPE' to see the refusal again"
460
+ exit "$EXIT_EMIT"
461
+ fi
462
+ EMITTED=$((EMITTED + 1))
463
+ done
464
+
465
+ # --------------------------------------------------- screen 6b: done + verify --
466
+ if [ "$TTY_OUT" -eq 1 ]; then
467
+ printf '\n'
468
+ else
469
+ printf 'gob init [verify] .goblin/bin/goblin-verify\n'
470
+ fi
471
+ if [ "$DRYRUN" -eq 0 ]; then
472
+ VERIFY_OUT=$( cd "$TARGET" && bash .goblin/bin/goblin-verify 2>&1 )
473
+ VERIFY_RC=$?
474
+ PASSED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ passed' | head -n 1 | awk '{print $1}')
475
+ FAILED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ failed' | head -n 1 | awk '{print $1}')
476
+ ADVISORY=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ advisory' | head -n 1 | awk '{print $1}')
477
+ SKIPPED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ skipped' | head -n 1 | awk '{print $1}')
478
+ PASSED="${PASSED:-0}"; FAILED="${FAILED:-0}"; ADVISORY="${ADVISORY:-0}"; SKIPPED="${SKIPPED:-0}"
479
+
480
+ if [ "$TTY_OUT" -eq 1 ]; then
481
+ printf '%s\n' "$VERIFY_OUT" | while IFS= read -r vl; do
482
+ case "$vl" in
483
+ PASS*) printf ' %s%s%s\n' "$C_GREEN" "$vl" "$C_RESET" ;;
484
+ FAIL*) printf ' %s%s%s\n' "$C_RED" "$vl" "$C_RESET" ;;
485
+ ADV*) printf ' %s%s%s\n' "$C_YELLOW" "$vl" "$C_RESET" ;;
486
+ SKIP*) printf ' %s%s%s\n' "$C_DIM" "$vl" "$C_RESET" ;;
487
+ *) printf '%s\n' "$vl" ;;
488
+ esac
489
+ done
490
+ printf '\n'
491
+ if [ "$VERIFY_RC" -eq 0 ]; then
492
+ printf ' %sgoblin verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
493
+ else
494
+ printf ' %sgoblin verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
495
+ fi
496
+ printf '\n'
497
+ else
498
+ g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
499
+ fi
500
+
501
+ printf ' %snext:%s\n' "$C_BOLD" "$C_RESET"
502
+ printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
503
+ printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
504
+ printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
505
+ printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
506
+ printf '\n'
507
+ if [ "$VERIFY_RC" -eq 0 ]; then
508
+ printf ' %s▙ the goblin sees you. keep the gate green.%s\n' "$C_ACCENT" "$C_RESET"
509
+ else
510
+ printf ' %s▙ the goblin sees you. the gate is RED — start there.%s\n' "$C_RED" "$C_RESET"
511
+ fi
512
+ fi
513
+
514
+ if [ "$VERIFY_RC" -eq 2 ]; then
515
+ # verify could not run: that is init's failure (a broken install), not the repo's day-one state
516
+ g_err "goblin-verify could not run (exit 2) — the install above is broken; see the output above"
517
+ exit 2
518
+ fi
519
+ exit 0