@techgoblin/gobstack 0.0.0-stage → 0.4.4-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +351 -0
- package/LICENSE +21 -0
- package/README.md +217 -2
- package/VERSION +1 -0
- package/adapters/_template/adapter.tsv +16 -0
- package/adapters/_template/detect.sh +10 -0
- package/adapters/_template/emit.sh +5 -0
- package/adapters/_template/verify.sh +4 -0
- package/adapters/claude/adapter.tsv +8 -0
- package/adapters/claude/detect.sh +8 -0
- package/adapters/claude/verify.sh +47 -0
- package/adapters/codex/adapter.tsv +12 -0
- package/adapters/codex/detect.sh +9 -0
- package/adapters/codex/verify.sh +45 -0
- package/adapters/copilot/adapter.tsv +10 -0
- package/adapters/copilot/detect.sh +8 -0
- package/adapters/copilot/verify.sh +45 -0
- package/adapters/cursor/adapter.tsv +11 -0
- package/adapters/cursor/detect.sh +10 -0
- package/adapters/cursor/verify.sh +45 -0
- package/adapters/gemini/adapter.tsv +15 -0
- package/adapters/gemini/detect.sh +11 -0
- package/adapters/gemini/verify.sh +49 -0
- package/adapters/hermes/adapter.tsv +9 -0
- package/adapters/hermes/detect.sh +8 -0
- package/adapters/hermes/verify.sh +27 -0
- package/adapters/opencode/adapter.tsv +14 -0
- package/adapters/opencode/detect.sh +9 -0
- package/adapters/opencode/verify.sh +45 -0
- package/automations/README.md +53 -0
- package/automations/bugreporter-intake.sh +145 -0
- package/automations/drift-audit.sh +139 -0
- package/automations/report.schema.tsv +10 -0
- package/bans/README.md +82 -0
- package/bans/grep-ban.sh +84 -0
- package/bans/layer-check.sh +57 -0
- package/bin/goblin +119 -0
- package/bin/goblin-audit +145 -0
- package/bin/goblin-bans +178 -0
- package/bin/goblin-doctor +233 -0
- package/bin/goblin-emit +484 -0
- package/bin/goblin-init +519 -0
- package/bin/goblin-install +720 -0
- package/bin/goblin-lib.sh +289 -0
- package/bin/goblin-model +105 -0
- package/bin/goblin-upgrade +572 -0
- package/bin/goblin-verify +2798 -0
- package/bin/goblin.js +103 -0
- package/docs/ADOPTION.md +168 -0
- package/docs/CI.md +187 -0
- package/docs/CONTRACTS.md +197 -0
- package/docs/DESIGN.md +92 -0
- package/docs/ENFORCEMENT.md +225 -0
- package/docs/FLOWS.md +164 -0
- package/docs/GUARDRAILS.md +126 -0
- package/docs/GUIDE.md +610 -0
- package/docs/INTEGRATION.md +92 -0
- package/docs/LIMITS.md +591 -0
- package/docs/LOOP.md +165 -0
- package/docs/RE-PLAYBOOK.md +183 -0
- package/docs/RISKS.md +70 -0
- package/docs/ROLES.md +105 -0
- package/manifest/bans.tsv +9 -0
- package/manifest/classes.tsv +61 -0
- package/manifest/enforcement.tsv +88 -0
- package/manifest/glossary.tsv +25 -0
- package/manifest/playbooks.tsv +16 -0
- package/package.json +37 -4
- package/presets/A-shipped-software.yaml +48 -0
- package/presets/B-service-config.yaml +40 -0
- package/presets/C-game.yaml +38 -0
- package/presets/D-knowledge.yaml +41 -0
- package/presets/E-fleet-config.yaml +42 -0
- package/presets/F-electron.yaml +67 -0
- package/roles.yaml +54 -0
- package/skills/goblin-bootstrap/SKILL.md +51 -0
- package/skills/goblin-bugfix/SKILL.md +26 -0
- package/skills/goblin-bugreporter/SKILL.md +52 -0
- package/skills/goblin-drift-audit/SKILL.md +43 -0
- package/skills/goblin-eval/SKILL.md +68 -0
- package/skills/goblin-feature/SKILL.md +26 -0
- package/skills/goblin-feature-map/SKILL.md +140 -0
- package/skills/goblin-handoff/SKILL.md +28 -0
- package/skills/goblin-investigation/SKILL.md +26 -0
- package/skills/goblin-judge/SKILL.md +74 -0
- package/skills/goblin-loop/SKILL.md +88 -0
- package/skills/goblin-mode/SKILL.md +70 -0
- package/skills/goblin-overnight/SKILL.md +42 -0
- package/skills/goblin-pr-gate/SKILL.md +42 -0
- package/skills/goblin-re-mobile/SKILL.md +51 -0
- package/skills/goblin-refactor/SKILL.md +23 -0
- package/skills/goblin-sweep/SKILL.md +23 -0
- package/skills/goblin-tdd-repro/SKILL.md +27 -0
- package/skills/goblin-verify-author/SKILL.md +50 -0
- package/skills/practice/SKILL.md +37 -0
- package/templates/AGENTS.md.tmpl +23 -0
- package/templates/HANDOFF.md.tmpl +43 -0
- package/templates/SPEC.md.tmpl +34 -0
- package/templates/audit-waiver.tsv.tmpl +10 -0
- package/templates/boundary-waivers.tmpl +8 -0
- package/templates/checks/assert.mjs.tmpl +60 -0
- package/templates/checks/gate.sh.tmpl +29 -0
- package/templates/ci/goblin-gate.yml.tmpl +46 -0
- package/templates/goblin.yaml.tmpl +138 -0
- package/templates/install-hooks.allowlist.tmpl +9 -0
- package/templates/loop/decisions.tsv.tmpl +1 -0
- package/templates/loop/predicate.tmpl +16 -0
- package/templates/report.yaml.tmpl +16 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# goblin-lib.sh — shared helpers for goblin-stack.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by bin/goblin-install, bin/goblin-verify and bin/goblin-model.
|
|
5
|
+
# Dependencies: bash 4+, git, awk, sed, grep, sha256sum (or shasum).
|
|
6
|
+
# No npm, no jq, no yq, no network.
|
|
7
|
+
#
|
|
8
|
+
# The config format is a FLAT, LINE-ORIENTED YAML SUBSET. It is parsed here, never by a
|
|
9
|
+
# YAML library, so the same file parses identically on any machine:
|
|
10
|
+
#
|
|
11
|
+
# key: value top-level scalar
|
|
12
|
+
# key: block or list header
|
|
13
|
+
# member: value two-space-indented block member
|
|
14
|
+
# - item two-space-indented list item
|
|
15
|
+
# gates:
|
|
16
|
+
# - name: typecheck four-space-indented second member of a list entry
|
|
17
|
+
# cmd: npx tsc --noEmit
|
|
18
|
+
|
|
19
|
+
GOBLIN_LIB_VERSION="0.4.4"
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------- output -----
|
|
22
|
+
g_pass() { printf 'PASS %-6s %s\n' "$1" "$2"; }
|
|
23
|
+
g_fail() { printf 'FAIL %-6s %s\n' "$1" "$2"; }
|
|
24
|
+
g_adv() { printf 'ADV %-6s %s\n' "$1" "$2"; }
|
|
25
|
+
g_skip() { printf 'SKIP %-6s %s\n' "$1" "$2"; }
|
|
26
|
+
g_info() { printf '%s\n' "$*"; }
|
|
27
|
+
g_err() { printf 'error: %s\n' "$*" >&2; }
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------ hash -----
|
|
30
|
+
g_sha256_file() {
|
|
31
|
+
if [ -f "$1" ]; then
|
|
32
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
33
|
+
sha256sum "$1" | awk '{print $1}'
|
|
34
|
+
else
|
|
35
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
36
|
+
fi
|
|
37
|
+
else
|
|
38
|
+
printf 'missing\n'
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
g_sha256_text() {
|
|
43
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
44
|
+
printf '%s' "$1" | sha256sum | awk '{print $1}'
|
|
45
|
+
else
|
|
46
|
+
printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ paths ----
|
|
51
|
+
# g_abspath <path> — absolute path without requiring the file to exist.
|
|
52
|
+
g_abspath() {
|
|
53
|
+
case "$1" in
|
|
54
|
+
/*) printf '%s\n' "$1" ;;
|
|
55
|
+
"~"|"~/"*) printf '%s\n' "$HOME/${1#\~/}" ;;
|
|
56
|
+
*) printf '%s\n' "$PWD/$1" ;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# g_expand_tilde <path>
|
|
61
|
+
g_expand_tilde() {
|
|
62
|
+
case "$1" in
|
|
63
|
+
"~"|"~/"*) printf '%s\n' "$HOME/${1#\~/}" ;;
|
|
64
|
+
*) printf '%s\n' "$1" ;;
|
|
65
|
+
esac
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------ yaml -----
|
|
69
|
+
# g_models_field <mapping-file> <profile> <field> — read one field of one profile out of a
|
|
70
|
+
# model mapping file. That file is two-level (a profile header, then its fields), so it is
|
|
71
|
+
# read with python3 — the same way the fleet's own tool reads it. Prints nothing if absent.
|
|
72
|
+
g_models_field() {
|
|
73
|
+
python3 -c '
|
|
74
|
+
import re, sys
|
|
75
|
+
path, profile, field = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
76
|
+
try:
|
|
77
|
+
lines = open(path, encoding="utf-8", errors="replace").read().split("\n")
|
|
78
|
+
except OSError:
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
start, base = None, 0
|
|
81
|
+
for i, ln in enumerate(lines):
|
|
82
|
+
m = re.match(r"^(\s*)([A-Za-z_][\w.-]*):\s*$", ln)
|
|
83
|
+
if m and m.group(2) == profile:
|
|
84
|
+
start, base = i, len(m.group(1)); break
|
|
85
|
+
if start is not None:
|
|
86
|
+
for ln in lines[start + 1:]:
|
|
87
|
+
if not ln.strip() or ln.lstrip().startswith("#"):
|
|
88
|
+
continue
|
|
89
|
+
if len(ln) - len(ln.lstrip()) <= base:
|
|
90
|
+
break
|
|
91
|
+
m = re.match(r"^\s*([A-Za-z_][\w.-]*):\s*(.*)$", ln)
|
|
92
|
+
if m and m.group(1) == field:
|
|
93
|
+
v = m.group(2).strip()
|
|
94
|
+
print(v[1:-1] if len(v) > 1 and v[0] == v[-1] and v[0] in "\"\x27" else v)
|
|
95
|
+
break
|
|
96
|
+
' "$1" "$2" "$3"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# g_unquote <value> — strip one layer of surrounding double quotes.
|
|
100
|
+
g_unquote() {
|
|
101
|
+
local v="$1"
|
|
102
|
+
v=${v%\"}; v=${v#\"}
|
|
103
|
+
printf '%s' "$v"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# g_yaml_scalar <file> <key> — value of a top-level scalar key (empty if absent).
|
|
107
|
+
g_yaml_scalar() {
|
|
108
|
+
sed -n "s/^$2:[[:space:]]*//p" "$1" | head -n 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# g_yaml_block_scalar <file> <block> <key> — value of a 2-space-indented block member.
|
|
112
|
+
g_yaml_block_scalar() {
|
|
113
|
+
awk -v b="$2" -v k="$3" '
|
|
114
|
+
$0 ~ ("^" b ":[[:space:]]*$") { inb = 1; next }
|
|
115
|
+
inb && /^[^ ]/ { inb = 0 }
|
|
116
|
+
inb && $0 ~ ("^ " k ":") {
|
|
117
|
+
v = $0; sub("^ " k ":[[:space:]]*", "", v); print v; exit
|
|
118
|
+
}
|
|
119
|
+
' "$1"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# g_yaml_list <file> <key> — 2-space-indented list items under a top-level list key.
|
|
123
|
+
g_yaml_list() {
|
|
124
|
+
awk -v k="$2" '
|
|
125
|
+
$0 ~ ("^" k ":[[:space:]]*$") { inb = 1; next }
|
|
126
|
+
inb && /^[^ ]/ { inb = 0 }
|
|
127
|
+
inb && /^ - / { v = $0; sub(/^ - /, "", v); print v }
|
|
128
|
+
' "$1"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# g_yaml_gates <file> — one "name<TAB>cmd" line per declared gate.
|
|
132
|
+
g_yaml_gates() {
|
|
133
|
+
awk '
|
|
134
|
+
/^gates:[[:space:]]*$/ { inb = 1; next }
|
|
135
|
+
inb && /^[^ ]/ { inb = 0 }
|
|
136
|
+
inb && /^ - name:/ { n = $0; sub(/^ - name:[[:space:]]*/, "", n); name = n; next }
|
|
137
|
+
inb && /^ cmd:/ { c = $0; sub(/^ cmd:[[:space:]]*/, "", c);
|
|
138
|
+
if (name != "") { print name "\t" c; name = "" } }
|
|
139
|
+
' "$1"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# g_yaml_gate_names <file> — one declared gate NAME per line, whether or not a `cmd:` follows it.
|
|
143
|
+
# g_yaml_gates prints a name only when a `cmd:` LINE follows it, so a gate whose `cmd:` was
|
|
144
|
+
# deleted, blanked or re-indented vanishes from it in silence. This reader keeps the name
|
|
145
|
+
# visible, which is what lets GT-01 FAIL on the disappearance instead of reporting the
|
|
146
|
+
# survivors (G8-3).
|
|
147
|
+
g_yaml_gate_names() {
|
|
148
|
+
awk '
|
|
149
|
+
/^gates:[[:space:]]*$/ { inb = 1; next }
|
|
150
|
+
inb && /^[^ ]/ { inb = 0 }
|
|
151
|
+
inb && /^ - name:/ { n = $0; sub(/^ - name:[[:space:]]*/, "", n); print n }
|
|
152
|
+
' "$1"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# g_yaml_gates_all <file> — one "name<TAB>cmd" line per DECLARED gate, including a gate whose
|
|
156
|
+
# `cmd:` is missing or blank (the value is printed empty). g_yaml_gates emits only name+cmd
|
|
157
|
+
# PAIRS, which is right for running a gate (GT-02) and wrong for counting declarations
|
|
158
|
+
# (GT-01): a gate could lose its `cmd:` and the row still printed the survivors (G8-3).
|
|
159
|
+
g_yaml_gates_all() {
|
|
160
|
+
awk '
|
|
161
|
+
function flush() { if (name != "") { print name "\t"; name = "" } }
|
|
162
|
+
/^gates:[[:space:]]*$/ { inb = 1; next }
|
|
163
|
+
inb && /^[^ ]/ { flush(); inb = 0 }
|
|
164
|
+
inb && /^ - name:/ { flush(); n = $0; sub(/^ - name:[[:space:]]*/, "", n); name = n; next }
|
|
165
|
+
inb && /^ cmd:/ { c = $0; sub(/^ cmd:[[:space:]]*/, "", c); print name "\t" c; name = "" }
|
|
166
|
+
END { if (inb) flush() }
|
|
167
|
+
' "$1"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# g_yaml_disabled <file> — inline list of disabled parts, one per line.
|
|
171
|
+
g_yaml_disabled() {
|
|
172
|
+
local v
|
|
173
|
+
v=$(g_yaml_scalar "$1" disabled)
|
|
174
|
+
case "$v" in
|
|
175
|
+
""|"[]") return 0 ;;
|
|
176
|
+
esac
|
|
177
|
+
v=${v#[}; v=${v%]}
|
|
178
|
+
printf '%s\n' "$v" | tr ',' '\n' \
|
|
179
|
+
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^$' || true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# g_part_disabled <config> <part>
|
|
183
|
+
g_part_disabled() {
|
|
184
|
+
g_yaml_disabled "$1" | grep -qx "$2"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------- class data ----
|
|
188
|
+
# g_class_need <classes.tsv> <class> <part> -> R | O | -
|
|
189
|
+
g_class_need() {
|
|
190
|
+
awk -F'\t' -v c="$2" -v p="$3" '
|
|
191
|
+
NR > 1 && $1 == c && $2 == p { print $3; found = 1; exit }
|
|
192
|
+
END { if (!found) print "-" }
|
|
193
|
+
' "$1"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------- json -------
|
|
197
|
+
# installed.json is emitted by goblin-install in a fixed, line-oriented shape so it can
|
|
198
|
+
# be read without a JSON library. g_json_object <installed.json> <object-name> -> "key<TAB>value".
|
|
199
|
+
g_json_object() {
|
|
200
|
+
awk -v name="$2" '
|
|
201
|
+
index($0, "\"" name "\"") && /:[[:space:]]*\{/ { inf = 1; next }
|
|
202
|
+
inf && /^[[:space:]]*\}/ { inf = 0 }
|
|
203
|
+
inf && /"/ {
|
|
204
|
+
line = $0
|
|
205
|
+
sub(/^[[:space:]]*"/, "", line)
|
|
206
|
+
p = line; sub(/".*/, "", p)
|
|
207
|
+
h = line; sub(/^[^"]*"[[:space:]]*:[[:space:]]*"/, "", h); sub(/".*/, "", h)
|
|
208
|
+
if (p != "") print p "\t" h
|
|
209
|
+
}
|
|
210
|
+
' "$1"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
g_installed_files() { g_json_object "$1" files; }
|
|
214
|
+
|
|
215
|
+
g_installed_scalar() {
|
|
216
|
+
sed -n "s/^[[:space:]]*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}$/\1/p" "$1" | head -n 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------- self-test -----
|
|
220
|
+
# Proves the parser actually parses. Every assertion is a real comparison against a
|
|
221
|
+
# value written to a temp file in this function — blank the awk in g_yaml_scalar and
|
|
222
|
+
# this exits non-zero (that is the RED control for the reader itself).
|
|
223
|
+
g_self_test() {
|
|
224
|
+
local tmp rc=0
|
|
225
|
+
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t goblin) || { g_err "mktemp failed"; return 2; }
|
|
226
|
+
cat > "$tmp/g.yaml" <<'YAML'
|
|
227
|
+
class: B
|
|
228
|
+
branch: master
|
|
229
|
+
archive: false
|
|
230
|
+
disabled: [spec, tokens]
|
|
231
|
+
gates:
|
|
232
|
+
- name: typecheck
|
|
233
|
+
cmd: npx tsc --noEmit
|
|
234
|
+
ratchet:
|
|
235
|
+
name: hex
|
|
236
|
+
ceiling: 160
|
|
237
|
+
runtime_data:
|
|
238
|
+
- .goblin/state.json
|
|
239
|
+
YAML
|
|
240
|
+
|
|
241
|
+
local got
|
|
242
|
+
got=$(g_yaml_scalar "$tmp/g.yaml" class)
|
|
243
|
+
[ "$got" = "B" ] || { g_err "scalar: expected B, got '$got'"; rc=1; }
|
|
244
|
+
got=$(g_yaml_scalar "$tmp/g.yaml" branch)
|
|
245
|
+
[ "$got" = "master" ] || { g_err "scalar: expected master, got '$got'"; rc=1; }
|
|
246
|
+
got=$(g_yaml_scalar "$tmp/g.yaml" nosuchkey)
|
|
247
|
+
[ -z "$got" ] || { g_err "scalar: absent key should be empty, got '$got'"; rc=1; }
|
|
248
|
+
got=$(g_yaml_block_scalar "$tmp/g.yaml" ratchet ceiling)
|
|
249
|
+
[ "$got" = "160" ] || { g_err "block scalar: expected 160, got '$got'"; rc=1; }
|
|
250
|
+
got=$(g_yaml_block_scalar "$tmp/g.yaml" ratchet name)
|
|
251
|
+
[ "$got" = "hex" ] || { g_err "block scalar: expected hex, got '$got'"; rc=1; }
|
|
252
|
+
got=$(g_yaml_gates "$tmp/g.yaml" | tr '\t' ':')
|
|
253
|
+
[ "$got" = "typecheck:npx tsc --noEmit" ] || { g_err "gates: got '$got'"; rc=1; }
|
|
254
|
+
# The gates-all readers exist for the one case the pair reader hides: a gate whose `cmd:` LINE
|
|
255
|
+
# is missing (deleted or re-indented). g_yaml_gates drops it; g_yaml_gates_all keeps it with an
|
|
256
|
+
# empty cmd and g_yaml_gate_names still names it. Blank either awk and this exits non-zero -
|
|
257
|
+
# that is this self-test's own RED control for the G8-3 fix (the disappearance GT-01 must FAIL).
|
|
258
|
+
printf 'gates:\n - name: typecheck\n cmd: npx tsc --noEmit\n - name: reindented\n cmd: false\n' > "$tmp/g2.yaml"
|
|
259
|
+
got=$(g_yaml_gates "$tmp/g2.yaml" | wc -l | tr -d '[:space:]')
|
|
260
|
+
[ "$got" = "1" ] || { g_err "gates: a cmd-less gate should vanish from the pair reader, got $got line(s)"; rc=1; }
|
|
261
|
+
got=$(g_yaml_gates_all "$tmp/g2.yaml" | wc -l | tr -d '[:space:]')
|
|
262
|
+
[ "$got" = "2" ] || { g_err "gates-all: expected 2 declared gates, got $got"; rc=1; }
|
|
263
|
+
got=$(g_yaml_gates_all "$tmp/g2.yaml" | awk -F'\t' '$1 == "reindented" && $2 == ""' | wc -l | tr -d '[:space:]')
|
|
264
|
+
[ "$got" = "1" ] || { g_err "gates-all: the cmd-less gate was not reported with an empty cmd"; rc=1; }
|
|
265
|
+
got=$(g_yaml_gate_names "$tmp/g2.yaml" | tr '\n' ',')
|
|
266
|
+
[ "$got" = "typecheck,reindented," ] || { g_err "gate-names: got '$got'"; rc=1; }
|
|
267
|
+
got=$(g_yaml_list "$tmp/g.yaml" runtime_data | tr '\n' ',')
|
|
268
|
+
[ "$got" = ".goblin/state.json," ] || { g_err "list: got '$got'"; rc=1; }
|
|
269
|
+
got=$(g_yaml_disabled "$tmp/g.yaml" | tr '\n' ',')
|
|
270
|
+
[ "$got" = "spec,tokens," ] || { g_err "disabled: got '$got'"; rc=1; }
|
|
271
|
+
|
|
272
|
+
rm -rf "$tmp"
|
|
273
|
+
if [ "$rc" -eq 0 ]; then
|
|
274
|
+
printf 'OK\n'
|
|
275
|
+
else
|
|
276
|
+
printf 'SELF-TEST FAILED\n' >&2
|
|
277
|
+
fi
|
|
278
|
+
return "$rc"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Allow `bash bin/goblin-lib.sh --self-test` and `source bin/goblin-lib.sh`.
|
|
282
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
283
|
+
case "${1:-}" in
|
|
284
|
+
--self-test) g_self_test ;;
|
|
285
|
+
--version) printf '%s\n' "$GOBLIN_LIB_VERSION" ;;
|
|
286
|
+
*) printf 'usage: goblin-lib.sh --self-test\n' >&2; exit 2 ;;
|
|
287
|
+
esac
|
|
288
|
+
exit $?
|
|
289
|
+
fi
|
package/bin/goblin-model
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# goblin-model — resolve a ROLE to provider/model/effort, by reading one mapping file.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# goblin-model <role> [--models <file>] [--config <goblin.yaml>] [--roles <roles.yaml>] [--json]
|
|
6
|
+
# goblin-model --list
|
|
7
|
+
#
|
|
8
|
+
# A role is what the work needs (code | judgment | review-panel | synthesis | investigate).
|
|
9
|
+
# A profile is a worker identity. This tool never invents a model: it looks the role's
|
|
10
|
+
# default profile up in the mapping file and prints what that file says.
|
|
11
|
+
#
|
|
12
|
+
# The mapping file is the ONE install-time machine input. goblin-stack READS it and never
|
|
13
|
+
# writes it. Absent on this machine -> every role resolves to `unknown`, and the model-related
|
|
14
|
+
# checks in goblin-verify report advisory instead of failing.
|
|
15
|
+
#
|
|
16
|
+
# Output, one line per lane: <profile> <provider> <model> <effort>
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
21
|
+
# shellcheck source=goblin-lib.sh
|
|
22
|
+
. "$SELF_DIR/goblin-lib.sh"
|
|
23
|
+
|
|
24
|
+
ROLE=""
|
|
25
|
+
MODELS="${GOBLIN_MODELS:-$HOME/projects/fleet-model.yaml}"
|
|
26
|
+
CONFIG=""
|
|
27
|
+
ROLES=""
|
|
28
|
+
JSON=0
|
|
29
|
+
LIST=0
|
|
30
|
+
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--models) MODELS="${2:-}"; MODELS_EXPLICIT=1; shift 2 ;;
|
|
34
|
+
--config) CONFIG="${2:-}"; shift 2 ;;
|
|
35
|
+
--roles) ROLES="${2:-}"; shift 2 ;;
|
|
36
|
+
--json) JSON=1; shift ;;
|
|
37
|
+
--list) LIST=1; shift ;;
|
|
38
|
+
-h|--help) sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
39
|
+
-*) g_err "unknown option: $1"; exit 2 ;;
|
|
40
|
+
*) ROLE="$1"; shift ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
45
|
+
[ -n "$ROOT" ] || ROOT="$PWD"
|
|
46
|
+
|
|
47
|
+
# Config: explicit, else the installed one, else none.
|
|
48
|
+
if [ -z "$CONFIG" ]; then
|
|
49
|
+
if [ -f "$ROOT/.goblin/goblin.yaml" ]; then CONFIG="$ROOT/.goblin/goblin.yaml"; fi
|
|
50
|
+
fi
|
|
51
|
+
if [ -n "$CONFIG" ] && [ -z "${MODELS_EXPLICIT:-}" ]; then
|
|
52
|
+
m=$(g_yaml_scalar "$CONFIG" models_file)
|
|
53
|
+
[ -n "$m" ] && MODELS="$m"
|
|
54
|
+
fi
|
|
55
|
+
MODELS=$(g_expand_tilde "$MODELS")
|
|
56
|
+
|
|
57
|
+
if [ -z "$ROLES" ]; then
|
|
58
|
+
if [ -f "$ROOT/.goblin/roles.yaml" ]; then ROLES="$ROOT/.goblin/roles.yaml"
|
|
59
|
+
else ROLES="$SELF_DIR/../roles.yaml"; fi
|
|
60
|
+
fi
|
|
61
|
+
[ -f "$ROLES" ] || { g_err "no roles file at $ROLES"; exit 2; }
|
|
62
|
+
|
|
63
|
+
roles_list() {
|
|
64
|
+
awk '/^role-/{ n = $0; sub(/^role-/, "", n); sub(/:.*/, "", n); print n }' "$ROLES"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if [ "$LIST" -eq 1 ]; then
|
|
68
|
+
for r in $(roles_list); do
|
|
69
|
+
printf '%-14s %s\n' "$r" "$(g_yaml_block_scalar "$ROLES" "role-$r" capability)"
|
|
70
|
+
done
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
[ -n "$ROLE" ] || { g_err "a role is required (see --list)"; exit 2; }
|
|
75
|
+
|
|
76
|
+
if ! roles_list | grep -qx "$ROLE"; then
|
|
77
|
+
g_err "unknown role '$ROLE'; known: $(roles_list | paste -sd'|' -)"
|
|
78
|
+
exit 2
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
profiles=$(g_yaml_block_scalar "$ROLES" "role-$ROLE" profiles)
|
|
82
|
+
profiles=$(printf '%s' "$profiles" | tr -d '[]' | tr ',' ' ')
|
|
83
|
+
[ -n "${profiles// /}" ] || { g_err "role $ROLE declares no profiles"; exit 2; }
|
|
84
|
+
|
|
85
|
+
if [ ! -f "$MODELS" ]; then
|
|
86
|
+
g_info "no model mapping configured: $MODELS is absent"
|
|
87
|
+
for p in $profiles; do
|
|
88
|
+
if [ "$JSON" -eq 1 ]; then printf '{"role":"%s","profile":"%s","provider":"unknown","model":"unknown","effort":"unknown"}\n' "$ROLE" "$p"
|
|
89
|
+
else printf '%s unknown unknown unknown\n' "$p"; fi
|
|
90
|
+
done
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
for p in $profiles; do
|
|
95
|
+
prov=$(g_models_field "$MODELS" "$p" provider)
|
|
96
|
+
model=$(g_models_field "$MODELS" "$p" model)
|
|
97
|
+
eff=$(g_models_field "$MODELS" "$p" effort)
|
|
98
|
+
prov=${prov:-unknown}; model=${model:-unknown}; eff=${eff:-unknown}
|
|
99
|
+
if [ "$JSON" -eq 1 ]; then
|
|
100
|
+
printf '{"role":"%s","profile":"%s","provider":"%s","model":"%s","effort":"%s"}\n' "$ROLE" "$p" "$prov" "$model" "$eff"
|
|
101
|
+
else
|
|
102
|
+
printf '%s %s %s %s\n' "$p" "$prov" "$model" "$eff"
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
exit 0
|