@techgoblin/gobstack 0.6.0-alpha.3 → 0.6.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/VERSION +1 -1
- package/bin/goblin-audit +1 -1
- package/bin/goblin-bans +1 -1
- package/bin/goblin-extras +7 -2
- package/bin/goblin-init +50 -1
- package/bin/goblin-install +29 -8
- package/bin/goblin-lib.sh +17 -11
- package/bin/goblin-map +18 -10
- package/bin/goblin-mcp.js +28 -2
- package/bin/goblin-upgrade +1 -1
- package/bin/goblin-verify +6 -6
- package/docs/CONTRACTS.md +6 -3
- package/docs/ENFORCEMENT.md +1 -1
- package/docs/GUIDE.md +6 -5
- package/manifest/enforcement.tsv +1 -1
- package/package.json +1 -1
- package/skills/goblin-bootstrap/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ is unwired in this alpha: the shim refuses the verb by name and prints the usage
|
|
|
87
87
|
| `gob bans` | run the ban list (per-pattern red lines over the source tree) |
|
|
88
88
|
| `gob extras` | the curated extras catalogue: `list [category]` / `show <id>` / `install <id...> [-target <dir>] [--platform <id>] [--with-mcp-config]` — init only SUGGESTS; installs come from the catalogue (the allowlist rule), never the web |
|
|
89
89
|
| `gob mcp` | serve the harness to your coding agent over MCP stdio — three tools (`gob_verify`, `gob_map_status`, `gob_init_status`), local only, no SDK, no network |
|
|
90
|
-
| `gob uninstall` | remove everything an install wrote, byte-exactly (`
|
|
90
|
+
| `gob uninstall` | remove everything an install wrote, byte-exactly (the same job as `goblin-install --target <dir> --uninstall` from the checkout/install tree) |
|
|
91
91
|
|
|
92
92
|
`goblin` remains as a legacy alias for every command above — existing scripts keep working, but
|
|
93
93
|
new commands and docs use `gob`.
|
|
@@ -254,14 +254,17 @@ capability.
|
|
|
254
254
|
|
|
255
255
|
gob uninstall --target .
|
|
256
256
|
|
|
257
|
-
(
|
|
257
|
+
(the same job as `goblin-install --target . --uninstall` — through the legacy alias, spell it `goblin`
|
|
258
258
|
instead of `gob`). The uninstall is **byte-exact**: it removes exactly the files
|
|
259
259
|
`installed.json` records — hash-compared preimages, so a file you edited after install is
|
|
260
260
|
reported and kept, never clobbered — then every directory that leaves empty, and it strips the
|
|
261
261
|
`<!-- gob:begin --> … <!-- gob:end -->` block out of `AGENTS.md` (the body prose stays). After
|
|
262
262
|
it, the repo has zero harness files; only the project's own record (`HANDOFF.md`, `AGENTS.md`'s
|
|
263
263
|
prose, `reviews/`, the `.gitignore` block) survives, because that is the project's, not the
|
|
264
|
-
harness's to delete.
|
|
264
|
+
harness's to delete. Two residues are left **by design, and named in the output**: the
|
|
265
|
+
`.gitignore` ignore-rules block stays (removing it could eat a line the project added inside
|
|
266
|
+
it — delete that block by hand if you want it gone), and prose that named `.gob/` paths
|
|
267
|
+
(HANDOFF, the AGENTS.md body, SPECs) now points at removed files. And because the engine is vendored, the repo needs no gobstack installed to
|
|
265
268
|
run this — it is self-contained until the moment you remove it.
|
|
266
269
|
|
|
267
270
|
The short version, for a full removal from a machine and its repos: (b) in each initialized
|
|
@@ -269,7 +272,7 @@ repo, then (a).
|
|
|
269
272
|
|
|
270
273
|
## Re-pin the referenced standard
|
|
271
274
|
|
|
272
|
-
|
|
275
|
+
goblin-install --target <dir> --re-pin # from the gobstack checkout / install tree
|
|
273
276
|
|
|
274
277
|
`practice_sha256:` in the AGENTS.md gob block pins the referenced standard and `IN-02`
|
|
275
278
|
re-checks it, so editing that standard — a legitimate, intended edit — reds `IN-02` in every
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.0-alpha.
|
|
1
|
+
0.6.0-alpha.4
|
package/bin/goblin-audit
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# because an empty record reads to SC-07 as "clean" and that would be a fabricated pass
|
|
24
24
|
set -uo pipefail
|
|
25
25
|
|
|
26
|
-
GOBLIN_AUDIT_VERSION="0.6.0-alpha.
|
|
26
|
+
GOBLIN_AUDIT_VERSION="0.6.0-alpha.4"
|
|
27
27
|
|
|
28
28
|
usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
29
29
|
|
package/bin/goblin-bans
CHANGED
package/bin/goblin-extras
CHANGED
|
@@ -54,7 +54,7 @@ usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
|
54
54
|
CATALOGUE="${GOB_EXTRAS_CATALOGUE:-$SRC/extras-catalogue/catalogue.tsv}"
|
|
55
55
|
PAYLOAD="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
|
|
56
56
|
|
|
57
|
-
die() { g_err "extras:
|
|
57
|
+
die() { local msg="$1"; shift; g_err "extras: $msg"; exit "${1:-2}"; }
|
|
58
58
|
|
|
59
59
|
# catalogue_row <id> — the whole data row (tab-separated), empty when absent.
|
|
60
60
|
catalogue_row() {
|
|
@@ -250,7 +250,12 @@ install_skill() {
|
|
|
250
250
|
[ -d "$d" ] || continue
|
|
251
251
|
name=$(basename "$d")
|
|
252
252
|
n=$((n + 1))
|
|
253
|
-
|
|
253
|
+
# QA fix (v2-qa issue 9): the display path must carry exactly one separator between
|
|
254
|
+
# $dst_root and $name — the old single format printed `.gob/extras` + `taste` as
|
|
255
|
+
# `.gob/extrastaste/` (files landed in the right place, only the LOG lied). The
|
|
256
|
+
# platform branch normalizes $dst_root to end in /, the standalone branch does not,
|
|
257
|
+
# so the trailing slashes are stripped at print time and one / is always inserted.
|
|
258
|
+
printf 'extras: skill %s -> %s/%s/\n' "$name" "${dst_root%/}" "$name"
|
|
254
259
|
copy_tree "$d" "$dst_root/$name"
|
|
255
260
|
done
|
|
256
261
|
[ "$n" -gt 0 ] || die "payload for '$id' holds no skill dirs ($src_root/*/ with SKILL.md each) — see extras-catalogue/payload/README.md" 2
|
package/bin/goblin-init
CHANGED
|
@@ -133,6 +133,10 @@ back one proposal file. Do this now:
|
|
|
133
133
|
found in step 1 — react, vue, playwright, godot, unity, node, postgres, …);
|
|
134
134
|
- RECOMMEND rows are pre-ticked (write `- id` with `[x]`); MAYBE rows are
|
|
135
135
|
listed but NOT pre-ticked (`[ ]`) — they are visible choices, never defaults;
|
|
136
|
+
- a row with NO vendored payload (no `extras-catalogue/payload/<id>/` directory)
|
|
137
|
+
must NOT be pre-ticked even when RECOMMEND — print it as `[ ]` instead;
|
|
138
|
+
`--write` installs through `gob extras install`, which refuses a
|
|
139
|
+
payload-missing id and fails the whole --write;
|
|
136
140
|
- a row whose `conflicts:` names an id you also picked: pick at most one of them;
|
|
137
141
|
- SKIP rows are never suggested and refuse install; leave them out entirely.
|
|
138
142
|
|
|
@@ -226,13 +230,21 @@ print_heuristic() {
|
|
|
226
230
|
# files). The SAME matches column the agent brief reads — the fallback suggests from
|
|
227
231
|
# the catalogue, never from its own private list. Verdicts are respected: only
|
|
228
232
|
# RECOMMEND rows pre-tick; MAYBE rows print unticked; SKIP rows never appear.
|
|
233
|
+
# QA fix (v2-qa): a row pre-ticks ONLY when its vendored payload exists —
|
|
234
|
+
# extras-catalogue/payload/<id>/ — because --write installs through
|
|
235
|
+
# `gob extras install`, which refuses a payload-missing row and fails the whole
|
|
236
|
+
# --write after everything else was already written. A RECOMMEND row with no
|
|
237
|
+
# payload prints as a visible, unticked `[ ]` choice instead. (41 researched rows,
|
|
238
|
+
# 1 vendored payload: the old pre-tick turned a day-one init into a guaranteed
|
|
239
|
+
# exit 2 halfway through writing.)
|
|
229
240
|
if [ -f "$CATALOGUE_TSV" ]; then
|
|
230
241
|
local scan
|
|
231
242
|
scan=$(cat "$TARGET/package.json" "$TARGET"/*.lock "$TARGET"/pnpm-lock.yaml \
|
|
232
243
|
"$TARGET"/vue.config.* "$TARGET"/nuxt.config.* "$TARGET"/godot/project.godot \
|
|
233
244
|
"$TARGET"/unity/ProjectSettings/ProjectVersion.txt 2>/dev/null | tr 'A-Z' 'a-z')
|
|
234
245
|
if [ -n "$scan" ]; then
|
|
235
|
-
local ticked="" maybe=""
|
|
246
|
+
local ticked="" maybe="" payload_root
|
|
247
|
+
payload_root="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
|
|
236
248
|
while IFS=$'\t' read -r cid ccat ckind cname crepo cpath chmd clic cstars cpush \
|
|
237
249
|
cmatches cconf creq chint cverdict cby cdate; do
|
|
238
250
|
[ -n "$cid" ] || continue
|
|
@@ -242,6 +254,12 @@ print_heuristic() {
|
|
|
242
254
|
case "$scan" in *"$tok"*) hit=1; break ;; esac
|
|
243
255
|
done
|
|
244
256
|
[ "$hit" -eq 1 ] || continue
|
|
257
|
+
if [ ! -d "$payload_root/$cid" ]; then
|
|
258
|
+
# no vendored payload: the row stays visible but never pre-ticks —
|
|
259
|
+
# a tick --write cannot honour is a refusal waiting to happen
|
|
260
|
+
maybe="$maybe - [ ] $cid # matches this repo, NO VENDORED PAYLOAD yet (gob extras install would refuse — vendor it under extras-catalogue/payload/$cid/ first)"$'\n'
|
|
261
|
+
continue
|
|
262
|
+
fi
|
|
245
263
|
case "$cverdict" in
|
|
246
264
|
RECOMMEND) ticked="$ticked - [x] $cid # matches this repo ($cmatches)"$'\n' ;;
|
|
247
265
|
MAYBE) maybe="$maybe - [ ] $cid # matches, verdict MAYBE (not pre-ticked)"$'\n' ;;
|
|
@@ -326,6 +344,37 @@ SYNC_SEL=$(g_agents_list "$PROP" sync_platforms | tr '\n' ' ')
|
|
|
326
344
|
# cannot install anything the catalogue does not carry.
|
|
327
345
|
EXTRAS_SEL=$(sed -n '/catalogue_suggestions:/,/^[^ -]/{s/^[[:space:]]*-[[:space:]]*\[x\][[:space:]]*\([A-Za-z0-9_-]*\).*/\1/p;}' "$PROP" | sort -u | tr '\n' ' ')
|
|
328
346
|
|
|
347
|
+
# ---------------------------------------------------------------- pre-flight ---
|
|
348
|
+
# QA fix (v2-qa issue 1b): the TICKED extras are validated BEFORE anything is written.
|
|
349
|
+
# The old order installed the harness first and ran `gob extras install` last, so a
|
|
350
|
+
# missing vendored payload refused AFTER 23 files were already in the tree (the day-one
|
|
351
|
+
# exit 2 with the repo half-initialized). Here every ticked id is resolved through the
|
|
352
|
+
# same allowlist path goblin-extras uses (row exists, verdict installable, license gate,
|
|
353
|
+
# vendored payload present) while the target is still untouched; a refusal names the id
|
|
354
|
+
# and the fix and exits before the first write.
|
|
355
|
+
if [ -n "$EXTRAS_SEL" ] && [ "$DRYRUN" -eq 0 ]; then
|
|
356
|
+
PAYLOAD_ROOT="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
|
|
357
|
+
for xid in $EXTRAS_SEL; do
|
|
358
|
+
row=$(awk -F'\t' -v id="$xid" 'NR>1 && $1==id { print; exit }' "$CATALOGUE_TSV" 2>/dev/null)
|
|
359
|
+
[ -n "$row" ] || { g_err "pre-flight: catalogue has no row '$xid' — fix the proposal's catalogue_suggestions (gob extras list names the allowlist)"; exit 2; }
|
|
360
|
+
verdict=$(printf '%s' "$row" | awk -F'\t' '{print $15}')
|
|
361
|
+
lic=$(printf '%s' "$row" | awk -F'\t' '{print $18}')
|
|
362
|
+
case "$verdict" in
|
|
363
|
+
RECOMMEND|MAYBE) ;;
|
|
364
|
+
*) g_err "pre-flight: '$xid' carries verdict '$verdict' — untick it in the proposal's catalogue_suggestions"; exit 2 ;;
|
|
365
|
+
esac
|
|
366
|
+
# the same license gate goblin-extras enforces, read before any write
|
|
367
|
+
case "$lic" in
|
|
368
|
+
verified) ;;
|
|
369
|
+
*) g_err "pre-flight: '$xid' refuses install: license pending verification — untick it in the proposal's catalogue_suggestions"; exit 2 ;;
|
|
370
|
+
esac
|
|
371
|
+
[ -d "$PAYLOAD_ROOT/$xid" ] || {
|
|
372
|
+
g_err "pre-flight: no vendored payload for '$xid' at $PAYLOAD_ROOT/$xid — untick it in the proposal's catalogue_suggestions, or vendor the payload there first";
|
|
373
|
+
exit 2;
|
|
374
|
+
}
|
|
375
|
+
done
|
|
376
|
+
fi
|
|
377
|
+
|
|
329
378
|
if [ "$DRYRUN" -eq 1 ]; then
|
|
330
379
|
g_info "gob init --write --dry-run: validated OK (class $CLASS, branch $BRANCH, owner $EMAIL, 1+ gate)"
|
|
331
380
|
g_info " sync: ${SYNC_SEL:-none}"
|
package/bin/goblin-install
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# goblin-install — drop the harness into a target repo, idempotently.
|
|
3
3
|
#
|
|
4
|
-
# Usage:
|
|
4
|
+
# Usage: bash bin/goblin-install --target <dir> [options] # checkout-only; `gob init --write` is the user surface
|
|
5
5
|
# --target <dir> required; the repo root to install into
|
|
6
6
|
# --class <name> required unless --uninstall or --re-pin; one of
|
|
7
7
|
# software|service|game|research|fleet, or the aliases A-E / app / agent
|
|
8
8
|
# / desktop (desktop/F => software + --electron); anything else is refused
|
|
9
9
|
# --electron the electron opt-in overlay over the software class (electron: true):
|
|
10
10
|
# the app_bundle_bytes ratchet, BN-06..09 bans, the host-gate declaration
|
|
11
|
-
# --models <path> model mapping file (default: $GOBLIN_MODELS
|
|
12
|
-
#
|
|
11
|
+
# --models <path> model mapping file (default: $GOBLIN_MODELS; unset = rendered as an
|
|
12
|
+
# empty models_file: the repo's machine-specific input, never a path
|
|
13
|
+
# from the machine the installer ran on)
|
|
14
|
+
# --practice <path> the referenced standard (default: $GOBLIN_PRACTICE; unset = empty
|
|
15
|
+
# practice: pin — a named path that is absent is reported, never
|
|
16
|
+
# silently dropped)
|
|
13
17
|
# --parts <list> comma list to install; default = every part the class requires
|
|
14
18
|
# --archive mark the project archive: verify requires no HANDOFF and no gates
|
|
15
19
|
# --skills yes|no install agent skills under .hermes/skills (default no; opt in per
|
|
16
|
-
#
|
|
20
|
+
# catalogue row with: gob extras install <id>)
|
|
17
21
|
# --dry-run print the plan; write nothing
|
|
18
22
|
# --upgrade re-install at the current version; report created/updated/unchanged/skipped
|
|
19
23
|
# --opt-out <part> record the part in disabled: so its required checks are skipped
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
|
|
32
36
|
set -uo pipefail
|
|
33
37
|
|
|
34
|
-
GOBLIN_INSTALL_VERSION="0.6.0-alpha.
|
|
38
|
+
GOBLIN_INSTALL_VERSION="0.6.0-alpha.4"
|
|
35
39
|
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
36
40
|
SRC=$(cd "$SELF_DIR/.." && pwd)
|
|
37
41
|
# shellcheck source=goblin-lib.sh
|
|
@@ -39,8 +43,16 @@ SRC=$(cd "$SELF_DIR/.." && pwd)
|
|
|
39
43
|
|
|
40
44
|
TARGET=""
|
|
41
45
|
CLASS=""
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
# QA fix (v2-qa issue 4): the old defaults were $HOME/projects/fleet-model.yaml and
|
|
47
|
+
# $HOME/projects/PROJECT-PRACTICE.md - the MAINTAINER's home layout baked into every
|
|
48
|
+
# rendered config on every machine, including a fresh contributor's (their rendered
|
|
49
|
+
# AGENTS.md pointed at a file that does not exist on their machine). The neutral default
|
|
50
|
+
# is EMPTY: no --models / no GOBLIN_MODELS renders an empty models_file: line, and the
|
|
51
|
+
# practice pin stays empty (verify already treats both as the legitimate no-mapping /
|
|
52
|
+
# no-standard state). A machine that HAS a fleet mapping exports GOBLIN_MODELS (or
|
|
53
|
+
# passes --models) and gets the same rendering as before.
|
|
54
|
+
MODELS="${GOBLIN_MODELS:-}"
|
|
55
|
+
PRACTICE="${GOBLIN_PRACTICE:-}"
|
|
44
56
|
# W5-3: `--practice <path>` naming a file that is not there used to be IGNORED IN SILENCE -
|
|
45
57
|
# exit 0, `practice: ~`, `practice_sha256: ~`, and not one line about it in the install log.
|
|
46
58
|
# The empty pin is a legitimate state (a repo may reference no standard, and the default path is
|
|
@@ -220,6 +232,15 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
220
232
|
printf '.goblin\n.gob/bin\n.gob/manifest\n.gob/bans\n.hermes\n.hermes/skills\n'
|
|
221
233
|
} | awk 'NF' | awk -F/ '{print NF " " $0}' | sort -rn | cut -d' ' -f2- | awk '!seen[$0]++')
|
|
222
234
|
g_info "removed $n file(s) and $nd empty director(y|ies). Left in place: HANDOFF.md, AGENTS.md, ROUND-000-SPEC.md, reviews/, the .gitignore block${KEPT:+, and the decision record(s) you edited:$KEPT}."
|
|
235
|
+
# QA fix (v2-qa issue 10): honesty about what the uninstall leaves UNTOUCHED. Two
|
|
236
|
+
# by-design residues a reader could otherwise read as a clean sweep:
|
|
237
|
+
# - the .gitignore block the install appended stays (removing it could eat a
|
|
238
|
+
# line the project added inside the block; the preimage is untracked text),
|
|
239
|
+
# - prose in kept files (HANDOFF.md, AGENTS.md body, SPECs) may still name
|
|
240
|
+
# now-deleted paths (.gob/bin/goblin-verify, checks/) — those references are
|
|
241
|
+
# dead after this command.
|
|
242
|
+
g_info " note: the .gitignore ignore-rules block stays (by design: it may carry lines the project added; remove it by hand if you want it gone)"
|
|
243
|
+
g_info " note: prose that names .gob/ paths (HANDOFF.md, AGENTS.md body, SPECs) now points at removed files — those references are dead"
|
|
223
244
|
exit 0
|
|
224
245
|
fi
|
|
225
246
|
|
|
@@ -852,7 +873,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
|
|
|
852
873
|
if [ "$SKILLS" = "yes" ]; then
|
|
853
874
|
g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
|
|
854
875
|
else
|
|
855
|
-
g_info " 4. agent skills are opt-in: gob
|
|
876
|
+
g_info " 4. agent skills are opt-in: gob extras install <id...> # gob extras list shows the catalogue"
|
|
856
877
|
fi
|
|
857
878
|
g_info ""
|
|
858
879
|
g_info "automations (optional; neither writes outside this repo, and A-02 has no agent in it):"
|
package/bin/goblin-lib.sh
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# - name: typecheck four-space-indented second member of a list entry
|
|
17
17
|
# cmd: npx tsc --noEmit
|
|
18
18
|
|
|
19
|
-
GOBLIN_LIB_VERSION="0.6.0-alpha.
|
|
19
|
+
GOBLIN_LIB_VERSION="0.6.0-alpha.4"
|
|
20
20
|
|
|
21
21
|
# ---------------------------------------------------------------- output -----
|
|
22
22
|
# g_trunc <width> <text> — fold a long detail to one line at <width> columns, keeping the
|
|
@@ -31,6 +31,9 @@ g_trunc() {
|
|
|
31
31
|
# <indent> (the cannot-see footer shape). Byte-safe: awk length on bytes approximates
|
|
32
32
|
# columns for ASCII prose, which is all this text is.
|
|
33
33
|
g_fold() {
|
|
34
|
+
# QA fix (v2-qa issue 9): the rest-assignment used to inline substr() as sub()'s third
|
|
35
|
+
# parameter — not a changeable lvalue, a hard error on gawk 5.2+ (the helper had never
|
|
36
|
+
# been exercised until the remedy fold called it). A temp variable is portable awk.
|
|
34
37
|
awk -v w="$1" -v ind="$2" '
|
|
35
38
|
{
|
|
36
39
|
line = $0
|
|
@@ -39,10 +42,9 @@ g_fold() {
|
|
|
39
42
|
while (cut > 1 && substr(line, cut, 1) != " ") cut--
|
|
40
43
|
if (cut <= 1) cut = w
|
|
41
44
|
print substr(line, 1, cut)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
line = ind line
|
|
45
|
+
rest = substr(line, cut + 1)
|
|
46
|
+
sub(/^[ ]+/, "", rest)
|
|
47
|
+
line = ind rest
|
|
46
48
|
}
|
|
47
49
|
print line
|
|
48
50
|
}'
|
|
@@ -71,12 +73,16 @@ g_fail() {
|
|
|
71
73
|
case "$rem" in
|
|
72
74
|
—*) return 0 ;; # an em-dash cell: history and blindness, never a remedy
|
|
73
75
|
esac
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
[
|
|
76
|
+
# QA fix (v2-qa issue 9): a remedy is an instruction, not a detail line — truncating it
|
|
77
|
+
# with a ~ cut the command the operator had to type (measured: 'git -c user.ema~').
|
|
78
|
+
# The remedy is now WORD-FOLDED to continuation lines at the report width in every
|
|
79
|
+
# mode (whole text either way); --verbose keeps the single-line form it always had.
|
|
80
|
+
if [ -n "$rem" ]; then
|
|
81
|
+
if [ "${GOB_VERIFY_VERBOSE:-0}" -eq 1 ]; then
|
|
82
|
+
printf 'remedy: %s\n' "$rem"
|
|
83
|
+
else
|
|
84
|
+
printf 'remedy: %s\n' "$rem" | g_fold "${GOB_REPORT_COLS:-100}" " "
|
|
85
|
+
fi
|
|
80
86
|
fi
|
|
81
87
|
}
|
|
82
88
|
g_remedy() {
|
package/bin/goblin-map
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
#
|
|
46
46
|
# Never-clobber contract:
|
|
47
47
|
# features/ absent (or empty of .md) -> generate README + one file per slug, exit 0
|
|
48
|
-
# features/ already a map, no --force ->
|
|
48
|
+
# features/ already a map, no --force -> validate only; the index is kept (never rewritten)
|
|
49
49
|
# --force -> regenerate ONLY README.md (indexing every existing
|
|
50
50
|
# feature file too) and add files for NEW slugs;
|
|
51
51
|
# existing feature files are never touched, exit 0
|
|
@@ -129,8 +129,9 @@ Notes:
|
|
|
129
129
|
human pass has actually driven the feature once.
|
|
130
130
|
- entry_paths tokens must be repo-relative paths that EXIST, and should occur in
|
|
131
131
|
source text (a route literal, an import) so FM-02 can grep for them.
|
|
132
|
-
-
|
|
133
|
-
|
|
132
|
+
- --write validates and never rewrites: your hand-written README index is KEPT
|
|
133
|
+
(pass --force only if you want the index REGENERATED from the feature files);
|
|
134
|
+
existing feature files are never touched either way.
|
|
134
135
|
|
|
135
136
|
== FEATURE-FILE SCHEMA =========================================================
|
|
136
137
|
|
|
@@ -201,13 +202,15 @@ if [ -n "$WRITE_DIR" ]; then
|
|
|
201
202
|
WDIR=$(cd "$WRITE_DIR" && pwd) || { g_err "cannot enter: $WRITE_DIR"; exit 2; }
|
|
202
203
|
# A map, for --write purposes, is the README index: --write never writes feature
|
|
203
204
|
# files (they are its INPUT), so the only thing it could clobber is the index.
|
|
205
|
+
# QA fix (v2-qa issue 5): an existing index used to be a refusal ("pass --force") —
|
|
206
|
+
# but the AGENT BRIEF itself tells the agent to WRITE features/README.md before
|
|
207
|
+
# running --write, so the first --write of the brief's own prescribed flow demanded
|
|
208
|
+
# the scary flag. The coherent contract: --write VALIDATES and never rewrites the
|
|
209
|
+
# index without --force; an existing index is the author's own output and is kept,
|
|
210
|
+
# named in the done line. (The never-clobber promise is unchanged: --write still
|
|
211
|
+
# writes NOTHING without --force — the refusal was protecting an empty write.)
|
|
204
212
|
HAVE_WMAP=0
|
|
205
213
|
if [ -f "$WDIR/README.md" ]; then HAVE_WMAP=1; fi
|
|
206
|
-
if [ "$HAVE_WMAP" -eq 1 ] && [ "$FORCE" -ne 1 ]; then
|
|
207
|
-
g_err "refusing to overwrite an existing feature map: $WDIR"
|
|
208
|
-
printf 'error: map: pass --force to regenerate the README index and add NEW files only; existing files are never rewritten\n' >&2
|
|
209
|
-
exit 1
|
|
210
|
-
fi
|
|
211
214
|
# Validation: the frontmatter + H2 + verified contract, per file, fail closed.
|
|
212
215
|
BAD=0; N=0
|
|
213
216
|
for f in "$WDIR"/*.md; do
|
|
@@ -264,7 +267,10 @@ if [ -n "$WRITE_DIR" ]; then
|
|
|
264
267
|
for f in "$WDIR"/*.md; do
|
|
265
268
|
[ -f "$f" ] || continue
|
|
266
269
|
slug=$(basename "$f"); slug=${slug%.md}
|
|
267
|
-
|
|
270
|
+
# QA fix (v2-qa issue 6): $slug is the STEM (README, not README.md) — the old
|
|
271
|
+
# pattern compared README against README.md, never matched, and the regenerated
|
|
272
|
+
# index listed the index itself as a feature.
|
|
273
|
+
case "$slug" in README) continue ;; esac
|
|
268
274
|
printf '%s\t%s\n' "$slug" "$(printf '%s' "${f#"$WDIR"/}" | sed 's#^#./#')" >> "$SLUGS_TSV"
|
|
269
275
|
done
|
|
270
276
|
TODAY=$(date +%F)
|
|
@@ -509,7 +515,9 @@ MID
|
|
|
509
515
|
for f in "$FEAT"/*.md; do
|
|
510
516
|
[ -f "$f" ] || continue
|
|
511
517
|
slug=$(basename "$f"); slug=${slug%.md}
|
|
512
|
-
|
|
518
|
+
# QA fix (v2-qa issue 6): stem comparison, same bug as the --write index loop —
|
|
519
|
+
# the slug is README after .md is stripped, so the old README.md pattern never fired.
|
|
520
|
+
case "$slug" in README) continue ;; esac
|
|
513
521
|
awk -F'\t' -v s="$slug" '$1 == s { found=1 } END { exit !found }' "$SLUGS_TSV" && continue
|
|
514
522
|
title=$(file_title "$f" "$slug")
|
|
515
523
|
printf '%s\n' "- [${title}](./${slug}.md) covers TODO — existing feature file kept by --force (not re-described)"
|
package/bin/goblin-mcp.js
CHANGED
|
@@ -211,7 +211,29 @@ function remedyMap(root) {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
function toolGobVerify(args) {
|
|
214
|
-
|
|
214
|
+
// QA fix (v2-qa issue 7): the tool honours an explicit `target` — an agent calling from
|
|
215
|
+
// an installed repo with a different repo named as target used to get the CWD's matrix
|
|
216
|
+
// with the bogus target silently dropped (wrong-repo-as-verified). Resolution rules:
|
|
217
|
+
// - target absent/empty -> process.cwd() (the documented default)
|
|
218
|
+
// - target relative -> resolved against process.cwd()
|
|
219
|
+
// - resolved dir carries no .gob/engine and no AGENTS.md gob block -> REFUSAL (the
|
|
220
|
+
// exact resolved path is named), never a silent fall-back to the CWD's matrix
|
|
221
|
+
// The result's first line names the verified path, so the agent sees WHICH repo was judged.
|
|
222
|
+
const cwd = process.cwd();
|
|
223
|
+
let root = cwd;
|
|
224
|
+
const target = args && typeof args.target === "string" ? args.target.trim() : "";
|
|
225
|
+
if (target) {
|
|
226
|
+
root = path.resolve(cwd, target);
|
|
227
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
228
|
+
return errText("gob_verify: target is not a directory: " + root);
|
|
229
|
+
}
|
|
230
|
+
const blockText = readBlock(path.join(root, "AGENTS.md"));
|
|
231
|
+
if (!fs.existsSync(path.join(root, ".gob", "bin", "goblin-verify")) && !blockText) {
|
|
232
|
+
return errText(
|
|
233
|
+
"gob_verify: target is not a gobstack repo (no .gob/bin/goblin-verify, no AGENTS.md gob block): " + root
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
215
237
|
const engine = path.join(root, ".gob", "bin", "goblin-verify");
|
|
216
238
|
if (!fs.existsSync(engine)) {
|
|
217
239
|
return errText(
|
|
@@ -235,7 +257,9 @@ function toolGobVerify(args) {
|
|
|
235
257
|
const verdict = parsed.failed === 0 && r.status === 0 ? "PASS" : "FAIL";
|
|
236
258
|
const lines = [];
|
|
237
259
|
lines.push(
|
|
238
|
-
|
|
260
|
+
// the resolved repo is named first: the consumer sees WHICH tree was judged
|
|
261
|
+
"verified: " + root +
|
|
262
|
+
"\ngate: " + verdict + " — " + parsed.passed + " passed, " + parsed.failed + " failed, " +
|
|
239
263
|
parsed.advisory + " advisory, " + parsed.skipped + " skipped (exit " + r.status + ")"
|
|
240
264
|
);
|
|
241
265
|
for (const row of parsed.rows) {
|
|
@@ -358,7 +382,9 @@ const TOOLS = [
|
|
|
358
382
|
type: "object",
|
|
359
383
|
properties: {
|
|
360
384
|
only: { type: "string", description: "optional comma-separated rule ids to run (e.g. FM-01,FM-02)" },
|
|
385
|
+
target: { type: "string", description: "optional repo root to verify (absolute, or relative to the CWD); defaults to the current working directory" },
|
|
361
386
|
},
|
|
387
|
+
additionalProperties: false,
|
|
362
388
|
},
|
|
363
389
|
},
|
|
364
390
|
{
|
package/bin/goblin-upgrade
CHANGED
package/bin/goblin-verify
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
set -uo pipefail
|
|
21
21
|
|
|
22
|
-
GOBLIN_VERIFY_VERSION="0.6.0-alpha.
|
|
22
|
+
GOBLIN_VERIFY_VERSION="0.6.0-alpha.4"
|
|
23
23
|
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
24
24
|
# shellcheck source=goblin-lib.sh
|
|
25
25
|
. "$SELF_DIR/goblin-lib.sh"
|
|
@@ -276,9 +276,9 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
276
276
|
if [ "$ROOT" != "$PWD" ]; then
|
|
277
277
|
g_err "not installed: $CONFIG is absent — and $PWD is not the installed repo ($ROOT)."
|
|
278
278
|
g_err "fix: cd to your project repo, then re-run gob verify."
|
|
279
|
-
g_err " (installing here would initialize $PWD:
|
|
279
|
+
g_err " (installing here would initialize $PWD: npx @techgoblin/gobstack init)"
|
|
280
280
|
else
|
|
281
|
-
g_err "not installed: $CONFIG is absent. Run:
|
|
281
|
+
g_err "not installed: $CONFIG is absent. Run: npx @techgoblin/gobstack init # or: gob init, inside $ROOT"
|
|
282
282
|
fi
|
|
283
283
|
exit 2
|
|
284
284
|
fi
|
|
@@ -484,7 +484,7 @@ check_in_01() {
|
|
|
484
484
|
# IN-01 recovery line (review 1, scope 4): the record is corrupt or missing; name the two
|
|
485
485
|
# restores. The matrix's remedy: line rides under the payload via g_fail.
|
|
486
486
|
printf 'installed.json is absent or records no version\n'
|
|
487
|
-
printf 'recover:
|
|
487
|
+
printf 'recover: git checkout -- .gob/installed.json, or re-run npx @techgoblin/gobstack init --write to re-install\n'
|
|
488
488
|
return 1
|
|
489
489
|
}
|
|
490
490
|
|
|
@@ -609,7 +609,7 @@ check_in_02() {
|
|
|
609
609
|
# catch a SILENT edit, so re-pinning is a deliberate act: one explicit command, which
|
|
610
610
|
# prints the old and new hash (docs/CONTRACTS.md, "An edited standard is not a dead end").
|
|
611
611
|
printf ' if the edit is yours and intended, re-pin it deliberately:\n'
|
|
612
|
-
printf ' goblin-install --target %s --re-pin\n' "$ROOT"
|
|
612
|
+
printf ' goblin-install --target %s --re-pin # from the gobstack checkout/install tree\n' "$ROOT"
|
|
613
613
|
bad=1; pin=" | practice EDITED"
|
|
614
614
|
else
|
|
615
615
|
pin=" | practice pin ok"
|
|
@@ -626,7 +626,7 @@ check_in_02() {
|
|
|
626
626
|
# naming the drifted file the payload above reported. Sits between the row's own lines and
|
|
627
627
|
# the summary; g_fail appends the matrix's remedy: line under the whole payload.
|
|
628
628
|
if [ "$bad" -ne 0 ] && [ -n "$DRIFTED" ]; then
|
|
629
|
-
printf 'recover: git checkout -- %s, or
|
|
629
|
+
printf 'recover: git checkout -- %s, or re-run npx @techgoblin/gobstack init --write to re-install\n' "$DRIFTED"
|
|
630
630
|
fi
|
|
631
631
|
[ "$bad" -eq 0 ]
|
|
632
632
|
}
|
package/docs/CONTRACTS.md
CHANGED
|
@@ -13,12 +13,15 @@ same way the fleet's own tool reads it. Everything else is line-oriented shell.
|
|
|
13
13
|
software · service · game · research · fleet. The letters A-E and the
|
|
14
14
|
older names app (software), agent (fleet) and desktop/F (software + the
|
|
15
15
|
electron opt-in) are read-time aliases.
|
|
16
|
-
--models <path> model mapping file (default: $GOBLIN_MODELS
|
|
17
|
-
|
|
16
|
+
--models <path> model mapping file (default: $GOBLIN_MODELS; unset = an empty
|
|
17
|
+
models_file: the repo's machine-specific input, set per machine)
|
|
18
|
+
--practice <path> the referenced standard (default: $GOBLIN_PRACTICE; unset = no
|
|
19
|
+
practice pin; a named path that is absent is reported, never
|
|
20
|
+
silently dropped)
|
|
18
21
|
--parts <list> comma list to install; default = every part the class requires
|
|
19
22
|
--archive mark the project archive: verify requires no HANDOFF and no gates
|
|
20
23
|
--skills yes|no install agent skills under .hermes/skills (default no — the harness is
|
|
21
|
-
neutral; opt in per
|
|
24
|
+
neutral; opt in per catalogue row with: gob extras install <id...>). On a repo whose
|
|
22
25
|
record already has skills installed, an OMITTED flag keeps them; an explicit
|
|
23
26
|
--skills no removes them.
|
|
24
27
|
--dry-run print the plan; write nothing
|
package/docs/ENFORCEMENT.md
CHANGED
|
@@ -31,7 +31,7 @@ Measured shape of this table: **87 rows** - 82 target, 5 source; advisory 10, ga
|
|
|
31
31
|
| id | scope | enforced by | rule | check | if it cannot be enforced, why |
|
|
32
32
|
|---|---|---|---|---|---|
|
|
33
33
|
| `IN-01` | target | script | The install exists and records its version + every file's hash. | goblin-verify --only IN-01 | — (W1: the check is a builtin so the engine.mode=global clause can run — a global, declaration-only repo has no install record and SKIPs with `global engine mode — no per-repo install record`; the vendored clauses are the old one-liner: the record exists and names its version) |
|
|
34
|
-
| `IN-02` | target | script | Every installed file still matches its recorded hash. | goblin-verify --only IN-02 | recover:
|
|
34
|
+
| `IN-02` | target | script | Every installed file still matches its recorded hash. | goblin-verify --only IN-02 | recover: git checkout -- .gob/installed.json (or re-run `npx @techgoblin/gobstack init --write` to re-install); for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: `goblin-install --target <dir> --re-pin` from the checkout/install tree) |
|
|
35
35
|
| `IN-03` | target | script | The verifier's own manifest is complete: every rule has a check or is advisory. | goblin-verify --only IN-03 | — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script\|lint\|gate\|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in bin/goblin-verify), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine) |
|
|
36
36
|
| `IN-04` | target | script | No file goblin-stack did not create has been overwritten. | goblin-verify --only IN-04 | Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch. |
|
|
37
37
|
| `HP-01` | target | gate | HANDOFF.md exists at the root. | test -f HANDOFF.md | — |
|
package/docs/GUIDE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A step-by-step guide for your first week. **Read this before the README.** The README tells you
|
|
4
4
|
what the pieces are; this tells you what to *do*, in order, and what you should see when it works.
|
|
5
5
|
|
|
6
|
-
Version: `0.6.0-alpha.
|
|
6
|
+
Version: `0.6.0-alpha.4` · Last measured: 2026-10-09 · Every command and every output below was run on a
|
|
7
7
|
real repository while writing this guide.
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -267,7 +267,8 @@ Open it. The keys that matter on day one:
|
|
|
267
267
|
branch: main # DECLARED, never assumed
|
|
268
268
|
owner_email: you@example.com # the commit identity this repo expects
|
|
269
269
|
practice: /path/to/your-standard.md # optional: your own house rules, hash-pinned
|
|
270
|
-
models_file: /path/to/fleet-model.yaml # the ONE machine-specific input
|
|
270
|
+
models_file: /path/to/fleet-model.yaml # the ONE machine-specific input (per machine; a
|
|
271
|
+
# fresh install without --models leaves it empty)
|
|
271
272
|
gate_<name>_cmd: <one command> # YOUR real commands, one line each
|
|
272
273
|
|
|
273
274
|
**The single most valuable edit you will make:** replace the gate line(s) with the commands you
|
|
@@ -290,7 +291,7 @@ repos follow an old version.
|
|
|
290
291
|
|
|
291
292
|
When *you* legitimately edit your own standard:
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
goblin-install --target . --re-pin # from the gobstack checkout / install tree
|
|
294
295
|
|
|
295
296
|
It re-records the hash and prints the old and new value. Nothing re-pins automatically — not even
|
|
296
297
|
a re-install — an edited standard is never a silent no-op.
|
|
@@ -528,8 +529,8 @@ same commands are available outside any repo through the npm CLI: `gob init` / `
|
|
|
528
529
|
|
|
529
530
|
.gob/bin/goblin-verify [--only <id[,id...]>] [--json] [--list]
|
|
530
531
|
.gob/bin/goblin-bans # run the ban list
|
|
531
|
-
gob
|
|
532
|
-
|
|
532
|
+
gob uninstall --target <dir> # the uninstall job
|
|
533
|
+
goblin-install --target <dir> --re-pin # the deliberate re-pin (from the checkout/install tree)
|
|
533
534
|
bin/goblin-model <role> # checkout-only; resolve a role to a profile (docs/ROLES.md)
|
|
534
535
|
|
|
535
536
|
### Register the harness with your agent (MCP)
|
package/manifest/enforcement.tsv
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
id scope rule enforced_by artifact check if_not_why
|
|
2
2
|
IN-01 target The install exists and records its version + every file's hash. script .gob/installed.json goblin-verify --only IN-01 — (W1: the check is a builtin so the engine.mode=global clause can run — a global, declaration-only repo has no install record and SKIPs with `global engine mode — no per-repo install record`; the vendored clauses are the old one-liner: the record exists and names its version)
|
|
3
|
-
IN-02 target Every installed file still matches its recorded hash. script .gob/installed.json goblin-verify --only IN-02 recover:
|
|
3
|
+
IN-02 target Every installed file still matches its recorded hash. script .gob/installed.json goblin-verify --only IN-02 recover: git checkout -- .gob/installed.json (or re-run npx @techgoblin/gobstack init --write to re-install); for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: goblin-install --target <dir> --re-pin from the checkout/install tree)
|
|
4
4
|
IN-03 target The verifier's own manifest is complete: every rule has a check or is advisory. script manifest/enforcement.tsv goblin-verify --only IN-03 — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script|lint|gate|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in bin/goblin-verify), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine)
|
|
5
5
|
IN-04 target No file goblin-stack did not create has been overwritten. script .gob/installed.json goblin-verify --only IN-04 Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch.
|
|
6
6
|
HP-01 target HANDOFF.md exists at the root. gate HANDOFF.md test -f HANDOFF.md —
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ Use when adopting goblin-stack in a repo, or starting one.
|
|
|
13
13
|
with the electron bans and a host gate, not a sixth class. The letters `A`-`E` and the older
|
|
14
14
|
names are read-time aliases.
|
|
15
15
|
2. **`goblin-install --target <dir> --class <x>`** — the default install is a NEUTRAL harness:
|
|
16
|
-
no agent skills. Opt in per
|
|
16
|
+
no agent skills. Opt in per catalogue row afterwards with `gob extras install <id>` (or vendor the
|
|
17
17
|
Hermes project tier with `--skills yes`).
|
|
18
18
|
3. **`goblin-verify`** — a default software-class install (no agent skills) verifies green:
|
|
19
19
|
`37 passed, 0 failed, 11 advisory, 34 skipped`, exit 0, once `HANDOFF.md` names a commit that
|