@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,145 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bugreporter-intake.sh — A-01's intake validator (the gate in front of `goblin-bugreporter`, P13).
|
|
3
|
+
#
|
|
4
|
+
# bash bugreporter-intake.sh <slug> [--reports <dir>] [--cards]
|
|
5
|
+
#
|
|
6
|
+
# A report is a FILE WITH A SCHEMA, not prose in a card body: prose invites the agent to fill
|
|
7
|
+
# the gaps by guessing, and a guess at intake poisons everything downstream. This script does
|
|
8
|
+
# the schema check with line-oriented shell, computes the content-only dedup key, and prints
|
|
9
|
+
# the exact card command.
|
|
10
|
+
#
|
|
11
|
+
# Exit codes: 0 the report passed intake | 1 REFUSED (the gap is named) | 2 could not run.
|
|
12
|
+
#
|
|
13
|
+
# The report is UNTRUSTED INPUT, so nothing reads it into a shell: the card is created by
|
|
14
|
+
# exec'ing an argv array (`run_card`), and the form printed for an operator (`card_cmd`) quotes
|
|
15
|
+
# every value that came out of the report. There is no `eval` on any path. A `symptom:` carrying
|
|
16
|
+
# a backtick, `$(...)` or a quote is text, in the card title and on the printed line alike.
|
|
17
|
+
#
|
|
18
|
+
# An under-specified report is filed, not dropped and not guessed: the refusal command carries
|
|
19
|
+
# NO --assignee, so the dispatcher buckets the card `skipped_unassigned` and it is structurally
|
|
20
|
+
# un-spawnable, while remaining visible on the board to a human.
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
SLUG=""
|
|
24
|
+
REPORTS="reports"
|
|
25
|
+
CARDS=0
|
|
26
|
+
|
|
27
|
+
while [ $# -gt 0 ]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--reports) REPORTS="${2:-}"; shift 2 ;;
|
|
30
|
+
--cards) CARDS=1; shift ;;
|
|
31
|
+
-h|--help) sed -n '2,/^set -/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
32
|
+
-*)
|
|
33
|
+
printf 'bugreporter-intake: unknown option: %s\n' "$1" >&2; exit 2 ;;
|
|
34
|
+
*) SLUG="$1"; shift ;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
[ -n "$SLUG" ] || { printf 'bugreporter-intake: usage: bugreporter-intake.sh <slug> [--reports <dir>]\n' >&2; exit 2; }
|
|
39
|
+
REPORT="$REPORTS/$SLUG/report.yaml"
|
|
40
|
+
[ -f "$REPORT" ] || { printf 'bugreporter-intake: no report at %s\n' "$REPORT" >&2; exit 2; }
|
|
41
|
+
|
|
42
|
+
field() { sed -n "s/^$1:[[:space:]]*//p" "$REPORT" | head -n 1; }
|
|
43
|
+
# present <key> — the key exists AND carries something: an inline value, or at least one
|
|
44
|
+
# indented item under it (repro_steps is a block list, so a bare `field` read is empty for a
|
|
45
|
+
# perfectly good report - that false refusal was measured and is why this helper exists).
|
|
46
|
+
present() {
|
|
47
|
+
grep -q "^$1:" "$REPORT" || return 1
|
|
48
|
+
[ -n "$(field "$1")" ] && return 0
|
|
49
|
+
awk -v k="$1" '
|
|
50
|
+
$0 ~ ("^" k ":[[:space:]]*$") { inb = 1; next }
|
|
51
|
+
inb && /^[^[:space:]]/ { inb = 0 }
|
|
52
|
+
inb && /^[[:space:]]+[^[:space:]]/ { f = 1 }
|
|
53
|
+
END { exit !f }
|
|
54
|
+
' "$REPORT"
|
|
55
|
+
}
|
|
56
|
+
sha12() {
|
|
57
|
+
if command -v sha256sum >/dev/null 2>&1; then printf '%s' "$1" | sha256sum | cut -c1-12
|
|
58
|
+
else printf '%s' "$1" | shasum -a 256 | cut -c1-12; fi
|
|
59
|
+
}
|
|
60
|
+
# normalised = lowercased, whitespace collapsed, trimmed. The dedup key is a function of THIS
|
|
61
|
+
# and nothing else: no date, no run id, no counter - or it dedups nothing.
|
|
62
|
+
normalise() { printf '%s' "$1" | tr 'A-Z' 'a-z' | tr -s '[:space:]' ' ' | sed 's/^ //; s/ $//'; }
|
|
63
|
+
|
|
64
|
+
REPO=$(field repo)
|
|
65
|
+
SYMPTOM=$(field symptom)
|
|
66
|
+
REVISION=$(field revision)
|
|
67
|
+
KEY=$(field dedup_key)
|
|
68
|
+
|
|
69
|
+
MISSING=""
|
|
70
|
+
for k in repo symptom expected observed repro_steps revision; do
|
|
71
|
+
present "$k" || MISSING="$MISSING $k"
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
WANT_KEY="bug:${REPO}:$(sha12 "$(normalise "$SYMPTOM")")"
|
|
75
|
+
|
|
76
|
+
TITLE="bug: ${SYMPTOM}"
|
|
77
|
+
|
|
78
|
+
# shq <word> — the word quoted for a shell, so the printed form can be pasted without executing
|
|
79
|
+
# anything the report says. Single quotes make backticks and `$( )` text; an embedded single
|
|
80
|
+
# quote is written as the four-character `'\''` (close, escaped quote, reopen).
|
|
81
|
+
shq() { local s="$1"; s=${s//\'/\'\\\'\'}; printf "'%s'" "$s"; }
|
|
82
|
+
|
|
83
|
+
# card_cmd <title-suffix> <idempotency-key> <max-runtime> <max-retries> [assignee-args...]
|
|
84
|
+
# The card command as it is PRINTED for an operator. Every value that came out of the report is
|
|
85
|
+
# shell-quoted; flags and numbers are printed literally, so the line stays greppable. The printed
|
|
86
|
+
# form is what skills/goblin-bugreporter/SKILL.md step 1 tells the operator to run, so it must be
|
|
87
|
+
# paste-safe: the report is the untrusted input this gate exists to validate (G8-1).
|
|
88
|
+
card_cmd() {
|
|
89
|
+
local title="$TITLE$1" key="$2" rt="$3" rr="$4"; shift 4
|
|
90
|
+
local a
|
|
91
|
+
printf 'hermes kanban create %s' "$(shq "$title")"
|
|
92
|
+
for a in "$@"; do printf ' %s' "$a"; done
|
|
93
|
+
printf ' --idempotency-key %s --body-file %s --max-runtime %s --max-retries %s\n' \
|
|
94
|
+
"$(shq "$key")" "$(shq "$REPORT")" "$rt" "$rr"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# run_card <title-suffix> <idempotency-key> <max-runtime> <max-retries> [assignee-args...]
|
|
98
|
+
# The same card as an ARGV ARRAY: no `eval`, no command string, so the title reaches the board as
|
|
99
|
+
# ONE argument whatever it contains. Never build a shell command out of file content (G8-1).
|
|
100
|
+
run_card() {
|
|
101
|
+
local title="$TITLE$1" key="$2" rt="$3" rr="$4"; shift 4
|
|
102
|
+
hermes kanban create "$title" "$@" --idempotency-key "$key" --body-file "$REPORT" \
|
|
103
|
+
--max-runtime "$rt" --max-retries "$rr"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if [ -n "$MISSING" ]; then
|
|
107
|
+
printf 'bugreporter-intake: REFUSED - %s carries no value for:%s\n' "$REPORT" "$MISSING"
|
|
108
|
+
printf '# the card is created WITHOUT --assignee, so no agent can be spawned on it\n'
|
|
109
|
+
card_cmd " (incomplete intake)" "${KEY:-incomplete:$SLUG}" 600 0
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ -n "$KEY" ] && [ "$KEY" != "$WANT_KEY" ]; then
|
|
114
|
+
printf 'bugreporter-intake: REFUSED - the recorded dedup_key is not the content key\n'
|
|
115
|
+
printf ' recorded %s\n recomputed %s\n' "$KEY" "$WANT_KEY"
|
|
116
|
+
printf ' a key derived from a date, a run id or a counter dedups nothing\n'
|
|
117
|
+
card_cmd " (bad key)" "$WANT_KEY" 600 0
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# The false-positive guard: a reproduction must name a revision that exists. A report with an
|
|
122
|
+
# unverifiable `revision:` is a refusal, not a card.
|
|
123
|
+
if [ -d "$REPO" ]; then
|
|
124
|
+
if ! ( cd "$REPO" && git rev-parse --verify --quiet "$REVISION^{commit}" >/dev/null 2>&1 ); then
|
|
125
|
+
printf 'bugreporter-intake: REFUSED - revision "%s" does not resolve in %s\n' "$REVISION" "$REPO"
|
|
126
|
+
card_cmd " (unresolvable revision)" "$WANT_KEY" 600 0
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
else
|
|
130
|
+
printf '# note: %s is not a local directory - the revision gate is unexercised here\n' "$REPO" >&2
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [ "$CARDS" -eq 0 ]; then
|
|
134
|
+
card_cmd "" "$WANT_KEY" 1800 1 --assignee researcher --skill goblin-bugreporter
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
if ! command -v hermes >/dev/null 2>&1; then
|
|
138
|
+
printf 'bugreporter-intake: hermes is not on PATH - the command is printed, no card was created\n' >&2
|
|
139
|
+
card_cmd "" "$WANT_KEY" 1800 1 --assignee researcher --skill goblin-bugreporter
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
run_card "" "$WANT_KEY" 1800 1 --assignee researcher --skill goblin-bugreporter >/dev/null 2>&1 \
|
|
143
|
+
|| { printf 'bugreporter-intake: the card was NOT created\n' >&2; exit 1; }
|
|
144
|
+
printf 'bugreporter-intake: card created with key %s\n' "$WANT_KEY"
|
|
145
|
+
exit 0
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# drift-audit.sh — A-02's producer, the no-agent half of `goblin-drift-audit` (P14).
|
|
3
|
+
#
|
|
4
|
+
# bash drift-audit.sh [--root <glob>] [--state <file>] [--dry-run] [--cards] [--limit <n>]
|
|
5
|
+
#
|
|
6
|
+
# It compares a RECORDED CLAIM with an ARTIFACT, never a memory and never a judgement, so its
|
|
7
|
+
# false-positive rate is structurally zero: the comparison is made by this script, which cannot
|
|
8
|
+
# invent a finding.
|
|
9
|
+
#
|
|
10
|
+
# claim 1: the HANDOFF names a commit that exists here and is an ancestor of HEAD (HP-05)
|
|
11
|
+
# claim 2: every installed file still matches its recorded hash (IN-02)
|
|
12
|
+
# artifact: the repo's git state and the files on disk
|
|
13
|
+
#
|
|
14
|
+
# It is deliberately the FIRST automation to turn on: no agent sits inside the producer, so it
|
|
15
|
+
# costs zero tokens when there is nothing to report.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes: 0 clean, or nothing to do | 1 drift found | 2 could not run.
|
|
18
|
+
#
|
|
19
|
+
# Kill switch <state> carrying `enabled: false` stops it dead, and prints nothing on stdout.
|
|
20
|
+
# Ceiling `run:` lines in <state> from the last 24 h, capped by --limit (default 5). A
|
|
21
|
+
# capped run prints one `# capped` line, so "silent because clean" and "silent
|
|
22
|
+
# because the ceiling was hit" are never confused for each other.
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
ROOT_GLOB="${HOME}/projects/*"
|
|
26
|
+
STATE="${GOBLIN_DRIFT_STATE:-${HOME}/.goblin/automations/drift-audit.state}"
|
|
27
|
+
DRY_RUN=0
|
|
28
|
+
CARDS=0
|
|
29
|
+
LIMIT=5
|
|
30
|
+
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--root) ROOT_GLOB="${2:-}"; shift 2 ;;
|
|
34
|
+
--state) STATE="${2:-}"; shift 2 ;;
|
|
35
|
+
--limit) LIMIT="${2:-}"; shift 2 ;;
|
|
36
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
37
|
+
--cards) CARDS=1; shift ;;
|
|
38
|
+
-h|--help) sed -n '2,/^set -/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
39
|
+
*) printf 'drift-audit: unknown option: %s\n' "$1" >&2; exit 2 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
sha12() {
|
|
44
|
+
if command -v sha256sum >/dev/null 2>&1; then printf '%s' "$1" | sha256sum | cut -c1-12
|
|
45
|
+
else printf '%s' "$1" | shasum -a 256 | cut -c1-12; fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ---- kill switch -------------------------------------------------------------
|
|
49
|
+
if [ -f "$STATE" ] && grep -q '^enabled:[[:space:]]*false' "$STATE"; then
|
|
50
|
+
printf 'drift-audit: disabled (enabled: false in %s)\n' "$STATE" >&2
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ---- the producer's own ceiling ----------------------------------------------
|
|
55
|
+
TODAY=$(date -u +%F)
|
|
56
|
+
RUNS=0
|
|
57
|
+
if [ -f "$STATE" ]; then
|
|
58
|
+
RUNS=$(grep -c "^run: $TODAY" "$STATE" 2>/dev/null || true)
|
|
59
|
+
case "$RUNS" in ''|*[!0-9]*) RUNS=0 ;; esac
|
|
60
|
+
fi
|
|
61
|
+
if [ "$RUNS" -ge "$LIMIT" ]; then
|
|
62
|
+
printf '# capped: %s of %s runs used today - nothing filed\n' "$RUNS" "$LIMIT"
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# ---- enumerate targets by glob, never by memory ------------------------------
|
|
67
|
+
TARGETS=0
|
|
68
|
+
SKIPPED=""
|
|
69
|
+
DRIFT=""
|
|
70
|
+
DETAIL=""
|
|
71
|
+
|
|
72
|
+
for d in $ROOT_GLOB; do
|
|
73
|
+
[ -d "$d" ] || continue
|
|
74
|
+
TARGETS=$((TARGETS + 1))
|
|
75
|
+
if [ ! -f "$d/.goblin/goblin.yaml" ]; then
|
|
76
|
+
SKIPPED="$SKIPPED $(basename "$d")"
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
if [ ! -f "$d/.goblin/bin/goblin-verify" ]; then
|
|
80
|
+
SKIPPED="$SKIPPED $(basename "$d")(no-verifier)"
|
|
81
|
+
continue
|
|
82
|
+
fi
|
|
83
|
+
# --only IN-02,HP-05 rather than a full run: both are builtins that write no runtime state,
|
|
84
|
+
# so the audit never edits the tree it is auditing (design rule 2 of the honesty section).
|
|
85
|
+
OUT=$( cd "$d" && bash .goblin/bin/goblin-verify --json --only IN-02,HP-05 2>/dev/null )
|
|
86
|
+
RC=$?
|
|
87
|
+
case "$RC" in
|
|
88
|
+
0|1) ;;
|
|
89
|
+
*) SKIPPED="$SKIPPED $(basename "$d")(verify-exit-$RC)"; continue ;;
|
|
90
|
+
esac
|
|
91
|
+
FAILED=$(printf '%s' "$OUT" | grep -o '"id":"[A-Z][A-Z0-9-]*","status":"FAIL"' | sed 's/"id":"//; s/","status":"FAIL"//')
|
|
92
|
+
for id in $FAILED; do
|
|
93
|
+
DRIFT="$DRIFT $d/$id"
|
|
94
|
+
DETAIL="$DETAIL$d $id cd $d && bash .goblin/bin/goblin-verify --only $id
|
|
95
|
+
"
|
|
96
|
+
done
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# ---- print nothing when there is nothing to file -----------------------------
|
|
100
|
+
# A clean run writes nothing either: the ceiling counts the runs that FILED something (the
|
|
101
|
+
# cards it created), never the runs that found nothing - otherwise a quiet week would exhaust
|
|
102
|
+
# the ceiling and silence a real finding.
|
|
103
|
+
if [ -z "$DRIFT" ]; then
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
printf '%s' "$DETAIL"
|
|
108
|
+
printf '# coverage: %s target(s) audited by glob, %s drifting, skipped:%s\n' \
|
|
109
|
+
"$TARGETS" "$(printf '%s' "$DRIFT" | wc -w | tr -d ' ')" "${SKIPPED:- none}"
|
|
110
|
+
printf '# run: %s of %s today\n' "$((RUNS + 1))" "$LIMIT"
|
|
111
|
+
|
|
112
|
+
if [ "$DRY_RUN" -eq 0 ]; then
|
|
113
|
+
[ -d "$(dirname "$STATE")" ] || mkdir -p "$(dirname "$STATE")"
|
|
114
|
+
printf 'run: %s\n' "$TODAY" >> "$STATE"
|
|
115
|
+
fi
|
|
116
|
+
if [ "$CARDS" -eq 1 ]; then
|
|
117
|
+
if command -v hermes >/dev/null 2>&1; then
|
|
118
|
+
printf '%s' "$DETAIL" | while IFS=$'\t' read -r repo id cmd; do
|
|
119
|
+
[ -n "$id" ] || continue
|
|
120
|
+
KEY="drift:$(basename "$repo"):$id"
|
|
121
|
+
BODY=$(mktemp)
|
|
122
|
+
{
|
|
123
|
+
printf '# Drift: %s %s\n\n' "$(basename "$repo")" "$id"
|
|
124
|
+
printf 'The producer compared a recorded claim with an artifact and they disagree.\n\n'
|
|
125
|
+
printf ' %s\n\n' "$cmd"
|
|
126
|
+
printf 'Reproduces at %s. Dedup key: %s\n' "$(date -u +%F)" "$KEY"
|
|
127
|
+
printf 'sha256 of the record: %s\n' "$(sha12 "$repo$id$cmd")"
|
|
128
|
+
} > "$BODY"
|
|
129
|
+
hermes kanban create "drift: $(basename "$repo") $id" --assignee architect \
|
|
130
|
+
--skill goblin-drift-audit --idempotency-key "$KEY" --body-file "$BODY" \
|
|
131
|
+
--max-runtime 1800 --max-retries 1 >/dev/null 2>&1 \
|
|
132
|
+
|| printf 'drift-audit: could not create the card for %s %s\n' "$(basename "$repo")" "$id" >&2
|
|
133
|
+
rm -f "$BODY"
|
|
134
|
+
done
|
|
135
|
+
else
|
|
136
|
+
printf 'drift-audit: hermes is not on PATH - the record is printed, no card was created\n' >&2
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
exit 1
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
key required shape
|
|
2
|
+
repo yes path or remote of the repository the defect is in
|
|
3
|
+
symptom yes one line, observable
|
|
4
|
+
expected yes one line
|
|
5
|
+
observed yes one line
|
|
6
|
+
repro_steps yes ordered list, one item per line, 4-space indented
|
|
7
|
+
revision yes a commit that exists in that repository
|
|
8
|
+
dedup_key yes bug:<repo>:<sha256(normalised symptom)[:12]> - content only, never a date
|
|
9
|
+
environment no platform/version; absent is recorded as unknown, never guessed
|
|
10
|
+
evidence no paths to captures; evidence for a human, never harness proof
|
package/bans/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# The ban list — forbidden must fail mechanically
|
|
2
|
+
|
|
3
|
+
`manifest/bans.tsv` is the table; `bin/goblin-bans` is the engine; this directory holds the
|
|
4
|
+
probes. Dune's rule 2, made a gate: **a ban without a mechanism is a wish.** A ban enters
|
|
5
|
+
`bans.tsv` only with a real command in its `detect` column, or it does not enter at all.
|
|
6
|
+
|
|
7
|
+
## How a ban is run
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.goblin/bin/goblin-bans # every ban the config's `bans:` list turns on
|
|
11
|
+
.goblin/bin/goblin-bans --only BN-01 # one ban
|
|
12
|
+
.goblin/bin/goblin-bans --list # the table
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A `detect` command runs from the repo root and exits
|
|
16
|
+
|
|
17
|
+
| exit | meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 0 | the tree is clean |
|
|
20
|
+
| 1 | the ban is violated — the exit code alone is the signal; stdout, when any, names the offending lines |
|
|
21
|
+
| 2 | the check could not run — **fail closed**, never a silent pass |
|
|
22
|
+
| 3 | nothing to check (e.g. no `layers:` declared) — SKIP with that reason |
|
|
23
|
+
|
|
24
|
+
A ban the config does not name is SKIPPED with that reason; a ban whose `globs` match no file
|
|
25
|
+
is SKIPPED with that reason. An empty or missing table is **exit 2**.
|
|
26
|
+
|
|
27
|
+
## Narrow exceptions (Dune rule 5)
|
|
28
|
+
|
|
29
|
+
Two escape hatches ship, and both are filtered **before the exit code is chosen** — a filter
|
|
30
|
+
applied to a probe's stdout afterwards cannot change a verdict, so it would be decorative, and
|
|
31
|
+
every violation inside an exempted path would be a permanent RED with no remedy (that was
|
|
32
|
+
**W5-1**, measured `rc 0` at `72490f0` → `rc 1` at `7fec08f`).
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
bans_exempt: # in .goblin/goblin.yaml — a path prefix, or a whole path
|
|
36
|
+
- BN-03 src/legacy # <ban id> <path prefix>: that ban, that path, nothing else
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
export const a: any = 1; // BAN-OK(BN-01): the value is narrowed at the boundary
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
An inline escape clears **one line** for **that ban**; a non-empty reason after the colon is
|
|
44
|
+
required, so `BAN-OK(BN-01)` on its own is not an escape. Path exemption is segment-aligned and
|
|
45
|
+
compared as written: `src` exempts `src/a.ts` and `src/legacy/b.ts`, never `src2/c.ts`, and a
|
|
46
|
+
prefix carrying a trailing slash (`src/legacy/`) strips nothing, so it matches no file and exempts
|
|
47
|
+
nothing - write the prefix without one. Both shipped probes are controlled in
|
|
48
|
+
`tests/t-verify-red.sh` (AA1): the trailing-slash case is the LAYER probe's control (`BN-05`, AA1
|
|
49
|
+
§7-6), and the same rule holds for the ban probe - measured on `BN-01`: `- BN-01 src` PASSes,
|
|
50
|
+
`- BN-01 src/` FAILs, because it exempts nothing.
|
|
51
|
+
|
|
52
|
+
The engine passes both to the probe through its environment rather than editing stdout:
|
|
53
|
+
|
|
54
|
+
| variable | meaning |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `GOBLIN_BANS_ID` | the ban being probed — what an inline `BAN-OK(<id>)` must name |
|
|
57
|
+
| `GOBLIN_BANS_EXEMPT` | newline-separated path prefixes this ban exempts |
|
|
58
|
+
|
|
59
|
+
`bans/grep-ban.sh` and `bans/layer-check.sh` honour both (the layer probe honours the path list
|
|
60
|
+
only — its documented escape is `layers:` or a move, not an inline marker). A project's **own**
|
|
61
|
+
probe that ignores the variables keeps the old behaviour: a violation inside an exempted path
|
|
62
|
+
stays RED. That is deliberate — the failure is **closed**, never open — and it is recorded in
|
|
63
|
+
`docs/LIMITS.md`.
|
|
64
|
+
|
|
65
|
+
## No npm, no AST
|
|
66
|
+
|
|
67
|
+
The engine uses `bash`/`grep`/`awk` only — the dependency contract in `docs/CONTRACTS.md`. So
|
|
68
|
+
these are **text probes**, not AST checks: a `: any` inside a string or a comment is reported,
|
|
69
|
+
and `Record<string, any>` (no leading `:`) is missed. The AST-grade form of the same bans needs
|
|
70
|
+
a parser the contract does not allow; that gap is `docs/LIMITS.md` #27, stated rather than
|
|
71
|
+
hidden. A text probe with a known false-positive set is still a gate — it goes red on the move
|
|
72
|
+
it forbids — which is what a ban is for.
|
|
73
|
+
|
|
74
|
+
## Turning a ban on
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
bans: [BN-01, BN-02, BN-05] # the bans this project turns on; an unlisted ban SKIPs
|
|
78
|
+
bans_exempt: # narrow, explicit, reviewed (Dune rule 5)
|
|
79
|
+
- BN-03 src/legacy # <ban id> <path prefix>
|
|
80
|
+
layers: # what BN-05 reads; "<from> <to>"
|
|
81
|
+
- src/renderer src/main
|
|
82
|
+
```
|
package/bans/grep-ban.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# grep-ban.sh — a grep-backed ban probe with a fail-closed exit contract.
|
|
3
|
+
#
|
|
4
|
+
# usage: grep-ban.sh -e <pattern> [-e <pattern> ...] <dir|file> ...
|
|
5
|
+
#
|
|
6
|
+
# Exit: 0 the tree is clean | 1 the ban is violated (offending lines on stdout)
|
|
7
|
+
# 2 the check could not run (no pattern, no path, or grep itself failed).
|
|
8
|
+
# It never exits 0 on a tool error - a ban that silently passes when its tool is missing
|
|
9
|
+
# is worse than advisory (PR-03 / D6).
|
|
10
|
+
#
|
|
11
|
+
# It reads TEXT, with no AST and no comments blanked: a `: any` inside a string or a
|
|
12
|
+
# comment is reported. That limit is stated, not hidden - the AST-grade form needs a
|
|
13
|
+
# parser the no-npm contract (docs/CONTRACTS.md) does not allow (docs/LIMITS.md #27).
|
|
14
|
+
#
|
|
15
|
+
# NARROW, EXPLICIT EXCEPTIONS (Dune rule 5). The engine exports two variables, and the hits are
|
|
16
|
+
# filtered BEFORE the exit code is chosen - a filter applied to stdout after the fact cannot
|
|
17
|
+
# change a verdict and is therefore decorative (W5-1: `bans_exempt:` produced a permanent RED):
|
|
18
|
+
#
|
|
19
|
+
# GOBLIN_BANS_ID the ban being probed (BN-01 ...)
|
|
20
|
+
# GOBLIN_BANS_EXEMPT newline-separated path prefixes this ban exempts
|
|
21
|
+
#
|
|
22
|
+
# A hit is dropped when its FILE is under an exempt prefix, or when the offending LINE carries
|
|
23
|
+
# the inline escape `// BAN-OK(<id>): <reason>`. Both are segment-aligned and both require the
|
|
24
|
+
# documented form: `src` exempts `src/a.ts` and `src/legacy/b.ts`, never `src2/c.ts`, and a
|
|
25
|
+
# `BAN-OK(<id>)` with no `: <reason>` after it is NOT an escape (it is a wish). When nothing is
|
|
26
|
+
# left the probe exits 0: there is no violation outside the declared exception.
|
|
27
|
+
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
pats=()
|
|
30
|
+
while [ $# -gt 0 ]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
-e) [ $# -ge 2 ] || { echo "grep-ban: -e needs a pattern" >&2; exit 2; }
|
|
33
|
+
pats+=("${2:-}"); shift 2 ;;
|
|
34
|
+
--) shift; break ;;
|
|
35
|
+
-*) echo "grep-ban: unknown option $1" >&2; exit 2 ;;
|
|
36
|
+
*) break ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
[ "${#pats[@]}" -gt 0 ] || { echo "grep-ban: no pattern given" >&2; exit 2; }
|
|
40
|
+
[ "$#" -gt 0 ] || { echo "grep-ban: no path given" >&2; exit 2; }
|
|
41
|
+
# A declared glob that does not exist on disk is not an error: the engine already turns
|
|
42
|
+
# "no matching files" into a SKIP with a reason before it calls this. Silently dropping a
|
|
43
|
+
# missing path here is what stops one absent sibling (BN-03's `app` beside `src/components`)
|
|
44
|
+
# from failing the ban closed on a tree that is clean.
|
|
45
|
+
paths=()
|
|
46
|
+
for p in "$@"; do [ -e "$p" ] && paths+=("$p"); done
|
|
47
|
+
[ "${#paths[@]}" -gt 0 ] || exit 0
|
|
48
|
+
args=()
|
|
49
|
+
for p in "${pats[@]}"; do args+=(-e "$p"); done
|
|
50
|
+
out=$(grep -rnE "${args[@]}" \
|
|
51
|
+
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' \
|
|
52
|
+
-- "${paths[@]}" 2>&1)
|
|
53
|
+
rc=$?
|
|
54
|
+
|
|
55
|
+
# The exemption filter. It runs on hits only, so the fail-closed arms below are untouched.
|
|
56
|
+
ban_exempt_filter() {
|
|
57
|
+
GOBLIN_BANS_EXEMPT="${GOBLIN_BANS_EXEMPT:-}" GOBLIN_BANS_ID="${GOBLIN_BANS_ID:-}" awk '
|
|
58
|
+
BEGIN {
|
|
59
|
+
n = 0
|
|
60
|
+
if (ENVIRON["GOBLIN_BANS_EXEMPT"] != "") n = split(ENVIRON["GOBLIN_BANS_EXEMPT"], ex, "\n")
|
|
61
|
+
id = ENVIRON["GOBLIN_BANS_ID"]
|
|
62
|
+
pat = ""
|
|
63
|
+
if (id != "") pat = "BAN-OK\\(" id "\\)[[:space:]]*:[[:space:]]*[^[:space:]]"
|
|
64
|
+
}
|
|
65
|
+
{
|
|
66
|
+
f = $0; sub(/:.*/, "", f)
|
|
67
|
+
for (i = 1; i <= n; i++) {
|
|
68
|
+
p = ex[i]
|
|
69
|
+
if (p == "") continue
|
|
70
|
+
if (f == p || index(f, p "/") == 1) next
|
|
71
|
+
}
|
|
72
|
+
if (pat != "" && $0 ~ pat) next
|
|
73
|
+
print
|
|
74
|
+
}'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "$rc" in
|
|
78
|
+
1) exit 0 ;; # no match: clean
|
|
79
|
+
0) out=$(printf '%s\n' "$out" | ban_exempt_filter) ;; # matches: drop the declared exceptions
|
|
80
|
+
*) printf '%s\n' "$out" >&2; exit 2 ;; # grep could not run: fail closed
|
|
81
|
+
esac
|
|
82
|
+
[ -n "$(printf '%s' "$out" | tr -d '[:space:]')" ] || exit 0
|
|
83
|
+
printf '%s\n' "$out"
|
|
84
|
+
exit 1
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# layer-check.sh — BN-05's detect. The boundary ban Dune actually mechanises.
|
|
3
|
+
#
|
|
4
|
+
# Reads the config's `layers:` list, each item "<from> <to>", and fails when a file under
|
|
5
|
+
# <from> imports across into <to>. The import test is a text probe for an import path that
|
|
6
|
+
# names <to>'s last path segment - so it catches `from '../main/db'` in a renderer, and it
|
|
7
|
+
# does NOT resolve module aliases or dynamic imports. Stated, not hidden (docs/LIMITS.md #27).
|
|
8
|
+
#
|
|
9
|
+
# Exit: 0 clean | 1 a crossing import (lines on stdout) | 2 could not run | 3 no layers declared.
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
13
|
+
ROOT=$(cd "$SELF_DIR/../.." && pwd) # .goblin/bans/ -> repo root
|
|
14
|
+
CONFIG="$ROOT/.goblin/goblin.yaml"
|
|
15
|
+
[ -f "$CONFIG" ] || { echo "layer-check: no $CONFIG" >&2; exit 2; }
|
|
16
|
+
|
|
17
|
+
layers=$(awk '
|
|
18
|
+
$0 ~ /^layers:[[:space:]]*$/ { inb = 1; next }
|
|
19
|
+
inb && /^[^ ]/ { inb = 0 }
|
|
20
|
+
inb && /^ - / { v = $0; sub(/^ - /, "", v); print v }
|
|
21
|
+
' "$CONFIG")
|
|
22
|
+
[ -n "$layers" ] || { echo "no layers declared in $CONFIG - declare a from/to pair to turn BN-05 on"; exit 3; }
|
|
23
|
+
|
|
24
|
+
bad=""
|
|
25
|
+
while read -r from to; do
|
|
26
|
+
[ -n "$from" ] && [ -n "$to" ] || continue
|
|
27
|
+
[ -d "$ROOT/$from" ] || continue
|
|
28
|
+
seg=${to##*/}
|
|
29
|
+
hits=$(cd "$ROOT" && grep -rnE "from[[:space:]]+['\"][^'\"]*${seg}/" \
|
|
30
|
+
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' "$from" 2>/dev/null || true)
|
|
31
|
+
[ -n "$hits" ] && bad="$bad$hits"$'\n'
|
|
32
|
+
done <<EOF
|
|
33
|
+
$layers
|
|
34
|
+
EOF
|
|
35
|
+
|
|
36
|
+
# A narrow, explicit exception (Dune rule 5) reaches this probe through the same environment the
|
|
37
|
+
# engine gives grep-ban.sh: GOBLIN_BANS_EXEMPT is a newline-separated list of path prefixes, and a
|
|
38
|
+
# hit whose FILE is under one of them does not count. Filtering here, before the exit code, is what
|
|
39
|
+
# makes the exception able to change a verdict (W5-1). BN-05's documented escape is `layers:` or a
|
|
40
|
+
# move, not the inline marker, so no BAN-OK is honoured here. A probe that ignores the variable
|
|
41
|
+
# keeps the old, fail-closed behaviour.
|
|
42
|
+
if [ -n "$bad" ] && [ -n "${GOBLIN_BANS_EXEMPT:-}" ]; then
|
|
43
|
+
bad=$(GOBLIN_BANS_EXEMPT="$GOBLIN_BANS_EXEMPT" printf '%s' "$bad" | awk '
|
|
44
|
+
BEGIN { n = split(ENVIRON["GOBLIN_BANS_EXEMPT"], ex, "\n") }
|
|
45
|
+
{
|
|
46
|
+
f = $0; sub(/:.*/, "", f)
|
|
47
|
+
for (i = 1; i <= n; i++) {
|
|
48
|
+
p = ex[i]
|
|
49
|
+
if (p == "") continue
|
|
50
|
+
if (f == p || index(f, p "/") == 1) next
|
|
51
|
+
}
|
|
52
|
+
print
|
|
53
|
+
}')
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ -n "$bad" ]; then printf '%s' "$bad"; exit 1; fi
|
|
57
|
+
exit 0
|
package/bin/goblin
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# goblin — the global CLI dispatcher (W1, PLAN-V1 §2.3 / W1-SPEC §3).
|
|
3
|
+
#
|
|
4
|
+
# goblin verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
|
|
5
|
+
# goblin bans [--only <id[,id...]>] [--list]
|
|
6
|
+
# goblin audit [--target <dir>] [--print]
|
|
7
|
+
# goblin doctor [--platform <p>] # W4a
|
|
8
|
+
# goblin emit --platform <p> [...] # W4a/W4b
|
|
9
|
+
# goblin init [...] # W6: the first-run wizard
|
|
10
|
+
# goblin upgrade [--target .] [...] # W3
|
|
11
|
+
# goblin --version
|
|
12
|
+
#
|
|
13
|
+
# Identity: the package is goblin-stack, the command is `goblin`. W1 ships the DISPATCH
|
|
14
|
+
# SHELL only — the node shim and npm packaging are W2, doctor/emit are W4a (exit-2
|
|
15
|
+
# placeholders naming their workstream), upgrade is W3 (same). G3 forbids a runtime
|
|
16
|
+
# rewrite: the engine stays bash, this file only routes and propagates.
|
|
17
|
+
#
|
|
18
|
+
# The four-value verify contract is unchanged and non-negotiable (bin/goblin-verify:5-9):
|
|
19
|
+
# 0 every executed check passed
|
|
20
|
+
# 1 at least one check failed
|
|
21
|
+
# 2 could not run
|
|
22
|
+
# 3 the manifest itself is broken
|
|
23
|
+
# Every subcommand propagates the engine's exit code verbatim; no wrapper translates a
|
|
24
|
+
# 1 into a 0. A placeholder that silently exited 0 would be a green build doing nothing.
|
|
25
|
+
#
|
|
26
|
+
# Root resolution: find_installed_root() at bin/goblin-verify:64-77 is preserved
|
|
27
|
+
# BYTE-FOR-BYTE (walk up from $PWD to .goblin/goblin.yaml; fall back to git toplevel,
|
|
28
|
+
# then $PWD — the D20 nested-repo fix). W1 changes nothing about its logic.
|
|
29
|
+
#
|
|
30
|
+
# No npm, no jq, no yq, no network. bash/awk/sed/grep only.
|
|
31
|
+
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
35
|
+
SRC=$(cd "$SELF_DIR/.." && pwd)
|
|
36
|
+
VERSION=$(cat "$SRC/VERSION" 2>/dev/null || printf 'unknown')
|
|
37
|
+
|
|
38
|
+
g_err() { printf 'error: %s\n' "$*" >&2; }
|
|
39
|
+
|
|
40
|
+
usage() {
|
|
41
|
+
cat <<'USAGE'
|
|
42
|
+
goblin — the goblin-stack command line.
|
|
43
|
+
|
|
44
|
+
goblin verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
|
|
45
|
+
goblin bans [--only <id[,id...]>] [--list]
|
|
46
|
+
goblin audit [--target <dir>] [--print]
|
|
47
|
+
goblin doctor [--platform <p>] [--target <dir>]
|
|
48
|
+
goblin emit --platform <p> --scope project|global [...]
|
|
49
|
+
goblin init [--target <dir>] [--class app|A-F] [--dry-run]
|
|
50
|
+
goblin upgrade [--target .] [--dry-run] [--yes] [--engine-dir <path>]
|
|
51
|
+
goblin --version
|
|
52
|
+
|
|
53
|
+
Exit codes (verify): 0 pass | 1 a check failed | 2 could not run | 3 the manifest is
|
|
54
|
+
broken. Every subcommand propagates the engine's exit code verbatim.
|
|
55
|
+
USAGE
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ---- the repo-root locator, preserved byte-for-byte from bin/goblin-verify:64-77 ----
|
|
59
|
+
find_installed_root() {
|
|
60
|
+
local d="$PWD"
|
|
61
|
+
while :; do
|
|
62
|
+
if [ -f "$d/.goblin/goblin.yaml" ]; then printf '%s\n' "$d"; return 0; fi
|
|
63
|
+
[ "$d" = "/" ] && return 1
|
|
64
|
+
d=$(dirname "$d")
|
|
65
|
+
done
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# ---- subcommand dispatch ------------------------------------------------------
|
|
69
|
+
CMD="${1:-}"
|
|
70
|
+
[ -n "$CMD" ] || { usage; exit 2; }
|
|
71
|
+
shift
|
|
72
|
+
|
|
73
|
+
case "$CMD" in
|
|
74
|
+
verify)
|
|
75
|
+
# The engine is whatever the per-repo chain resolves (W1 §2.3): --source, vendored,
|
|
76
|
+
# engine_dir:, GOBLIN_ENGINE_DIR, ~/.goblin/engine — all inside goblin-verify. The
|
|
77
|
+
# CLI is a pass-through, not a second root-resolution opinion (§3.4).
|
|
78
|
+
exec bash "$SRC/bin/goblin-verify" "$@"
|
|
79
|
+
;;
|
|
80
|
+
bans)
|
|
81
|
+
exec bash "$SRC/bin/goblin-bans" "$@"
|
|
82
|
+
;;
|
|
83
|
+
audit)
|
|
84
|
+
exec bash "$SRC/bin/goblin-audit" "$@"
|
|
85
|
+
;;
|
|
86
|
+
doctor)
|
|
87
|
+
# W4a: one run, three platforms (the §7 report); the dispatcher routes and
|
|
88
|
+
# propagates the doctor's 0/1/2 verbatim, like every other subcommand.
|
|
89
|
+
exec bash "$SRC/bin/goblin-doctor" "$@"
|
|
90
|
+
;;
|
|
91
|
+
emit)
|
|
92
|
+
# W4a: per-platform emission (the §4 contract); the 0/1/2 exit contract is the
|
|
93
|
+
# same three values verify's wrapper states, propagated verbatim.
|
|
94
|
+
exec bash "$SRC/bin/goblin-emit" "$@"
|
|
95
|
+
;;
|
|
96
|
+
init)
|
|
97
|
+
# W6: the first-run wizard. It drives install/emit/verify and propagates their
|
|
98
|
+
# exit contract verbatim, like every other subcommand here.
|
|
99
|
+
exec bash "$SRC/bin/goblin-init" "$@"
|
|
100
|
+
;;
|
|
101
|
+
upgrade)
|
|
102
|
+
# W3: the migration lives in its own checkout-level script (W3-SPEC §1.1);
|
|
103
|
+
# the dispatcher routes and propagates the four-value contract verbatim.
|
|
104
|
+
exec bash "$SRC/bin/goblin-upgrade" "$@"
|
|
105
|
+
;;
|
|
106
|
+
--version|-V|-v)
|
|
107
|
+
printf '%s\n' "$VERSION"
|
|
108
|
+
exit 0
|
|
109
|
+
;;
|
|
110
|
+
-h|--help|help)
|
|
111
|
+
usage
|
|
112
|
+
exit 0
|
|
113
|
+
;;
|
|
114
|
+
*)
|
|
115
|
+
g_err "unknown subcommand: $CMD"
|
|
116
|
+
usage >&2
|
|
117
|
+
exit 2
|
|
118
|
+
;;
|
|
119
|
+
esac
|