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