@techgoblin/gobstack 0.5.0-beta.8 → 0.6.0-alpha.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/CHANGELOG.md +38 -0
- package/README.md +112 -124
- package/VERSION +1 -1
- package/automations/drift-audit.sh +4 -4
- package/bans/layer-check.sh +10 -8
- package/bin/goblin +61 -58
- package/bin/goblin-audit +11 -13
- package/bin/goblin-bans +11 -11
- package/bin/goblin-init +275 -713
- package/bin/goblin-install +160 -114
- package/bin/goblin-lib.sh +234 -1
- package/bin/goblin-map +226 -21
- package/bin/goblin-mcp.js +492 -0
- package/bin/goblin-model +4 -4
- package/bin/goblin-upgrade +1 -1
- package/bin/goblin-verify +159 -145
- package/bin/goblin.js +33 -51
- package/docs/ADOPTION.md +15 -15
- package/docs/CONTRACTS.md +16 -15
- package/docs/DESIGN.md +1 -1
- package/docs/ENFORCEMENT.md +89 -90
- package/docs/FLOWS.md +1 -1
- package/docs/GLOSSARY.md +3 -3
- package/docs/GUARDRAILS.md +5 -5
- package/docs/GUIDE.md +167 -177
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITS.md +25 -0
- package/docs/LOOP.md +12 -12
- package/docs/RE-PLAYBOOK.md +3 -3
- package/docs/ROLES.md +5 -5
- package/manifest/bans.tsv +8 -8
- package/manifest/classes.tsv +3 -3
- package/manifest/enforcement.tsv +40 -40
- package/manifest/glossary.tsv +3 -3
- package/manifest/playbooks.tsv +1 -1
- package/package.json +1 -1
- package/presets/electron-overlay.yaml +2 -2
- package/presets/fleet.yaml +8 -7
- package/presets/game.yaml +1 -1
- package/presets/research.yaml +1 -1
- package/presets/service.yaml +1 -1
- package/presets/software.yaml +1 -1
- package/skills/goblin-bootstrap/SKILL.md +2 -2
- package/templates/AGENTS.md.tmpl +8 -18
- package/templates/HANDOFF.md.tmpl +5 -5
- package/templates/agents-block.tmpl +45 -0
- package/templates/audit-waiver.tsv.tmpl +2 -2
- package/templates/boundary-waivers.tmpl +1 -1
- package/templates/checks/gate.sh.tmpl +6 -6
- package/templates/install-hooks.allowlist.tmpl +1 -1
- package/templates/ci/goblin-gate.yml.tmpl +0 -46
- package/templates/goblin.yaml.tmpl +0 -146
- package/templates/loop/decisions.tsv.tmpl +0 -1
- package/templates/loop/predicate.tmpl +0 -16
package/bin/goblin-install
CHANGED
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
# --opt-out <part> record the part in disabled: so its required checks are skipped
|
|
20
20
|
# --uninstall remove exactly the files in installed.json
|
|
21
21
|
# --re-pin re-record practice_sha256: for an edited standard; nothing else changes
|
|
22
|
-
#
|
|
23
|
-
# .github/workflows/goblin-gate.yml, ci-gate '-' never does). An
|
|
22
|
+
# (the CI lane was REMOVED from the product in v2: no .github/workflows payload, the
|
|
24
23
|
# explicit no opts the part out (recorded in disabled: so CL-01
|
|
25
24
|
# reports the opt-out, never a silent absence); an explicit yes is
|
|
26
25
|
# refused for a class that forbids the part.
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
|
|
33
32
|
set -uo pipefail
|
|
34
33
|
|
|
35
|
-
GOBLIN_INSTALL_VERSION="0.
|
|
34
|
+
GOBLIN_INSTALL_VERSION="0.6.0-alpha.1"
|
|
36
35
|
SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
37
36
|
SRC=$(cd "$SELF_DIR/.." && pwd)
|
|
38
37
|
# shellcheck source=goblin-lib.sh
|
|
@@ -66,11 +65,8 @@ UNINSTALL=0
|
|
|
66
65
|
RE_PIN=0
|
|
67
66
|
FORCE=0
|
|
68
67
|
OPT_OUT=""
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# "no" is an explicit opt-out of the ci-gate part; "yes" is an explicit opt-in, refused for
|
|
72
|
-
# a class that forbids the part (CL-01 would fail it on the very first verify).
|
|
73
|
-
CI_GATE_OPT=""
|
|
68
|
+
GATE_NAME_ARG="" # parsed by --gate <cmd> [name] below; reset before the loop
|
|
69
|
+
# CI_GATE_OPT is gone with the CI lane (v2): no flag, no workflow file, no ci_gate field.
|
|
74
70
|
# The electron opt-in (the merged desktop/F class): renders presets/electron-overlay.yaml over
|
|
75
71
|
# the software preset. Auto-set when --class resolves via the desktop/F/f alias, so the alias
|
|
76
72
|
# behaves as the old class F did rather than silently installing weaker software.
|
|
@@ -98,7 +94,7 @@ while [ $# -gt 0 ]; do
|
|
|
98
94
|
--uninstall) UNINSTALL=1; shift ;;
|
|
99
95
|
--re-pin) RE_PIN=1; shift ;;
|
|
100
96
|
--opt-out) OPT_OUT="${2:-}"; shift 2 ;;
|
|
101
|
-
--
|
|
97
|
+
--gate) GOB_GATE="${2:-}"; GATE_NAME_ARG="${3:-}"; shift 2; [ -n "${GATE_NAME_ARG:-}" ] && shift; shift ;;
|
|
102
98
|
--force) FORCE=1; shift ;;
|
|
103
99
|
--yes) shift ;;
|
|
104
100
|
-h|--help) usage; exit 0 ;;
|
|
@@ -117,8 +113,8 @@ TARGET=$(cd "$TARGET" && pwd)
|
|
|
117
113
|
|
|
118
114
|
VERSION=$(head -n 1 "$SRC/VERSION")
|
|
119
115
|
CLASSES_TSV="$SRC/manifest/classes.tsv"
|
|
120
|
-
INSTALLED="$TARGET/.
|
|
121
|
-
CONFIG="$TARGET
|
|
116
|
+
INSTALLED="$TARGET/.gob/installed.json"
|
|
117
|
+
CONFIG="$TARGET/AGENTS.md"
|
|
122
118
|
STAMP=$(date +%F)
|
|
123
119
|
|
|
124
120
|
# --uninstall and --re-pin are different jobs, and this guard has to sit ABOVE the uninstall
|
|
@@ -145,18 +141,35 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
145
141
|
# .github/ behind with nothing named (the shape t-uninstall.sh asserts against).
|
|
146
142
|
d="$p"; while [ "${d%/*}" != "$d" ]; do d="${d%/*}"; PARENTS="$PARENTS$d"$'\n'; done
|
|
147
143
|
done < <(g_installed_files "$INSTALLED")
|
|
148
|
-
# The config
|
|
149
|
-
#
|
|
150
|
-
if [ -f "$CONFIG" ]; then
|
|
151
|
-
[ "$DRY_RUN" -eq 1 ]
|
|
152
|
-
|
|
144
|
+
# The config lives in the AGENTS.md gob block: uninstall strips ONLY the marker block and
|
|
145
|
+
# keeps the file (the body prose is the project's). A file with no gob block is untouched.
|
|
146
|
+
if [ -f "$CONFIG" ] && grep -q "^$GOB_AGENTS_BEGIN" "$CONFIG" 2>/dev/null; then
|
|
147
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
148
|
+
g_info "would remove the gob block from $CONFIG (the body prose stays)"
|
|
149
|
+
else
|
|
150
|
+
awk -v begin="^$GOB_AGENTS_BEGIN" -v end="^$GOB_AGENTS_END" '
|
|
151
|
+
$0 ~ begin { inb = 1; next } $0 ~ end { inb = 0; next } !inb { print }
|
|
152
|
+
' "$CONFIG" > "$CONFIG.nogob" && mv "$CONFIG.nogob" "$CONFIG"
|
|
153
|
+
g_info "removed the gob block from AGENTS.md (the body prose stays)"
|
|
154
|
+
fi
|
|
153
155
|
fi
|
|
154
156
|
# Runtime state goblin-stack's own checks wrote.
|
|
155
|
-
for f in .
|
|
157
|
+
for f in .gob/last-gate-line .gob/ratchet-last .gob/.ds-report; do
|
|
156
158
|
[ -f "$TARGET/$f" ] || continue
|
|
157
159
|
[ "$DRY_RUN" -eq 1 ] || rm -f "$TARGET/$f"
|
|
158
160
|
g_info "removed $f (runtime state)"
|
|
159
161
|
done
|
|
162
|
+
# The repo-distributed MCP registration (init --with-mcp-config): removed ONLY when it
|
|
163
|
+
# still carries the generated bytes — the preimage pattern, the same clause the decision
|
|
164
|
+
# records below get. A hand-tuned or team-customized .mcp.json is the project's and stays.
|
|
165
|
+
if [ -f "$TARGET/.mcp.json" ]; then
|
|
166
|
+
if [ "$(cat "$TARGET/.mcp.json" 2>/dev/null)" = '{"mcpServers":{"gob":{"command":"npx","args":["-y","@techgoblin/gobstack","mcp"]}}}' ]; then
|
|
167
|
+
[ "$DRY_RUN" -eq 1 ] || rm -f "$TARGET/.mcp.json"
|
|
168
|
+
g_info "removed .mcp.json (the generated registration)"
|
|
169
|
+
else
|
|
170
|
+
g_info "kept .mcp.json (not the generated bytes: a registration you customized stays)"
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
160
173
|
# The three decision records (audit waivers, boundary waivers, the install-hook allowlist) are
|
|
161
174
|
# put_once files. An UNEDITED one is still goblin-stack's template and goes; an EDITED one is
|
|
162
175
|
# the project's decision and stays, named in the summary. Deleting a decision silently would be
|
|
@@ -165,7 +178,7 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
165
178
|
while IFS=$'\t' read -r p h; do
|
|
166
179
|
[ -n "$p" ] || continue
|
|
167
180
|
case "$p" in
|
|
168
|
-
.
|
|
181
|
+
.gob/audit-waiver.tsv|.gob/boundary-waivers|.gob/install-hooks.allowlist) ;;
|
|
169
182
|
*) continue ;;
|
|
170
183
|
esac
|
|
171
184
|
[ -e "$TARGET/$p" ] || continue
|
|
@@ -179,7 +192,7 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
179
192
|
g_info "kept $p (you edited it: a decision record is the project's, not the harness's)"
|
|
180
193
|
fi
|
|
181
194
|
done < <(g_json_object "$INSTALLED" owned)
|
|
182
|
-
# installed.json goes BEFORE the directory pass. While it was still on disk, .
|
|
195
|
+
# installed.json goes BEFORE the directory pass. While it was still on disk, .gob/ was
|
|
183
196
|
# never empty when it was tested, so the directory survived unnamed (D17 / F2-7).
|
|
184
197
|
if [ "$DRY_RUN" -eq 0 ]; then
|
|
185
198
|
rm -f "$INSTALLED"
|
|
@@ -189,7 +202,7 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
189
202
|
fi
|
|
190
203
|
# Empty directories goblin-stack created: the parents of the files just removed, plus the
|
|
191
204
|
# harness roots. Deepest first, so a parent is only tested once its child has gone. The
|
|
192
|
-
# old fixed list never tried .hermes/skills/goblin-* and tested .
|
|
205
|
+
# old fixed list never tried .hermes/skills/goblin-* and tested .gob/ too early.
|
|
193
206
|
nd=0
|
|
194
207
|
while read -r d; do
|
|
195
208
|
[ -n "$d" ] || continue
|
|
@@ -204,29 +217,32 @@ if [ "$UNINSTALL" -eq 1 ]; then
|
|
|
204
217
|
fi
|
|
205
218
|
done < <({
|
|
206
219
|
printf '%s' "$PARENTS"
|
|
207
|
-
printf '.goblin\n.
|
|
220
|
+
printf '.goblin\n.gob/bin\n.gob/manifest\n.gob/bans\n.hermes\n.hermes/skills\n'
|
|
208
221
|
} | awk 'NF' | awk -F/ '{print NF " " $0}' | sort -rn | cut -d' ' -f2- | awk '!seen[$0]++')
|
|
209
222
|
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}."
|
|
210
223
|
exit 0
|
|
211
224
|
fi
|
|
212
225
|
|
|
213
226
|
# ---------------------------------------------------------------- re-pin ------
|
|
214
|
-
# The practice pin (practice_sha256: in .
|
|
215
|
-
# a SILENTLY edited standard. The standard itself is a living document edited in place, and
|
|
216
|
-
#
|
|
227
|
+
# The practice pin (practice_sha256: in the AGENTS.md gob block, re-checked by IN-02) exists to
|
|
228
|
+
# catch a SILENTLY edited standard. The standard itself is a living document edited in place, and
|
|
229
|
+
# the block is `owned` content the installer never rewrites - so the remedy for an edit that IS
|
|
217
230
|
# intended has to be explicit, printed and deliberate. This mode rewrites exactly one line and
|
|
218
231
|
# reports the old and new hash. It is unreachable from --upgrade and from goblin-verify, by
|
|
219
232
|
# design: an automatic re-pin would be the silent edit the pin exists to catch.
|
|
220
233
|
# See docs/CONTRACTS.md, "An edited standard is not a dead end".
|
|
234
|
+
# The write's own failure output is captured (not discarded) so the fail-closed branch below can
|
|
235
|
+
# name it: a g_agents_write that could not land used to vanish into >/dev/null and the pipe still
|
|
236
|
+
# exited 0 (the sed inside writes via mktemp+mv, and only the mv reports).
|
|
237
|
+
RE_PIN_WRITE_LOG=""
|
|
221
238
|
if [ "$RE_PIN" -eq 1 ]; then
|
|
222
239
|
[ -f "$CONFIG" ] || { g_err "nothing to re-pin: $CONFIG is absent (install first)"; exit 2; }
|
|
223
|
-
PIN_PATH=$(g_expand_tilde "$(
|
|
240
|
+
PIN_PATH=$(g_expand_tilde "$(g_agents_read "$CONFIG" practice)")
|
|
224
241
|
[ -n "$PIN_PATH" ] || { g_err "no practice: recorded in $CONFIG — nothing to re-pin"; exit 2; }
|
|
225
242
|
[ -f "$PIN_PATH" ] || { g_err "the recorded standard is absent: $PIN_PATH"; exit 2; }
|
|
226
|
-
grep -q '^practice_sha256:' "$CONFIG" || { g_err "no practice_sha256: line in $CONFIG to rewrite"; exit 2; }
|
|
227
243
|
# g_unquote, exactly as goblin-verify reads it: a hand-quoted value is accepted by the verifier,
|
|
228
244
|
# so it must not read as "different" here and trigger a rewrite of a line that already matches.
|
|
229
|
-
OLD_SHA=$(g_unquote "$(
|
|
245
|
+
OLD_SHA=$(g_unquote "$(g_agents_read "$CONFIG" practice_sha256)")
|
|
230
246
|
NEW_SHA=$(g_sha256_file "$PIN_PATH")
|
|
231
247
|
if [ "$OLD_SHA" = "$NEW_SHA" ]; then
|
|
232
248
|
g_info "practice pin already current: $PIN_PATH"
|
|
@@ -239,23 +255,31 @@ if [ "$RE_PIN" -eq 1 ]; then
|
|
|
239
255
|
g_info " now $NEW_SHA"
|
|
240
256
|
exit 0
|
|
241
257
|
fi
|
|
242
|
-
# One
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
# One key, in place: g_agents_write rewrites only the marker block, and the rewrite keeps
|
|
259
|
+
# every OTHER key by reading the current block first (the `owned` contract holds for
|
|
260
|
+
# everything except the single value just asked for). The write's failure output is captured,
|
|
261
|
+
# not discarded: a g_agents_write that could not land used to vanish into >/dev/null and only
|
|
262
|
+
# the generic fail-closed message remained.
|
|
263
|
+
RE_PIN_WRITE_LOG=$(mktemp "${TMPDIR:-/tmp}/gob-repin.XXXXXX")
|
|
264
|
+
{
|
|
265
|
+
while IFS=$'\t' read -r k v; do
|
|
266
|
+
[ "$k" = "practice_sha256" ] && continue
|
|
267
|
+
printf '%s\t%s\n' "$k" "$v"
|
|
268
|
+
done < <(g_agents_pairs "$CONFIG")
|
|
269
|
+
printf 'practice_sha256\t%s\n' "$NEW_SHA"
|
|
270
|
+
} | g_agents_write "$CONFIG" > "$RE_PIN_WRITE_LOG" 2>&1 \
|
|
271
|
+
|| { g_err "could not rewrite the pin in $CONFIG"; sed -n '1,3p' "$RE_PIN_WRITE_LOG" >&2 2>/dev/null; rm -f "$RE_PIN_WRITE_LOG"; exit 1; }
|
|
272
|
+
rm -f "$RE_PIN_WRITE_LOG"
|
|
273
|
+
# Fail closed: never report a re-pin that did not land.
|
|
274
|
+
[ "$(g_agents_read "$CONFIG" practice_sha256)" = "$NEW_SHA" ] \
|
|
251
275
|
|| { g_err "the new pin did not land in $CONFIG"; exit 1; }
|
|
252
276
|
g_info "practice re-pinned: $PIN_PATH"
|
|
253
277
|
g_info " recorded $OLD_SHA"
|
|
254
278
|
g_info " now $NEW_SHA"
|
|
255
279
|
g_info ""
|
|
256
280
|
g_info "next:"
|
|
257
|
-
g_info " 1. cd $TARGET && git add .
|
|
258
|
-
g_info " 2. .
|
|
281
|
+
g_info " 1. cd $TARGET && git add AGENTS.md && git commit # the re-pin is a change like any other"
|
|
282
|
+
g_info " 2. .gob/bin/goblin-verify --only IN-02 # practice pin ok"
|
|
259
283
|
exit 0
|
|
260
284
|
fi
|
|
261
285
|
|
|
@@ -268,7 +292,6 @@ CLASS_RAW="$CLASS"
|
|
|
268
292
|
CLASS=$(g_class_canon "$CLASS") \
|
|
269
293
|
|| { g_err "--class must be software|service|game|research|fleet (or A-E / app / agent / desktop), got '$CLASS_RAW'"; exit 2; }
|
|
270
294
|
case "$SKILLS" in yes|no) ;; *) g_err "--skills must be yes or no"; exit 2 ;; esac
|
|
271
|
-
case "$CI_GATE_OPT" in ""|yes|no) ;; *) g_err "--ci-gate must be yes or no, got '$CI_GATE_OPT'"; exit 2 ;; esac
|
|
272
295
|
|
|
273
296
|
# W6 migration safety: on a --upgrade (or any re-install) of a repo whose record shows skills
|
|
274
297
|
# were installed, an OMITTED --skills flag must READ that state, not silently strip it. The
|
|
@@ -306,7 +329,14 @@ preset() {
|
|
|
306
329
|
HARNESS_DIR=$(preset harness_dir); [ -n "$HARNESS_DIR" ] || HARNESS_DIR=checks
|
|
307
330
|
SCAFFOLD=$(preset scaffold_checks); [ -n "$SCAFFOLD" ] || SCAFFOLD=no
|
|
308
331
|
GATE_NAME=$(preset gate_name)
|
|
332
|
+
# (GATE1/GATE2 render names are set in render_config; the preset keys are unchanged.)
|
|
309
333
|
GATE_CMD=$(preset gate_cmd)
|
|
334
|
+
# `--gate <cmd>` (gob init --write): the proposal's first gate REPLACES the class default.
|
|
335
|
+
# `--gate <cmd> <name>` (optional 2nd word): the proposal may also name the gate — the
|
|
336
|
+
# HANDOFF template renders THAT name, so HP-03 (anchored on the DECLARED names) sees the
|
|
337
|
+
# gate line a fresh HANDOFF carries even when the name is not the class default.
|
|
338
|
+
case "${GATE_NAME_ARG:-}" in "") ;; *) GATE_NAME="$GATE_NAME_ARG" ;; esac
|
|
339
|
+
[ -n "${GOB_GATE:-}" ] && GATE_CMD="$GOB_GATE"
|
|
310
340
|
RAT_NAME=$(preset ratchet_name)
|
|
311
341
|
RAT_CMD=$(preset ratchet_cmd)
|
|
312
342
|
RAT_CEIL=$(preset ratchet_ceiling)
|
|
@@ -343,7 +373,7 @@ need() { g_class_need "$CLASSES_TSV" "$CLASS" "$1"; }
|
|
|
343
373
|
|
|
344
374
|
# parts that are OFF for this class become real opt-outs, not silent absences
|
|
345
375
|
DISABLED=""
|
|
346
|
-
for p in handoff spec gate replay ratchet pr-gate review-panel playbooks tokens
|
|
376
|
+
for p in handoff spec gate replay ratchet pr-gate review-panel playbooks tokens; do
|
|
347
377
|
[ "$(need "$p")" = "-" ] && DISABLED="$DISABLED$p,"
|
|
348
378
|
done
|
|
349
379
|
[ "$SKILLS" = "no" ] && DISABLED="$DISABLED""playbooks,"
|
|
@@ -363,18 +393,9 @@ WANT_SPEC=1; [ "$(need spec)" = "-" ] && WANT_SPEC=0
|
|
|
363
393
|
WANT_REVIEWS=0
|
|
364
394
|
case "$(need pr-gate)$(need review-panel)" in *R*|*O*) WANT_REVIEWS=1 ;; esac
|
|
365
395
|
WANT_RATCHET=1; [ "$(need ratchet)" = "-" ] && WANT_RATCHET=0
|
|
366
|
-
# The CI lane
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
# Wizard v2 adds the explicit opt-in/out: the empty default keeps the class-driven shape
|
|
370
|
-
# byte-identical for every existing invocation; `no` opts the part out (recorded in disabled:
|
|
371
|
-
# so CL-01 reports the opt-out rather than a silent absence, the --skills no precedent); `yes`
|
|
372
|
-
# is refused for a class that forbids the part, because the file could never survive verify.
|
|
373
|
-
WANT_CI_GATE=1; [ "$(need ci-gate)" = "-" ] && WANT_CI_GATE=0
|
|
374
|
-
case "$CI_GATE_OPT" in
|
|
375
|
-
no) WANT_CI_GATE=0; OPT_OUT="${OPT_OUT:+$OPT_OUT,}ci-gate" ;;
|
|
376
|
-
yes) [ "$WANT_CI_GATE" -eq 1 ] || { g_err "--ci-gate yes: class $CLASS forbids the ci-gate part - CI is not in this class's contract (see docs/CI.md)"; exit 2; } ;;
|
|
377
|
-
esac
|
|
396
|
+
# The CI lane is GONE (v2 product decision): no .github/workflows file is written, the ci-gate
|
|
397
|
+
# part is recorded as a class-level opt-out, and no flag configures it.
|
|
398
|
+
WANT_CI_GATE=0
|
|
378
399
|
[ "$ARCHIVE" = "true" ] && { WANT_SPEC=0; WANT_REVIEWS=0; SCAFFOLD=no; }
|
|
379
400
|
|
|
380
401
|
OWNER_EMAIL=$( cd "$TARGET" && git config user.email 2>/dev/null )
|
|
@@ -393,7 +414,7 @@ else
|
|
|
393
414
|
# has to say so - `practice: ~` with no line in the log is the "silent no-op" species.
|
|
394
415
|
if [ -n "$PRACTICE_ARG" ]; then
|
|
395
416
|
g_info "warning: --practice $PRACTICE_ARG does not exist - no standard is referenced"
|
|
396
|
-
g_info " (practice: and practice_sha256: stay empty in .
|
|
417
|
+
g_info " (practice: and practice_sha256: stay empty in the AGENTS.md gob block)"
|
|
397
418
|
fi
|
|
398
419
|
PRACTICE=""
|
|
399
420
|
PRACTICE_SHA=""
|
|
@@ -526,16 +547,13 @@ put_once() {
|
|
|
526
547
|
# ------------------------------------------------------- generated content ---
|
|
527
548
|
render_config() {
|
|
528
549
|
local gates runtime
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
fi
|
|
537
|
-
runtime=" - $RUNTIME_DATA"
|
|
538
|
-
render "$SRC/templates/goblin.yaml.tmpl" \
|
|
550
|
+
# v2 gate shape: one flat key per gate (gate_<name>_cmd:). A class may declare a second
|
|
551
|
+
# gate (A: the TODO ceiling that used to be the ratchet, G4 D2); a class with none renders
|
|
552
|
+
# an empty gate name and the key line is dropped below.
|
|
553
|
+
GATE1_NAME="$GATE_NAME"; GATE1_CMD="$GATE_CMD"
|
|
554
|
+
GATE2_RENDER_NAME="$GATE2_NAME"; GATE2_RENDER_CMD="$GATE2_CMD"
|
|
555
|
+
runtime="$RUNTIME_DATA"
|
|
556
|
+
render "$SRC/templates/agents-block.tmpl" \
|
|
539
557
|
CLASS "$CLASS" \
|
|
540
558
|
ELECTRON "$( [ "$ELECTRON" -eq 1 ] && printf 'true' || printf 'false' )" \
|
|
541
559
|
BRANCH "$BRANCH" \
|
|
@@ -566,7 +584,10 @@ render_config() {
|
|
|
566
584
|
REFERENCE_MANIFEST "" \
|
|
567
585
|
QUARANTINE_ROOT "" \
|
|
568
586
|
LOOP_MAX_TURNS "$LOOP_MAX_TURNS" \
|
|
569
|
-
|
|
587
|
+
GATE1_NAME "$GATE1_NAME" \
|
|
588
|
+
GATE1_CMD "$GATE1_CMD" \
|
|
589
|
+
GATE2_NAME "$GATE2_RENDER_NAME" \
|
|
590
|
+
GATE2_CMD "$GATE2_RENDER_CMD" \
|
|
570
591
|
RATCHET_NAME "$RAT_NAME" \
|
|
571
592
|
RATCHET_CMD "$RAT_CMD" \
|
|
572
593
|
RATCHET_CEILING "$RAT_CEIL" \
|
|
@@ -576,7 +597,9 @@ render_config() {
|
|
|
576
597
|
LAYERS "$LAYERS_BLOCK" \
|
|
577
598
|
REPLAY_ENV "$REPLAY_ENV" \
|
|
578
599
|
REPLAY_CMD "$REPLAY_CMD" \
|
|
579
|
-
DISABLED "$DISABLED" > "$TMP/
|
|
600
|
+
DISABLED "$DISABLED" > "$TMP/agents-block.raw"
|
|
601
|
+
# A gate with no name renders a `gate__cmd:` line - dropped, not shipped.
|
|
602
|
+
grep -v '^gate__cmd: $' "$TMP/agents-block.raw" > "$TMP/agents-block"
|
|
580
603
|
}
|
|
581
604
|
|
|
582
605
|
render_config
|
|
@@ -610,33 +633,57 @@ if [ -f "$INSTALLED" ] && [ "$(g_install_engine_mode "$INSTALLED")" = "global" ]
|
|
|
610
633
|
gsub(q, "", v); print v; exit
|
|
611
634
|
}
|
|
612
635
|
' "$INSTALLED")
|
|
613
|
-
g_err "goblin-install: $TARGET is migrated to the global engine at ${g_rec_engine_dir:-<unknown>}. Re-installing would re-shadow it and rewrite its migration record. To return to a vendored engine: git revert the two W3 commits (see docs/GUIDE.md \"Rolling back a migration\"). To re-point at a different engine: edit .
|
|
636
|
+
g_err "goblin-install: $TARGET is migrated to the global engine at ${g_rec_engine_dir:-<unknown>}. Re-installing would re-shadow it and rewrite its migration record. To return to a vendored engine: git revert the two W3 commits (see docs/GUIDE.md \"Rolling back a migration\"). To re-point at a different engine: edit AGENTS.md's engine_dir line."
|
|
614
637
|
exit 1
|
|
615
638
|
fi
|
|
616
639
|
|
|
617
|
-
put ".
|
|
618
|
-
put ".
|
|
640
|
+
put ".gob/bin/goblin-verify" "$SRC/bin/goblin-verify"
|
|
641
|
+
put ".gob/bin/goblin-lib.sh" "$SRC/bin/goblin-lib.sh"
|
|
619
642
|
# The audit record is produced by a DELIBERATE, separate command - SC-07 reads the record and
|
|
620
643
|
# never the network (RISKS.md K4: no network at verify time).
|
|
621
|
-
put ".
|
|
622
|
-
put ".
|
|
623
|
-
put ".
|
|
624
|
-
put ".
|
|
625
|
-
put ".
|
|
626
|
-
put ".
|
|
644
|
+
put ".gob/bin/goblin-audit" "$SRC/bin/goblin-audit"
|
|
645
|
+
put ".gob/roles.yaml" "$SRC/roles.yaml"
|
|
646
|
+
put ".gob/manifest/enforcement.tsv" "$SRC/manifest/enforcement.tsv"
|
|
647
|
+
put ".gob/manifest/playbooks.tsv" "$SRC/manifest/playbooks.tsv"
|
|
648
|
+
put ".gob/manifest/classes.tsv" "$SRC/manifest/classes.tsv"
|
|
649
|
+
put ".gob/manifest/glossary.tsv" "$SRC/manifest/glossary.tsv"
|
|
627
650
|
# The ban list (G5): the table, the engine, and the two probes. A ban with no mechanism is a
|
|
628
651
|
# wish, so the table and the engine install together or not at all.
|
|
629
|
-
put ".
|
|
630
|
-
put ".
|
|
631
|
-
put ".
|
|
632
|
-
put ".
|
|
633
|
-
put ".
|
|
634
|
-
|
|
652
|
+
put ".gob/manifest/bans.tsv" "$SRC/manifest/bans.tsv"
|
|
653
|
+
put ".gob/bin/goblin-bans" "$SRC/bin/goblin-bans"
|
|
654
|
+
put ".gob/bans/README.md" "$SRC/bans/README.md"
|
|
655
|
+
put ".gob/bans/grep-ban.sh" "$SRC/bans/grep-ban.sh"
|
|
656
|
+
put ".gob/bans/layer-check.sh" "$SRC/bans/layer-check.sh"
|
|
657
|
+
# The AGENTS.md frontmatter: the body prose is rendered once (put_once), then the marker
|
|
658
|
+
# block is written into it through g_agents_write — the same path every later rewrite takes,
|
|
659
|
+
# so the block is never a second copy of the file and the body is never machine-owned.
|
|
660
|
+
render "$SRC/templates/AGENTS.md.tmpl" CLASS "$CLASS" DATE "$STAMP" > "$TMP/agents-body"
|
|
661
|
+
put_once "AGENTS.md" "$TMP/agents-body"
|
|
662
|
+
if [ "$DRY_RUN" -eq 0 ] && [ -f "$CONFIG" ]; then
|
|
663
|
+
# The install-once contract for the project's own answers: on a RE-install (the previous
|
|
664
|
+
# record exists), the block rewrite carries forward the existing practice pin instead of the
|
|
665
|
+
# freshly hashed one - an --upgrade must never re-pin the standard by itself, because a pin
|
|
666
|
+
# that silently follows the file defeats the only check that guards it (IN-02). A FIRST
|
|
667
|
+
# install (no previous record) has nothing to carry forward: the rendered value is the pin.
|
|
668
|
+
# The block file here is the RENDERED template ("key: value" lines, no tabs), so the match
|
|
669
|
+
# anchors on the key prefix, not on a tab field ($1 == "practice" never fired under the
|
|
670
|
+
# rendered shape and the freshly rendered NEW hash flowed straight through — an --upgrade
|
|
671
|
+
# re-pinned the standard silently, exactly what IN-02 exists to catch).
|
|
672
|
+
if [ -n "$PREV_FILES" ]; then
|
|
673
|
+
KEEP_PRACTICE=$(g_agents_read "$CONFIG" practice)
|
|
674
|
+
KEEP_PRACTICE_SHA=$(g_agents_read "$CONFIG" practice_sha256)
|
|
675
|
+
if [ -n "$KEEP_PRACTICE" ]; then
|
|
676
|
+
sed -i "s#^practice: .*\$#practice: $KEEP_PRACTICE#; s#^practice_sha256: .*\$#practice_sha256: $KEEP_PRACTICE_SHA#" "$TMP/agents-block"
|
|
677
|
+
fi
|
|
678
|
+
fi
|
|
679
|
+
g_agents_write "$CONFIG" < "$TMP/agents-block" >/dev/null \
|
|
680
|
+
|| { g_err "could not write the gob block into $CONFIG"; exit 1; }
|
|
681
|
+
fi
|
|
635
682
|
# The security lane's two decision records and the audit allowlist are created once, then YOURS:
|
|
636
683
|
# they are answers to questions only the project can answer (G4 B3).
|
|
637
|
-
[ -f "$SRC/templates/audit-waiver.tsv.tmpl" ] && put_once ".
|
|
638
|
-
[ -f "$SRC/templates/boundary-waivers.tmpl" ] && put_once ".
|
|
639
|
-
[ -f "$SRC/templates/install-hooks.allowlist.tmpl" ] && put_once ".
|
|
684
|
+
[ -f "$SRC/templates/audit-waiver.tsv.tmpl" ] && put_once ".gob/audit-waiver.tsv" "$SRC/templates/audit-waiver.tsv.tmpl"
|
|
685
|
+
[ -f "$SRC/templates/boundary-waivers.tmpl" ] && put_once ".gob/boundary-waivers" "$SRC/templates/boundary-waivers.tmpl"
|
|
686
|
+
[ -f "$SRC/templates/install-hooks.allowlist.tmpl" ] && put_once ".gob/install-hooks.allowlist" "$SRC/templates/install-hooks.allowlist.tmpl"
|
|
640
687
|
|
|
641
688
|
if [ "$SKILLS" = "yes" ] && part_wanted playbooks; then
|
|
642
689
|
for d in "$SRC"/skills/*/; do
|
|
@@ -654,7 +701,7 @@ if [ "$SKILLS_NAMED" -eq 1 ] && [ "$SKILLS" = "no" ] && [ -n "$PREV_FILES" ]; th
|
|
|
654
701
|
while IFS= read -r rel; do
|
|
655
702
|
case "$rel" in .hermes/skills/*) ;; *) continue ;; esac
|
|
656
703
|
dst="$TARGET/$rel"
|
|
657
|
-
[ -f "$dst" ] || { rm -f "$TARGET/.
|
|
704
|
+
[ -f "$dst" ] || { rm -f "$TARGET/.gob/$(basename "$rel")" 2>/dev/null; continue; }
|
|
658
705
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
659
706
|
g_info "plan: remove $rel (explicit --skills no)"
|
|
660
707
|
else
|
|
@@ -677,10 +724,10 @@ if [ "$PLAYBOOKS_ON" -eq 1 ] && part_wanted playbooks; then
|
|
|
677
724
|
for f in "$SRC"/automations/drift-audit.sh "$SRC"/automations/bugreporter-intake.sh \
|
|
678
725
|
"$SRC"/automations/README.md "$SRC"/automations/report.schema.tsv; do
|
|
679
726
|
[ -f "$f" ] || continue
|
|
680
|
-
put ".
|
|
727
|
+
put ".gob/automations/$(basename "$f")" "$f"
|
|
681
728
|
done
|
|
682
729
|
if [ -f "$SRC/templates/report.yaml.tmpl" ]; then
|
|
683
|
-
put ".
|
|
730
|
+
put ".gob/automations/report.yaml.tmpl" "$SRC/templates/report.yaml.tmpl"
|
|
684
731
|
fi
|
|
685
732
|
fi
|
|
686
733
|
|
|
@@ -689,26 +736,25 @@ if [ "$WANT_SPEC" -eq 1 ] && part_wanted spec; then
|
|
|
689
736
|
put_once "ROUND-000-SPEC.md" "$TMP/spec"
|
|
690
737
|
fi
|
|
691
738
|
if [ "$ARCHIVE" != "true" ] && part_wanted handoff; then
|
|
692
|
-
|
|
739
|
+
# The HANDOFF names the gate the way GT-02 runs it: by the command's own name key
|
|
740
|
+
# (gate_<name>_cmd), so HP-03 anchors on a declared name even when --gate replaced the
|
|
741
|
+
# class default command. GATE2_LINE renders the class's second gate (if any) the same
|
|
742
|
+
# way — a Gates section that names only gate 1 while AGENTS.md declares two is the
|
|
743
|
+
# HP-03 day-one anchoring red. A class with no second gate renders an empty line.
|
|
744
|
+
if [ -n "$GATE2_NAME" ]; then
|
|
745
|
+
GATE2_LINE="- \`$GATE2_NAME\` = 0 · measured $STAMP"
|
|
746
|
+
else
|
|
747
|
+
GATE2_LINE=""
|
|
748
|
+
fi
|
|
749
|
+
render "$SRC/templates/HANDOFF.md.tmpl" CLASS "$CLASS" DATE "$STAMP" HEAD "$HEAD_SHORT" BRANCH "$BRANCH" GATE_NAME "$GATE_NAME" GATE_CMD "$GATE_CMD" GATE2_LINE "$GATE2_LINE" > "$TMP/handoff"
|
|
693
750
|
put_once "HANDOFF.md" "$TMP/handoff"
|
|
694
751
|
fi
|
|
695
|
-
render "$SRC/templates/AGENTS.md.tmpl" CLASS "$CLASS" DATE "$STAMP" > "$TMP/agents"
|
|
696
|
-
put_once "AGENTS.md" "$TMP/agents"
|
|
697
752
|
|
|
698
753
|
if [ "$WANT_REVIEWS" -eq 1 ]; then
|
|
699
754
|
printf '# Review verdicts live here: reviews/<slug>-<head7>.md, each naming the SHA it reviewed.\n' > "$TMP/gitkeep"
|
|
700
755
|
put_once "reviews/.gitkeep" "$TMP/gitkeep"
|
|
701
756
|
fi
|
|
702
757
|
|
|
703
|
-
# The CI lane (G6): one workflow, rendered and placed. The header of the template states what
|
|
704
|
-
# makes a workflow a GATE (a required check, no admin bypass, a push identity that is not the
|
|
705
|
-
# repo's sole admin) - because a workflow file nobody is bound by is decoration. The job carries
|
|
706
|
-
# no `if:` at any level, so PG-05 passes on goblin-stack's own output.
|
|
707
|
-
if [ "$WANT_CI_GATE" -eq 1 ] && part_wanted ci-gate && [ -f "$SRC/templates/ci/goblin-gate.yml.tmpl" ]; then
|
|
708
|
-
render "$SRC/templates/ci/goblin-gate.yml.tmpl" CLASS "$CLASS" BRANCH "$BRANCH" DATE "$STAMP" > "$TMP/gate.yml"
|
|
709
|
-
put ".github/workflows/goblin-gate.yml" "$TMP/gate.yml"
|
|
710
|
-
fi
|
|
711
|
-
|
|
712
758
|
if [ "$SCAFFOLD" = "yes" ]; then
|
|
713
759
|
put "$HARNESS_DIR/gate.sh" "$SRC/templates/checks/gate.sh.tmpl"
|
|
714
760
|
put "$HARNESS_DIR/assert.mjs" "$SRC/templates/checks/assert.mjs.tmpl"
|
|
@@ -725,7 +771,7 @@ else
|
|
|
725
771
|
[ -f "$TARGET/.gitignore" ] && cat "$TARGET/.gitignore"
|
|
726
772
|
printf '\n%s\n' "$GI_MARK"
|
|
727
773
|
printf '# goblin-stack runtime state (gates write these; they are not fixture)\n'
|
|
728
|
-
printf '.
|
|
774
|
+
printf '.gob/last-gate-line\n.gob/.ds-report\n.gob/ratchet-last\n'
|
|
729
775
|
if [ "$SEC_GITIGNORE_FAMILY" = "yes" ]; then
|
|
730
776
|
# SC-02: the whole secret family, not one member of it. The row is behavioural - it asks
|
|
731
777
|
# git's own matcher (`git check-ignore`) rather than reading this file - so a rule that
|
|
@@ -751,7 +797,7 @@ fi
|
|
|
751
797
|
printf ' "source_sha": "%s",\n' "$( cd "$SRC" && git rev-parse HEAD 2>/dev/null || echo unknown )"
|
|
752
798
|
printf ' "class": "%s",\n' "$CLASS"
|
|
753
799
|
printf ' "installed_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
754
|
-
printf ' "options": {"skills": "%s", "archive": %s, "parts": "%s"
|
|
800
|
+
printf ' "options": {"skills": "%s", "archive": %s, "parts": "%s"},\n' "$SKILLS" "$ARCHIVE" "${PARTS:-all}"
|
|
755
801
|
printf ' "files": {\n'
|
|
756
802
|
awk -F'\t' 'NF==2 {printf " \"%s\": \"%s\",\n", $1, $2}' "$TMP/files.tsv" | sed '$ s/,$//'
|
|
757
803
|
printf ' },\n'
|
|
@@ -776,7 +822,6 @@ fi
|
|
|
776
822
|
|
|
777
823
|
|
|
778
824
|
if [ "$DRY_RUN" -eq 0 ]; then
|
|
779
|
-
mkdir -p "$TARGET/.goblin"
|
|
780
825
|
if [ "$CREATED" -eq 0 ] && [ "$UPDATED" -eq 0 ] && [ "$PREV_VERSION" = "$VERSION" ] \
|
|
781
826
|
&& [ "$(printf '%s' "$RECORD_SKILLS_EFFECTIVE" )" = "$(printf '%s' "$SKILLS")" ]; then
|
|
782
827
|
g_info "no-op: $UNCHANGED files unchanged (v$VERSION already installed)"
|
|
@@ -787,22 +832,23 @@ if [ "$DRY_RUN" -eq 0 ]; then
|
|
|
787
832
|
fi
|
|
788
833
|
g_info "created $CREATED · updated $UPDATED · unchanged $UNCHANGED · skipped $SKIPPED"
|
|
789
834
|
fi
|
|
790
|
-
if [ -n "$OPT_OUT" ]; then
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
835
|
+
if [ -n "$OPT_OUT" ] && grep -q "^$GOB_AGENTS_BEGIN" "$CONFIG" 2>/dev/null; then
|
|
836
|
+
cur=$(g_agents_read "$CONFIG" disabled)
|
|
837
|
+
new=$(printf '%s,%s' "${cur#[]}" "${OPT_OUT}" | tr ',' '\n' | grep -v '^$' | sort -u | paste -sd, -)
|
|
838
|
+
{
|
|
839
|
+
while IFS=$'\t' read -r k v; do
|
|
840
|
+
[ "$k" = "disabled" ] && continue
|
|
841
|
+
printf '%s\t%s\n' "$k" "$v"
|
|
842
|
+
done < <(g_agents_pairs "$CONFIG")
|
|
843
|
+
printf 'disabled\t[%s]\n' "$new"
|
|
844
|
+
} | g_agents_write "$CONFIG" >/dev/null
|
|
799
845
|
g_info "recorded opt-out: $OPT_OUT"
|
|
800
846
|
fi
|
|
801
847
|
g_info ""
|
|
802
848
|
g_info "next:"
|
|
803
849
|
g_info " 1. cd $TARGET && git add -A && git commit # the install is a change like any other"
|
|
804
|
-
g_info " 2. .
|
|
805
|
-
g_info " 3. edit .
|
|
850
|
+
g_info " 2. .gob/bin/goblin-verify # or add .gob/bin to PATH"
|
|
851
|
+
g_info " 3. edit AGENTS.md: replace the default gate with your real commands (P8 step 3)"
|
|
806
852
|
if [ "$SKILLS" = "yes" ]; then
|
|
807
853
|
g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
|
|
808
854
|
else
|
|
@@ -810,8 +856,8 @@ if [ "$DRY_RUN" -eq 0 ]; then
|
|
|
810
856
|
fi
|
|
811
857
|
g_info ""
|
|
812
858
|
g_info "automations (optional; neither writes outside this repo, and A-02 has no agent in it):"
|
|
813
|
-
g_info " bash $TARGET/.
|
|
814
|
-
g_info " cp $TARGET/.
|
|
859
|
+
g_info " bash $TARGET/.gob/automations/drift-audit.sh --dry-run # read the record first"
|
|
860
|
+
g_info " cp $TARGET/.gob/automations/drift-audit.sh ~/.hermes/scripts/"
|
|
815
861
|
g_info " hermes cron create \"0 9 * * 1\" --name \"Drift audit - weekly\" --no-agent \\"
|
|
816
862
|
g_info " --script drift-audit.sh --deliver <channel>"
|
|
817
863
|
g_info " P13/P14 in docs/FLOWS.md; the whole model in automations/README.md"
|