@techgoblin/gobstack 0.5.0-beta.7 → 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.
- package/CHANGELOG.md +38 -0
- package/README.md +112 -123
- package/VERSION +1 -1
- package/automations/drift-audit.sh +4 -4
- package/bans/layer-check.sh +10 -8
- package/bin/goblin +61 -52
- package/bin/goblin-audit +11 -13
- package/bin/goblin-bans +11 -11
- package/bin/goblin-init +275 -713
- package/bin/goblin-install +160 -114
- package/bin/goblin-lib.sh +234 -1
- package/bin/goblin-map +607 -0
- package/bin/goblin-mcp.js +492 -0
- package/bin/goblin-model +4 -4
- package/bin/goblin-upgrade +1 -1
- package/bin/goblin-verify +159 -145
- package/bin/goblin.js +33 -49
- package/docs/ADOPTION.md +15 -15
- package/docs/CONTRACTS.md +16 -15
- package/docs/DESIGN.md +1 -1
- package/docs/ENFORCEMENT.md +89 -90
- package/docs/FLOWS.md +1 -1
- package/docs/GLOSSARY.md +3 -3
- package/docs/GUARDRAILS.md +5 -5
- package/docs/GUIDE.md +178 -169
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITS.md +25 -0
- package/docs/LOOP.md +12 -12
- package/docs/RE-PLAYBOOK.md +3 -3
- package/docs/ROLES.md +5 -5
- package/manifest/bans.tsv +8 -8
- package/manifest/classes.tsv +3 -3
- package/manifest/enforcement.tsv +40 -40
- package/manifest/glossary.tsv +3 -3
- package/manifest/playbooks.tsv +1 -1
- package/package.json +1 -1
- package/presets/electron-overlay.yaml +2 -2
- package/presets/fleet.yaml +8 -7
- package/presets/game.yaml +1 -1
- package/presets/research.yaml +1 -1
- package/presets/service.yaml +1 -1
- package/presets/software.yaml +1 -1
- package/skills/goblin-bootstrap/SKILL.md +2 -2
- package/templates/AGENTS.md.tmpl +8 -18
- package/templates/HANDOFF.md.tmpl +5 -5
- package/templates/agents-block.tmpl +45 -0
- package/templates/audit-waiver.tsv.tmpl +2 -2
- package/templates/boundary-waivers.tmpl +1 -1
- package/templates/checks/gate.sh.tmpl +6 -6
- package/templates/install-hooks.allowlist.tmpl +1 -1
- package/templates/ci/goblin-gate.yml.tmpl +0 -46
- package/templates/goblin.yaml.tmpl +0 -146
- package/templates/loop/decisions.tsv.tmpl +0 -1
- 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
|
|
2
|
+
# goblin-init — `gob init`: the AI-DRIVEN first step (v2 prompt engine).
|
|
3
3
|
#
|
|
4
|
-
# gob init
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# the
|
|
14
|
-
# the
|
|
15
|
-
# a
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
|
22
|
-
#
|
|
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
|
|
28
|
-
#
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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)
|
|
280
|
-
--
|
|
281
|
-
--
|
|
282
|
-
--
|
|
283
|
-
--
|
|
284
|
-
--
|
|
285
|
-
--
|
|
286
|
-
|
|
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
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
550
|
-
|
|
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 [ "$
|
|
566
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if [ "$
|
|
580
|
-
|
|
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
|
-
#
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
g_info "
|
|
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
|
-
#
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
# .
|
|
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 "$
|
|
256
|
+
printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
|
|
632
257
|
bash "$SRC/bin/goblin-install" "${INSTALL_ARGS[@]}"
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
#
|
|
683
|
-
#
|
|
684
|
-
#
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|