@techgoblin/gobstack 0.5.0-beta.8 → 0.6.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +161 -124
- package/VERSION +1 -1
- package/automations/drift-audit.sh +4 -4
- package/bans/layer-check.sh +10 -8
- package/bin/goblin +69 -57
- package/bin/goblin-audit +11 -13
- package/bin/goblin-bans +11 -11
- package/bin/goblin-extras +342 -0
- package/bin/goblin-init +382 -711
- 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 +35 -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 +194 -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/extras-catalogue/catalogue.tsv +42 -0
- package/extras-catalogue/payload/README.md +14 -0
- package/extras-catalogue/payload/taste-skill/taste/REFERENCE.md +3 -0
- package/extras-catalogue/payload/taste-skill/taste/SKILL.md +9 -0
- 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 +3 -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-verify
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
set -uo pipefail
|
|
21
21
|
|
|
22
|
-
GOBLIN_VERIFY_VERSION="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 .
|
|
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
|
|
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/.
|
|
90
|
-
# 3. engine_dir: from $ROOT
|
|
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
|
|
100
|
+
CONFIG="$ROOT/AGENTS.md"
|
|
101
101
|
|
|
102
|
-
# engine_yaml_raw — the raw
|
|
103
|
-
# the line, so a non-string scalar is visible here as
|
|
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
|
-
|
|
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
|
-
|
|
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 .
|
|
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/.
|
|
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/.
|
|
168
|
-
MANIFEST="$ROOT/.
|
|
169
|
-
CLASSES="$ROOT/.
|
|
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/.
|
|
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/.
|
|
190
|
-
_cli_default="$HOME/.
|
|
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/.
|
|
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:
|
|
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=$(
|
|
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=$(
|
|
311
|
-
ARCHIVE=$(
|
|
312
|
-
MAX_DIRTY=$(
|
|
313
|
-
ADV_CEILING=$(
|
|
314
|
-
HARNESS_DIR_RAW=$(
|
|
315
|
-
SCAFFOLD_CHECKS=$(
|
|
316
|
-
MODELS_FILE=$(g_expand_tilde "$(
|
|
317
|
-
PRACTICE=$(g_expand_tilde "$(
|
|
318
|
-
PRACTICE_SHA=$(g_unquote "$(
|
|
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=$(
|
|
330
|
-
SOURCE_ROOT=$(g_unquote "$(
|
|
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 "$(
|
|
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 "$(
|
|
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=$(
|
|
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/.
|
|
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 .
|
|
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=$(
|
|
394
|
-
SEC_BUILD_OUTPUT=$(
|
|
395
|
-
SEC_AUDIT_CMD=$(
|
|
396
|
-
SEC_AUDIT_MAX_AGE=$(
|
|
397
|
-
SEC_WAIVER_MAX_AGE=$(
|
|
398
|
-
SEC_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=$(
|
|
402
|
-
PERF_CMD=$(
|
|
403
|
-
PERF_BASELINE_COMMIT=$(g_unquote "$(
|
|
404
|
-
PERF_BASELINE_VALUE=$(
|
|
405
|
-
PERF_MEASURED=$(g_unquote "$(
|
|
406
|
-
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=$(
|
|
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 (.
|
|
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|\.
|
|
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
|
-
# .
|
|
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 -- .
|
|
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 (.
|
|
555
|
-
# .
|
|
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
|
-
.
|
|
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
|
|
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=$(
|
|
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 < <(
|
|
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 < <(
|
|
779
|
+
done < <(g_agents_gates "$CONFIG")
|
|
779
780
|
if [ "$write_line" = "yes" ]; then
|
|
780
781
|
local rname rcmd rval
|
|
781
|
-
rname=$(
|
|
782
|
-
rcmd=$(
|
|
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 $(
|
|
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/.
|
|
788
|
-
cat "$ROOT/.
|
|
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=$(
|
|
803
|
-
c=$(
|
|
804
|
-
ceil=$(
|
|
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=$(
|
|
815
|
-
c=$(
|
|
816
|
-
ceil=$(
|
|
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/.
|
|
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/.
|
|
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=$(
|
|
865
|
-
cmd=$(
|
|
866
|
-
commit=$(g_unquote "$(
|
|
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/.
|
|
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
|
|
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 < <(
|
|
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 < <(
|
|
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 < <(
|
|
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/.
|
|
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/.
|
|
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/.
|
|
1245
|
-
a=$(sed -n 's/^after=//p' "$ROOT/.
|
|
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/.
|
|
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/.
|
|
1250
|
-
printf '%s · runtime_data %s\n' "$line" "${b:-<none declared>}" > "$ROOT/.
|
|
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 .
|
|
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/.
|
|
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 .
|
|
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/.
|
|
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.
|
|
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 .
|
|
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/.
|
|
1534
|
-
&& grep -qxF "$p" "$ROOT/.
|
|
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/.
|
|
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 .
|
|
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/.
|
|
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/.
|
|
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: .
|
|
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=$(
|
|
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=$(
|
|
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
|
-
"$(
|
|
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) [ "$(
|
|
1841
|
-
replay) [ -n "$(
|
|
1842
|
-
ratchet) [ -n "$(
|
|
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/.
|
|
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
|
|
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 ./.
|
|
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
|
-
# (.
|
|
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=.
|
|
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
|
-
# .
|
|
2139
|
-
# .
|
|
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
|
-
# .
|
|
2142
|
-
# .
|
|
2143
|
-
# .
|
|
2144
|
-
# .
|
|
2145
|
-
# .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
2326
|
-
[ -f "$pin" ] || { printf 'no .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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: .
|
|
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
|
-
.
|
|
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
|
|
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 (.
|
|
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=$(
|
|
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" ] &&
|
|
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 .
|
|
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=$(
|
|
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 .
|
|
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 (.
|
|
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 .
|
|
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"/.
|
|
2961
|
-
"$ROOT"/.
|
|
2962
|
-
"$ROOT"/.
|
|
2963
|
-
"$ROOT"/.
|
|
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
|