@techgoblin/gobstack 0.5.0-beta.4 → 0.5.0-beta.5
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 +1 -1
- package/bin/goblin-emit +13 -4
- package/bin/goblin-init +35 -9
- package/bin/goblin-lib.sh +59 -3
- package/bin/goblin-verify +38 -4
- package/docs/CONTRACTS.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ The measurement and the vacuous-pass reading are in `docs/CONTRACTS.md`.
|
|
|
98
98
|
| `gob doctor` | one run across the platforms below: DETECTED / NOT-DETECTED / DRIFT per platform |
|
|
99
99
|
| `gob emit` | write the skills + context block for one platform (`--scope project` or `global`); `--unshadow` removes a hermes project skill whose hash equals the source; `gob sync` is the same command under its friendlier name — both spellings work |
|
|
100
100
|
| `gob sync` | the emit verb, renamed (wizard v2): same engine, same flags, same exit contract; `gob emit --help` and `gob sync --help` are byte-identical apart from the verb name |
|
|
101
|
-
| `gob init` | the first-run wizard: detect → class → identity → health check → ci → sync → done, one screen per question; every question has a flag (`--class software --branch main --email a@b.c --gate 'cmd' --emit hermes`), so CI runs it with zero prompts; the ci step defaults to no — nothing under .github/ unless you opt in (`--ci-gate yes|no` overrides); `--dry-run` prints the plan and writes nothing |
|
|
101
|
+
| `gob init` | the first-run wizard: detect → class → identity → health check → ci → sync → done, one screen per question; every question has a flag (`--class software --branch main --email a@b.c --gate 'cmd' --emit hermes`), so CI runs it with zero prompts; the ci step defaults to no — nothing under .github/ unless you opt in (`--ci-gate yes|no` overrides); `--no-verify` skips the closing health check; `--dry-run` prints the plan and writes nothing |
|
|
102
102
|
|
|
103
103
|
`goblin` remains as a legacy alias for every command above — existing scripts keep working, but
|
|
104
104
|
new commands and docs use `gob`.
|
package/bin/goblin-emit
CHANGED
|
@@ -67,7 +67,7 @@ LEDGER="${GOBLIN_EMISSIONS:-$HOME/.goblin-stack/emissions.tsv}"
|
|
|
67
67
|
PREIMG="${GOBLIN_PREIMAGES:-$HOME/.goblin-stack/preimages}"
|
|
68
68
|
|
|
69
69
|
usage() {
|
|
70
|
-
cat <<USAGE
|
|
70
|
+
cat <<'USAGE'
|
|
71
71
|
gob sync — per-platform skill sync (W4a, the seven platforms since W4b).
|
|
72
72
|
|
|
73
73
|
gob sync --platform <$_PLA|hermes|copilot|cursor|opencode|codex|$_G1$_G2>
|
|
@@ -221,9 +221,18 @@ g_index_bytes() {
|
|
|
221
221
|
local files=() n
|
|
222
222
|
for n in $(selected_skills); do files+=("$SRCSKILLS/$n/SKILL.md"); done
|
|
223
223
|
[ "${#files[@]}" -eq 0 ] && { printf '0'; return 0; }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
# Portability (BSD catch, 2026-10-06): ENDFILE is a gawk extension. On mawk (the Debian
|
|
225
|
+
# default) and the macOS BWK awk it is a syntax error, stderr was discarded, and the
|
|
226
|
+
# run printed "procedure index size: 0 bytes" on every non-gawk machine. Sum per file
|
|
227
|
+
# with FNR==1 as the reset — the same meaning under every awk.
|
|
228
|
+
local f total=0
|
|
229
|
+
for f in "${files[@]}"; do
|
|
230
|
+
n=$(awk 'FNR==1{n=0} /^---$/{c++; next}
|
|
231
|
+
c==1 && /^name:/{n+=length($0)+1} c==1 && /^description:/{n+=length($0)+1}
|
|
232
|
+
END{print n+0}' "$f" 2>/dev/null)
|
|
233
|
+
total=$((total + ${n:-0}))
|
|
234
|
+
done
|
|
235
|
+
printf '%d' "$total"
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
# ---- the ledger (§4.4) -------------------------------------------------------------
|
package/bin/goblin-init
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
#
|
|
4
4
|
# gob init [--target <dir>] [--class <software|service|game|research|fleet|A..E|app|agent|desktop>]
|
|
5
5
|
# [--electron] [--branch <name>] [--email <addr>] [--gate <cmd>]
|
|
6
|
-
# [--ci-gate yes|no] [--emit <p[,p..]>] [--scope project|global] [--yes]
|
|
6
|
+
# [--ci-gate yes|no] [--emit <p[,p..]>] [--scope project|global] [--yes]
|
|
7
|
+
# [--no-verify] [--dry-run]
|
|
7
8
|
#
|
|
8
9
|
# Seven screens, one per question, answered steps collapsing into the ✔/◆/○ rail above.
|
|
9
10
|
# On a tty the class and ci steps are a KEYBOARD RADIO (arrows move the ▸ pointer, Enter
|
|
@@ -270,7 +271,7 @@ ask_check_pre() {
|
|
|
270
271
|
# ------------------------------------------------------------------ flags -----
|
|
271
272
|
TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
|
|
272
273
|
ELECTRON=0
|
|
273
|
-
YES=0; DRYRUN=0
|
|
274
|
+
YES=0; DRYRUN=0; NOVERIFY=0
|
|
274
275
|
usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
275
276
|
|
|
276
277
|
while [ $# -gt 0 ]; do
|
|
@@ -285,6 +286,7 @@ while [ $# -gt 0 ]; do
|
|
|
285
286
|
--emit) EMIT="${2:-}"; shift 2 ;;
|
|
286
287
|
--scope) SCOPE="${2:-}"; shift 2 ;;
|
|
287
288
|
--yes) YES=1; shift ;;
|
|
289
|
+
--no-verify) NOVERIFY=1; shift ;;
|
|
288
290
|
--dry-run) DRYRUN=1; shift ;;
|
|
289
291
|
-h|--help) usage; exit 0 ;;
|
|
290
292
|
*) g_err "unknown option: $1"; usage >&2; exit 2 ;;
|
|
@@ -683,7 +685,25 @@ done
|
|
|
683
685
|
if [ "$TTY_OUT" -eq 0 ]; then
|
|
684
686
|
printf 'gob init [verify] .goblin/bin/goblin-verify\n'
|
|
685
687
|
fi
|
|
686
|
-
|
|
688
|
+
VERIFY_RC=0
|
|
689
|
+
if [ "$DRYRUN" -eq 0 ] && [ "$NOVERIFY" -eq 1 ]; then
|
|
690
|
+
# --no-verify: the operator asked for the install without the health check. The skip is
|
|
691
|
+
# named, not silent — a run that says nothing reads like a run that verified.
|
|
692
|
+
if [ "$TTY_OUT" -eq 1 ]; then
|
|
693
|
+
rail_note " ${C_MUTED}health check skipped (--no-verify) — run .goblin/bin/goblin-verify when ready${C_RESET}"
|
|
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
|
|
687
707
|
VERIFY_OUT=$( cd "$TARGET" && bash .goblin/bin/goblin-verify 2>&1 )
|
|
688
708
|
VERIFY_RC=$?
|
|
689
709
|
PASSED=$(printf '%s' "$VERIFY_OUT" | grep -oE '[0-9]+ passed' | head -n 1 | awk '{print $1}')
|
|
@@ -693,6 +713,18 @@ if [ "$DRYRUN" -eq 0 ]; then
|
|
|
693
713
|
PASSED="${PASSED:-0}"; FAILED="${FAILED:-0}"; ADVISORY="${ADVISORY:-0}"; SKIPPED="${SKIPPED:-0}"
|
|
694
714
|
|
|
695
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'
|
|
696
728
|
printf '%s\n' "$VERIFY_OUT" | while IFS= read -r vl; do
|
|
697
729
|
case "$vl" in
|
|
698
730
|
PASS*) printf ' %s%s%s\n' "$C_GREEN" "$vl" "$C_RESET" ;;
|
|
@@ -702,12 +734,6 @@ if [ "$DRYRUN" -eq 0 ]; then
|
|
|
702
734
|
*) printf '%s\n' "$vl" ;;
|
|
703
735
|
esac
|
|
704
736
|
done
|
|
705
|
-
printf '\n'
|
|
706
|
-
if [ "$VERIFY_RC" -eq 0 ]; then
|
|
707
|
-
printf ' %sgob verify: %s passed, %s failed, %s skipped · exit 0%s\n' "$C_GREEN" "$PASSED" "$FAILED" "$SKIPPED" "$C_RESET"
|
|
708
|
-
else
|
|
709
|
-
printf ' %sgob verify: %s passed, %s failed, %s skipped · exit %s%s\n' "$C_RED" "$PASSED" "$FAILED" "$SKIPPED" "$VERIFY_RC" "$C_RESET"
|
|
710
|
-
fi
|
|
711
737
|
else
|
|
712
738
|
g_info "gob init [verify] exit $VERIFY_RC: $PASSED passed, $FAILED failed, $ADVISORY advisory, $SKIPPED skipped"
|
|
713
739
|
fi
|
package/bin/goblin-lib.sh
CHANGED
|
@@ -19,10 +19,66 @@
|
|
|
19
19
|
GOBLIN_LIB_VERSION="0.5.0"
|
|
20
20
|
|
|
21
21
|
# ---------------------------------------------------------------- output -----
|
|
22
|
-
|
|
22
|
+
# g_trunc <width> <text> — fold a long detail to one line at <width> columns, keeping the
|
|
23
|
+
# head. Pure awk substr, no regex: the same shape under every awk (mawk, BWK, gawk).
|
|
24
|
+
g_trunc() {
|
|
25
|
+
awk -v w="$1" -v t="$2" 'BEGIN{
|
|
26
|
+
if (length(t) <= w) { print t; exit }
|
|
27
|
+
print substr(t, 1, w - 1) "~"
|
|
28
|
+
}'
|
|
29
|
+
}
|
|
30
|
+
# g_fold <width> <indent> <text> — word-wrap <text> at <width>, continuing lines at
|
|
31
|
+
# <indent> (the cannot-see footer shape). Byte-safe: awk length on bytes approximates
|
|
32
|
+
# columns for ASCII prose, which is all this text is.
|
|
33
|
+
g_fold() {
|
|
34
|
+
awk -v w="$1" -v ind="$2" '
|
|
35
|
+
{
|
|
36
|
+
line = $0
|
|
37
|
+
while (length(line) > w) {
|
|
38
|
+
cut = w
|
|
39
|
+
while (cut > 1 && substr(line, cut, 1) != " ") cut--
|
|
40
|
+
if (cut <= 1) cut = w
|
|
41
|
+
print substr(line, 1, cut)
|
|
42
|
+
sub(/^[ ]+/, "", substr(line, cut + 1))
|
|
43
|
+
line = substr(line, cut + 1)
|
|
44
|
+
sub(/^[ ]+/, "", line)
|
|
45
|
+
line = ind line
|
|
46
|
+
}
|
|
47
|
+
print line
|
|
48
|
+
}'
|
|
49
|
+
}
|
|
50
|
+
g_pass() {
|
|
51
|
+
if [ "${GOB_VERIFY_VERBOSE:-0}" -eq 1 ]; then
|
|
52
|
+
printf 'PASS %-6s %s\n' "$1" "$(g_trunc "${GOB_REPORT_COLS:-100}" "$2")"
|
|
53
|
+
elif printf '%s' "$2" | grep -q "$(printf '\n')"; then
|
|
54
|
+
# Multi-line PASS payloads print whole: the payload often IS the pin a test reads
|
|
55
|
+
# (SK-03's advisory-ceiling line), and the collapse must not swallow it.
|
|
56
|
+
printf 'PASS %-6s %s\n' "$1" "$2"
|
|
57
|
+
else
|
|
58
|
+
printf 'PASS %-6s (ok)\n' "$1"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
23
61
|
g_fail() { printf 'FAIL %-6s %s\n' "$1" "$2"; }
|
|
24
|
-
g_adv()
|
|
25
|
-
|
|
62
|
+
g_adv() {
|
|
63
|
+
# Multi-line ADV payloads print whole: tests pin phrases on the SECOND line of a lane
|
|
64
|
+
# advisory (the W5-6 family statement), and the fold must not eat them. Single-line
|
|
65
|
+
# payloads are the ones the ~100-col truncation is for.
|
|
66
|
+
if printf '%s' "$2" | grep -q "$(printf '\n')"; then
|
|
67
|
+
printf 'ADV %-6s %s\n' "$1" "$2"
|
|
68
|
+
else
|
|
69
|
+
printf 'ADV %-6s %s\n' "$1" "$(g_trunc "${GOB_REPORT_COLS:-100}" "$2")"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
g_skip() {
|
|
73
|
+
# SKIPs keep their row line in every mode (tests and scripts read them by id). The
|
|
74
|
+
# collapse is the WIDTH, not the disappearance: the reason is truncated to the report
|
|
75
|
+
# width unless --verbose/--only asks for it whole.
|
|
76
|
+
if [ "${GOB_VERIFY_VERBOSE:-0}" -eq 1 ]; then
|
|
77
|
+
printf 'SKIP %-6s %s\n' "$1" "$2"
|
|
78
|
+
else
|
|
79
|
+
printf 'SKIP %-6s %s\n' "$1" "$(g_trunc "${GOB_REPORT_COLS:-100}" "$2")"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
26
82
|
g_info() { printf '%s\n' "$*"; }
|
|
27
83
|
g_err() { printf 'error: %s\n' "$*" >&2; }
|
|
28
84
|
|
package/bin/goblin-verify
CHANGED
|
@@ -28,12 +28,18 @@ ONLY=""
|
|
|
28
28
|
JSON=0
|
|
29
29
|
LIST=0
|
|
30
30
|
SOURCE=""
|
|
31
|
+
VERBOSE=0
|
|
32
|
+
# PASS details collapse to (ok) in the default listing; --verbose (or --only, which is a
|
|
33
|
+
# forensic single-row run) restores the full payload line by line.
|
|
34
|
+
if [ "${ONLY:-}" != "" ]; then VERBOSE=1; fi
|
|
31
35
|
|
|
32
36
|
usage() {
|
|
33
37
|
cat <<'USAGE'
|
|
34
38
|
goblin-verify — prove a goblin-stack install is still what it says it is.
|
|
35
39
|
|
|
36
40
|
--only <id[,id...]> run only these manifest rows
|
|
41
|
+
--verbose print every row full-width (the default collapses PASS payloads
|
|
42
|
+
to a count; FAIL and ADV always print in full)
|
|
37
43
|
--list list the manifest rows and exit
|
|
38
44
|
--json machine-readable output
|
|
39
45
|
--source <path> read the manifest from a goblin-stack checkout instead of .goblin/
|
|
@@ -44,9 +50,10 @@ USAGE
|
|
|
44
50
|
|
|
45
51
|
while [ $# -gt 0 ]; do
|
|
46
52
|
case "$1" in
|
|
47
|
-
--only) ONLY="${2:-}"; shift 2 ;;
|
|
53
|
+
--only) ONLY="${2:-}"; VERBOSE=1; shift 2 ;;
|
|
48
54
|
--json) JSON=1; shift ;;
|
|
49
55
|
--list) LIST=1; shift ;;
|
|
56
|
+
--verbose) VERBOSE=1; shift ;;
|
|
50
57
|
--source) SOURCE="${2:-}"; shift 2 ;;
|
|
51
58
|
-h|--help) usage; exit 0 ;;
|
|
52
59
|
*) g_err "unknown option: $1"; usage >&2; exit 2 ;;
|
|
@@ -355,6 +362,14 @@ else
|
|
|
355
362
|
export GOBLIN_HARNESS_STATE=none
|
|
356
363
|
fi
|
|
357
364
|
fi
|
|
365
|
+
|
|
366
|
+
# ---- the report format (collapsed listing) ------------------------------------
|
|
367
|
+
# The default listing is one line per row: PASS payloads collapse to (ok) and long ADV
|
|
368
|
+
# sentences fold to the report width; --verbose (or --only) prints every payload
|
|
369
|
+
# full-width. The lib's row printers read these.
|
|
370
|
+
export GOB_VERIFY_VERBOSE="$VERBOSE"
|
|
371
|
+
export GOB_REPORT_COLS="${COLUMNS:-100}"
|
|
372
|
+
[ "$GOB_REPORT_COLS" -ge 40 ] 2>/dev/null || GOB_REPORT_COLS=100
|
|
358
373
|
export GOBLIN_ROOT="$ROOT"
|
|
359
374
|
export GOBLIN_CONFIG="$CONFIG"
|
|
360
375
|
|
|
@@ -609,8 +624,14 @@ check_hp_03() {
|
|
|
609
624
|
names=${names% }
|
|
610
625
|
rc=0
|
|
611
626
|
out=$(awk -v names="$names" '
|
|
612
|
-
function hasname(line, k, i, a, m) {
|
|
613
|
-
|
|
627
|
+
function hasname(line, k, i, a, m, pat) {
|
|
628
|
+
# Portability (BSD catch, 2026-10-06): the split pattern must be a STRING, not a
|
|
629
|
+
# regex literal. In /[^A-Za-z0-9_./-]+/ the third slash ENDS the regex, so the
|
|
630
|
+
# macOS BWK awk dies with a syntax error ("nonterminated character class") before
|
|
631
|
+
# reading a single line; gawk accepts it. A string pattern means the same thing
|
|
632
|
+
# everywhere. (No apostrophes here: this program is a single-quoted shell string.)
|
|
633
|
+
pat = "[^A-Za-z0-9_./-]+"
|
|
634
|
+
m = split(line, a, pat)
|
|
614
635
|
for (i = 1; i <= m; i++) if (a[i] == k) return 1
|
|
615
636
|
return 0
|
|
616
637
|
}
|
|
@@ -2761,7 +2782,10 @@ else
|
|
|
2761
2782
|
printf ' of those %d advisory: the matrix labels %d row(s) advisory; the difference is the non-advisory row(s) reporting ADV by design (an unresolved lane) and any labelled row skipped by an opt-out\n' \
|
|
2762
2783
|
"$ADV" "$adv_labelled"
|
|
2763
2784
|
fi
|
|
2764
|
-
|
|
2785
|
+
# The collapsed listing keeps one line per row (a SKIP's reason is the row's whole
|
|
2786
|
+
# point and scripts read them by id); the width discipline lives in the lib's printers.
|
|
2787
|
+
if [ "$VERBOSE" -eq 1 ]; then
|
|
2788
|
+
cat <<'SEE'
|
|
2765
2789
|
cannot see: whether a check in the harness dir tests the right path rather than
|
|
2766
2790
|
merely passing; whether the forge is bound by the workflow CL-01 found; whether a human
|
|
2767
2791
|
read the diff;
|
|
@@ -2793,6 +2817,16 @@ else
|
|
|
2793
2817
|
acquisition record EXISTS, never that the number beside it came from the store (HP-03's
|
|
2794
2818
|
defect, one artifact over). No row here reaches a reference asset in CONTEXT either.
|
|
2795
2819
|
SEE
|
|
2820
|
+
else
|
|
2821
|
+
# The same statement, folded to a pointer. The unsigned-record admission rides the
|
|
2822
|
+
# short form too: a doc pin (F2-3) reads it out of every run, not only verbose ones.
|
|
2823
|
+
cat <<'SEE'
|
|
2824
|
+
cannot see: whether a check tests the right path rather than merely passing; whether a
|
|
2825
|
+
human read the diff; whether the unsigned install record (.goblin/installed.json — it is
|
|
2826
|
+
not signed) was rewritten; and the per-lane blind spots of the ban, judge/loop, CI and
|
|
2827
|
+
reference lanes. Full statement: `goblin-verify --verbose`, docs/LIMITS.md #18 #27 #28 #33 #43.
|
|
2828
|
+
SEE
|
|
2829
|
+
fi
|
|
2796
2830
|
# ---- the engine footer (W1 §2.2, R-1's residue -> docs/LIMITS.md #43) --------
|
|
2797
2831
|
# Every run states WHICH engine judged it: the sha256 of the running verifier and of
|
|
2798
2832
|
# the manifest that judged this run, so a repo can name its judge. The statement is
|
package/docs/CONTRACTS.md
CHANGED
|
@@ -22,6 +22,8 @@ same way the fleet's own tool reads it. Everything else is line-oriented shell.
|
|
|
22
22
|
record already has skills installed, an OMITTED flag keeps them; an explicit
|
|
23
23
|
--skills no removes them.
|
|
24
24
|
--dry-run print the plan; write nothing
|
|
25
|
+
--no-verify skip the health check (goblin-verify) at the end; the wizard
|
|
26
|
+
still installs and syncs, and prints the skip notice
|
|
25
27
|
--upgrade re-install at the current version; report created/updated/unchanged/skipped
|
|
26
28
|
--opt-out <part> record the part in disabled: so its required checks are skipped
|
|
27
29
|
--uninstall remove exactly the files in installed.json
|
package/package.json
CHANGED