@techgoblin/gobstack 0.4.4-beta.7 → 0.5.0-beta.1
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 +28 -12
- package/VERSION +1 -1
- package/bin/goblin-audit +1 -1
- package/bin/goblin-bans +9 -1
- package/bin/goblin-init +55 -37
- package/bin/goblin-install +100 -12
- package/bin/goblin-lib.sh +59 -1
- package/bin/goblin-upgrade +1 -1
- package/bin/goblin-verify +52 -11
- package/docs/ADOPTION.md +42 -37
- package/docs/CI.md +11 -8
- package/docs/CONTRACTS.md +30 -15
- package/docs/ENFORCEMENT.md +24 -20
- package/docs/GUIDE.md +37 -33
- package/docs/LIMITS.md +20 -3
- package/manifest/classes.tsv +50 -60
- package/package.json +1 -1
- package/presets/electron-overlay.yaml +38 -0
- package/presets/{E-fleet-config.yaml → fleet.yaml} +1 -1
- package/presets/{C-game.yaml → game.yaml} +1 -1
- package/presets/{D-knowledge.yaml → research.yaml} +1 -1
- package/presets/{B-service-config.yaml → service.yaml} +1 -1
- package/presets/{A-shipped-software.yaml → software.yaml} +1 -1
- package/skills/goblin-bootstrap/SKILL.md +15 -7
- package/templates/AGENTS.md.tmpl +14 -5
- package/templates/goblin.yaml.tmpl +9 -1
- package/presets/F-electron.yaml +0 -67
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ dependencies disagree with npm's. If you see `ERESOLVE` after a local install, r
|
|
|
28
28
|
dependency from `package.json` and install globally instead.
|
|
29
29
|
|
|
30
30
|
**The two-layer model.** The global install gives you the CLI only. `gob init` (or
|
|
31
|
-
`gob install --target <dir> --class
|
|
31
|
+
`gob install --target <dir> --class software`) then vendors a self-contained engine into the target
|
|
32
32
|
repo under `.goblin/` — verifier, manifest, ban probes, skills, all of it. That second layer is
|
|
33
33
|
why an initialized repo keeps working on machines with **no gobstack installed at all**: the
|
|
34
34
|
engine lives in the repo, not in your `node_modules`, and `bash .goblin/bin/goblin-verify` (or a
|
|
@@ -36,7 +36,7 @@ plain `git` + `bash` box) is the only runtime the repo's gate needs.
|
|
|
36
36
|
|
|
37
37
|
Then, from any project:
|
|
38
38
|
|
|
39
|
-
gob install --target /path/to/repo --class
|
|
39
|
+
gob install --target /path/to/repo --class software
|
|
40
40
|
|
|
41
41
|
The installer writes only paths it records, hash-compares before writing, and prints `no-op` on a
|
|
42
42
|
second run with the same arguments. It never overwrites `HANDOFF.md`, `AGENTS.md`, a `*-SPEC.md`,
|
|
@@ -46,18 +46,27 @@ of file it manages: `docs/CONTRACTS.md`.
|
|
|
46
46
|
A repo that already has its own `HANDOFF.md` exits 1 on the refusal. That is the contract, not a
|
|
47
47
|
failure: reconcile the file rather than forcing over it — `docs/ADOPTION.md`.
|
|
48
48
|
|
|
49
|
+
`--class` picks the preset: **software** (the default — shipped features, PRs, review gates),
|
|
50
|
+
**service** (backend jobs, config, unattended runs), **game** (playable builds, perf budgets),
|
|
51
|
+
**research** (specs, replays, reference corpora) and **fleet** (config-of-the-agent repos).
|
|
52
|
+
The single letters `A`–`E` are accepted aliases. What each preset turns on is the matrix in
|
|
53
|
+
`docs/ADOPTION.md`; the older names still resolve (see `docs/CONTRACTS.md`).
|
|
54
|
+
|
|
49
55
|
After installing, in this order:
|
|
50
56
|
|
|
51
57
|
cd <target> && git add -A && git commit # the install is a change like any other
|
|
52
58
|
gob verify # or .goblin/bin/goblin-verify, inside the target
|
|
53
|
-
|
|
59
|
+
gob emit --platform <p> # optional, per platform: the agent skills are an opt-in
|
|
54
60
|
gob audit # once, deliberately: the ONLY network step (SC-07)
|
|
55
61
|
|
|
56
|
-
**A class
|
|
62
|
+
**A default software-class install (no agent skills — those are `gob emit`'s job) verifies green —
|
|
63
|
+
`38 passed, 0 failed, 11 advisory, 33 skipped`, exit 0 — once
|
|
57
64
|
`HANDOFF.md` names a commit that exists. Before that edit the scaffold's `0000000` placeholder is
|
|
58
|
-
the one expected red: `
|
|
59
|
-
the fix are step 2 of `docs/GUIDE.md`.**
|
|
60
|
-
|
|
65
|
+
the one expected red: `37 passed, 1 failed`, `HP-05`. Both numbers measured at W6 (neutral-first);
|
|
66
|
+
the run and the fix are step 2 of `docs/GUIDE.md`.**
|
|
67
|
+
Thirty-three rows skip — the five skill rows (`SK-01`..`SK-04`, `AU-04`) skip on the `playbooks`
|
|
68
|
+
opt-out a skills-free install records, then the not-yet rows (`HS-02` has no pinned pre-change
|
|
69
|
+
commit yet, so the REPLAY is not provable;
|
|
61
70
|
`AU-02` and `AU-03` have no report to audit; `SC-06`, `SC-07` and `SC-08` have no dependency
|
|
62
71
|
manifest, no lockfile and no audit record to read; `PF-01` has no measured perf baseline;
|
|
63
72
|
`BN-01`/`BN-02`/`BN-05` have no `src/` for a ban to read, and `BN-03` plus the four electron bans
|
|
@@ -83,12 +92,12 @@ The measurement and the vacuous-pass reading are in `docs/CONTRACTS.md`.
|
|
|
83
92
|
| `gob verify` | run the rule matrix against the current repo — `PASS`/`FAIL`/`SKIP` per row, exit 0 pass · 1 a check failed · 2 could not run · 3 the manifest is broken |
|
|
84
93
|
| `gob bans` | run the ban list (per-pattern red lines over the source tree) |
|
|
85
94
|
| `gob audit` | check recorded dependency claims against live advisory feeds — the only command that touches the network |
|
|
86
|
-
| `gob install` | install the manifest, skills
|
|
95
|
+
| `gob install` | install the harness into a target repo: manifest, verifier, gates, HANDOFF — no agent skills (those are an opt-in: `gob emit --platform <p>`, or `--skills yes`) |
|
|
87
96
|
| `gob uninstall` | remove everything an install wrote, byte-exactly (`gob install --target <dir> --uninstall` is the same job) |
|
|
88
97
|
| `gob upgrade` | migrate a repo to the shared global engine at `~/.goblin/engine` — 8 steps, two commits, one report |
|
|
89
98
|
| `gob doctor` | one run across the platforms below: DETECTED / NOT-DETECTED / DRIFT per platform |
|
|
90
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 |
|
|
91
|
-
| `gob init` | the first-run wizard: detect → class → branch/email → first gate → emit → verify, one screen per question; every question has a flag (`--class
|
|
100
|
+
| `gob init` | the first-run wizard: detect → class → branch/email → first gate → emit → verify, 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; `--dry-run` prints the plan and writes nothing |
|
|
92
101
|
|
|
93
102
|
`goblin` remains as a legacy alias for every command above — existing scripts keep working, but
|
|
94
103
|
new commands and docs use `gob`.
|
|
@@ -111,6 +120,13 @@ One run of `gob emit --platform <p> --scope project` writes the skills and the c
|
|
|
111
120
|
session of that platform reads; `--scope global` writes to the machine-level anchor. `--dry-run`
|
|
112
121
|
prints the full write plan first.
|
|
113
122
|
|
|
123
|
+
**Agent skills are opt-in.** A `gob install` writes the neutral harness only — `.goblin/`,
|
|
124
|
+
`HANDOFF.md`, `AGENTS.md`, the checks and the `.gitignore` block; no skills directory, and no
|
|
125
|
+
files belonging to any coding agent. The guided path is `gob init`'s emit screen; the one-shot
|
|
126
|
+
path is `gob emit --platform <p>` after installing. Repos whose install predates the opt-in
|
|
127
|
+
default keep their skills through `gob upgrade` (the install record names them; only an explicit
|
|
128
|
+
`--skills no`, or `--uninstall`, removes them).
|
|
129
|
+
|
|
114
130
|
## What it is not
|
|
115
131
|
|
|
116
132
|
Not a rules document (every rule carries a runnable check or is explicitly counted as
|
|
@@ -135,9 +151,9 @@ below is the reference material the guide points into, so the two do not compete
|
|
|
135
151
|
| `docs/CONTRACTS.md` | the installer/verifier interface, exit codes, idempotency, uninstall |
|
|
136
152
|
| `docs/INTEGRATION.md` | the board, cron, the skills precedence order, the referenced standard |
|
|
137
153
|
| `docs/RISKS.md` | the risk register, the advisory rows named, the non-goals |
|
|
138
|
-
| `docs/CI.md` | the CI lane: what makes a workflow a gate, the four settings a repository cannot set, and the
|
|
154
|
+
| `docs/CI.md` | the CI lane: what makes a workflow a gate, the four settings a repository cannot set, and the electron opt-in |
|
|
139
155
|
| `docs/LOOP.md` | the judge role and the loop contract: what a goal-mode loop actually does, the record, and what neither can see |
|
|
140
|
-
| `docs/ADOPTION.md` | the
|
|
156
|
+
| `docs/ADOPTION.md` | the five classes, the preset matrix, the adoption order |
|
|
141
157
|
| `docs/LIMITS.md` | where this is weaker than its sources, and what is unproven |
|
|
142
158
|
|
|
143
159
|
`manifest/enforcement.tsv` is the source of truth for rules; `manifest/classes.tsv` for what a
|
|
@@ -156,7 +172,7 @@ run · `3` the manifest is broken. Every run prints what it cannot see.
|
|
|
156
172
|
bash tests/run-tests.sh
|
|
157
173
|
|
|
158
174
|
Runs the source-scope rules (PR-01..PR-05) and the test scripts, including `t-verify-red.sh` —
|
|
159
|
-
one control per target-scope row (
|
|
175
|
+
one control per target-scope row (168 over 82 target rows), each required to go RED and then
|
|
160
176
|
restored, plus `t-audit.sh` for the SC-07 producer. **A verifier that only ever prints GREEN is a
|
|
161
177
|
failure**, so that file is the one that matters most.
|
|
162
178
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
package/bin/goblin-audit
CHANGED
package/bin/goblin-bans
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
set -uo pipefail
|
|
30
30
|
|
|
31
|
-
GOBLIN_BANS_VERSION="0.
|
|
31
|
+
GOBLIN_BANS_VERSION="0.5.0"
|
|
32
32
|
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
33
33
|
# shellcheck source=goblin-lib.sh
|
|
34
34
|
. "$SELF_DIR/goblin-lib.sh"
|
|
@@ -88,6 +88,14 @@ if [ -f "$CONFIG" ]; then
|
|
|
88
88
|
ENABLED=$(g_yaml_scalar "$CONFIG" bans)
|
|
89
89
|
ENABLED=${ENABLED#[}; ENABLED=${ENABLED%]}
|
|
90
90
|
ENABLED=$(printf '%s' "$ENABLED" | tr ',' ' ' | tr -s ' ' ' ')
|
|
91
|
+
# The electron opt-in (software class): `electron: true` turns the electron ban set ON even
|
|
92
|
+
# when a hand-edited bans: list omits it — the declaration is the contract, and the engine
|
|
93
|
+
# (not the LLM) is what makes a listed ban non-skippable. A repo without the key is unaffected.
|
|
94
|
+
if [ "$(g_yaml_scalar "$CONFIG" electron)" = "true" ]; then
|
|
95
|
+
for e in BN-06 BN-07 BN-08 BN-09; do
|
|
96
|
+
case " $ENABLED " in *" $e "*) ;; *) ENABLED="$ENABLED $e" ;; esac
|
|
97
|
+
done
|
|
98
|
+
fi
|
|
91
99
|
fi
|
|
92
100
|
enabled() {
|
|
93
101
|
[ -z "$ENABLED" ] && return 1
|
package/bin/goblin-init
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# goblin-init — the W6 first-run wizard (`gob init`).
|
|
3
3
|
#
|
|
4
|
-
# gob init [--target <dir>] [--class <
|
|
5
|
-
# [--branch <name>] [--email <addr>] [--gate <cmd>]
|
|
4
|
+
# gob init [--target <dir>] [--class <software|service|game|research|fleet|A..E|app|agent|desktop>]
|
|
5
|
+
# [--electron] [--branch <name>] [--email <addr>] [--gate <cmd>]
|
|
6
6
|
# [--emit <p[,p..]>] [--scope project|global] [--yes] [--dry-run]
|
|
7
7
|
#
|
|
8
8
|
# One screen per question, answered steps collapsing into the ✔/◆/○ rail above. On a tty
|
|
@@ -143,6 +143,7 @@ ask_value() {
|
|
|
143
143
|
|
|
144
144
|
# ------------------------------------------------------------------ flags -----
|
|
145
145
|
TARGET="" CLASS="" BRANCH="" EMAIL="" GATE="" EMIT="" SCOPE=""
|
|
146
|
+
ELECTRON=0
|
|
146
147
|
YES=0; DRYRUN=0
|
|
147
148
|
usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
148
149
|
|
|
@@ -150,6 +151,7 @@ while [ $# -gt 0 ]; do
|
|
|
150
151
|
case "$1" in
|
|
151
152
|
--target) TARGET="${2:-}"; shift 2 ;;
|
|
152
153
|
--class) CLASS="${2:-}"; shift 2 ;;
|
|
154
|
+
--electron) ELECTRON=1; shift ;;
|
|
153
155
|
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
154
156
|
--email) EMAIL="${2:-}"; shift 2 ;;
|
|
155
157
|
--gate) GATE="${2:-}"; shift 2 ;;
|
|
@@ -255,45 +257,45 @@ if [ -z "$CLASS" ]; then
|
|
|
255
257
|
if [ "$TTY_IN" -eq 1 ]; then
|
|
256
258
|
rail_say ""
|
|
257
259
|
rail_say " ${C_MUTED}what kind of work does this repo do?$C_RESET"
|
|
258
|
-
rail_say " $C_TEXT 1 ▸
|
|
260
|
+
rail_say " $C_TEXT 1 ▸ software shipped features, PRs, review gates ${C_DIM}(class A)$C_RESET"
|
|
259
261
|
rail_say " $C_TEXT 2 service backend jobs, config, unattended runs ${C_DIM}(class B)$C_RESET"
|
|
260
262
|
rail_say " $C_TEXT 3 game playable builds, perf budgets ${C_DIM}(class C)$C_RESET"
|
|
261
263
|
rail_say " $C_TEXT 4 research specs, replays, reference corpora ${C_DIM}(class D)$C_RESET"
|
|
262
|
-
rail_say " $C_TEXT 5
|
|
263
|
-
rail_say " $
|
|
264
|
-
rail_say " ${
|
|
264
|
+
rail_say " $C_TEXT 5 fleet fleets, loops, unattended automation ${C_DIM}(class E)$C_RESET"
|
|
265
|
+
rail_say " ${C_MUTED}↑ number + Enter · Enter = software · a class letter also works (A-E)$C_RESET"
|
|
266
|
+
rail_say " ${C_DIM}an Electron desktop shell: pick software, then --electron$C_RESET"
|
|
265
267
|
printf ' '
|
|
266
268
|
read -r REPLY_CLASS
|
|
267
269
|
ASKED_ANY=1
|
|
268
270
|
rail_say ""
|
|
269
271
|
else
|
|
270
272
|
REPLY_CLASS="1"
|
|
271
|
-
cascade "class:
|
|
273
|
+
cascade "class: software (default; --class software|service|game|research|fleet or A-E)"
|
|
272
274
|
fi
|
|
273
275
|
case "$REPLY_CLASS" in
|
|
274
|
-
1) CLASS="
|
|
275
|
-
4) CLASS="research" ;; 5) CLASS="
|
|
276
|
-
*) CLASS="${REPLY_CLASS:-
|
|
276
|
+
1) CLASS="software" ;; 2) CLASS="service" ;; 3) CLASS="game" ;;
|
|
277
|
+
4) CLASS="research" ;; 5) CLASS="fleet" ;;
|
|
278
|
+
*) CLASS="${REPLY_CLASS:-software}" ;;
|
|
277
279
|
esac
|
|
278
280
|
fi
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
research|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
*)
|
|
290
|
-
g_err "--class must be app|service|game|research|agent|desktop (or A-F), got '$CLASS'"
|
|
291
|
-
exit 2 ;;
|
|
281
|
+
# Resolve every accepted spelling to the canonical class NAME. The letters A-E and the old
|
|
282
|
+
# taught names (app/agent/desktop) are read-time aliases; anything else is refused here, with
|
|
283
|
+
# the full enum, rather than deep inside the installer. `desktop`/F means the OLD desktop
|
|
284
|
+
# install, so it also sets the electron opt-in.
|
|
285
|
+
g_class_is_electron_alias "$CLASS" && ELECTRON=1
|
|
286
|
+
CLASS_RAW="$CLASS"
|
|
287
|
+
CLASS=$(g_class_canon "$CLASS") \
|
|
288
|
+
|| { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
|
|
289
|
+
case "$CLASS_RAW" in
|
|
290
|
+
app|agent|desktop) g_info "note: '$CLASS_RAW' is now called '$CLASS' (accepted as an alias)";;
|
|
292
291
|
esac
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
if [ "$ELECTRON" -eq 1 ]; then
|
|
293
|
+
g_info "electron: on (the desktop opt-in — the app_bundle_bytes ratchet, BN-06..09 bans, a host gate)"
|
|
294
|
+
fi
|
|
295
|
+
CLASS_NAME="$SRC/presets/$CLASS.yaml"
|
|
296
|
+
STEP_VAL[1]="$C_TEXT$CLASS$C_RESET"
|
|
295
297
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
296
|
-
rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$
|
|
298
|
+
rail_say " ✔ $((CURRENT - 1)). class $C_TEXT$CLASS$C_RESET"
|
|
297
299
|
else
|
|
298
300
|
cascade "class: $CLASS"
|
|
299
301
|
fi
|
|
@@ -375,29 +377,37 @@ if [ -n "$EMIT" ]; then
|
|
|
375
377
|
[ -n "$ok" ] || { g_err "--emit: unknown platform '$w' — I ship $PLA, hermes, copilot, cursor, opencode, codex, $GEM"; exit 2; }
|
|
376
378
|
done
|
|
377
379
|
else
|
|
380
|
+
# neutral-first: a flags-only run with no --emit emits NOTHING. The detected rows are
|
|
381
|
+
# shown on the emit screen as pre-ticked SUGGESTIONS for the interactive path; in
|
|
382
|
+
# non-interactive mode silence means the neutral harness only.
|
|
378
383
|
EMIT_SELECTED=""
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
if [ "$YES" -eq 0 ] && [ "$TTY_IN" -eq 1 ]; then
|
|
385
|
+
while IFS=$'\t' read -r p _proj _glob _hit; do
|
|
386
|
+
[ -n "$p" ] || continue
|
|
387
|
+
EMIT_SELECTED="$EMIT_SELECTED $p"
|
|
388
|
+
done <<< "$DET_ROWS"
|
|
389
|
+
fi
|
|
383
390
|
fi
|
|
384
391
|
EMIT_N=$(printf '%s' "$EMIT_SELECTED" | awk 'NF' | wc -l | tr -d '[:space:]')
|
|
385
392
|
|
|
386
393
|
if [ "$TTY_IN" -eq 1 ] && [ -z "$EMIT" ] && [ "$YES" -eq 0 ]; then
|
|
387
394
|
rail_say ""
|
|
388
|
-
rail_say " ${C_MUTED}
|
|
395
|
+
rail_say " ${C_MUTED}agent skills are opt-in, per platform — emit them into:$C_RESET"
|
|
389
396
|
for p in $PLATFORMS; do
|
|
390
397
|
hit=""
|
|
391
398
|
while IFS=$' ' read -r dp _a _b _c; do [ "$dp" = "$p" ] && hit=1; done <<< "$DET_ROWS"
|
|
392
399
|
[ -n "$hit" ] || continue
|
|
393
400
|
rail_say " ${C_GREEN}✔$C_RESET $C_TEXT$p$C_RESET"
|
|
394
401
|
done
|
|
395
|
-
rail_say " ${C_MUTED}
|
|
402
|
+
rail_say " ${C_MUTED}the install itself writes none — the harness stays neutral$C_RESET"
|
|
403
|
+
rail_say " ${C_MUTED}Enter = these; or type platform names to override (none: type x)$C_RESET"
|
|
396
404
|
printf ' '
|
|
397
405
|
read -r REPLY_EMIT
|
|
398
406
|
ASKED_ANY=1
|
|
399
407
|
rail_say ""
|
|
400
|
-
if [
|
|
408
|
+
if [ "$REPLY_EMIT" = "x" ] || [ "$REPLY_EMIT" = "none" ]; then
|
|
409
|
+
EMIT_SELECTED=""
|
|
410
|
+
elif [ -n "$REPLY_EMIT" ]; then
|
|
401
411
|
EMIT_SELECTED=""
|
|
402
412
|
for w in $REPLY_EMIT; do
|
|
403
413
|
ok=""
|
|
@@ -452,11 +462,15 @@ fi
|
|
|
452
462
|
# never rewinds past its own buffer by counting lines it did not print.
|
|
453
463
|
if [ "$TTY_OUT" -eq 1 ]; then
|
|
454
464
|
rail_note ""
|
|
455
|
-
rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS${C_RESET}"
|
|
465
|
+
rail_note " ${C_MUTED}running: goblin-install --target $TARGET --class $CLASS$([ "$ELECTRON" -eq 1 ] && printf ' --electron')${C_RESET}"
|
|
456
466
|
else
|
|
457
|
-
printf 'gob init [run] goblin-install --target %s --class %s\n' "$TARGET" "$CLASS"
|
|
467
|
+
printf 'gob init [run] goblin-install --target %s --class %s%s\n' "$TARGET" "$CLASS" "$([ "$ELECTRON" -eq 1 ] && printf ' --electron')"
|
|
468
|
+
fi
|
|
469
|
+
if [ "$ELECTRON" -eq 1 ]; then
|
|
470
|
+
bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --electron --skills no --yes
|
|
471
|
+
else
|
|
472
|
+
bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --skills no --yes
|
|
458
473
|
fi
|
|
459
|
-
bash "$SRC/bin/goblin-install" --target "$TARGET" --class "$CLASS" --yes
|
|
460
474
|
EXIT_INSTALL=$?
|
|
461
475
|
if [ "$EXIT_INSTALL" -ne 0 ]; then
|
|
462
476
|
g_err "install failed (exit $EXIT_INSTALL) — the refusals above name the path and the fix"
|
|
@@ -546,7 +560,11 @@ if [ "$DRYRUN" -eq 0 ]; then
|
|
|
546
560
|
printf ' 1. %sthe %s skipped checks ARE your checklist — gob verify names each one%s\n' "$C_TEXT" "$SKIPPED" "$C_RESET"
|
|
547
561
|
printf ' 2. %sreplace the placeholder gate in .goblin/goblin.yaml with your real commands (P8 step 3)%s\n' "$C_TEXT" "$C_RESET"
|
|
548
562
|
printf ' 3. %snot emitted here: gob emit --platform <p> --scope global%s\n' "$C_TEXT" "$C_RESET"
|
|
549
|
-
|
|
563
|
+
if [ "$EMIT_N" -gt 0 ]; then
|
|
564
|
+
printf ' 4. %shermes skills trust %s # one-time, so the project-tier skills load%s\n' "$C_TEXT" "$TARGET" "$C_RESET"
|
|
565
|
+
else
|
|
566
|
+
printf ' 4. %sagent skills are opt-in per platform: gob emit --platform <p>%s\n' "$C_TEXT" "$C_RESET"
|
|
567
|
+
fi
|
|
550
568
|
printf '\n'
|
|
551
569
|
if [ "$VERIFY_RC" -eq 0 ]; then
|
|
552
570
|
printf ' %s▙ the goblin sees you. keep the gate green.%s\n' "$C_ACCENT" "$C_RESET"
|
package/bin/goblin-install
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage: gob install --target <dir> [options] (script: bin/goblin-install)
|
|
5
5
|
# --target <dir> required; the repo root to install into
|
|
6
|
-
# --class
|
|
6
|
+
# --class <name> required unless --uninstall or --re-pin; one of
|
|
7
|
+
# software|service|game|research|fleet, or the aliases A-E / app / agent
|
|
8
|
+
# / desktop (desktop/F => software + --electron); anything else is refused
|
|
9
|
+
# --electron the electron opt-in overlay over the software class (electron: true):
|
|
10
|
+
# the app_bundle_bytes ratchet, BN-06..09 bans, the host-gate declaration
|
|
7
11
|
# --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
|
|
8
12
|
# --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md); a named path that is absent is reported, never silently dropped
|
|
9
13
|
# --parts <list> comma list to install; default = every part the class requires
|
|
10
14
|
# --archive mark the project archive: verify requires no HANDOFF and no gates
|
|
11
|
-
# --skills yes|no install .hermes/skills (default
|
|
15
|
+
# --skills yes|no install agent skills under .hermes/skills (default no; opt in per
|
|
16
|
+
# platform with: gob emit --platform <p>)
|
|
12
17
|
# --dry-run print the plan; write nothing
|
|
13
18
|
# --upgrade re-install at the current version; report created/updated/unchanged/skipped
|
|
14
19
|
# --opt-out <part> record the part in disabled: so its required checks are skipped
|
|
@@ -22,7 +27,7 @@
|
|
|
22
27
|
|
|
23
28
|
set -uo pipefail
|
|
24
29
|
|
|
25
|
-
GOBLIN_INSTALL_VERSION="0.
|
|
30
|
+
GOBLIN_INSTALL_VERSION="0.5.0"
|
|
26
31
|
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
27
32
|
SRC=$(cd "$SELF_DIR/.." && pwd)
|
|
28
33
|
# shellcheck source=goblin-lib.sh
|
|
@@ -40,13 +45,26 @@ PRACTICE="${GOBLIN_PRACTICE:-$HOME/projects/PROJECT-PRACTICE.md}"
|
|
|
40
45
|
PRACTICE_ARG=""
|
|
41
46
|
PARTS=""
|
|
42
47
|
ARCHIVE="false"
|
|
43
|
-
|
|
48
|
+
# W6 neutral-first: a default install is a NEUTRAL harness (manifest + verify + HANDOFF +
|
|
49
|
+
# .goblin) with NO agent skills. Agent skills are an explicit opt-in (--skills yes, or the
|
|
50
|
+
# per-platform gob emit path the wizard's emit screen drives). The old default (yes) was the
|
|
51
|
+
# house's own setup, not a product default: a non-agent user got a dead .hermes/ tree, and a
|
|
52
|
+
# user of another agent harness got nothing at all.
|
|
53
|
+
SKILLS="no"
|
|
54
|
+
# Whether --skills was named on the command line: an OMITTED flag on a re-install means
|
|
55
|
+
# "whatever the record says" (the migration contract below); a NAMED --skills no is an
|
|
56
|
+
# explicit opt-out and is honoured verbatim.
|
|
57
|
+
SKILLS_NAMED=0
|
|
44
58
|
DRY_RUN=0
|
|
45
59
|
UPGRADE=0
|
|
46
60
|
UNINSTALL=0
|
|
47
61
|
RE_PIN=0
|
|
48
62
|
FORCE=0
|
|
49
63
|
OPT_OUT=""
|
|
64
|
+
# The electron opt-in (the merged desktop/F class): renders presets/electron-overlay.yaml over
|
|
65
|
+
# the software preset. Auto-set when --class resolves via the desktop/F/f alias, so the alias
|
|
66
|
+
# behaves as the old class F did rather than silently installing weaker software.
|
|
67
|
+
ELECTRON=0
|
|
50
68
|
|
|
51
69
|
usage() {
|
|
52
70
|
# The header comment runs from line 2 to the first blank line, so adding an option cannot
|
|
@@ -63,7 +81,8 @@ while [ $# -gt 0 ]; do
|
|
|
63
81
|
--practice) PRACTICE="${2:-}"; PRACTICE_ARG="$PRACTICE"; shift 2 ;;
|
|
64
82
|
--parts) PARTS="${2:-}"; shift 2 ;;
|
|
65
83
|
--archive) ARCHIVE="true"; shift ;;
|
|
66
|
-
--
|
|
84
|
+
--electron) ELECTRON=1; shift ;;
|
|
85
|
+
--skills) SKILLS="${2:-}"; SKILLS_NAMED=1; shift 2 ;;
|
|
67
86
|
--dry-run) DRY_RUN=1; shift ;;
|
|
68
87
|
--upgrade) UPGRADE=1; shift ;;
|
|
69
88
|
--uninstall) UNINSTALL=1; shift ;;
|
|
@@ -229,14 +248,48 @@ if [ "$RE_PIN" -eq 1 ]; then
|
|
|
229
248
|
exit 0
|
|
230
249
|
fi
|
|
231
250
|
|
|
232
|
-
[ -n "$CLASS" ] || { g_err "--class is required (
|
|
233
|
-
|
|
251
|
+
[ -n "$CLASS" ] || { g_err "--class is required (software|service|game|research|fleet, or A-E / app / agent / desktop)"; exit 2; }
|
|
252
|
+
# Resolve every accepted spelling to the canonical class NAME before anything reads it. The
|
|
253
|
+
# letters A-E and the old names app/agent/desktop are read-time aliases; desktop/F/f also sets
|
|
254
|
+
# the electron opt-in so the merged class behaves as the old class F did.
|
|
255
|
+
g_class_is_electron_alias "$CLASS" && ELECTRON=1
|
|
256
|
+
CLASS_RAW="$CLASS"
|
|
257
|
+
CLASS=$(g_class_canon "$CLASS") \
|
|
258
|
+
|| { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
|
|
234
259
|
case "$SKILLS" in yes|no) ;; *) g_err "--skills must be yes or no"; exit 2 ;; esac
|
|
235
260
|
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
# W6 migration safety: on a --upgrade (or any re-install) of a repo whose record shows skills
|
|
262
|
+
# were installed, an OMITTED --skills flag must READ that state, not silently strip it. The
|
|
263
|
+
# idempotence contract (created/updated/unchanged) holds only if the effective skills choice
|
|
264
|
+
# for a flag-less re-install is `the record's choice`, so previously-installed skills survive
|
|
265
|
+
# every upgrade unchanged. An EXPLICIT --skills no is honoured verbatim (the switch stays
|
|
266
|
+
# real), and --uninstall still removes exactly what the record lists.
|
|
267
|
+
if [ -f "$INSTALLED" ] && [ "$SKILLS_NAMED" -eq 0 ]; then
|
|
268
|
+
PREV_SKILLS=$(g_installed_scalar_options "$INSTALLED" skills)
|
|
269
|
+
if [ "$PREV_SKILLS" = "yes" ]; then
|
|
270
|
+
SKILLS="yes"
|
|
271
|
+
g_info "skills: this repo's install record has agent skills installed - they are kept (remove them with an explicit --skills no, or --uninstall)"
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
PRESET="$SRC/presets/$CLASS.yaml"
|
|
276
|
+
[ -f "$PRESET" ] || { g_err "no preset for class $CLASS ($PRESET)"; exit 2; }
|
|
277
|
+
|
|
278
|
+
# The electron overlay (the merged desktop/F class): every key it declares — including an empty
|
|
279
|
+
# one like sec_write_routes: "" — wins over the class preset; an absent key falls through. This
|
|
280
|
+
# is config, not a second code path: the same preset() reader, one layer up.
|
|
281
|
+
OVERLAY=""
|
|
282
|
+
if [ "$ELECTRON" -eq 1 ]; then
|
|
283
|
+
OVERLAY="$SRC/presets/electron-overlay.yaml"
|
|
284
|
+
[ -f "$OVERLAY" ] || { g_err "no electron overlay at $OVERLAY"; exit 2; }
|
|
285
|
+
fi
|
|
286
|
+
overlay_has() { [ -n "$OVERLAY" ] && grep -qE "^$1:" "$OVERLAY"; }
|
|
238
287
|
|
|
239
|
-
preset() {
|
|
288
|
+
preset() {
|
|
289
|
+
local v
|
|
290
|
+
if overlay_has "$1"; then v=$(g_yaml_scalar "$OVERLAY" "$1"); else v=$(g_yaml_scalar "$PRESET" "$1"); fi
|
|
291
|
+
printf '%s' "${v%\"}" | sed 's/^"//'
|
|
292
|
+
}
|
|
240
293
|
|
|
241
294
|
HARNESS_DIR=$(preset harness_dir); [ -n "$HARNESS_DIR" ] || HARNESS_DIR=checks
|
|
242
295
|
SCAFFOLD=$(preset scaffold_checks); [ -n "$SCAFFOLD" ] || SCAFFOLD=no
|
|
@@ -464,6 +517,7 @@ render_config() {
|
|
|
464
517
|
runtime=" - $RUNTIME_DATA"
|
|
465
518
|
render "$SRC/templates/goblin.yaml.tmpl" \
|
|
466
519
|
CLASS "$CLASS" \
|
|
520
|
+
ELECTRON "$( [ "$ELECTRON" -eq 1 ] && printf 'true' || printf 'false' )" \
|
|
467
521
|
BRANCH "$BRANCH" \
|
|
468
522
|
MODELS_FILE "$MODELS" \
|
|
469
523
|
PRACTICE "$PRACTICE" \
|
|
@@ -572,6 +626,30 @@ if [ "$SKILLS" = "yes" ] && part_wanted playbooks; then
|
|
|
572
626
|
done
|
|
573
627
|
fi
|
|
574
628
|
|
|
629
|
+
# W6 neutral-first: an EXPLICIT --skills no re-install of a repo that HAS recorded skills
|
|
630
|
+
# removes them — the switch stays real in both directions. The paths come from the previous
|
|
631
|
+
# record, so exactly what was installed is what goes; a skill file the project edited by hand
|
|
632
|
+
# is refused by put's own contract (never clobbered silently), reported in the summary.
|
|
633
|
+
if [ "$SKILLS_NAMED" -eq 1 ] && [ "$SKILLS" = "no" ] && [ -n "$PREV_FILES" ]; then
|
|
634
|
+
while IFS= read -r rel; do
|
|
635
|
+
case "$rel" in .hermes/skills/*) ;; *) continue ;; esac
|
|
636
|
+
dst="$TARGET/$rel"
|
|
637
|
+
[ -f "$dst" ] || { rm -f "$TARGET/.goblin/$(basename "$rel")" 2>/dev/null; continue; }
|
|
638
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
639
|
+
g_info "plan: remove $rel (explicit --skills no)"
|
|
640
|
+
else
|
|
641
|
+
rm -f "$dst"
|
|
642
|
+
g_info "removed $rel (explicit --skills no)"
|
|
643
|
+
fi
|
|
644
|
+
done <<EOF
|
|
645
|
+
$PREV_FILES
|
|
646
|
+
EOF
|
|
647
|
+
# the emptied .hermes/skills/*/<name> and .hermes/ directories go too (deepest first)
|
|
648
|
+
if [ "$DRY_RUN" -eq 0 ]; then
|
|
649
|
+
find "$TARGET/.hermes" -depth -type d -exec rmdir {} + 2>/dev/null
|
|
650
|
+
fi
|
|
651
|
+
fi
|
|
652
|
+
|
|
575
653
|
# The automation producers. They land in the target so the target is self-contained and
|
|
576
654
|
# IN-02/SK-02 hash them like every other installed file; the cron copy step then reads from
|
|
577
655
|
# here (see the "next:" block and automations/README.md).
|
|
@@ -667,12 +745,20 @@ fi
|
|
|
667
745
|
} > "$TMP/installed.json"
|
|
668
746
|
|
|
669
747
|
PREV_VERSION=""
|
|
670
|
-
|
|
748
|
+
RECORD_SKILLS_EFFECTIVE=""
|
|
749
|
+
if [ -f "$INSTALLED" ]; then
|
|
750
|
+
PREV_VERSION=$(g_installed_scalar "$INSTALLED" version)
|
|
751
|
+
# The record's own skills choice (after the migration read above): a flip of the effective
|
|
752
|
+
# choice IS a change even when no file hash moved — the explicit --skills no re-install must
|
|
753
|
+
# rewrite the record (and drop the files), never report no-op over a state change.
|
|
754
|
+
RECORD_SKILLS_EFFECTIVE=$(g_installed_scalar_options "$INSTALLED" skills)
|
|
755
|
+
fi
|
|
671
756
|
|
|
672
757
|
|
|
673
758
|
if [ "$DRY_RUN" -eq 0 ]; then
|
|
674
759
|
mkdir -p "$TARGET/.goblin"
|
|
675
|
-
if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ]
|
|
760
|
+
if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ] \
|
|
761
|
+
&& [ "$(printf '%s' "$RECORD_SKILLS_EFFECTIVE" )" = "$(printf '%s' "$SKILLS")" ]; then
|
|
676
762
|
g_info "no-op: $UNCHANGED files unchanged (v$VERSION already installed)"
|
|
677
763
|
else
|
|
678
764
|
cp "$TMP/installed.json" "$INSTALLED"
|
|
@@ -699,6 +785,8 @@ if [ "$DRY_RUN" -eq 0 ]; then
|
|
|
699
785
|
g_info " 3. edit .goblin/goblin.yaml: replace the default gate with your real commands (P8 step 3)"
|
|
700
786
|
if [ "$SKILLS" = "yes" ]; then
|
|
701
787
|
g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
|
|
788
|
+
else
|
|
789
|
+
g_info " 4. agent skills are opt-in: gob emit --platform <p> # run gob doctor for the platform list"
|
|
702
790
|
fi
|
|
703
791
|
g_info ""
|
|
704
792
|
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.
|
|
19
|
+
GOBLIN_LIB_VERSION="0.5.0"
|
|
20
20
|
|
|
21
21
|
# ---------------------------------------------------------------- output -----
|
|
22
22
|
g_pass() { printf 'PASS %-6s %s\n' "$1" "$2"; }
|
|
@@ -185,6 +185,33 @@ g_part_disabled() {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
# ------------------------------------------------------------- class data ----
|
|
188
|
+
# g_class_canon <spelling> -> the canonical class NAME
|
|
189
|
+
# software | service | game | research | fleet
|
|
190
|
+
# The taxonomy is five domain-named classes. The letters A-E and the older taught domain names
|
|
191
|
+
# (app, agent, desktop) stay as READ-TIME aliases so every existing installed.json / goblin.yaml
|
|
192
|
+
# - which record a letter or an old name - keeps verifying with no rewrite. `desktop` / `F` / `f`
|
|
193
|
+
# resolve to `software`: F was merged into A (their classes.tsv need columns are identical), and
|
|
194
|
+
# what made a desktop shell different is the `electron:` opt-in + ban list, config keys the repo
|
|
195
|
+
# already carries. Unknown spelling -> empty output; the caller refuses with the enum.
|
|
196
|
+
g_class_canon() {
|
|
197
|
+
case "$1" in
|
|
198
|
+
software|A|a|app) printf 'software' ;;
|
|
199
|
+
service|B|b) printf 'service' ;;
|
|
200
|
+
game|C|c) printf 'game' ;;
|
|
201
|
+
research|D|d) printf 'research' ;;
|
|
202
|
+
fleet|E|e|agent) printf 'fleet' ;;
|
|
203
|
+
desktop|F|f) printf 'software' ;;
|
|
204
|
+
*) return 1 ;;
|
|
205
|
+
esac
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# g_class_is_electron_alias <spelling> -> 0 when the spelling is the merged desktop/F spelling.
|
|
209
|
+
# `--class desktop` (or F/f) must mean the OLD desktop install, not a silently weaker software
|
|
210
|
+
# one: the installer auto-sets electron: true so the alias behaves as F did.
|
|
211
|
+
g_class_is_electron_alias() {
|
|
212
|
+
case "$1" in desktop|F|f) return 0 ;; *) return 1 ;; esac
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
# g_class_need <classes.tsv> <class> <part> -> R | O | -
|
|
189
216
|
g_class_need() {
|
|
190
217
|
awk -F'\t' -v c="$2" -v p="$3" '
|
|
@@ -216,6 +243,37 @@ g_installed_scalar() {
|
|
|
216
243
|
sed -n "s/^[[:space:]]*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}$/\1/p" "$1" | head -n 1
|
|
217
244
|
}
|
|
218
245
|
|
|
246
|
+
# g_installed_scalar_options <file> <key> — the value of one key inside the "options" object
|
|
247
|
+
# of an install record (e.g. skills). The installer writes options on ONE line
|
|
248
|
+
# (`"options": {"skills": "yes", ...}`), so the awk reads that line's shape directly; a
|
|
249
|
+
# multi-line variant is read the same way the other installed_* readers read their block.
|
|
250
|
+
g_installed_scalar_options() {
|
|
251
|
+
awk -v k="$2" '
|
|
252
|
+
/^[[:space:]]*"options"[[:space:]]*:[[:space:]]*\{/ && index($0, "\"" k "\"") {
|
|
253
|
+
# single-line form: pull the value out between the key and the next , or }
|
|
254
|
+
line = $0
|
|
255
|
+
sub(/^[^{]*\{/, "", line) # everything up to and including the opening brace
|
|
256
|
+
n = split(line, pair, ",")
|
|
257
|
+
for (i = 1; i <= n; i++) {
|
|
258
|
+
p = pair[i]
|
|
259
|
+
sub(/^[[:space:]]*/, "", p); sub(/[[:space:]]*$/, "", p)
|
|
260
|
+
key = p; sub(/[[:space:]]*:.*/, "", key); gsub(/"/, "", key)
|
|
261
|
+
if (key == k) {
|
|
262
|
+
v = p; sub(/^[^:]*:[[:space:]]*/, "", v); gsub(/"/, "", v); sub(/\}[[:space:]]*$/, "", v)
|
|
263
|
+
print v; exit
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exit
|
|
267
|
+
}
|
|
268
|
+
/^[[:space:]]*"options"[[:space:]]*:[[:space:]]*\{/ { inf = 1; next }
|
|
269
|
+
inf && /^[[:space:]]*\}/ { inf = 0 }
|
|
270
|
+
inf && index($0, "\"" k "\"") {
|
|
271
|
+
v = $0; sub(/^[^:]*:[[:space:]]*/, "", v); sub(/,?[[:space:]]*$/, "", v); gsub(/"/, "", v)
|
|
272
|
+
print v; exit
|
|
273
|
+
}
|
|
274
|
+
' "$1"
|
|
275
|
+
}
|
|
276
|
+
|
|
219
277
|
# ------------------------------------------------------------- self-test -----
|
|
220
278
|
# Proves the parser actually parses. Every assertion is a real comparison against a
|
|
221
279
|
# value written to a temp file in this function — blank the awk in g_yaml_scalar and
|
package/bin/goblin-upgrade
CHANGED