@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +112 -124
  3. package/VERSION +1 -1
  4. package/automations/drift-audit.sh +4 -4
  5. package/bans/layer-check.sh +10 -8
  6. package/bin/goblin +61 -58
  7. package/bin/goblin-audit +11 -13
  8. package/bin/goblin-bans +11 -11
  9. package/bin/goblin-init +275 -713
  10. package/bin/goblin-install +160 -114
  11. package/bin/goblin-lib.sh +234 -1
  12. package/bin/goblin-map +226 -21
  13. package/bin/goblin-mcp.js +492 -0
  14. package/bin/goblin-model +4 -4
  15. package/bin/goblin-upgrade +1 -1
  16. package/bin/goblin-verify +159 -145
  17. package/bin/goblin.js +33 -51
  18. package/docs/ADOPTION.md +15 -15
  19. package/docs/CONTRACTS.md +16 -15
  20. package/docs/DESIGN.md +1 -1
  21. package/docs/ENFORCEMENT.md +89 -90
  22. package/docs/FLOWS.md +1 -1
  23. package/docs/GLOSSARY.md +3 -3
  24. package/docs/GUARDRAILS.md +5 -5
  25. package/docs/GUIDE.md +167 -177
  26. package/docs/INTEGRATION.md +1 -1
  27. package/docs/LIMITS.md +25 -0
  28. package/docs/LOOP.md +12 -12
  29. package/docs/RE-PLAYBOOK.md +3 -3
  30. package/docs/ROLES.md +5 -5
  31. package/manifest/bans.tsv +8 -8
  32. package/manifest/classes.tsv +3 -3
  33. package/manifest/enforcement.tsv +40 -40
  34. package/manifest/glossary.tsv +3 -3
  35. package/manifest/playbooks.tsv +1 -1
  36. package/package.json +1 -1
  37. package/presets/electron-overlay.yaml +2 -2
  38. package/presets/fleet.yaml +8 -7
  39. package/presets/game.yaml +1 -1
  40. package/presets/research.yaml +1 -1
  41. package/presets/service.yaml +1 -1
  42. package/presets/software.yaml +1 -1
  43. package/skills/goblin-bootstrap/SKILL.md +2 -2
  44. package/templates/AGENTS.md.tmpl +8 -18
  45. package/templates/HANDOFF.md.tmpl +5 -5
  46. package/templates/agents-block.tmpl +45 -0
  47. package/templates/audit-waiver.tsv.tmpl +2 -2
  48. package/templates/boundary-waivers.tmpl +1 -1
  49. package/templates/checks/gate.sh.tmpl +6 -6
  50. package/templates/install-hooks.allowlist.tmpl +1 -1
  51. package/templates/ci/goblin-gate.yml.tmpl +0 -46
  52. package/templates/goblin.yaml.tmpl +0 -146
  53. package/templates/loop/decisions.tsv.tmpl +0 -1
  54. package/templates/loop/predicate.tmpl +0 -16
package/bin/goblin-verify CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  set -uo pipefail
21
21
 
22
- GOBLIN_VERIFY_VERSION="0.5.0"
22
+ GOBLIN_VERIFY_VERSION="0.6.0-alpha.1"
23
23
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
24
24
  # shellcheck source=goblin-lib.sh
25
25
  . "$SELF_DIR/goblin-lib.sh"
@@ -42,7 +42,7 @@ goblin-verify — prove a goblin-stack install is still what it says it is.
42
42
  to a count; FAIL and ADV always print in full)
43
43
  --list list the manifest rows and exit
44
44
  --json machine-readable output
45
- --source <path> read the manifest from a goblin-stack checkout instead of .goblin/
45
+ --source <path> read the manifest from a goblin-stack checkout instead of .gob/
46
46
 
47
47
  Exit codes: 0 pass | 1 a check failed | 2 could not run | 3 the manifest is broken.
48
48
  USAGE
@@ -71,7 +71,7 @@ done
71
71
  find_installed_root() {
72
72
  local d="$PWD"
73
73
  while :; do
74
- if [ -f "$d/.goblin/goblin.yaml" ]; then printf '%s\n' "$d"; return 0; fi
74
+ if [ -f "$d/AGENTS.md" ] && grep -q "^$GOB_AGENTS_BEGIN" "$d/AGENTS.md" 2>/dev/null; then printf '%s\n' "$d"; return 0; fi
75
75
  [ "$d" = "/" ] && return 1
76
76
  d=$(dirname "$d")
77
77
  done
@@ -86,8 +86,8 @@ fi
86
86
  # Every engine-path lookup (enforcement.tsv, classes.tsv, bans.tsv, bans/*.sh, the ban
87
87
  # engine) resolves through this chain, first hit wins:
88
88
  # 1. --source <path> (existing flag; highest, for tests and pinned CI)
89
- # 2. $ROOT/.goblin/... (vendored per-repo engine — today's default)
90
- # 3. engine_dir: from $ROOT/.goblin/goblin.yaml
89
+ # 2. $ROOT/.gob/... (vendored per-repo engine — today's default)
90
+ # 3. engine_dir: from $ROOT/AGENTS.md
91
91
  # 4. GOBLIN_ENGINE_DIR env, then ~/.goblin/engine
92
92
  # 5. nothing found -> exit 2, naming every path tried (the pre-W1 message when the
93
93
  # vendored chain is the only one tried; the §2.4 message when engine_dir: fails).
@@ -97,12 +97,13 @@ fi
97
97
  # back to a vendored engine would judge the repo with a different rule table than the one
98
98
  # it declared. Absent key => the chain degrades to exactly today's behaviour (§1.3).
99
99
 
100
- CONFIG="$ROOT/.goblin/goblin.yaml"
100
+ CONFIG="$ROOT/AGENTS.md"
101
101
 
102
- # engine_yaml_raw — the raw top-level engine_dir: value (the line-oriented parser reads
103
- # the line, so a non-string scalar is visible here as something that is not a path).
102
+ # engine_yaml_raw — the raw engine_dir: value from the AGENTS.md frontmatter (the
103
+ # line-oriented parser reads the line, so a non-string scalar is visible here as
104
+ # something that is not a path).
104
105
  engine_yaml_raw() {
105
- sed -n "s/^engine_dir:[[:space:]]*//p" "$CONFIG" | head -n 1
106
+ g_agents_read "$CONFIG" engine_dir
106
107
  }
107
108
 
108
109
  # resolve_engine_dir — validate the declared key (§1.2, fail closed at use time, stages
@@ -118,7 +119,7 @@ resolve_engine_dir() {
118
119
  ENGINE_FAIL=""
119
120
  ENGINE_DIR_DECLARED=""
120
121
  [ -f "$CONFIG" ] || return 0
121
- grep -q '^engine_dir:' "$CONFIG" || return 0
122
+ [ -n "$(g_agents_keys "$CONFIG" | grep -qx engine_dir && echo 1)" ] || return 0
122
123
  local dir
123
124
  dir=$(engine_yaml_raw)
124
125
  if [ -z "$dir" ]; then
@@ -146,7 +147,7 @@ resolve_engine_dir() {
146
147
  esac
147
148
  # Stage 3: existence.
148
149
  if [ ! -d "$dir" ]; then
149
- ENGINE_FAIL="engine_dir: $dir does not exist (declared in .goblin/goblin.yaml)"
150
+ ENGINE_FAIL="engine_dir: $dir does not exist (declared in AGENTS.md)"
150
151
  return 1
151
152
  fi
152
153
  # Stage 4: the payload — a directory without the rule table is not an engine.
@@ -159,14 +160,14 @@ resolve_engine_dir() {
159
160
  }
160
161
 
161
162
  resolve_engine_dir
162
- ENGINE_CHAIN_VENDORED="\$ROOT/.goblin/manifest/enforcement.tsv"
163
+ ENGINE_CHAIN_VENDORED="\$ROOT/.gob/manifest/enforcement.tsv"
163
164
  GOBLIN_ENGINE="" # the resolved engine root, when NOT vendored/--source
164
165
  if [ -n "$SOURCE" ]; then
165
166
  MANIFEST="$SOURCE/manifest/enforcement.tsv"
166
167
  CLASSES="$SOURCE/manifest/classes.tsv"
167
- elif [ -f "$ROOT/.goblin/manifest/enforcement.tsv" ]; then
168
- MANIFEST="$ROOT/.goblin/manifest/enforcement.tsv"
169
- CLASSES="$ROOT/.goblin/manifest/classes.tsv"
168
+ elif [ -f "$ROOT/.gob/manifest/enforcement.tsv" ]; then
169
+ MANIFEST="$ROOT/.gob/manifest/enforcement.tsv"
170
+ CLASSES="$ROOT/.gob/manifest/classes.tsv"
170
171
  elif [ -n "$ENGINE_DECLARED" ]; then
171
172
  GOBLIN_ENGINE="$ENGINE_DECLARED"
172
173
  MANIFEST="$GOBLIN_ENGINE/manifest/enforcement.tsv"
@@ -175,7 +176,7 @@ elif [ -n "$ENGINE_FAIL" ]; then
175
176
  # §2.4: an explicit engine_dir: that does not resolve is exit 2, and nothing falls
176
177
  # back. The refusal names the declared value and every path the chain tried.
177
178
  g_err "$ENGINE_FAIL"
178
- g_err "no manifest at any of: \$SOURCE (${SOURCE:-unset}), $ROOT/.goblin/manifest/enforcement.tsv (absent),"
179
+ g_err "no manifest at any of: \$SOURCE (${SOURCE:-unset}), $ROOT/.gob/manifest/enforcement.tsv (absent),"
179
180
  g_err " $ENGINE_DIR_DECLARED/manifest/enforcement.tsv (unusable - see above)"
180
181
  exit 2
181
182
  else
@@ -186,8 +187,8 @@ else
186
187
  _cli_default=""
187
188
  if [ -n "${GOBLIN_ENGINE_DIR:-}" ]; then
188
189
  _cli_default="$GOBLIN_ENGINE_DIR"
189
- elif [ -d "$HOME/.goblin/engine" ]; then
190
- _cli_default="$HOME/.goblin/engine"
190
+ elif [ -d "$HOME/.gob/engine" ]; then
191
+ _cli_default="$HOME/.gob/engine"
191
192
  fi
192
193
  if [ -n "$_cli_default" ] && [ -f "$_cli_default/manifest/enforcement.tsv" ]; then
193
194
  GOBLIN_ENGINE="$_cli_default"
@@ -204,7 +205,7 @@ fi
204
205
  exit 2
205
206
  }
206
207
 
207
- INSTALLED="$ROOT/.goblin/installed.json"
208
+ INSTALLED="$ROOT/.gob/installed.json"
208
209
 
209
210
  # engine_block_field <key> — one field of installed.json's engine: block (empty if absent).
210
211
  engine_block_field() {
@@ -282,11 +283,11 @@ if [ ! -f "$CONFIG" ]; then
282
283
  exit 2
283
284
  fi
284
285
 
285
- # The class is read-time resolved: goblin.yaml records the canonical name on new installs, but
286
+ # The class is read-time resolved: the gob block records the canonical name on new installs, but
286
287
  # every repo installed before the 5-class merge records a letter (A..F) or an old name
287
288
  # (app/agent/desktop). g_class_canon maps them all to the five canonical names; the tsv carries
288
289
  # the canonical names, so nothing else in this file has to know about letters. `desktop`/F -> software.
289
- CLASS_RAW=$(g_yaml_scalar "$CONFIG" class)
290
+ CLASS_RAW=$(g_agents_read "$CONFIG" class)
290
291
  CLASS=$(g_class_canon "$CLASS_RAW") \
291
292
  || { g_err "unknown class '${CLASS_RAW:-<empty>}' in $CONFIG (expected software|service|game|research|fleet, or A-E / app / agent / desktop)"; exit 2; }
292
293
 
@@ -307,15 +308,15 @@ if [ -n "$GIT_TOP" ] && [ "$GIT_TOP" != "$ROOT_PHYS" ]; then
307
308
  exit 2
308
309
  fi
309
310
 
310
- BRANCH=$(g_yaml_scalar "$CONFIG" branch)
311
- ARCHIVE=$(g_yaml_scalar "$CONFIG" archive)
312
- MAX_DIRTY=$(g_yaml_scalar "$CONFIG" max_dirty)
313
- ADV_CEILING=$(g_yaml_scalar "$CONFIG" advisory_ceiling)
314
- HARNESS_DIR_RAW=$(g_yaml_scalar "$CONFIG" harness_dir)
315
- SCAFFOLD_CHECKS=$(g_yaml_scalar "$CONFIG" scaffold_checks)
316
- MODELS_FILE=$(g_expand_tilde "$(g_yaml_scalar "$CONFIG" models_file)")
317
- PRACTICE=$(g_expand_tilde "$(g_yaml_scalar "$CONFIG" practice)")
318
- PRACTICE_SHA=$(g_unquote "$(g_yaml_scalar "$CONFIG" practice_sha256)")
311
+ BRANCH=$(g_agents_read "$CONFIG" branch)
312
+ ARCHIVE=$(g_agents_read "$CONFIG" archive)
313
+ MAX_DIRTY=$(g_agents_read "$CONFIG" max_dirty)
314
+ ADV_CEILING=$(g_agents_read "$CONFIG" advisory_ceiling)
315
+ HARNESS_DIR_RAW=$(g_agents_read "$CONFIG" harness_dir)
316
+ SCAFFOLD_CHECKS=$(g_agents_read "$CONFIG" scaffold_checks)
317
+ MODELS_FILE=$(g_expand_tilde "$(g_agents_read "$CONFIG" models_file)")
318
+ PRACTICE=$(g_expand_tilde "$(g_agents_read "$CONFIG" practice)")
319
+ PRACTICE_SHA=$(g_unquote "$(g_agents_read "$CONFIG" practice_sha256)")
319
320
  [ -n "$MAX_DIRTY" ] || MAX_DIRTY=0
320
321
  [ -n "$ADV_CEILING" ] || ADV_CEILING=10
321
322
  [ -n "$HARNESS_DIR_RAW" ] || HARNESS_DIR_RAW=checks
@@ -326,10 +327,10 @@ PRACTICE_SHA=$(g_unquote "$(g_yaml_scalar "$CONFIG" practice_sha256)")
326
327
  # The feature map (G1). feature_map: EMPTY is the meaningful state, not a broken config: a
327
328
  # fresh install has no map and the two FM rows must SKIP with that reason (the replay.commit:
328
329
  # "" shape, docs/LIMITS.md #11). source_root: is what an entry path resolves against.
329
- FEATURE_MAP=$(g_yaml_scalar "$CONFIG" feature_map)
330
- SOURCE_ROOT=$(g_unquote "$(g_yaml_scalar "$CONFIG" source_root)")
330
+ FEATURE_MAP=$(g_agents_read "$CONFIG" feature_map)
331
+ SOURCE_ROOT=$(g_unquote "$(g_agents_read "$CONFIG" source_root)")
331
332
  [ -n "$SOURCE_ROOT" ] || SOURCE_ROOT=.
332
- VERIFY_DOCTOR=$(g_unquote "$(g_yaml_scalar "$CONFIG" verify_doctor)")
333
+ VERIFY_DOCTOR=$(g_unquote "$(g_agents_read "$CONFIG" verify_doctor)")
333
334
  export FEATURE_MAP SOURCE_ROOT VERIFY_DOCTOR
334
335
 
335
336
  # The reference corpus (P15, RC-01..RC-04). `reference_manifest:` is the ONE state that may SKIP
@@ -342,19 +343,19 @@ export FEATURE_MAP SOURCE_ROOT VERIFY_DOCTOR
342
343
  # `quarantine_root:` is the quarantine directory of the playbook's S5 - outside the repo by
343
344
  # design - and no row reads it; it is declared so the procedure has a machine-readable anchor
344
345
  # instead of a path in prose (it ships empty here, the way feature_map: does).
345
- REFERENCE_MANIFEST=$(g_unquote "$(g_yaml_scalar "$CONFIG" reference_manifest)")
346
+ REFERENCE_MANIFEST=$(g_unquote "$(g_agents_read "$CONFIG" reference_manifest)")
346
347
  export REFERENCE_MANIFEST
347
348
 
348
349
  # The loop ceiling (G2, LP-03). The most turns ONE loop record may declare in its budget; 20 is
349
350
  # the engine's own default for a kanban goal loop, so a ceiling that disagreed with the engine
350
351
  # would be a number someone made up. A config written before this key existed keeps the default
351
352
  # (the SCAFFOLD_CHECKS shape): an absent key is not a broken config.
352
- LOOP_MAX_TURNS_CEILING=$(g_yaml_scalar "$CONFIG" loop_max_turns_ceiling)
353
+ LOOP_MAX_TURNS_CEILING=$(g_agents_read "$CONFIG" loop_max_turns_ceiling)
353
354
  [ -n "$LOOP_MAX_TURNS_CEILING" ] || LOOP_MAX_TURNS_CEILING=20
354
355
  # The loop record (G2): a COMMITTED artifact of one unattended run. Nothing in this section
355
356
  # re-runs the predicate and nothing writes: the record IS the evidence, and what that costs is
356
357
  # stated in docs/LOOP.md and docs/LIMITS.md.
357
- LOOP_DIR="$ROOT/.goblin/loop"
358
+ LOOP_DIR="$ROOT/.gob/loop"
358
359
  export LOOP_MAX_TURNS_CEILING LOOP_DIR
359
360
 
360
361
  if [ -d "$ROOT/$HARNESS_DIR_RAW" ]; then
@@ -387,27 +388,27 @@ export GOBLIN_ROOT="$ROOT"
387
388
  export GOBLIN_CONFIG="$CONFIG"
388
389
 
389
390
  # ---- the security surface (SC-01..SC-09) and the perf baseline (PF-01) -----------------------
390
- # Every one of these is DECLARED in .goblin/goblin.yaml. A stack-specific rule guessed from the
391
+ # Every one of these is DECLARED in .gob/goblin.yaml. A stack-specific rule guessed from the
391
392
  # files on disk is how a matrix starts lying; a declared key that is absent is a skip with a
392
393
  # reason, never a silent pass.
393
- SEC_GITIGNORE_FAMILY=$(g_yaml_block_scalar "$CONFIG" security gitignore_family)
394
- SEC_BUILD_OUTPUT=$(g_yaml_block_scalar "$CONFIG" security build_output)
395
- SEC_AUDIT_CMD=$(g_yaml_block_scalar "$CONFIG" security audit_cmd)
396
- SEC_AUDIT_MAX_AGE=$(g_yaml_block_scalar "$CONFIG" security audit_max_age_days)
397
- SEC_WAIVER_MAX_AGE=$(g_yaml_block_scalar "$CONFIG" security waiver_max_age_days)
398
- SEC_WRITE_ROUTES=$(g_yaml_block_scalar "$CONFIG" security write_routes)
394
+ SEC_GITIGNORE_FAMILY=$(g_agents_read "$CONFIG" security.gitignore_family)
395
+ SEC_BUILD_OUTPUT=$(g_agents_read "$CONFIG" security.build_output)
396
+ SEC_AUDIT_CMD=$(g_agents_read "$CONFIG" security.audit_cmd)
397
+ SEC_AUDIT_MAX_AGE=$(g_agents_read "$CONFIG" security.audit_max_age_days)
398
+ SEC_WAIVER_MAX_AGE=$(g_agents_read "$CONFIG" security.waiver_max_age_days)
399
+ SEC_WRITE_ROUTES=$(g_agents_read "$CONFIG" security.write_routes)
399
400
  [ -n "$SEC_AUDIT_MAX_AGE" ] || SEC_AUDIT_MAX_AGE=90
400
401
  [ -n "$SEC_WAIVER_MAX_AGE" ] || SEC_WAIVER_MAX_AGE=180
401
- PERF_METRIC=$(g_yaml_block_scalar "$CONFIG" perf metric)
402
- PERF_CMD=$(g_yaml_block_scalar "$CONFIG" perf cmd)
403
- PERF_BASELINE_COMMIT=$(g_unquote "$(g_yaml_block_scalar "$CONFIG" perf baseline_commit)")
404
- PERF_BASELINE_VALUE=$(g_yaml_block_scalar "$CONFIG" perf baseline_value)
405
- PERF_MEASURED=$(g_unquote "$(g_yaml_block_scalar "$CONFIG" perf measured)")
406
- PERF_HOST_GATE=$(g_yaml_block_scalar "$CONFIG" perf host_gate)
402
+ PERF_METRIC=$(g_agents_read "$CONFIG" perf.metric)
403
+ PERF_CMD=$(g_agents_read "$CONFIG" perf.cmd)
404
+ PERF_BASELINE_COMMIT=$(g_unquote "$(g_agents_read "$CONFIG" perf.baseline_commit)")
405
+ PERF_BASELINE_VALUE=$(g_agents_read "$CONFIG" perf.baseline_value)
406
+ PERF_MEASURED=$(g_unquote "$(g_agents_read "$CONFIG" perf.measured)")
407
+ PERF_HOST_GATE=$(g_agents_read "$CONFIG" perf.host_gate)
407
408
  # The electron opt-in (software class): true means the electron ban set stays on regardless of a
408
409
  # hand-edited bans: list, and a perf host gate must be declared (PF-01). Absent on a repo
409
410
  # installed before the merge — read as off, so a legacy class-F repo verifies with no rewrite.
410
- ELECTRON=$(g_yaml_scalar "$CONFIG" electron)
411
+ ELECTRON=$(g_agents_read "$CONFIG" electron)
411
412
  export SEC_GITIGNORE_FAMILY SEC_BUILD_OUTPUT SEC_AUDIT_CMD SEC_AUDIT_MAX_AGE SEC_WAIVER_MAX_AGE
412
413
  export SEC_WRITE_ROUTES PERF_METRIC PERF_CMD PERF_BASELINE_VALUE PERF_HOST_GATE ELECTRON
413
414
 
@@ -421,14 +422,14 @@ age_days() {
421
422
  printf '%s' "$(( (now_s - then_s) / 86400 ))"
422
423
  }
423
424
 
424
- # The source files a secret-shaped search should look at. The vendored harness (.goblin,
425
+ # The source files a secret-shaped search should look at. The vendored harness (.gob,
425
426
  # .hermes), the build output and dependencies are excluded: SC-03 must not match the row text
426
427
  # that describes it.
427
428
  sc_sources() {
428
429
  ( cd "$ROOT" && grep -rniE "$1" \
429
430
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
430
431
  --include='*.mjs' --include='*.cjs' --include='*.vue' --include='*.svelte' . 2>/dev/null \
431
- | grep -vE '(^|/)(\.next|node_modules|\.goblin|\.hermes|dist|build|out|coverage)/' || true )
432
+ | grep -vE '(^|/)(\.next|node_modules|\.gob|\.hermes|dist|build|out|coverage)/' || true )
432
433
  }
433
434
 
434
435
  only_selected() {
@@ -469,7 +470,7 @@ row_part() {
469
470
  # Each builtin prints its detail and returns 0 pass | 1 fail | 2 advisory | 3 skip.
470
471
 
471
472
  # The install-record rows (W1 §2.5). IN-01 and IN-04 were shell one-liners over
472
- # .goblin/installed.json; the engine.mode=global clause needs engine logic (a global,
473
+ # .gob/installed.json; the engine.mode=global clause needs engine logic (a global,
473
474
  # declaration-only repo has NO install record and must SKIP with the reason, never be
474
475
  # born RED - the D8/FM-01 shape), so the one-liners move into builtins that keep the
475
476
  # same clauses and gain the mode check. In vendored mode they are exactly the old cells.
@@ -483,7 +484,7 @@ check_in_01() {
483
484
  # IN-01 recovery line (review 1, scope 4): the record is corrupt or missing; name the two
484
485
  # restores. The matrix's remedy: line rides under the payload via g_fail.
485
486
  printf 'installed.json is absent or records no version\n'
486
- printf 'recover: gob install --upgrade, or git checkout -- .goblin/installed.json\n'
487
+ printf 'recover: gob install --upgrade, or git checkout -- .gob/installed.json\n'
487
488
  return 1
488
489
  }
489
490
 
@@ -551,8 +552,8 @@ check_in_02() {
551
552
  # the practice pin rides along - is byte-for-byte what this row has always done, and a
552
553
  # fresh install of any class keeps it (installed.json carries no engine: block).
553
554
  # Clause 2 - engine.mode=global with file entries remaining: hash what IS recorded;
554
- # an entry under the engine paths (.goblin/bin, .goblin/manifest, .goblin/bans,
555
- # .goblin/automations, .goblin/roles.yaml) that is still present is hashed too
555
+ # an entry under the engine paths (.gob/bin, .gob/manifest, .gob/bans,
556
+ # .gob/automations, .gob/roles.yaml) that is still present is hashed too
556
557
  # (harmless), and one that is MISSING is not drift - the engine is no longer the repo's.
557
558
  # Clause 3 - global with no file entries: the declaration + practice pin only; never
558
559
  # FAIL for absence of install artifacts (a declaration-only repo is not born RED).
@@ -564,7 +565,7 @@ check_in_02() {
564
565
  [ -n "$p" ] || continue
565
566
  if [ "$ENGINE_MODE" = "global" ]; then
566
567
  case "$p" in
567
- .goblin/bin/*|.goblin/manifest/*|.goblin/bans/*|.goblin/automations/*|.goblin/roles.yaml)
568
+ .gob/bin/*|.gob/manifest/*|.gob/bans/*|.gob/automations/*|.gob/roles.yaml)
568
569
  [ -e "$ROOT/$p" ] || continue ;; # engine removed by design: not drift
569
570
  # W4a (§5.3/§5.2): the procedure tier is EMITTED per platform, not carried in the
570
571
  # repo - a recorded .hermes/skills/ file absent from disk is the tier living at its
@@ -632,7 +633,7 @@ check_in_02() {
632
633
 
633
634
  check_hp_03() {
634
635
  # Every gate-bearing line inside the HANDOFF's `Gates` section carries `measured <date>`.
635
- # Anchored on the gate names `.goblin/goblin.yaml` DECLARES (GT-01's source of truth), not on
636
+ # Anchored on the gate names the AGENTS.md gob block DECLARES (GT-01's source of truth), not on
636
637
  # a hardcoded keyword list: the shipped gates are named `commit` and `todo_ceiling`, and the
637
638
  # old list `(tsc|build|hex|safelist|n/n)` matched neither, so on a fresh install the only line
638
639
  # it could see was the template's own "Example of the required form" prose. Measured at
@@ -644,7 +645,7 @@ check_hp_03() {
644
645
  # template prose, not a gate number, and is skipped. n == 0 is a FAIL: a Gates section with no
645
646
  # gate line at all (which is what is left when only the example remains) is not a green row.
646
647
  local names out rc n detail
647
- names=$(g_yaml_gates "$CONFIG" | cut -f1 | tr '\n' ' ')
648
+ names=$(g_agents_gates "$CONFIG" | cut -f1 | tr '\n' ' ')
648
649
  names=${names% }
649
650
  rc=0
650
651
  out=$(awk -v names="$names" '
@@ -732,7 +733,7 @@ check_gt_01() {
732
733
  printf 'declared gate "%s" has no cmd - it is declared and never runs\n' "$name"
733
734
  bad=1
734
735
  fi
735
- done < <(g_yaml_gates_all "$CONFIG")
736
+ done < <(g_agents_gates "$CONFIG")
736
737
  [ "$n" -ge 1 ] || { printf 'no gate declared in %s\n' "$CONFIG"; return 1; }
737
738
  [ "$bad" -eq 0 ] || return 1
738
739
  printf '%d declared gate(s), each with a cmd\n' "$n"
@@ -775,17 +776,17 @@ EOF_TAIL
775
776
  GT02_TAIL="${GT02_TAIL}gate ${name} printed nothing on stdout or stderr before exit ${rc} (a silent failure - the command itself is the diagnostic)"
776
777
  fi
777
778
  fi
778
- done < <(g_yaml_gates "$CONFIG")
779
+ done < <(g_agents_gates "$CONFIG")
779
780
  if [ "$write_line" = "yes" ]; then
780
781
  local rname rcmd rval
781
- rname=$(g_yaml_block_scalar "$CONFIG" ratchet name)
782
- rcmd=$(g_yaml_block_scalar "$CONFIG" ratchet cmd)
782
+ rname=$(g_agents_read "$CONFIG" ratchet.name)
783
+ rcmd=$(g_agents_read "$CONFIG" ratchet.cmd)
783
784
  if [ -n "$rname" ] && [ -n "$rcmd" ]; then
784
785
  rval=$( cd "$ROOT" && bash -c "$rcmd" 2>/dev/null | tr -d '[:space:]' )
785
- line="$line · $rname $rval (ceiling $(g_yaml_block_scalar "$CONFIG" ratchet ceiling))"
786
+ line="$line · $rname $rval (ceiling $(g_agents_read "$CONFIG" ratchet.ceiling))"
786
787
  fi
787
- printf '%s · measured %s\n' "${line# }" "$(date +%F)" > "$ROOT/.goblin/last-gate-line"
788
- cat "$ROOT/.goblin/last-gate-line"
788
+ printf '%s · measured %s\n' "${line# }" "$(date +%F)" > "$ROOT/.gob/last-gate-line"
789
+ cat "$ROOT/.gob/last-gate-line"
789
790
  fi
790
791
  return $bad
791
792
  }
@@ -799,9 +800,9 @@ check_gt_02() {
799
800
 
800
801
  check_gt_04() {
801
802
  local n c ceil
802
- n=$(g_yaml_block_scalar "$CONFIG" ratchet name)
803
- c=$(g_yaml_block_scalar "$CONFIG" ratchet cmd)
804
- ceil=$(g_yaml_block_scalar "$CONFIG" ratchet ceiling)
803
+ n=$(g_agents_read "$CONFIG" ratchet.name)
804
+ c=$(g_agents_read "$CONFIG" ratchet.cmd)
805
+ ceil=$(g_agents_read "$CONFIG" ratchet.ceiling)
805
806
  [ -n "$n" ] || { printf 'ratchet.name missing\n'; return 1; }
806
807
  [ -n "$c" ] || { printf 'ratchet.cmd missing\n'; return 1; }
807
808
  case "$ceil" in ''|*[!0-9]*) printf 'ratchet.ceiling must be numeric, got %s\n' "$ceil"; return 1 ;; esac
@@ -811,19 +812,19 @@ check_gt_04() {
811
812
 
812
813
  check_gt_05() {
813
814
  local n c ceil val old prev="" note=""
814
- n=$(g_yaml_block_scalar "$CONFIG" ratchet name)
815
- c=$(g_yaml_block_scalar "$CONFIG" ratchet cmd)
816
- ceil=$(g_yaml_block_scalar "$CONFIG" ratchet ceiling)
815
+ n=$(g_agents_read "$CONFIG" ratchet.name)
816
+ c=$(g_agents_read "$CONFIG" ratchet.cmd)
817
+ ceil=$(g_agents_read "$CONFIG" ratchet.ceiling)
817
818
  [ -n "$n" ] && [ -n "$c" ] || { printf 'no ratchet declared for this class - skipped\n'; return 3; }
818
819
  val=$( cd "$ROOT" && bash -c "$c" 2>/dev/null | tr -d '[:space:]' )
819
820
  case "$val" in ''|*[!0-9]*) printf 'ratchet cmd produced a non-number: %s\n' "$val"; return 1 ;; esac
820
- if [ -f "$ROOT/.goblin/ratchet-last" ]; then prev=$(cat "$ROOT/.goblin/ratchet-last"); fi
821
+ if [ -f "$ROOT/.gob/ratchet-last" ]; then prev=$(cat "$ROOT/.gob/ratchet-last"); fi
821
822
  if [ -n "$prev" ] && [ "$prev" -lt "$val" ]; then
822
823
  note="old $prev + $((val - prev)) new = $val"
823
824
  else
824
825
  note="$n $val (ceiling $ceil)"
825
826
  fi
826
- printf '%s\n' "$val" > "$ROOT/.goblin/ratchet-last"
827
+ printf '%s\n' "$val" > "$ROOT/.gob/ratchet-last"
827
828
  if [ "$val" -gt "$ceil" ]; then printf '%s over ceiling %s\n' "$note" "$ceil"; return 1; fi
828
829
  printf '%s\n' "$note"
829
830
  return 0
@@ -861,9 +862,9 @@ check_hs_01() {
861
862
 
862
863
  check_hs_02() {
863
864
  local env cmd commit d tmp bad=0 f rc base name qname pat
864
- env=$(g_yaml_block_scalar "$CONFIG" replay env)
865
- cmd=$(g_yaml_block_scalar "$CONFIG" replay cmd)
866
- commit=$(g_unquote "$(g_yaml_block_scalar "$CONFIG" replay commit)")
865
+ env=$(g_agents_read "$CONFIG" replay.env)
866
+ cmd=$(g_agents_read "$CONFIG" replay.cmd)
867
+ commit=$(g_unquote "$(g_agents_read "$CONFIG" replay.commit)")
867
868
  [ -n "$env" ] || { printf 'replay.env not declared\n'; return 1; }
868
869
  [ -n "$cmd" ] || { printf 'replay.cmd not declared\n'; return 1; }
869
870
  # Z1-4: the DECLARED command has to be what runs the harness. Until this fix it was read only
@@ -943,7 +944,7 @@ check_cm_03() {
943
944
  resolve_role_models() {
944
945
  # prints "role<TAB>profile<TAB>provider<TAB>model<TAB>effort" for each lane of a role
945
946
  local role="$1" f="$SELF_DIR/../roles.yaml"
946
- [ -f "$f" ] || f="$ROOT/.goblin/roles.yaml"
947
+ [ -f "$f" ] || f="$ROOT/.gob/roles.yaml"
947
948
  local profiles
948
949
  profiles=$(g_yaml_block_scalar "$f" "role-$role" profiles)
949
950
  [ -n "$profiles" ] || profiles=$(g_yaml_block_scalar "$f" "$role" profiles)
@@ -1144,7 +1145,7 @@ check_pg_06() {
1144
1145
  # PG-06 (G6 section A.6, row 2) - "The gate CI runs is the gate the project declares." The
1145
1146
  # failure mode is R5 section 1.1 measured: review and CI and goblin-verify reporting different
1146
1147
  # truths about one SHA, so a green required check says nothing about the gate the project
1147
- # actually declares. The declared set comes from g_yaml_gates - GT-01's own reader, reused
1148
+ # actually declares. The declared set comes from g_agents_gates - GT-01's own reader, reused
1148
1149
  # rather than re-derived, so a gate cannot vanish from the comparison in silence (G8-3).
1149
1150
  # A workflow satisfies the row when the whole declared set is RUN: either the verifier with no
1150
1151
  # `--only` (a full run executes every declared gate through GT-02), or each declared gate
@@ -1158,7 +1159,7 @@ check_pg_06() {
1158
1159
  [ -n "$gname" ] || continue
1159
1160
  ngates=$((ngates + 1))
1160
1161
  [ -n "$gcmd" ] && gates="$gates$gcmd"$'\n'
1161
- done < <(g_yaml_gates "$CONFIG")
1162
+ done < <(g_agents_gates "$CONFIG")
1162
1163
  for f in "$ROOT"/.github/workflows/*.yml "$ROOT"/.github/workflows/*.yaml; do
1163
1164
  [ -e "$f" ] || continue
1164
1165
  n=$((n + 1))
@@ -1210,7 +1211,7 @@ hash_runtime_data() {
1210
1211
  [ -n "$p" ] || continue
1211
1212
  h=$(g_sha256_file "$ROOT/$p")
1212
1213
  out="$out$p=$h "
1213
- done < <(g_yaml_list "$CONFIG" runtime_data)
1214
+ done < <(g_agents_list "$CONFIG" runtime_data)
1214
1215
  printf '%s' "${out% }"
1215
1216
  }
1216
1217
 
@@ -1222,11 +1223,11 @@ check_ds_01() {
1222
1223
  [ -n "$p" ] || continue
1223
1224
  ( cd "$ROOT" && git check-ignore -q -- "$p" ) \
1224
1225
  || { printf 'runtime_data path is not git-ignored: %s\n' "$p"; bad=1; }
1225
- done < <(g_yaml_list "$CONFIG" runtime_data)
1226
+ done < <(g_agents_list "$CONFIG" runtime_data)
1226
1227
  before=$(hash_runtime_data)
1227
1228
  run_gates no >/dev/null 2>&1
1228
1229
  after=$(hash_runtime_data)
1229
- printf 'before=%s\nafter=%s\n' "$before" "$after" > "$ROOT/.goblin/.ds-report"
1230
+ printf 'before=%s\nafter=%s\n' "$before" "$after" > "$ROOT/.gob/.ds-report"
1230
1231
  if [ "$before" != "$after" ]; then
1231
1232
  printf 'a gate run wrote runtime data: %s -> %s\n' "$before" "$after"
1232
1233
  return 1
@@ -1239,15 +1240,15 @@ check_ds_01() {
1239
1240
  }
1240
1241
 
1241
1242
  check_ds_02() {
1242
- [ -f "$ROOT/.goblin/.ds-report" ] || { printf 'no DS-01 report in this run\n'; return 1; }
1243
+ [ -f "$ROOT/.gob/.ds-report" ] || { printf 'no DS-01 report in this run\n'; return 1; }
1243
1244
  local b a
1244
- b=$(sed -n 's/^before=//p' "$ROOT/.goblin/.ds-report")
1245
- a=$(sed -n 's/^after=//p' "$ROOT/.goblin/.ds-report")
1245
+ b=$(sed -n 's/^before=//p' "$ROOT/.gob/.ds-report")
1246
+ a=$(sed -n 's/^after=//p' "$ROOT/.gob/.ds-report")
1246
1247
  [ "$b" = "$a" ] || { printf 'before/after differ\n'; return 1; }
1247
- [ -f "$ROOT/.goblin/last-gate-line" ] || { printf 'no gate line to annotate\n'; return 0; }
1248
+ [ -f "$ROOT/.gob/last-gate-line" ] || { printf 'no gate line to annotate\n'; return 0; }
1248
1249
  local line
1249
- line=$(tr -d '\n' < "$ROOT/.goblin/last-gate-line")
1250
- printf '%s · runtime_data %s\n' "$line" "${b:-<none declared>}" > "$ROOT/.goblin/last-gate-line"
1250
+ line=$(tr -d '\n' < "$ROOT/.gob/last-gate-line")
1251
+ printf '%s · runtime_data %s\n' "$line" "${b:-<none declared>}" > "$ROOT/.gob/last-gate-line"
1251
1252
  printf 'snapshot %s\n' "${b:-<none declared>}"
1252
1253
  return 0
1253
1254
  }
@@ -1397,7 +1398,7 @@ EOF
1397
1398
 
1398
1399
  check_sc_05() {
1399
1400
  # Every write handler (POST|PUT|PATCH|DELETE) in the declared route dirs must CALL a validator,
1400
- # or be named in .goblin/boundary-waivers. Partial by design: it proves a validator is called,
1401
+ # or be named in .gob/boundary-waivers. Partial by design: it proves a validator is called,
1401
1402
  # not that the schema is right.
1402
1403
  local d f rel n=0 bad=0 w=0
1403
1404
  for d in $SEC_WRITE_ROUTES; do
@@ -1408,7 +1409,7 @@ check_sc_05() {
1408
1409
  n=$((n + 1))
1409
1410
  rel="${f#"$ROOT"/}"
1410
1411
  if grep -qE 'safeParse|zod|valibot|yup|ajv|superstruct|validate\(' "$f"; then continue; fi
1411
- if [ -f "$ROOT/.goblin/boundary-waivers" ] && grep -qE "^[[:space:]]*${rel}([[:space:]]|$)" "$ROOT/.goblin/boundary-waivers"; then
1412
+ if [ -f "$ROOT/.gob/boundary-waivers" ] && grep -qE "^[[:space:]]*${rel}([[:space:]]|$)" "$ROOT/.gob/boundary-waivers"; then
1412
1413
  w=$((w + 1)); continue
1413
1414
  fi
1414
1415
  printf 'a write handler validates nothing and is not waived: %s\n' "$rel"; bad=1
@@ -1450,18 +1451,18 @@ check_in_03() {
1450
1451
  }
1451
1452
 
1452
1453
  check_sc_07() {
1453
- # Offline by construction: the record is written by .goblin/bin/goblin-audit, run ONCE,
1454
+ # Offline by construction: the record is written by .gob/bin/goblin-audit, run ONCE,
1454
1455
  # deliberately. A verify-time network call would break the offline dependency contract
1455
1456
  # (docs/RISKS.md K4). The record and the waiver table are repo-local by design (W1 §2.5);
1456
1457
  # only the SKIP remedy names the runner, and global mode names the CLI verb - the record
1457
1458
  # is the repo's, the tool that produced it is wherever the engine resolves (chain §2.3).
1458
1459
  [ -n "$SEC_AUDIT_CMD" ] || { printf 'this class declares no audit command (security.audit_cmd is empty)\n'; return 3; }
1459
- local rec="$ROOT/.goblin/audit.tsv" wfile="$ROOT/.goblin/audit-waiver.tsv"
1460
+ local rec="$ROOT/.gob/audit.tsv" wfile="$ROOT/.gob/audit-waiver.tsv"
1460
1461
  if [ ! -f "$rec" ]; then
1461
1462
  if [ "$ENGINE_MODE" = "global" ]; then
1462
- printf 'no audit record yet. Run `gob audit` once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
1463
+ printf 'no audit record yet. Run `gob audit` once, deliberately, then commit\n.gob/audit.tsv: SC-07 reads the record and never the network\n'
1463
1464
  else
1464
- printf 'no audit record yet. Run .goblin/bin/goblin-audit once, deliberately, then commit\n.goblin/audit.tsv: SC-07 reads the record and never the network\n'
1465
+ printf 'no audit record yet. Run .gob/bin/goblin-audit once, deliberately, then commit\n.gob/audit.tsv: SC-07 reads the record and never the network\n'
1465
1466
  fi
1466
1467
  return 3
1467
1468
  fi
@@ -1530,8 +1531,8 @@ check_sc_08() {
1530
1531
  ' "$lock" | sort -u)
1531
1532
  for p in $pkgs; do
1532
1533
  n=$((n + 1))
1533
- if [ -f "$ROOT/.goblin/install-hooks.allowlist" ] \
1534
- && grep -qxF "$p" "$ROOT/.goblin/install-hooks.allowlist"; then
1534
+ if [ -f "$ROOT/.gob/install-hooks.allowlist" ] \
1535
+ && grep -qxF "$p" "$ROOT/.gob/install-hooks.allowlist"; then
1535
1536
  ok=$((ok + 1))
1536
1537
  else
1537
1538
  printf 'an install-time script is not on the allowlist: %s\n' "$p"; bad=1
@@ -1555,7 +1556,7 @@ au01_globs() {
1555
1556
  if [ "$ENGINE_MODE" = "global" ]; then
1556
1557
  printf '%s\n' "$GOBLIN_ENGINE/automations/*.sh" "$ROOT/automations/*.sh" "$ROOT/automations/*/*.sh"
1557
1558
  else
1558
- printf '%s\n' "$ROOT/.goblin/automations/*.sh" "$ROOT/automations/*.sh" "$ROOT/automations/*/*.sh"
1559
+ printf '%s\n' "$ROOT/.gob/automations/*.sh" "$ROOT/automations/*.sh" "$ROOT/automations/*/*.sh"
1559
1560
  fi
1560
1561
  }
1561
1562
 
@@ -1568,7 +1569,7 @@ check_au_01() {
1568
1569
  # declare none.
1569
1570
  local f n=0 own=0 glob
1570
1571
  # The repo's OWN producers: anything under a root automations/ directory (the engine's
1571
- # payload lives under .goblin/automations/ in vendored mode, engine_dir in global mode;
1572
+ # payload lives under .gob/automations/ in vendored mode, engine_dir in global mode;
1572
1573
  # neither is a declaration by this repo).
1573
1574
  for f in "$ROOT"/automations/*.sh "$ROOT"/automations/*/*.sh; do
1574
1575
  [ -e "$f" ] || continue
@@ -1612,7 +1613,7 @@ check_bn_00() {
1612
1613
  # The ban table resolves through the engine chain (W1 §2.3): vendored first, then the
1613
1614
  # declared/global engine - the fallback slots into chain steps 2/3/4 where it used to
1614
1615
  # hardcode two of them.
1615
- bans="$ROOT/.goblin/manifest/bans.tsv"
1616
+ bans="$ROOT/.gob/manifest/bans.tsv"
1616
1617
  [ -f "$bans" ] || bans="${GOBLIN_ENGINE:-$SELF_DIR/../..}/manifest/bans.tsv"
1617
1618
  [ -f "$bans" ] || bans="$SELF_DIR/../manifest/bans.tsv"
1618
1619
  [ -f "$bans" ] || { printf 'no ban table found (manifest/bans.tsv)\n'; return 1; }
@@ -1648,9 +1649,9 @@ check_bn_run() {
1648
1649
  # is still reported FAIL-closed here rather than silently skipped - the ban lane must
1649
1650
  # never read clean because a binary went missing.
1650
1651
  local id="$1" engine out rc detail
1651
- engine="$ROOT/.goblin/bin/goblin-bans"
1652
+ engine="$ROOT/.gob/bin/goblin-bans"
1652
1653
  if [ ! -f "$engine" ] && [ -n "$GOBLIN_ENGINE" ]; then engine="$GOBLIN_ENGINE/bin/goblin-bans"; fi
1653
- [ -f "$engine" ] || { printf 'the ban engine is not installed: .goblin/bin/goblin-bans\n'; return 1; }
1654
+ [ -f "$engine" ] || { printf 'the ban engine is not installed: .gob/bin/goblin-bans\n'; return 1; }
1654
1655
  out=$( cd "$ROOT" && bash "$engine" --only "$id" 2>&1 ); rc=$?
1655
1656
  detail=$(printf '%s\n' "$out" | grep -vE '^ [0-9]+ passed' \
1656
1657
  | sed 's/^\(PASS\|FAIL\|SKIP\|ADV\) \{1,\}[A-Z0-9-]* *//' | sed '/^$/d' \
@@ -1682,7 +1683,7 @@ check_pf_01() {
1682
1683
  # - a skip with that reason, because a row that is RED on every fresh install teaches people
1683
1684
  # to ignore it.
1684
1685
  local rname bad=0
1685
- rname=$(g_yaml_block_scalar "$CONFIG" ratchet name)
1686
+ rname=$(g_agents_read "$CONFIG" ratchet.name)
1686
1687
  # The electron opt-in's done-definition: an Electron repo declares its host gate (the FPS /
1687
1688
  # main_thread_busy_pct number a hermetic rule cannot measure). `electron: true` with no host
1688
1689
  # gate is a repo missing half its definition — FAIL, before the baseline skip below can mask it.
@@ -1716,7 +1717,7 @@ check_pf_01() {
1716
1717
  # (ceiling 100000)` - the contradiction on its own passing line, seen by nothing. The ceiling
1717
1718
  # is anchored on the recorded baseline; raising it is what "I never measured again" looks like.
1718
1719
  local ceiling
1719
- ceiling=$(g_yaml_block_scalar "$CONFIG" ratchet ceiling)
1720
+ ceiling=$(g_agents_read "$CONFIG" ratchet.ceiling)
1720
1721
  case "$ceiling" in
1721
1722
  ''|*[!0-9]*) printf 'ratchet.ceiling is not a number: %s\n' "${ceiling:-<empty>}"; bad=1 ;;
1722
1723
  *)
@@ -1729,7 +1730,7 @@ check_pf_01() {
1729
1730
  [ "$bad" -eq 0 ] || return 1
1730
1731
  printf '%s = %s measured %s on %s (ceiling %s), host gate: %s\n' \
1731
1732
  "$PERF_METRIC" "$PERF_BASELINE_VALUE" "$PERF_MEASURED" "$PERF_BASELINE_COMMIT" \
1732
- "$(g_yaml_block_scalar "$CONFIG" ratchet ceiling)" "${PERF_HOST_GATE:-none}"
1733
+ "$(g_agents_read "$CONFIG" ratchet.ceiling)" "${PERF_HOST_GATE:-none}"
1733
1734
  return 0
1734
1735
  }
1735
1736
 
@@ -1837,9 +1838,9 @@ class_part_artifact() {
1837
1838
  case "$1" in
1838
1839
  handoff) [ -f "$ROOT/HANDOFF.md" ] ;;
1839
1840
  spec) ls "$ROOT"/*-SPEC.md >/dev/null 2>&1 ;;
1840
- gate) [ "$(g_yaml_gates "$CONFIG" | grep -c . || true)" -ge 1 ] ;;
1841
- replay) [ -n "$(g_yaml_block_scalar "$CONFIG" replay env)" ] && [ -n "$(g_yaml_block_scalar "$CONFIG" replay cmd)" ] ;;
1842
- ratchet) [ -n "$(g_yaml_block_scalar "$CONFIG" ratchet name)" ] ;;
1841
+ gate) [ "$(g_agents_gates "$CONFIG" | grep -c . || true)" -ge 1 ] ;;
1842
+ replay) [ -n "$(g_agents_read "$CONFIG" replay.env)" ] && [ -n "$(g_agents_read "$CONFIG" replay.cmd)" ] ;;
1843
+ ratchet) [ -n "$(g_agents_read "$CONFIG" ratchet.name)" ] ;;
1843
1844
  pr-gate) [ -d "$ROOT/reviews" ] ;;
1844
1845
  review-panel) [ -d "$ROOT/reviews" ] ;;
1845
1846
  # The W4a re-point (W4A-SPEC §5.3): the artifact test accepts the goblin-mode skill
@@ -1881,11 +1882,14 @@ class_part_artifact() {
1881
1882
  && awk -F'\t' -v r="$ROOT" 'NR>1 && index($3, r) == 1 { found=1 } END { exit !found }' \
1882
1883
  "${GOBLIN_EMISSIONS:-${HOME:-}/.goblin-stack/emissions.tsv}" && return 0
1883
1884
  return 1 ;;
1884
- tokens) [ -f "$ROOT/.goblin/tokens.yaml" ] ;;
1885
+ tokens) [ -f "$ROOT/.gob/tokens.yaml" ] ;;
1886
+ # ci-gate: the part is GONE from the product (v2). Every class carries need="-" in
1887
+ # classes.tsv, and the artifact can never be written again — so the probe is a
1888
+ # constant ABSENT and the CL-01 "-" branch (forbids, artifact absent) always passes.
1889
+ ci-gate) return 1 ;;
1885
1890
  # The CI lane's artifact is the workflow goblin-stack WRITES, not the directory: a repo is
1886
1891
  # allowed to have its own workflows, and `-` on this part means "goblin-stack must not have
1887
1892
  # placed one here", not "this repo may not have CI" (G6 A.6).
1888
- ci-gate) [ -f "$ROOT/.github/workflows/goblin-gate.yml" ] ;;
1889
1893
  *) return 0 ;;
1890
1894
  esac
1891
1895
  }
@@ -1903,7 +1907,7 @@ check_cl_01() {
1903
1907
  if [ "$present" -eq 0 ]; then printf 'class %s forbids part %s, artifact present\n' "$CLASS" "$part"; bad=1; fi
1904
1908
  continue
1905
1909
  fi
1906
- if g_part_disabled "$CONFIG" "$part"; then
1910
+ if g_agents_part_disabled "$CONFIG" "$part"; then
1907
1911
  printf 'part %s: opt-out\n' "$part"
1908
1912
  continue
1909
1913
  fi
@@ -2066,15 +2070,15 @@ check_fm_02() {
2066
2070
  case "$sr" in /*) srpath="$sr" ;; *) srpath="$ROOT/$sr" ;; esac
2067
2071
  reldir=$(fm_dir_rel)
2068
2072
  # W5-4: the search must not be satisfiable by the HARNESS ITSELF. `source_root: .` walked the
2069
- # install, so a stub map (`entry_paths: [export]`) "resolved" to ./.goblin/bin/goblin-verify and
2073
+ # install, so a stub map (`entry_paths: [export]`) "resolved" to ./.gob/bin/goblin-verify and
2070
2074
  # both FM rows passed a map that only echoed the template. The harness's own directories
2071
- # (.goblin/, .hermes/), git's, and the declared harness_dir are skipped by name, and the map's
2075
+ # (.gob/, .hermes/), git's, and the declared harness_dir are skipped by name, and the map's
2072
2076
  # own directory is skipped by path - without that last one the map's own entry-path list
2073
2077
  # satisfies the search and the row could never go RED. What is still NOT excluded is stated in
2074
2078
  # docs/LIMITS.md #37: a token that occurs only in the target's own docs/, tests/ or build output
2075
2079
  # still reads as resolved, because those are the target's files and excluding them would be a
2076
2080
  # guess about a layout goblin-stack does not know.
2077
- local exd=(--exclude-dir=.git --exclude-dir=.goblin --exclude-dir=.hermes)
2081
+ local exd=(--exclude-dir=.git --exclude-dir=.gob --exclude-dir=.hermes)
2078
2082
  case "$GOBLIN_HARNESS_REL" in
2079
2083
  ''|.|./) ;;
2080
2084
  *) exd+=(--exclude-dir="$(basename "$GOBLIN_HARNESS_REL")") ;;
@@ -2135,14 +2139,14 @@ check_va_01() {
2135
2139
  # ---- the judge and the loop (G2): JG-01..JG-03, LP-01..LP-05 --------------------------------
2136
2140
  # A loop record is the committed artifact of ONE unattended run:
2137
2141
  #
2138
- # .goblin/loop/predicate one shell command - the loop's own exit condition
2139
- # .goblin/loop/predicate.sha256 its digest, recorded at loop start (the pin); after a close, the
2142
+ # .gob/loop/predicate one shell command - the loop's own exit condition
2143
+ # .gob/loop/predicate.sha256 its digest, recorded at loop start (the pin); after a close, the
2140
2144
  # chain follows on `previous: <digest>` lines (W5-7)
2141
- # .goblin/loop/first-run "exit=<n> ts=<ISO8601>" - run BEFORE iteration 1
2142
- # .goblin/loop/budget the turn budget the loop declared
2143
- # .goblin/loop/decisions.tsv one row per iteration: ts phase decision why evidence result
2144
- # .goblin/loop/stuck.md the write-up when the loop ended without its predicate green
2145
- # .goblin/loop/closed-<date>/ a relaxed predicate AND the pin it was closed under, archived;
2145
+ # .gob/loop/first-run "exit=<n> ts=<ISO8601>" - run BEFORE iteration 1
2146
+ # .gob/loop/budget the turn budget the loop declared
2147
+ # .gob/loop/decisions.tsv one row per iteration: ts phase decision why evidence result
2148
+ # .gob/loop/stuck.md the write-up when the loop ended without its predicate green
2149
+ # .gob/loop/closed-<date>/ a relaxed predicate AND the pin it was closed under, archived;
2146
2150
  # the live pin names its digest, so the close is auditable
2147
2151
  #
2148
2152
  # Nothing here re-runs the predicate and nothing here writes: the record is the evidence, and
@@ -2166,7 +2170,7 @@ loop_malformed_rows() {
2166
2170
 
2167
2171
  # Does ONE evidence token resolve? The typed handles are sha:<hex> (a commit `git rev-list
2168
2172
  # --all` knows), file:<path> (a path under the root) and sha256:<hex> (the digest of a file
2169
- # under .goblin/loop/); a bare token resolves when it names an existing path. A `cmd:` token
2173
+ # under .gob/loop/); a bare token resolves when it names an existing path. A `cmd:` token
2170
2174
  # resolves NOTHING on purpose: the command ran, its output is not in the record, and a verdict
2171
2175
  # that rests on it is exactly the self-report JG-01 exists to refuse.
2172
2176
  loop_token_resolves() {
@@ -2226,7 +2230,7 @@ check_jg_01() {
2226
2230
  done
2227
2231
  if [ "$hit" -eq 0 ]; then
2228
2232
  printf 'row %s: %s cites no handle that resolves: "%s"\n' "$rn" "$dec" "${ev:-<empty>}"
2229
- printf ' a handle is sha:<hex> (a commit in this repo), file:<path>, sha256:<hex> (a file under .goblin/loop/), or an existing path\n'
2233
+ printf ' a handle is sha:<hex> (a commit in this repo), file:<path>, sha256:<hex> (a file under .gob/loop/), or an existing path\n'
2230
2234
  bad=1
2231
2235
  fi
2232
2236
  done < <(loop_decision_rows)
@@ -2290,13 +2294,13 @@ check_jg_02() {
2290
2294
  check_lp_01() {
2291
2295
  if ! loop_present; then loop_no_record; return 3; fi
2292
2296
  local p="$LOOP_DIR/predicate" f="$LOOP_DIR/first-run" n fr fr_ts row1_ts a b
2293
- [ -f "$p" ] || { printf 'no .goblin/loop/predicate - the loop declared no exit condition\n'; return 1; }
2297
+ [ -f "$p" ] || { printf 'no .gob/loop/predicate - the loop declared no exit condition\n'; return 1; }
2294
2298
  n=$(grep -cvE '^[[:space:]]*(#|$)' "$p")
2295
2299
  if [ "$n" -ne 1 ]; then
2296
2300
  printf 'the predicate must be exactly ONE command; found %s non-blank non-comment line(s)\n' "$n"
2297
2301
  return 1
2298
2302
  fi
2299
- [ -f "$f" ] || { printf 'no .goblin/loop/first-run - the predicate was never run before iteration 1\n'; return 1; }
2303
+ [ -f "$f" ] || { printf 'no .gob/loop/first-run - the predicate was never run before iteration 1\n'; return 1; }
2300
2304
  fr=$(head -n 1 "$f")
2301
2305
  case "$fr" in
2302
2306
  "exit="[0-9]*" ts="*) ;;
@@ -2322,13 +2326,13 @@ check_lp_01() {
2322
2326
  check_lp_02() {
2323
2327
  if ! loop_present; then loop_no_record; return 3; fi
2324
2328
  local p="$LOOP_DIR/predicate" pin="$LOOP_DIR/predicate.sha256" want have bad=0
2325
- [ -f "$p" ] || { printf 'no .goblin/loop/predicate to pin\n'; return 1; }
2326
- [ -f "$pin" ] || { printf 'no .goblin/loop/predicate.sha256 - the predicate was never pinned at loop start\n'; return 1; }
2329
+ [ -f "$p" ] || { printf 'no .gob/loop/predicate to pin\n'; return 1; }
2330
+ [ -f "$pin" ] || { printf 'no .gob/loop/predicate.sha256 - the predicate was never pinned at loop start\n'; return 1; }
2327
2331
  want=$(head -n 1 "$pin" | tr -d '[:space:]')
2328
2332
  have=$(g_sha256_file "$p")
2329
2333
  if [ "$want" != "$have" ]; then
2330
2334
  printf 'the predicate changed after the loop was pinned\n pinned %s\n now %s\n' "$want" "$have"
2331
- printf 'remedy: close this loop - commit the green state, archive the predicate AND its pin under .goblin/loop/closed-<date>/, then open a new loop and re-record the pin with a `previous: <archived digest>` line. Never automatic.\n'
2335
+ printf 'remedy: close this loop - commit the green state, archive the predicate AND its pin under .gob/loop/closed-<date>/, then open a new loop and re-record the pin with a `previous: <archived digest>` line. Never automatic.\n'
2332
2336
  return 1
2333
2337
  fi
2334
2338
  # W5-7: the close-and-reopen path. The pin covers the predicate file on disk, so a loop could
@@ -2369,7 +2373,7 @@ check_lp_02() {
2369
2373
  check_lp_03() {
2370
2374
  if ! loop_present; then loop_no_record; return 3; fi
2371
2375
  local f="$LOOP_DIR/budget" b ceil n bad=0
2372
- [ -f "$f" ] || { printf 'no .goblin/loop/budget - the loop declared no turn budget\n'; return 1; }
2376
+ [ -f "$f" ] || { printf 'no .gob/loop/budget - the loop declared no turn budget\n'; return 1; }
2373
2377
  b=$(head -n 1 "$f" | tr -d '[:space:]')
2374
2378
  case "$b" in
2375
2379
  ''|*[!0-9]*) printf 'budget must be a positive integer, got "%s"\n' "$b"; return 1 ;;
@@ -2436,7 +2440,7 @@ check_lp_05() {
2436
2440
  return 0
2437
2441
  fi
2438
2442
  s="$LOOP_DIR/stuck.md"
2439
- [ -f "$s" ] || { printf 'the loop ended at "%s" (row %s) and left no .goblin/loop/stuck.md\n' "${res:-<empty>}" "$rn"; return 1; }
2443
+ [ -f "$s" ] || { printf 'the loop ended at "%s" (row %s) and left no .gob/loop/stuck.md\n' "${res:-<empty>}" "$rn"; return 1; }
2440
2444
  n=$(grep -c '[^[:space:]]' "$s")
2441
2445
  if [ "$n" -lt 3 ]; then
2442
2446
  printf 'stuck.md holds %s non-blank line(s); a write-up needs at least 3\n' "$n"
@@ -2641,10 +2645,10 @@ check_rc_03() {
2641
2645
  scripts/*|notes/*|manifests/*) continue ;;
2642
2646
  # The harness's own directories and the .gitignore block it appends are the install, not the
2643
2647
  # lab's content - the same exclusion FM-02 makes for the same reason (W5-4). That set is the
2644
- # one the class-A install contract creates: .goblin/, .hermes/, the .gitignore block, the
2648
+ # one the class-A install contract creates: .gob/, .hermes/, the .gitignore block, the
2645
2649
  # declared harness_dir (checks/ by default), reviews/, and the CI scaffold .github/. A payload
2646
2650
  # hidden in any of them is still caught by clause 2 (the sha256 sweep below).
2647
- .goblin/*|.hermes/*|.gitignore|.github/*|reviews/*) continue ;;
2651
+ .gob/*|.hermes/*|.gitignore|.github/*|reviews/*) continue ;;
2648
2652
  "${GOBLIN_HARNESS_REL:-checks}"/*) continue ;;
2649
2653
  esac
2650
2654
  case "$p" in
@@ -2730,9 +2734,9 @@ rc_hdr_field() {
2730
2734
  # ---- the day-one framing (review 1, scope 3) ----------------------------------
2731
2735
  # Two shapes are undeserved reds for a person who just cloned a repo. They are DETECTED and
2732
2736
  # framed — the verdict's exit semantics, the counts and every row's payload are untouched:
2733
- # (a) owner_email in goblin.yaml is not the current git identity -> the CM-01 red names
2737
+ # (a) owner_email in AGENTS.md is not the current git identity -> the CM-01 red names
2734
2738
  # whose email the repo actually expects (the remedy column says where to point it);
2735
- # (b) the repo looks freshly cloned (.goblin committed, but fewer than 3 commits) -> one
2739
+ # (b) the repo looks freshly cloned (.gob committed, but fewer than 3 commits) -> one
2736
2740
  # banner line after the verdict. Commit count only: an mtime comparison against the
2737
2741
  # reflog fired after every ordinary GT-02 write, so the banner lingered on aged repos
2738
2742
  # (the verify itself refreshes last-gate-line forever); a repo under 3 commits is the
@@ -2740,7 +2744,7 @@ rc_hdr_field() {
2740
2744
  owner_mismatch() {
2741
2745
  [ -f "$CONFIG" ] || return 1
2742
2746
  local declared cur
2743
- declared=$(grep '^owner_email:' "$CONFIG" | head -n 1 | sed 's/^owner_email:[[:space:]]*//' | tr -d '"')
2747
+ declared=$(g_agents_read "$CONFIG" owner_email | tr -d '"')
2744
2748
  cur=$(cd "$ROOT" && git config user.email 2>/dev/null)
2745
2749
  [ -n "$declared" ] && [ -n "$cur" ] && [ "$declared" != "$cur" ]
2746
2750
  }
@@ -2806,7 +2810,7 @@ while IFS=$'\t' read -r id scope rule enforced_by artifact check why; do
2806
2810
  esac
2807
2811
  continue
2808
2812
  fi
2809
- if [ -n "$part" ] && g_part_disabled "$CONFIG" "$part"; then
2813
+ if [ -n "$part" ] && g_agents_part_disabled "$CONFIG" "$part"; then
2810
2814
  SKIP=$((SKIP + 1)); emit "$id" SKIP "$rule (opt-out: $part)"; continue
2811
2815
  fi
2812
2816
  if [ "$ARCHIVE" = "true" ]; then
@@ -2839,7 +2843,7 @@ while IFS=$'\t' read -r id scope rule enforced_by artifact check why; do
2839
2843
  # ... -nt ...` invocation dump told the reader the mechanics and nothing about what
2840
2844
  # to do; the human line says both. The TSV remedy column carries the same sentence.
2841
2845
  if [ "$id" = "GT-03" ]; then
2842
- detail="GT-03 the gate line is older than the last commit — re-run gob verify to refresh it (it rewrites .goblin/last-gate-line)"
2846
+ detail="GT-03 the gate line is older than the last commit — re-run gob verify to refresh it (it rewrites .gob/last-gate-line)"
2843
2847
  else
2844
2848
  detail="($check -> exit $rc)"
2845
2849
  fi
@@ -2865,7 +2869,7 @@ else
2865
2869
  # Day-one framing, part (a) (review 1, scope 3): the owner mismatch names whose email the
2866
2870
  # repo expects, under the verdict, before the row detail. One line, no exit semantics.
2867
2871
  if [ "$FAIL" -gt 0 ] && owner_mismatch; then
2868
- _owner_declared=$(grep '^owner_email:' "$CONFIG" | head -n 1 | sed 's/^owner_email:[[:space:]]*//' | tr -d '"')
2872
+ _owner_declared=$(g_agents_read "$CONFIG" owner_email | tr -d '"')
2869
2873
  printf ' note: this repo records a different owner (you are probably new here) — commit with --author or update owner_email: %s\n' "$_owner_declared"
2870
2874
  unset _owner_declared
2871
2875
  fi
@@ -2892,7 +2896,7 @@ else
2892
2896
  merely passing; whether the forge is bound by the workflow CL-01 found; whether a human
2893
2897
  read the diff;
2894
2898
  whether the model mapping names a family that actually differs (MD-02 reports it
2895
- and cannot change it); and whether .goblin/installed.json — the record every drift
2899
+ and cannot change it); and whether .gob/installed.json — the record every drift
2896
2900
  check trusts — was itself rewritten. It is not signed, so one edit to it (with the
2897
2901
  matching edit to the file it protects) defeats IN-02, SK-02 and HS-01 at once
2898
2902
  (docs/LIMITS.md #18). The ban lane carries the same blind spot and two of its own:
@@ -2924,11 +2928,21 @@ SEE
2924
2928
  # short form too: a doc pin (F2-3) reads it out of every run, not only verbose ones.
2925
2929
  cat <<'SEE'
2926
2930
  cannot see: whether a check tests the right path rather than merely passing; whether a
2927
- human read the diff; whether the unsigned install record (.goblin/installed.json — it is
2931
+ human read the diff; whether the unsigned install record (.gob/installed.json — it is
2928
2932
  not signed) was rewritten; and the per-lane blind spots of the ban, judge/loop, CI and
2929
2933
  reference lanes. Full statement: `goblin-verify --verbose`, docs/LIMITS.md #18 #27 #28 #33 #43.
2930
2934
  SEE
2931
2935
  fi
2936
+ # ---- the sign-off (locked v2 decision) ----------------------------------------
2937
+ # The mascot is a GREEN-run line only. A red run ends on the concrete first command:
2938
+ # the verdict IS the FAIL list, and the first row's remedy line says the fix — mascot
2939
+ # noise on the run's one actionable moment is exactly what the red tail must not be.
2940
+ # Human branch only: a --json run stays machine-clean.
2941
+ if [ "$FAIL" -gt 0 ] || [ "$BAD" -eq 1 ]; then
2942
+ printf ' start with the first FAIL above — its remedy line says the fix.\n'
2943
+ else
2944
+ printf ' ▙ the goblin sees you. keep the gate green.\n'
2945
+ fi
2932
2946
  # ---- the engine footer (W1 §2.2, R-1's residue -> docs/LIMITS.md #43) --------
2933
2947
  # Every run states WHICH engine judged it: the sha256 of the running verifier and of
2934
2948
  # the manifest that judged this run, so a repo can name its judge. The statement is
@@ -2947,7 +2961,7 @@ SEE
2947
2961
  "$(g_sha256_file "$SELF_DIR/goblin-verify")" \
2948
2962
  "$(g_sha256_file "$MANIFEST")" >&2
2949
2963
  # ---- the shadow advisory (W3 §3.2) -------------------------------------------
2950
- # A vendored payload under .goblin/ while the record says mode=global: the chain's
2964
+ # A vendored payload under .gob/ while the record says mode=global: the chain's
2951
2965
  # vendored step (2) beats the declared engine_dir (3) — W1's ordering, kept
2952
2966
  # deliberately — so a STALE vendored table judged this run while the footer above
2953
2967
  # still prints mode=global. This line names that split. It is deliberately a footer
@@ -2957,10 +2971,10 @@ SEE
2957
2971
  # mandate for. Detector, not prevention: the residue is LIMITS #45.
2958
2972
  if [ "$ENGINE_MODE" = "global" ]; then
2959
2973
  _shadow_n=0
2960
- for _f in "$ROOT"/.goblin/bin/goblin-verify "$ROOT"/.goblin/bin/goblin-lib.sh \
2961
- "$ROOT"/.goblin/bin/goblin-audit "$ROOT"/.goblin/bin/goblin-bans \
2962
- "$ROOT"/.goblin/manifest/* "$ROOT"/.goblin/bans/* \
2963
- "$ROOT"/.goblin/automations/* "$ROOT"/.goblin/roles.yaml; do
2974
+ for _f in "$ROOT"/.gob/bin/goblin-verify "$ROOT"/.gob/bin/goblin-lib.sh \
2975
+ "$ROOT"/.gob/bin/goblin-audit "$ROOT"/.gob/bin/goblin-bans \
2976
+ "$ROOT"/.gob/manifest/* "$ROOT"/.gob/bans/* \
2977
+ "$ROOT"/.gob/automations/* "$ROOT"/.gob/roles.yaml; do
2964
2978
  [ -e "$_f" ] || continue
2965
2979
  _shadow_n=$((_shadow_n + 1))
2966
2980
  done