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