@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
@@ -31,92 +31,92 @@ Measured shape of this table: **87 rows** - 82 target, 5 source; advisory 10, ga
31
31
  | id | scope | enforced by | rule | check | if it cannot be enforced, why |
32
32
  |---|---|---|---|---|---|
33
33
  | `IN-01` | target | script | The install exists and records its version + every file's hash. | goblin-verify --only IN-01 | — (W1: the check is a builtin so the engine.mode=global clause can run — a global, declaration-only repo has no install record and SKIPs with `global engine mode — no per-repo install record`; the vendored clauses are the old one-liner: the record exists and names its version) |
34
- | `IN-02` | target | script | Every installed file still matches its recorded hash. | `goblin-verify --only IN-02` (builtin) | recover: `gob install --upgrade`, or `git checkout -- .goblin/installed.json`; for a file it names drifted: `git checkout -- <path>` (if the edit is yours and intended, re-pin it: `gob install --target <dir> --re-pin`) |
35
- | `IN-03` | target | script | The verifier's own manifest is complete: every rule has a check or is advisory. | `goblin-verify --only IN-03` (builtin) | — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script\|lint\|gate\|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in `bin/goblin-verify`), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine) |
36
- | `IN-04` | target | script | No file goblin-stack did not create has been overwritten. | `goblin-verify --only IN-04` (builtin) | Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch. |
37
- | `HP-01` | target | gate | HANDOFF.md exists at the root. | `test -f HANDOFF.md` | — |
38
- | `HP-02` | target | lint | The HANDOFF carries its five required sections. | `for h in 'START HERE' 'STATE\|STATUS' 'GATES?' 'NEXT STEPS\|NEXT' 'NOT VERIFIED\|UNVERIFIED\|NOT PROVEN\|UNPROVEN\|PENDING[^.]*DEVICE TEST'; do grep -qiE "^#{2,3}[[:space:]]+[^[:alpha:]]*($h)\b" HANDOFF.md \|\| { echo "missing section: $h"; exit 1; }; done` | Partial: proves a heading exists whose first word names the slot, not that the section's content is complete. The slot may use the repo's own vocabulary (the model repo's `### NEXT` passes; `Status`, `Gate`, `Unverified`, `Pending <x> device test` are accepted); a heading that merely contains the word (`## BOARD STATE`) does not satisfy `State`. |
39
- | `HP-03` | target | lint | Every gate number is a measurement with a date, never a copy. | `goblin-verify --only HP-03` (builtin) | Builtin, anchored on the gate NAMES `.goblin/goblin.yaml` declares (GT-01's source of truth) rather than on a hardcoded keyword list: the shipped gates are named `commit` and `todo_ceiling`, neither of which the old list matched, so on a fresh install the only line it could see was the template's own example sentence, and a real gate line could lose its `date` and the row stayed GREEN (G8-2). A line whose text says "example of the required form" is template prose, not a gate number, and is skipped, so the template cannot satisfy the row. Partial: it proves a dated line inside the `Gates` section exists and that every gate-bearing line there carries a date - not that the number was re-measured that day, and not a gate-bearing line that names no declared gate and carries no gate-shaped keyword (so a line the config does not declare and the keyword list does not recognise is unseen). Historical gate lines outside that section are exempt by design - PROJECT-PRACTICE section 1's stale-sentence rule requires them to be kept. |
40
- | `HP-04` | target | advisory | A stale sentence is corrected in place with a dated parenthetical, never deleted. | `advisory` | Detecting a silent deletion needs semantic judgement; a diff heuristic (>=5 removed non-empty lines with no 'corrected' addition) is too noisy to gate on. goblin-verify --only HP-04 prints the heuristic as a warning only. |
41
- | `HP-05` | target | gate | The HANDOFF names the HEAD it describes. | `goblin-verify --only HP-05` (builtin) | Deviation from the design spec, with the reason: the spec's literal check is `grep -q "$(git rev-parse --short HEAD)" HANDOFF.md`, which can never pass - committing the HANDOFF moves HEAD, so the file can only name a commit that is now an ancestor. The mechanised form is therefore 'the HANDOFF names a commit that exists in this repo AND is an ancestor of HEAD', which still catches the defect it exists for (a review/handoff artifact that names no commit at all). |
42
- | `SP-01` | target | gate | A *-SPEC.md file exists at the repo root (any round, not the current one - round-scoping arrives with the W6 staged chain). | `ls ./*-SPEC.md >/dev/null 2>&1` | Skipped when class: D and disabled: [spec]. |
43
- | `SP-02` | target | script | The SPEC is committed, not left untracked. | `test -z "$(git ls-files --others --exclude-standard -- '*-SPEC.md')"` | — |
44
- | `SP-03` | target | lint | Every AC: item is checkable without a human. | `awk '/^[[:space:]]*[-*][[:space:]]/ && /AC[0-9]*:/ && !/\|==\|===\|exit\|<\|>/ {print; bad=1} END{exit bad}' ./*-SPEC.md` | Partial: structural only - a checkable-looking bullet can still be unfalsifiable. The matcher is `AC[0-9]*:` because the shipped template writes `- AC1: ...`; the literal `/AC:/` only saw a bullet that spelled the label without a number. |
45
- | `GT-01` | target | gate | The gate set is declared, never inferred from the stack. | `goblin-verify --only GT-01` (builtin) | Builtin: every `- name:` under `gates:` is a DECLARED gate and each one must carry a `cmd:` - a gate whose `cmd:` was deleted, blanked or re-indented FAILs here instead of vanishing from the count (G8-3, the condition of G8's own 9/10 sentence). The count is of declarations, not of runnable pairs. It cannot see whether a declared command is the RIGHT gate for the project: it proves a command exists, not that it is meaningful. |
46
- | `GT-02` | target | gate | Every declared gate runs and exits 0. | `goblin-verify --only GT-02` (builtin) | — |
47
- | `GT-03` | target | script | The round reports one line of measured numbers. | `test -f .goblin/last-gate-line && [ .goblin/last-gate-line -nt "$(git rev-parse --git-dir)/logs/HEAD" ]` | The freshness reference is HEAD's reflog (`.git/logs/HEAD`), which every HEAD movement rewrites - a commit in an attached or a detached worktree included, and independently of whether the refs are packed. The clause it replaced read `.git/HEAD`, a file a commit never rewrites (only branch operations do), so a commit landing after the measured line left the row GREEN while the round had moved on (D2, measured: `.git/HEAD`'s mtime unchanged across a real commit, `--only GT-03` exit 0). Two limits remain, recorded rather than hidden: a repo with the reflog disabled (`core.logAllRefUpdates=false`) has no reference to compare against, and `-nt` against a missing path is true, so the clause passes vacuously and the row then proves only that a measured line EXISTS (a repo with no commit yet is the same case); and the clause reads ANY HEAD movement as staleness, so a checkout, a branch rename or a reset FAILs it until the next gate run rewrites the line. That second behaviour is what makes a full run self-freshening: `GT-02` writes the line earlier in the same pass, so the row asserts that the line in front of you came from THIS run. `tests/t-gt03-freshness.sh` is the control, in both directions. |
48
- | `GT-04` | target | gate | A ratchet is declared with a ceiling. | `goblin-verify --only GT-04` (builtin) | — |
49
- | `GT-05` | target | gate | The ratchet has not risen. | `goblin-verify --only GT-05` (builtin) | — |
50
- | `HS-01` | target | lint | Asserting harnesses follow the house shape. | `goblin-verify --only HS-01` (builtin) | A declared harness_dir that is absent FAILS when the class scaffolds one (config `scaffold_checks: yes`, classes A and C); a class that ships no harness dir (B/D/E) SKIPs with a reason. Keying the skip off the path alone let one config line switch this row and HS-02 off. A report utility in the same dir is counted and reported separately rather than failing the run. |
51
- | `HS-02` | target | gate | A check green on both trees proves nothing - the REPLAY must show RED pre-change. | `goblin-verify --only HS-02` (builtin) | The declared `replay.cmd` is EXECUTED with `{name}` replaced by each harness's name, in the pre-change worktree, with `replay.env=<commit>` set (Z1-4). Two clauses that used to be unheard: a command that interpolates no `{name}` FAILs (it cannot be running the harness it names, so nothing was replayed), and a command that cannot be executed at all - exit 126 or 127 - FAILs rather than counting as a RED harness. The harness's name is substituted SHELL-QUOTED (`printf %q`), because the name is part of the command text the shell parses; unquoted, a name carrying `;` or `#` reached the shell as syntax (Z2-3, the G8-1 surface class), and the control for it carries a metacharacter-bearing file name. What it cannot see: a command that runs *something else* under the harness's name and exits non-zero, and whether the harness tests the right path rather than merely failing on this tree. |
52
- | `HS-03` | target | advisory | Source probes read text with comments blanked first. | `advisory` | Recognising 'this probe reads source text' is semantic; a grep for the blanking helper produces false FAILs on harnesses that do not probe source. |
53
- | `CM-01` | target | gate | Commits carry the owner identity, not an ambient one. Current scope: this gates the identity of HEAD (the last commit) at the moment of the run - earlier commits by other authors are not scanned. | `test "$(git log -1 --format='%ae')"` = `"$(grep '^owner_email:' .goblin/goblin.yaml \| cut -d' ' -f2)"` | note: this repo records a different owner (you are probably new here) — commit with `git -c user.email=<owner_email> --author=<owner_email>`, or update `owner_email:` in `.goblin/goblin.yaml` |
54
- | `CM-02` | target | advisory | The commit message was written to a file, not passed with -m. | `advisory` | A backtick lost to command substitution leaves no trace a later check can read. Reported as a heuristic (unbalanced backticks in a body) only. |
55
- | `CM-03` | target | script | Commit-as-you-go: the working tree is not carrying a dead run's work. | `goblin-verify --only CM-03` (builtin) | — |
56
- | `MD-01` | target | lint | No model name is hardcoded in any reusable rule. | `for d in skills manifest bin templates presets .goblin .hermes; do [ -d "$d" ] \|\| continue; grep -rniE '(d[e]epseek\|cl[a]ude\|g[p]t-[0-9]\|gr[o]k\|g[e]mini\|g[l]m-[0-9]\|k[i]mi)[a-z0-9.:_-]*' "$d" && exit 1; done; exit 0` | — (the pattern is written with character classes so this row cannot match itself; tests/t-verify-red.sh proves it still catches a real model name. SCOPE, stated so a repo-wide grep is not re-reported as a hole (G8-10): this row reads the seven directories an install writes rules into - skills/ manifest/ bin/ templates/ presets/ .goblin/ .hermes/. It does NOT read tests/, docs/, or the source automations/ directory; tests/ is where a control must be free to WRITE the thing it guards, and the source directories are covered by MD-01's own body in tests/run-tests.sh, which scans skills manifest bin templates presets automations. Both control strings are assembled at run time, so a repo-wide grep over the whole tree is 0 and the row's own scope is 0 by construction.) |
57
- | `MD-02` | target | advisory | The review lane is a different model family from the code lane. | `goblin-verify --only MD-02` (builtin) | goblin-stack cannot choose the fleet's models; today's map resolves both roles to the same family. Reported as ADV, never gated. W5-6: the JUDGE lane's resolved model is compared with the code lane's too, because `JG-02` proves only that the declared profile NAMES are disjoint; it is still ADV, never gated, and the measured state of this box is recorded in `docs/LIMITS.md` #38. |
58
- | `MD-03` | target | advisory | Role-pinned fan-out goes through kanban, not a model-less subagent spawn. | `advisory` | It is a fleet-runtime property: no repo-local file can observe which tool created a worker. Enforced at board level, described in docs/INTEGRATION.md. |
59
- | `PG-01` | target | gate | The reviewed artifact is named by SHA, and that SHA exists. | `goblin-verify --only PG-01` (builtin) | — |
60
- | `PG-02` | target | script | The gate is chosen by the change, not the repo, and the tier's evidence exists. | `goblin-verify --only PG-02` (builtin) | — |
61
- | `PG-03` | target | gate | A new head voids the verdict. | `goblin-verify --only PG-03` (builtin) | — |
62
- | `PG-04` | target | advisory | Never bypass what the forge enforces. | `advisory` | Needs the forge: GitHub's restrictions do not apply to admins, and a sole-admin repo has nobody the gate binds. Not observable from the repo. |
63
- | `PG-05` | target | lint | No required check that self-skips. | `goblin-verify --only PG-05` | Text, not a YAML parser. Three clauses per job: the job declares at least one `run:`/`uses:` step (else the check runs nothing); the JOB carries no `if:` (else the whole required check self-skips); and no STEP carries an `if:` (else that step - possibly the gate step - self-skips). The old body flagged only 'every step guarded', which PASSED the real shape: in the estate's one existing workflow a deliberately UNGUARDED credential step decides whether the guarded compile step runs, so `guarded < steps` and the row reported PASS on the workflow it exists to catch (G8 section 3, re-measured V3-4). Deliberately strict, and the strictness is the point: GitHub reports a SKIPPED job as Success even when it is a required check (docs S1/S2), so a conditional step is a step that can green-light a commit whose gate never ran. False positives it cannot avoid: a `#` inside a quoted string is read as a comment, a flow-style (`jobs: {...}`) mapping is refused rather than parsed, and a conditional step that is genuinely safe is indistinguishable from the trap - the remedy is to move the condition into the declared command, or into a second job that is not the required check. It cannot see branch-protection state, the required-check list, or whether the workflow ever ran (`PG-04`), and a repo with no workflow passes with the count printed on the line. |
64
- | `PG-06` | target | gate | The gate CI runs is the gate the project declares. | `goblin-verify --only PG-06` | The declared set comes from `g_yaml_gates`, the reader `GT-01` uses, so a gate cannot vanish from the comparison in silence (G8-3). A workflow passes when the whole declared set is RUN: the verifier with no `--only` (a full run executes every declared gate through `GT-02`), or each declared gate command verbatim. It reads TEXT with comments blanked first, and only `run:` payloads and block-scalar bodies count - a command sitting in a `name:`, `env:` or `with:` value is dropped, which is what stops a comment or a label from satisfying the row. What it cannot see: that the forge marks that job a REQUIRED check, that the job is the one the forge waits on, or that the workflow can fail at all (`PG-05`). A repo with no workflow reports SKIP with that reason instead of a vacuous pass. The lane's own enforcement is the TARGET repo's, not goblin-stack's (`docs/LIMITS.md` #34): goblin-stack can prove the file invokes the gate and cannot make the forge run it, so this row is a text reading of the target's CI and never a claim that CI gated the SHA. |
65
- | `DS-01` | target | script | Runtime data is not test fixture: a gate run must not write it. | `goblin-verify --only DS-01` (builtin) | — |
66
- | `DS-02` | target | gate | Snapshot before, verify after. | `goblin-verify --only DS-02` (builtin) | — |
67
- | `DOC-01` | target | advisory | A significant change updates the docs that teach it. | `advisory` | 'Significant' is a judgement; a diff-size heuristic fails on the cases that matter. |
68
- | `DOC-02` | target | advisory | System-level changes are recorded wherever the project's standard says they live. | `advisory` | Where the recording lives may be owned by a stricter external rule than goblin-stack may add; the repo can only state it. |
34
+ | `IN-02` | target | script | Every installed file still matches its recorded hash. | goblin-verify --only IN-02 | recover: gob install --upgrade, or git checkout -- .gob/installed.json; for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: gob install --target <dir> --re-pin) |
35
+ | `IN-03` | target | script | The verifier's own manifest is complete: every rule has a check or is advisory. | goblin-verify --only IN-03 | — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script\|lint\|gate\|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in bin/goblin-verify), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine) |
36
+ | `IN-04` | target | script | No file goblin-stack did not create has been overwritten. | goblin-verify --only IN-04 | Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch. |
37
+ | `HP-01` | target | gate | HANDOFF.md exists at the root. | test -f HANDOFF.md | — |
38
+ | `HP-02` | target | lint | The HANDOFF carries its five required sections. | for h in 'START HERE' 'STATE\|STATUS' 'GATES?' 'NEXT STEPS\|NEXT' 'NOT VERIFIED\|UNVERIFIED\|NOT PROVEN\|UNPROVEN\|PENDING[^.]*DEVICE TEST'; do grep -qiE "^#{2,3}[[:space:]]+[^[:alpha:]]*($h)\b" HANDOFF.md \|\| { echo "missing section: $h"; exit 1; }; done | Partial: proves a heading exists whose first word names the slot, not that the section's content is complete. The slot may use the repo's own vocabulary (the model repo's `### NEXT` passes; `Status`, `Gate`, `Unverified`, `Pending <x> device test` are accepted); a heading that merely contains the word (`## BOARD STATE`) does not satisfy `State`. |
39
+ | `HP-03` | target | lint | Every gate number is a measurement with a date, never a copy. | goblin-verify --only HP-03 | Builtin, anchored on the gate NAMES `AGENTS.md` declares (GT-01's source of truth) rather than on a hardcoded keyword list: the shipped gates are named `commit` and `todo_ceiling`, neither of which the old list matched, so on a fresh install the only line it could see was the template's own example sentence, and a real gate line could lose its `date` and the row stayed GREEN (G8-2). A line whose text says "example of the required form" is template prose, not a gate number, and is skipped, so the template cannot satisfy the row. Partial: it proves a dated line inside the `Gates` section exists and that every gate-bearing line there carries a date - not that the number was re-measured that day, and not a gate-bearing line that names no declared gate and carries no gate-shaped keyword (so a line the config does not declare and the keyword list does not recognise is unseen). Historical gate lines outside that section are exempt by design - PROJECT-PRACTICE section 1's stale-sentence rule requires them to be kept. |
40
+ | `HP-04` | target | advisory | A stale sentence is corrected in place with a dated parenthetical, never deleted. | advisory | Detecting a silent deletion needs semantic judgement; a diff heuristic (>=5 removed non-empty lines with no 'corrected' addition) is too noisy to gate on. goblin-verify --only HP-04 prints the heuristic as a warning only. |
41
+ | `HP-05` | target | gate | The HANDOFF names the HEAD it describes. | goblin-verify --only HP-05 | Deviation from the design spec, with the reason: the spec's literal check is `grep -q "$(git rev-parse --short HEAD)" HANDOFF.md`, which can never pass - committing the HANDOFF moves HEAD, so the file can only name a commit that is now an ancestor. The mechanised form is therefore 'the HANDOFF names a commit that exists in this repo AND is an ancestor of HEAD', which still catches the defect it exists for (a review/handoff artifact that names no commit at all). |
42
+ | `SP-01` | target | gate | A *-SPEC.md file exists at the repo root (any round, not the current one - round-scoping arrives with the W6 staged chain). | ls ./*-SPEC.md >/dev/null 2>&1 | Skipped when class: D and disabled: [spec]. |
43
+ | `SP-02` | target | script | The SPEC is committed, not left untracked. | test -z "$(git ls-files --others --exclude-standard -- '*-SPEC.md')" | — |
44
+ | `SP-03` | target | lint | Every AC: item is checkable without a human. | awk '/^[[:space:]]*[-*][[:space:]]/ && /AC[0-9]*:/ && !/`\|==\|===\|exit\|<\|>/ {print; bad=1} END{exit bad}' ./*-SPEC.md | Partial: structural only - a checkable-looking bullet can still be unfalsifiable. The matcher is `AC[0-9]*:` because the shipped template writes `- AC1: ...`; the literal `/AC:/` only saw a bullet that spelled the label without a number. |
45
+ | `GT-01` | target | gate | The gate set is declared, never inferred from the stack. | goblin-verify --only GT-01 | Builtin: every `gate_<name>_cmd:` key in the AGENTS.md gob block is a DECLARED gate and its declaration and command are one line - a gate cannot lose its cmd and survive the count (G8-3, the condition of G8's own 9/10 sentence). The count is of declarations, not of runnable pairs. It cannot see whether a declared command is the RIGHT gate for the project: it proves a command exists, not that it is meaningful. |
46
+ | `GT-02` | target | gate | Every declared gate runs and exits 0. | goblin-verify --only GT-02 | — |
47
+ | `GT-03` | target | script | The round reports one line of measured numbers. | test -f .gob/last-gate-line && [ .gob/last-gate-line -nt "$(git rev-parse --git-dir)/logs/HEAD" ] | The freshness reference is HEAD's reflog (`.git/logs/HEAD`), which every HEAD movement rewrites - a commit in an attached or a detached worktree included, and independently of whether the refs are packed. The clause it replaced read `.git/HEAD`, a file a commit never rewrites (only branch operations do), so a commit landing after the measured line left the row GREEN while the round had moved on (D2, measured: `.git/HEAD`'s mtime unchanged across a real commit, `--only GT-03` exit 0). Two limits remain, recorded rather than hidden: a repo with the reflog disabled (`core.logAllRefUpdates=false`) has no reference to compare against, and `-nt` against a missing path is true, so the clause passes vacuously and the row then proves only that a measured line EXISTS (a repo with no commit yet is the same case); and the clause reads ANY HEAD movement as staleness, so a checkout, a branch rename or a reset FAILs it until the next gate run rewrites the line. That second behaviour is what makes a full run self-freshening: `GT-02` writes the line earlier in the same pass, so the row asserts that the line in front of you came from THIS run. `tests/t-gt03-freshness.sh` is the control, in both directions. |
48
+ | `GT-04` | target | gate | A ratchet is declared with a ceiling. | goblin-verify --only GT-04 | — |
49
+ | `GT-05` | target | gate | The ratchet has not risen. | goblin-verify --only GT-05 | — |
50
+ | `HS-01` | target | lint | Asserting harnesses follow the house shape. | goblin-verify --only HS-01 | A declared harness_dir that is absent FAILS when the class scaffolds one (config `scaffold_checks: yes`, classes A and C); a class that ships no harness dir (B/D/E) SKIPs with a reason. Keying the skip off the path alone let one config line switch this row and HS-02 off. A report utility in the same dir is counted and reported separately rather than failing the run. |
51
+ | `HS-02` | target | gate | A check green on both trees proves nothing - the REPLAY must show RED pre-change. | goblin-verify --only HS-02 | The declared `replay.cmd` is EXECUTED with `{name}` replaced by each harness's name, in the pre-change worktree, with `replay.env=<commit>` set (Z1-4). Two clauses that used to be unheard: a command that interpolates no `{name}` FAILs (it cannot be running the harness it names, so nothing was replayed), and a command that cannot be executed at all - exit 126 or 127 - FAILs rather than counting as a RED harness. The harness's name is substituted SHELL-QUOTED (`printf %q`), because the name is part of the command text the shell parses; unquoted, a name carrying `;` or `#` reached the shell as syntax (Z2-3, the G8-1 surface class), and the control for it carries a metacharacter-bearing file name. What it cannot see: a command that runs *something else* under the harness's name and exits non-zero, and whether the harness tests the right path rather than merely failing on this tree. |
52
+ | `HS-03` | target | advisory | Source probes read text with comments blanked first. | advisory | Recognising 'this probe reads source text' is semantic; a grep for the blanking helper produces false FAILs on harnesses that do not probe source. |
53
+ | `CM-01` | target | gate | Commits carry the owner identity, not an ambient one. Current scope: this gates the identity of HEAD (the last commit) at the moment of the run - earlier commits by other authors are not scanned. | test "$(git log -1 --format='%ae')" = "$(grep '^owner_email:' AGENTS.md \| cut -d' ' -f2)" | note: this repo records a different owner (you are probably new here) — commit with git -c user.email=<owner_email> --author=<owner_email>, or update owner_email: in AGENTS.md |
54
+ | `CM-02` | target | advisory | The commit message was written to a file, not passed with -m. | advisory | A backtick lost to command substitution leaves no trace a later check can read. Reported as a heuristic (unbalanced backticks in a body) only. |
55
+ | `CM-03` | target | script | Commit-as-you-go: the working tree is not carrying a dead run's work. | goblin-verify --only CM-03 | — |
56
+ | `MD-01` | target | lint | No model name is hardcoded in any reusable rule. | for d in skills manifest bin templates presets .gob .hermes; do [ -d "$d" ] \|\| continue; grep -rniE '(d[e]epseek\|cl[a]ude\|g[p]t-[0-9]\|gr[o]k\|g[e]mini\|g[l]m-[0-9]\|k[i]mi)[a-z0-9.:_-]*' "$d" && exit 1; done; exit 0 | — (the pattern is written with character classes so this row cannot match itself; tests/t-verify-red.sh proves it still catches a real model name. SCOPE, stated so a repo-wide grep is not re-reported as a hole (G8-10): this row reads the seven directories an install writes rules into - skills/ manifest/ bin/ templates/ presets/ .gob/ .hermes/. It does NOT read tests/, docs/, or the source automations/ directory; tests/ is where a control must be free to WRITE the thing it guards, and the source directories are covered by MD-01's own body in tests/run-tests.sh, which scans skills manifest bin templates presets automations. Both control strings are assembled at run time, so a repo-wide grep over the whole tree is 0 and the row's own scope is 0 by construction.) |
57
+ | `MD-02` | target | advisory | The review lane is a different model family from the code lane. | goblin-verify --only MD-02 | goblin-stack cannot choose the fleet's models; today's map resolves both roles to the same family. Reported as ADV, never gated. W5-6: the JUDGE lane's resolved model is compared with the code lane's too, because `JG-02` proves only that the declared profile NAMES are disjoint; it is still ADV, never gated, and the measured state of this box is recorded in `docs/LIMITS.md` #38. |
58
+ | `MD-03` | target | advisory | Role-pinned fan-out goes through kanban, not a model-less subagent spawn. | advisory | It is a fleet-runtime property: no repo-local file can observe which tool created a worker. Enforced at board level, described in docs/INTEGRATION.md. |
59
+ | `PG-01` | target | gate | The reviewed artifact is named by SHA, and that SHA exists. | goblin-verify --only PG-01 | — |
60
+ | `PG-02` | target | script | The gate is chosen by the change, not the repo, and the tier's evidence exists. | goblin-verify --only PG-02 | — |
61
+ | `PG-03` | target | gate | A new head voids the verdict. | goblin-verify --only PG-03 | — |
62
+ | `PG-04` | target | advisory | Never bypass what the forge enforces. | advisory | Needs the forge: GitHub's restrictions do not apply to admins, and a sole-admin repo has nobody the gate binds. Not observable from the repo. |
63
+ | `PG-05` | target | lint | No required check that self-skips. | goblin-verify --only PG-05 | Text, not a YAML parser. Three clauses per job: the job declares at least one `run:`/`uses:` step (else the check runs nothing); the JOB carries no `if:` (else the whole required check self-skips); and no STEP carries an `if:` (else that step - possibly the gate step - self-skips). The old body flagged only 'every step guarded', which PASSED the real shape: in the estate's one existing workflow a deliberately UNGUARDED credential step decides whether the guarded compile step runs, so `guarded < steps` and the row reported PASS on the workflow it exists to catch (G8 section 3, re-measured V3-4). Deliberately strict, and the strictness is the point: GitHub reports a SKIPPED job as Success even when it is a required check (docs S1/S2), so a conditional step is a step that can green-light a commit whose gate never ran. False positives it cannot avoid: a `#` inside a quoted string is read as a comment, a flow-style (`jobs: {...}`) mapping is refused rather than parsed, and a conditional step that is genuinely safe is indistinguishable from the trap - the remedy is to move the condition into the declared command, or into a second job that is not the required check. It cannot see branch-protection state, the required-check list, or whether the workflow ever ran (`PG-04`), and a repo with no workflow passes with the count printed on the line. |
64
+ | `PG-06` | target | gate | The gate CI runs is the gate the project declares. | goblin-verify --only PG-06 | The declared set comes from `g_yaml_gates`, the reader `GT-01` uses, so a gate cannot vanish from the comparison in silence (G8-3). A workflow passes when the whole declared set is RUN: the verifier with no `--only` (a full run executes every declared gate through `GT-02`), or each declared gate command verbatim. It reads TEXT with comments blanked first, and only `run:` payloads and block-scalar bodies count - a command sitting in a `name:`, `env:` or `with:` value is dropped, which is what stops a comment or a label from satisfying the row. What it cannot see: that the forge marks that job a REQUIRED check, that the job is the one the forge waits on, or that the workflow can fail at all (`PG-05`). A repo with no workflow reports SKIP with that reason instead of a vacuous pass. The lane's own enforcement is the TARGET repo's, not goblin-stack's (`docs/LIMITS.md` #34): goblin-stack can prove the file invokes the gate and cannot make the forge run it, so this row is a text reading of the target's CI and never a claim that CI gated the SHA. |
65
+ | `DS-01` | target | script | Runtime data is not test fixture: a gate run must not write it. | goblin-verify --only DS-01 | — |
66
+ | `DS-02` | target | gate | Snapshot before, verify after. | goblin-verify --only DS-02 | — |
67
+ | `DOC-01` | target | advisory | A significant change updates the docs that teach it. | advisory | 'Significant' is a judgement; a diff-size heuristic fails on the cases that matter. |
68
+ | `DOC-02` | target | advisory | System-level changes are recorded wherever the project's standard says they live. | advisory | Where the recording lives may be owned by a stricter external rule than goblin-stack may add; the repo can only state it. |
69
69
  | `SK-01` | target | lint | Every shipped skill has name + description frontmatter. | goblin-verify --only SK-01 | W1: the check is a builtin so the engine.mode=global clause can run - in global mode the procedure tier is emitted per platform (not carried in this repo) and the row SKIPs with that reason instead of passing vacuously on a repo with no skills (§2.5 names SK-01 alongside SK-02/SK-04). In vendored mode it is exactly the old loop: every SKILL.md must open with frontmatter carrying both name and description. |
70
- | `SK-02` | target | script | The installed skills match their recorded hashes (no drift). | `goblin-verify --only SK-02` (builtin) | — |
71
- | `SK-03` | target | script | A rule with no mechanism is labelled advisory, and the advisory count is reported. | `goblin-verify --only SK-03` (builtin) | — |
70
+ | `SK-02` | target | script | The installed skills match their recorded hashes (no drift). | goblin-verify --only SK-02 | — |
71
+ | `SK-03` | target | script | A rule with no mechanism is labelled advisory, and the advisory count is reported. | goblin-verify --only SK-03 | — |
72
72
  | `SK-04` | target | lint | Every shipped skill says what it cannot see. | goblin-verify --only SK-04 | Partial: proves the section exists, not that what it says is complete or true - the limit every prose rule carries. Every shipped skill already carries it, so the row is GREEN on a fresh install and RED only under a real violation. W1: the check is a builtin so the engine.mode=global clause can run - in global mode the procedure tier is emitted per platform (not carried in this repo) and the row SKIPs with that reason. |
73
- | `PT-01` | target | lint | No tenant-specific string inside a reusable rule. | `for d in skills manifest bin templates presets .goblin .hermes; do [ -d "$d" ] \|\| continue; grep -rniE --exclude=goblin.yaml --exclude=installed.json '(h[a]rvey\|tech-g[o]blin\|/h[o]me/[a-z]+\|g[o]blin-ui\|op[e]n-door\|sup[r]eme\|bb[t]ech\|c[l]v)' "$d" && exit 1; done; exit 0` | — (the same directory list MD-01 uses: the rules an install actually writes live in `.goblin/` and `.hermes/`, not in the source layout. Two documented exceptions: `.goblin/goblin.yaml`, which holds `models_file:`/`practice:` - per-machine config, not a rule - and `.goblin/installed.json`, which since W1 records the machine's absolute `engine_dir` in its `engine:` block - both per-machine facts, not rules - each excluded by name. The pattern is written with character classes so this row cannot match itself.) |
74
- | `PT-02` | target | gate | The default branch is declared, not assumed. | `goblin-verify --only PT-02` (builtin) | — |
75
- | `CL-01` | target | script | Every part the class requires is present, and every part it forbids is absent. | `goblin-verify --only CL-01` (builtin) | — |
76
- | `CL-02` | target | script | An archive: true project verifies GREEN without a HANDOFF or gates. | `goblin-verify --only CL-02` (builtin) | Falsifiable: FAILs when `archive:` is not `true`/`false`, and when the config's value disagrees with the one the install recorded in `.goblin/installed.json` (so the waiver cannot be flipped on by hand). It cannot observe the *effect* of the waiver on the other rows without re-entering the runner. |
77
- | `SC-01` | target | lint | No secret file is tracked. | `n=$(git ls-files \| grep -iE '(^\|/)\.env\|\.pem$\|\.key$' \| grep -vcE '\.(example\|sample\|template)$'); printf '%s tracked secret file(s)\n' "$n"; [ "$n" = 0 ]` | Partial: it sees tracked PATHS, never contents - a secret pasted into a tracked file is invisible here, and the pattern is a name family, so a credential inside `config.ts` is missed by construction. |
78
- | `SC-02` | target | gate | The ignore rules cover the whole secret family. | `goblin-verify --only SC-02` | Builtin, and behavioural: clause 1 reads `.gitignore`; clause 2 asks git's own matcher (`git check-ignore`) for `.env`, `.env.local` and `.env.production` one path at a time, so a rule that looks right but does not match still fails. It cannot see a secret already in git history, or one committed under a name the family does not cover. SKIPs with a reason when there is no `.gitignore` and no `package.json`. |
79
- | `SC-03` | target | lint | No client-visible name is secret-shaped, and no build output carries a secret literal. | `goblin-verify --only SC-03` | Builtin, two clauses: the `NEXT_PUBLIC_*_(SECRET\|TOKEN\|KEY\|PASSWORD\|PRIVATE)` name pattern over source, and known secret prefixes over the declared build output. Partial: a prefix pattern cannot see a secret that does not look like one, and clause 2 says "no build output to scan" on the line rather than skipping silently. The source scan excludes `.goblin/` and `.hermes/`, so it cannot match the row text that describes it. |
80
- | `SC-04` | target | lint | Every cookie write carries its flags. | `goblin-verify --only SC-04` | Builtin, same-statement only: a write spread over three lines is not seen, and the row says so. It REPORTS that a JS-written cookie is readable by any script rather than failing on it - that is a design fact, not a bug - so the pass is about the flags, never about the choice. |
81
- | `SC-05` | target | gate | Every write route validates its input, or is waived. | `goblin-verify --only SC-05` | Builtin: it proves a validator is CALLED (`safeParse\|zod\|valibot\|yup\|ajv\|superstruct\|validate(`), never that the schema is right - a schema that accepts everything passes. `.goblin/boundary-waivers` is the escape hatch, and the waived count is printed, so a silent pile-up is visible. |
82
- | `SC-06` | target | gate | A lockfile exists, and the repo tracks it. | `goblin-verify --only SC-06` | Builtin: presence, then `git ls-files --error-unmatch`. It cannot see that the lockfile is STALE relative to `package.json` - resolving that needs the package manager, which is a deliberate network-shaped step, not a check. SKIPs with a reason when there is no `package.json`. |
83
- | `SC-07` | target | gate | The dependency audit record is present, dated, fresh, and clean-or-waived. | `goblin-verify --only SC-07` | Builtin, OFFLINE by construction: it reads the record and never the network. The record comes from `.goblin/bin/goblin-audit`, run once, deliberately; the waiver count is printed on the gate line so the debt is loud even when the row passes. It cannot see an advisory the registry did not know on the day the record was taken. SKIPs with a reason when no record exists yet. |
84
- | `SC-08` | target | gate | No dependency runs an install-time script that is not on the allowlist. | `goblin-verify --only SC-08` | Builtin over `package-lock.json`'s `hasInstallScript`: a pnpm/yarn lockfile has no such field, so those repos get a SKIP with that reason rather than a vacuous pass, and a MINIFIED (one-line) lockfile is read like a pretty-printed one - the text is normalised into the pretty shape before the line-anchored reader sees it, so a hook inside a one-line lock FAILs instead of being reported as `0 install hook(s)` (Z2-2: before that, a one-line lock PASSED vacuously). A `lockfileVersion` 1 lock records its hooks under `dependencies`, not under `node_modules/` keys, so the line-anchored reader enumerates nothing there either and prints the same `0 install hook(s), 0 allowlisted` PASS over a surface it did not read (measured at 0.4.2; npm <= 6 records no `hasInstallScript` at all, so for a genuine v1 lock this is blindness rather than a wrong answer). Lowest-value row of the ten - regression detection - and the first to cut if the matrix gets heavy. |
85
- | `SC-09` | target | advisory | Auth is applied consistently across sibling routes. | `advisory` | Prose on purpose: "consistently" is a semantic judgement about a private surface no repo here has yet. Counted (9 of ceiling 10) so the matrix cannot quietly grow prose. |
86
- | `PF-01` | target | lint | The perf baseline names the commit it measured. | `goblin-verify --only PF-01` | Builtin: the metric must equal `ratchet.name` so the budget and the measurement cannot silently disagree, the value must be numeric, the date must exist, the baseline commit must exist AND be an ancestor of HEAD (`HP-05`'s mechanic, reused rather than re-derived), and `ratchet.ceiling` must equal `perf.baseline_value` - otherwise a one-line ceiling raise passes while the row prints the contradiction, which is `I raised the budget and never measured again` (G8-6b). It cannot see whether the metric is the right one for the product, and it never re-measures: re-anchoring is a deliberate operator action. SKIPs with a reason when the class declares no metric, or none has been recorded yet. |
73
+ | `PT-01` | target | lint | No tenant-specific string inside a reusable rule. | for d in skills manifest bin templates presets .gob .hermes; do [ -d "$d" ] \|\| continue; grep -rniE --exclude=AGENTS.md --exclude=installed.json '(h[a]rvey\|tech-g[o]blin\|/h[o]me/[a-z]+\|g[o]blin-ui\|op[e]n-door\|sup[r]eme\|bb[t]ech\|c[l]v)' "$d" && exit 1; done; exit 0 | — (the same directory list MD-01 uses: the rules an install actually writes live in `.gob/` and `.hermes/`, not in the source layout. Two documented exceptions: `AGENTS.md`, which holds `models_file:`/`practice:` - per-machine config, not a rule - and `.gob/installed.json`, which since W1 records the machine's absolute `engine_dir` in its `engine:` block - both per-machine facts, not rules - each excluded by name. The pattern is written with character classes so this row cannot match itself.) |
74
+ | `PT-02` | target | gate | The default branch is declared, not assumed. | goblin-verify --only PT-02 | — |
75
+ | `CL-01` | target | script | Every part the class requires is present, and every part it forbids is absent. | goblin-verify --only CL-01 | — |
76
+ | `CL-02` | target | script | An archive: true project verifies GREEN without a HANDOFF or gates. | goblin-verify --only CL-02 | Falsifiable: FAILs when `archive:` is not `true`/`false`, and when the config's value disagrees with the one the install recorded in `.gob/installed.json` (so the waiver cannot be flipped on by hand). It cannot observe the *effect* of the waiver on the other rows without re-entering the runner. |
77
+ | `SC-01` | target | lint | No secret file is tracked. | n=$(git ls-files \| grep -iE '(^\|/)\.env\|\.pem$\|\.key$' \| grep -vcE '\.(example\|sample\|template)$'); printf '%s tracked secret file(s)\n' "$n"; [ "$n" = 0 ] | Partial: it sees tracked PATHS, never contents - a secret pasted into a tracked file is invisible here, and the pattern is a name family, so a credential inside `config.ts` is missed by construction. |
78
+ | `SC-02` | target | gate | The ignore rules cover the whole secret family. | goblin-verify --only SC-02 | Builtin, and behavioural: clause 1 reads `.gitignore`; clause 2 asks git's own matcher (`git check-ignore`) for `.env`, `.env.local` and `.env.production` one path at a time, so a rule that looks right but does not match still fails. It cannot see a secret already in git history, or one committed under a name the family does not cover. SKIPs with a reason when there is no `.gitignore` and no `package.json`. |
79
+ | `SC-03` | target | lint | No client-visible name is secret-shaped, and no build output carries a secret literal. | goblin-verify --only SC-03 | Builtin, two clauses: the `NEXT_PUBLIC_*_(SECRET\|TOKEN\|KEY\|PASSWORD\|PRIVATE)` name pattern over source, and known secret prefixes over the declared build output. Partial: a prefix pattern cannot see a secret that does not look like one, and clause 2 says "no build output to scan" on the line rather than skipping silently. The source scan excludes `.gob/` and `.hermes/`, so it cannot match the row text that describes it. |
80
+ | `SC-04` | target | lint | Every cookie write carries its flags. | goblin-verify --only SC-04 | Builtin, same-statement only: a write spread over three lines is not seen, and the row says so. It REPORTS that a JS-written cookie is readable by any script rather than failing on it - that is a design fact, not a bug - so the pass is about the flags, never about the choice. |
81
+ | `SC-05` | target | gate | Every write route validates its input, or is waived. | goblin-verify --only SC-05 | Builtin: it proves a validator is CALLED (`safeParse\|zod\|valibot\|yup\|ajv\|superstruct\|validate(`), never that the schema is right - a schema that accepts everything passes. `.gob/boundary-waivers` is the escape hatch, and the waived count is printed, so a silent pile-up is visible. |
82
+ | `SC-06` | target | gate | A lockfile exists, and the repo tracks it. | goblin-verify --only SC-06 | Builtin: presence, then `git ls-files --error-unmatch`. It cannot see that the lockfile is STALE relative to `package.json` - resolving that needs the package manager, which is a deliberate network-shaped step, not a check. SKIPs with a reason when there is no `package.json`. |
83
+ | `SC-07` | target | gate | The dependency audit record is present, dated, fresh, and clean-or-waived. | goblin-verify --only SC-07 | Builtin, OFFLINE by construction: it reads the record and never the network. The record comes from `.gob/bin/goblin-audit`, run once, deliberately; the waiver count is printed on the gate line so the debt is loud even when the row passes. It cannot see an advisory the registry did not know on the day the record was taken. SKIPs with a reason when no record exists yet. |
84
+ | `SC-08` | target | gate | No dependency runs an install-time script that is not on the allowlist. | goblin-verify --only SC-08 | Builtin over `package-lock.json`'s `hasInstallScript`: a pnpm/yarn lockfile has no such field, so those repos get a SKIP with that reason rather than a vacuous pass, and a MINIFIED (one-line) lockfile is read like a pretty-printed one - the text is normalised into the pretty shape before the line-anchored reader sees it, so a hook inside a one-line lock FAILs instead of being reported as `0 install hook(s)` (Z2-2: before that, a one-line lock PASSED vacuously). A `lockfileVersion` 1 lock records its hooks under `dependencies`, not under `node_modules/` keys, so the line-anchored reader enumerates nothing there either and prints the same `0 install hook(s), 0 allowlisted` PASS over a surface it did not read (measured at 0.4.2; npm <= 6 records no `hasInstallScript` at all, so for a genuine v1 lock this is blindness rather than a wrong answer). Lowest-value row of the ten - regression detection - and the first to cut if the matrix gets heavy. |
85
+ | `SC-09` | target | advisory | Auth is applied consistently across sibling routes. | advisory | Prose on purpose: "consistently" is a semantic judgement about a private surface no repo here has yet. Counted (9 of ceiling 10) so the matrix cannot quietly grow prose. |
86
+ | `PF-01` | target | lint | The perf baseline names the commit it measured. | goblin-verify --only PF-01 | Builtin: the metric must equal `ratchet.name` so the budget and the measurement cannot silently disagree, the value must be numeric, the date must exist, the baseline commit must exist AND be an ancestor of HEAD (`HP-05`'s mechanic, reused rather than re-derived), and `ratchet.ceiling` must equal `perf.baseline_value` - otherwise a one-line ceiling raise passes while the row prints the contradiction, which is `I raised the budget and never measured again` (G8-6b). It cannot see whether the metric is the right one for the product, and it never re-measures: re-anchoring is a deliberate operator action. SKIPs with a reason when the class declares no metric, or none has been recorded yet. |
87
87
  | `AU-01` | target | lint | An automation's producer is deterministic and network-free. | goblin-verify --only AU-01 | Partial: proves that no line of a producer begins with a network or forge verb, not that the script is otherwise deterministic. W1: in global mode the first producer glob resolves under the engine dir (the resolution chain); the repo-local globs stay. New clause: a repo with NO producer anywhere (repo or engine) SKIPs with `no automation producer found (repo or engine)` instead of FAILing — the old FAIL was a born-RED artifact of the glob; a repo that declares its own automations but has no producer still FAILs. |
88
- | `AU-02` | target | script | A report's dedup key is a function of content only - no date, no run id. | `goblin-verify --only AU-02` (builtin) | Builtin: it recomputes the key from the report's own `repo` and `symptom` and requires the recorded `dedup_key` to equal it, then refuses a key carrying a date. Skipped with a reason when the repo holds no report - nothing to dedup. It cannot see whether two reports should have been one: a normalisation that merges two genuinely different symptoms is a duplicate card, not a lost report. |
89
- | `AU-03` | target | gate | A reporter run leaves the tree and the harness untouched. | `goblin-verify --only AU-03` (builtin) | Builtin: asserts a clean working tree, and - when HEAD is a reporter commit - that no path under the declared harness_dir appears in it. Skipped with a reason when the repo holds no reports/ - no reporter has run here. It cannot see a reporter that edited the tree and committed the edit as part of the report. |
88
+ | `AU-02` | target | script | A report's dedup key is a function of content only - no date, no run id. | goblin-verify --only AU-02 | Builtin: it recomputes the key from the report's own `repo` and `symptom` and requires the recorded `dedup_key` to equal it, then refuses a key carrying a date. Skipped with a reason when the repo holds no report - nothing to dedup. It cannot see whether two reports should have been one: a normalisation that merges two genuinely different symptoms is a duplicate card, not a lost report. |
89
+ | `AU-03` | target | gate | A reporter run leaves the tree and the harness untouched. | goblin-verify --only AU-03 | Builtin: asserts a clean working tree, and - when HEAD is a reporter commit - that no path under the declared harness_dir appears in it. Skipped with a reason when the repo holds no reports/ - no reporter has run here. It cannot see a reporter that edited the tree and committed the edit as part of the report. |
90
90
  | `AU-04` | target | lint | An automation's skill declares its own write surface. | goblin-verify --only AU-04 | Partial: proves every installed automation skill carries a `## Write surface` section. W1: the check is a builtin so the engine.mode=global clause can run - in global mode the procedure tier is emitted per platform (not carried in this repo) and the row SKIPs with that reason. |
91
- | `BN-00` | target | script | Every ban has an enforcement row, every ban row names a replacement, and the ban table is not empty. | `goblin-verify --only BN-00` (builtin) | — (this row is the reason the ban list cannot decay into prose: IN-03's shape applied to bans.tsv, and it agrees in both directions) |
92
- | `BN-01` | target | lint | No `any` in application TypeScript. | `goblin-verify --only BN-01` (builtin) | Text probe, not an AST: a `: any` inside a string or a comment is reported, and `Record<string, any>` (no leading colon) is missed. The AST form needs a parser the no-npm contract (docs/CONTRACTS.md) forbids (docs/LIMITS.md #27). SKIPs when the ban is not in `bans:` or its globs match no file. |
93
- | `BN-02` | target | lint | No `@ts-ignore` / `@ts-expect-error` suppressions. | `goblin-verify --only BN-02` (builtin) | Text probe: it sees the directive wherever it appears, including inside a string, and cannot tell a suppression hiding a real error from one on a line that would compile anyway. SKIPs when the ban is not in `bans:` or its globs match no file. |
94
- | `BN-03` | target | lint | No direct network call from a component. | `goblin-verify --only BN-03` (builtin) | Text probe over the declared component globs: it stops the call and cannot tell whether a data layer was written or the call merely moved into a helper. SKIPs when the ban is not in `bans:` or its globs match no file. |
95
- | `BN-05` | target | lint | No import across a declared layer boundary. | `goblin-verify --only BN-05` (builtin) | Reads the `layers:` list; an empty list SKIPs with a reason, never a vacuous pass. It matches an import path naming the target directory's last segment - module aliases and dynamic imports are not seen. SKIPs when no file matches its globs. |
96
- | `BN-06` | target | lint | No renderer with Node access (`nodeIntegration: true`). | `goblin-verify --only BN-06` | Text probe over the ban table's globs, the same mechanism as BN-01..BN-05: it sees `nodeIntegration: true` wherever it appears, including inside a string, and cannot see a webPreferences object built at run time or spread in from another module. The STRONGER form is a runtime measurement - the renderer prints `process.contextIsolated` and `process.sandboxed` and the check requires true/true - and that needs a real Electron process, which the dependency contract (docs/CONTRACTS.md) does not allow a shipped rule to launch: it is the project's host gate (docs/LIMITS.md #34). SKIPs when the ban is not in `bans:` or its globs match no file. |
97
- | `BN-07` | target | lint | No renderer with context isolation or the process sandbox turned off. | `goblin-verify --only BN-07` | One probe for two properties because Electron's own documentation makes them one: disabling `contextIsolation` "also disables process sandboxing", so a repo that has turned either off has lost both. Text probe, with the same false-positive set as BN-06. SKIPs when the ban is not in `bans:` or its globs match no file. |
98
- | `BN-08` | target | lint | No dangerous webPreferences. | `goblin-verify --only BN-08` | Four one-line patterns from Electron's own security checklist (`webSecurity: false`, `allowRunningInsecureContent: true`, `enableBlinkFeatures`, `<webview allowpopups>`). Text probe: `enableBlinkFeatures` is banned by name rather than by value, so the string is reported even in a comment. SKIPs when the ban is not in `bans:` or its globs match no file. |
99
- | `BN-09` | target | lint | No synchronous IPC and no `@electron/remote`. | `goblin-verify --only BN-09` | The banned-list shape the wave's note 9 asks for, applied to Electron: `sendSync(` and `@electron/remote` block the renderer's own thread, which is the freeze the class exists to prevent. Text probe - it sees the call site, not the call graph, so a wrapper around `sendSync` in a file the globs do not match is missed. SKIPs when the ban is not in `bans:` or its globs match no file. |
100
- | `FM-01` | target | lint | Every feature file is indexed from the map README, declares its slug and at least one entry path, and carries the four-H2 entry contract. | `goblin-verify --only FM-01` (builtin) | SKIPs (exit 3) when feature_map: is empty - a fresh install has no map and must not be born RED (the D8 shape). When a map IS declared: the README must exist, every features/*.md must be linked from it in the (./<slug>.md) form and every relative .md link must resolve, each feature file's `feature:` must equal its filename stem, it must declare >=1 `entry_paths:`, and its H2s must be exactly Sub-features / How to get to it (user POV) / Driving it with <harness> / Gotchas, in that order. Partial: the README's own H2s are prose this row does not read, and 'the map lists every user-facing feature' is not mechanically checkable - that is docs/LIMITS.md #30, not a row. |
101
- | `FM-02` | target | lint | Every entry point a feature declares still resolves in source, and no entry path changed after the map was verified. | `goblin-verify --only FM-02` (builtin) | SKIPs (exit 3) when feature_map: is empty. A tripwire, not a proof. The token is searched under source_root with occurrences under the map's own directory excluded - without that exclusion the map's own entry-path list satisfies the search and the row could never go RED. Freshness is `git log -1 --format=%cs` on the resolved file against the feature's `verified:` date, and git sees a FILE change, not a behaviour change: the row can be RED-when-stale and never GREEN-means-fresh. A token that also occurs in a vendored copy or a build artifact is read as resolved, and a `verified:` date is itself a claim the row cannot test (docs/LIMITS.md #30). W5-4: the search skips the harness's own directories (`.goblin/`, `.hermes/`, the declared `harness_dir`) and the map's own directory, so a stub map whose token occurs only in the install no longer resolves; a token that occurs only in the target's own `docs/`, `tests/` or build output still does (docs/LIMITS.md #37). Z1-6: the resolved file must be TRACKED (`git ls-files --error-unmatch`) before its date is compared - an untracked file used to make the freshness clause skip in silence, so a map could claim `verified: 2020-01-01` over source that was never committed. |
102
- | `VA-01` | target | gate | The generated verification skill's doctor command runs and exits 0. | `goblin-verify --only VA-01` (builtin) | SKIPs (exit 3) when verify_doctor: is empty (the replay.commit: "" shape). Runs the DECLARED command exactly as GT-02 runs a declared gate, and never a string read out of file content (the v0.2 blocker). Closes P6's stated-but-unenforced clause 'a generated skill that was never executed is a draft': the doctor is the smallest executable proof that the skill's own instructions still run - and it proves only that, never that the doctor tests the right path. |
103
- | `JG-01` | target | script | A judge verdict is recorded as a row whose evidence resolves. | `goblin-verify --only JG-01` (builtin) | Builtin: it proves the named handle EXISTS - a commit `git rev-list --all` knows, a path under the root, or a `sha256:` matching a file under `.goblin/loop/` - never that the handle SUPPORTS the verdict beside it: a judge may cite a real commit that has nothing to do with the claim. It also cannot see whether the verdict was reached by the declared judge lane. A row with fewer columns than the header is a FAIL, not a silent skip. SKIPs (exit 3) when there is no loop record: nothing to check is reported as a reason, never as a pass. |
104
- | `JG-02` | target | gate | The judge lane is disjoint from the author lane. | `goblin-verify --only JG-02` (builtin) | Builtin: it proves the DECLARED lane sets are disjoint (profile names), not that a judge ran on one - no repo-local file observes which profile ran (the blindness `MD-03` records), and family equality is not this row's business (`MD-02` owns it). An unresolved lane is an ADV carrying the one-line remedy, never a FAIL: a box with no mapping file must not be failed for the fleet's routing. Both sentinels count as unresolved - `?` (the installed path) and `unknown` (the checkout path) - because a check that greps for one passes on the other. |
105
- | `JG-03` | target | advisory | A judge lane that has never returned a non-`done` verdict is escalated. | `advisory` | A lane that has returned two verdicts cannot be called always-yes, and a repo sees only its own rows; the history that would show a bad lane lives across cards and repos. The counter-measure is policy - one known-red control verdict per wave, recorded in docs/LOOP.md - and it is counted, not enforced. |
106
- | `LP-01` | target | script | The exit predicate is a command, and it ran before iteration 1. | `goblin-verify --only LP-01` (builtin) | Builtin: it proves the predicate is exactly ONE command and that a recorded first run exists whose timestamp is at or before the first log row - not that the command was actually run, and not that the `exit=` value was measured rather than typed (`HP-03`'s defect, one artifact over). It deliberately does NOT re-run the predicate: a stop condition is legitimately red before the loop finishes, so re-running it would fail a repo whose loop is working, and PROJECT-PRACTICE section 7 forbids a probe that writes. Timestamps compare on their `YYYY-MM-DDThh:mm:ss` prefix, so records written in different UTC offsets compare as text. |
107
- | `LP-02` | target | script | The predicate is pinned at loop start and is never relaxed. | `goblin-verify --only LP-02` (builtin) | Builtin: it proves the predicate file on disk still hashes to the recorded pin - never that the predicate is the RIGHT one, and never who edited it. The digest covers the predicate file alone: a loop that quietly relaxes a predicate living somewhere else is not seen. The remedy is printed with the failure and is never automatic, the `IN-02` `--re-pin` shape. W5-7: a close-and-reopen is auditable - every `closed-<date>/` archive must hold its predicate AND the pin it was closed under, and the live pin must name the archived digest on a `previous:` line - which catches a SILENT relaxation, never a WEAKER one (nothing in bash judges that: `docs/LIMITS.md` #39). |
108
- | `LP-03` | target | gate | The loop declares a budget, and the record never exceeds it. | `goblin-verify --only LP-03` (builtin) | Builtin: it proves the declared budget is a positive integer at or under the configured `loop_max_turns_ceiling`, and that the record holds no more verdict rows than the budget - not that the budget is affordable. Cost is not a field the record carries: a turn budget bounds turns, and the auxiliary judge call per turn is unpriced (`docs/LIMITS.md` #32). A worker cannot edit its own card body, so a CARD predicate is already protected; this row is the file-predicate half. |
109
- | `LP-04` | target | gate | A loop making no progress stops instead of thrashing. | `goblin-verify --only LP-04` (builtin) | Builtin: it measures a CHANGED EVIDENCE POINTER, which is a proxy for progress, not progress. Three consecutive verdict rows with an identical non-empty pointer and a non-`predicate:green` result is a FAIL that names the row numbers. A loop that edits a file each turn to keep the pointer moving is not caught, which is why LP-05 and the budget sit beside it. Nothing in the Hermes kanban goal loop detects a lack of progress at all - `run_kanban_goal_loop` carries no progress state. |
110
- | `LP-05` | target | script | A loop that ended without its predicate green carries a written-up reason. | `goblin-verify --only LP-05` (builtin) | Builtin: the three-non-blank-line threshold is arbitrary and stated as such - it proves a write-up EXISTS, the way `SP-03` proves a bullet LOOKS checkable. Nothing can make the write-up true, and the row cannot see whether it names the right dead end or whether the loop stopped too early. A loop whose last row IS `predicate:green` needs no write-up and the row passes without reading one. |
111
- | `RC-01` | target | gate | No file in the shipping tree matches a reference-corpus hash. | `goblin-verify --only RC-01` | Four clauses: (1) `reference_manifest:` empty -> SKIP with that reason (the `FM-01`/`VA-01` shape - not born RED); (2) declared but missing or unparseable -> FAIL, never a SKIP; (3) any file under the declared `security: build_output:` whose sha256 appears in an entry's sha256 -> FAIL, naming path + entry; (4) the manifest itself must not sit inside the build output, at the declared path or as a byte-identical copy - it is a listing of every corpus hash, so shipping it ships the corpus's shape. Cannot see: a re-encoded/resized/recoloured asset (level 2), copied text in a shipped string (level 3), or a manifest authored weak - that is `RC-02`. |
112
- | `RC-02` | target | lint | The reference manifest is shaped so `RC-01` cannot pass vacuously. | `goblin-verify --only RC-02` | Schema `reference-manifest/1`; `generated_from` non-empty; `reference_app.package`/`.version`/`apk_sha256` (64 hex); `entries` non-empty; every entry carries `path`, 64-hex `sha256`, numeric `bytes`; `entry_count` equals `len(entries)`. SKIPs with `RC-01`'s reason when the key is empty. Exists because `RC-01` alone carries the `bans.tsv` weakness: a weakened input passes the check it feeds (`LIMITS.md` #28). It cannot see whether the entries are the *right* hashes - that is as strong as tamper hashing, and `installed.json` is unsigned (`LIMITS.md` #18). |
113
- | `RC-03` | target | script | A lab repo tracks no extracted byte: scripts, notes, manifests, docs only. | `goblin-verify --only RC-03` | Two clauses: every tracked path falls under a declared allowlist (`scripts/`, `notes/`, `manifests/`, root docs - the harness's own `.goblin/` and `.hermes/` files and the `.gitignore` block it appends are the install, not the lab's content, and are excluded the way `FM-02` excludes them); and no tracked file's sha256 equals any hash in a `manifests/*.sha256`. The manifest itself lists paths and hashes, which the lab repo's own README explicitly permits - the check is about *bytes*, and it must not read the manifest as a violation. SKIPs with a reason when no `manifests/` exists. Cannot see a payload renamed and re-encoded - which is why the allowlist is a second, independent trap. |
114
- | `RC-04` | target | lint | The acquisition record exists, and the manifest names the target and its source. | `goblin-verify --only RC-04` | The header block must name a target slug + version, a source store, and a checksum (md5 or sha256 hex); the manifest must hold a `*.apk` row, the header's named apk must be that row, and a 64-hex token in the header, if present, must equal the row's sha256 (a header carrying only the md5 passes - the comparison is conditional in the engine). Weakest of the four: it proves a record EXISTS, not that the number came from the store (`HP-03`'s defect; the `JG-01` shape). It is the first to cut if the matrix gets heavy. |
115
- | `PR-01` | source | test | The installer never writes outside its target. | `tests/run-tests.sh` | — |
116
- | `PR-02` | source | test | A second install is a no-op, and an upgrade reports created/updated/unchanged. | `tests/run-tests.sh` | — |
117
- | `PR-03` | source | test | Every target-scope check goes RED under its own violation. | `tests/run-tests.sh` | — (the negative control the verifier re-runs) |
118
- | `PR-04` | source | lint | The repo is portable: no personal path in any reusable rule. | `tests/run-tests.sh (the PT-01 body over the source tree, plus tests/)` | — |
119
- | `PR-05` | source | test | The automation producer is silent when there is nothing to report. | `tests/run-tests.sh` | — (the mutation is the control: the same producer, on the same fixture, with one installed file edited, must go from an empty stdout to a record and exit 1. A producer that stays quiet after the mutation is not silent, it is broken.) |
91
+ | `BN-00` | target | script | Every ban has an enforcement row, every ban row names a replacement, and the ban table is not empty. | goblin-verify --only BN-00 | — (this row is the reason the ban list cannot decay into prose: IN-03's shape applied to bans.tsv, and it agrees in both directions) |
92
+ | `BN-01` | target | lint | No `any` in application TypeScript. | goblin-verify --only BN-01 | Text probe, not an AST: a `: any` inside a string or a comment is reported, and `Record<string, any>` (no leading colon) is missed. The AST form needs a parser the no-npm contract (docs/CONTRACTS.md) forbids (docs/LIMITS.md #27). SKIPs when the ban is not in `bans:` or its globs match no file. |
93
+ | `BN-02` | target | lint | No `@ts-ignore` / `@ts-expect-error` suppressions. | goblin-verify --only BN-02 | Text probe: it sees the directive wherever it appears, including inside a string, and cannot tell a suppression hiding a real error from one on a line that would compile anyway. SKIPs when the ban is not in `bans:` or its globs match no file. |
94
+ | `BN-03` | target | lint | No direct network call from a component. | goblin-verify --only BN-03 | Text probe over the declared component globs: it stops the call and cannot tell whether a data layer was written or the call merely moved into a helper. SKIPs when the ban is not in `bans:` or its globs match no file. |
95
+ | `BN-05` | target | lint | No import across a declared layer boundary. | goblin-verify --only BN-05 | Reads the `layers:` list; an empty list SKIPs with a reason, never a vacuous pass. It matches an import path naming the target directory's last segment - module aliases and dynamic imports are not seen. SKIPs when no file matches its globs. |
96
+ | `BN-06` | target | lint | No renderer with Node access (`nodeIntegration: true`). | goblin-verify --only BN-06 | Text probe over the ban table's globs, the same mechanism as BN-01..BN-05: it sees `nodeIntegration: true` wherever it appears, including inside a string, and cannot see a webPreferences object built at run time or spread in from another module. The STRONGER form is a runtime measurement - the renderer prints `process.contextIsolated` and `process.sandboxed` and the check requires true/true - and that needs a real Electron process, which the dependency contract (docs/CONTRACTS.md) does not allow a shipped rule to launch: it is the project's host gate (docs/LIMITS.md #34). SKIPs when the ban is not in `bans:` or its globs match no file. |
97
+ | `BN-07` | target | lint | No renderer with context isolation or the process sandbox turned off. | goblin-verify --only BN-07 | One probe for two properties because Electron's own documentation makes them one: disabling `contextIsolation` "also disables process sandboxing", so a repo that has turned either off has lost both. Text probe, with the same false-positive set as BN-06. SKIPs when the ban is not in `bans:` or its globs match no file. |
98
+ | `BN-08` | target | lint | No dangerous webPreferences. | goblin-verify --only BN-08 | Four one-line patterns from Electron's own security checklist (`webSecurity: false`, `allowRunningInsecureContent: true`, `enableBlinkFeatures`, `<webview allowpopups>`). Text probe: `enableBlinkFeatures` is banned by name rather than by value, so the string is reported even in a comment. SKIPs when the ban is not in `bans:` or its globs match no file. |
99
+ | `BN-09` | target | lint | No synchronous IPC and no `@electron/remote`. | goblin-verify --only BN-09 | The banned-list shape the wave's note 9 asks for, applied to Electron: `sendSync(` and `@electron/remote` block the renderer's own thread, which is the freeze the class exists to prevent. Text probe - it sees the call site, not the call graph, so a wrapper around `sendSync` in a file the globs do not match is missed. SKIPs when the ban is not in `bans:` or its globs match no file. |
100
+ | `FM-01` | target | lint | Every feature file is indexed from the map README, declares its slug and at least one entry path, and carries the four-H2 entry contract. | goblin-verify --only FM-01 | SKIPs (exit 3) when feature_map: is empty - a fresh install has no map and must not be born RED (the D8 shape). When a map IS declared: the README must exist, every features/*.md must be linked from it in the (./<slug>.md) form and every relative .md link must resolve, each feature file's `feature:` must equal its filename stem, it must declare >=1 `entry_paths:`, and its H2s must be exactly Sub-features / How to get to it (user POV) / Driving it with <harness> / Gotchas, in that order. Partial: the README's own H2s are prose this row does not read, and 'the map lists every user-facing feature' is not mechanically checkable - that is docs/LIMITS.md #30, not a row. |
101
+ | `FM-02` | target | lint | Every entry point a feature declares still resolves in source, and no entry path changed after the map was verified. | goblin-verify --only FM-02 | SKIPs (exit 3) when feature_map: is empty. A tripwire, not a proof. The token is searched under source_root with occurrences under the map's own directory excluded - without that exclusion the map's own entry-path list satisfies the search and the row could never go RED. Freshness is `git log -1 --format=%cs` on the resolved file against the feature's `verified:` date, and git sees a FILE change, not a behaviour change: the row can be RED-when-stale and never GREEN-means-fresh. A token that also occurs in a vendored copy or a build artifact is read as resolved, and a `verified:` date is itself a claim the row cannot test (docs/LIMITS.md #30). W5-4: the search skips the harness's own directories (`.gob/`, `.hermes/`, the declared `harness_dir`) and the map's own directory, so a stub map whose token occurs only in the install no longer resolves; a token that occurs only in the target's own `docs/`, `tests/` or build output still does (docs/LIMITS.md #37). Z1-6: the resolved file must be TRACKED (`git ls-files --error-unmatch`) before its date is compared - an untracked file used to make the freshness clause skip in silence, so a map could claim `verified: 2020-01-01` over source that was never committed. |
102
+ | `VA-01` | target | gate | The generated verification skill's doctor command runs and exits 0. | goblin-verify --only VA-01 | SKIPs (exit 3) when verify_doctor: is empty (the replay.commit: "" shape). Runs the DECLARED command exactly as GT-02 runs a declared gate, and never a string read out of file content (the v0.2 blocker). Closes P6's stated-but-unenforced clause 'a generated skill that was never executed is a draft': the doctor is the smallest executable proof that the skill's own instructions still run - and it proves only that, never that the doctor tests the right path. |
103
+ | `JG-01` | target | script | A judge verdict is recorded as a row whose evidence resolves. | goblin-verify --only JG-01 | Builtin: it proves the named handle EXISTS - a commit `git rev-list --all` knows, a path under the root, or a `sha256:` matching a file under `.gob/loop/` - never that the handle SUPPORTS the verdict beside it: a judge may cite a real commit that has nothing to do with the claim. It also cannot see whether the verdict was reached by the declared judge lane. A row with fewer columns than the header is a FAIL, not a silent skip. SKIPs (exit 3) when there is no loop record: nothing to check is reported as a reason, never as a pass. |
104
+ | `JG-02` | target | gate | The judge lane is disjoint from the author lane. | goblin-verify --only JG-02 | Builtin: it proves the DECLARED lane sets are disjoint (profile names), not that a judge ran on one - no repo-local file observes which profile ran (the blindness `MD-03` records), and family equality is not this row's business (`MD-02` owns it). An unresolved lane is an ADV carrying the one-line remedy, never a FAIL: a box with no mapping file must not be failed for the fleet's routing. Both sentinels count as unresolved - `?` (the installed path) and `unknown` (the checkout path) - because a check that greps for one passes on the other. |
105
+ | `JG-03` | target | advisory | A judge lane that has never returned a non-`done` verdict is escalated. | advisory | A lane that has returned two verdicts cannot be called always-yes, and a repo sees only its own rows; the history that would show a bad lane lives across cards and repos. The counter-measure is policy - one known-red control verdict per wave, recorded in docs/LOOP.md - and it is counted, not enforced. |
106
+ | `LP-01` | target | script | The exit predicate is a command, and it ran before iteration 1. | goblin-verify --only LP-01 | Builtin: it proves the predicate is exactly ONE command and that a recorded first run exists whose timestamp is at or before the first log row - not that the command was actually run, and not that the `exit=` value was measured rather than typed (`HP-03`'s defect, one artifact over). It deliberately does NOT re-run the predicate: a stop condition is legitimately red before the loop finishes, so re-running it would fail a repo whose loop is working, and PROJECT-PRACTICE section 7 forbids a probe that writes. Timestamps compare on their `YYYY-MM-DDThh:mm:ss` prefix, so records written in different UTC offsets compare as text. |
107
+ | `LP-02` | target | script | The predicate is pinned at loop start and is never relaxed. | goblin-verify --only LP-02 | Builtin: it proves the predicate file on disk still hashes to the recorded pin - never that the predicate is the RIGHT one, and never who edited it. The digest covers the predicate file alone: a loop that quietly relaxes a predicate living somewhere else is not seen. The remedy is printed with the failure and is never automatic, the `IN-02` `--re-pin` shape. W5-7: a close-and-reopen is auditable - every `closed-<date>/` archive must hold its predicate AND the pin it was closed under, and the live pin must name the archived digest on a `previous:` line - which catches a SILENT relaxation, never a WEAKER one (nothing in bash judges that: `docs/LIMITS.md` #39). |
108
+ | `LP-03` | target | gate | The loop declares a budget, and the record never exceeds it. | goblin-verify --only LP-03 | Builtin: it proves the declared budget is a positive integer at or under the configured `loop_max_turns_ceiling`, and that the record holds no more verdict rows than the budget - not that the budget is affordable. Cost is not a field the record carries: a turn budget bounds turns, and the auxiliary judge call per turn is unpriced (`docs/LIMITS.md` #32). A worker cannot edit its own card body, so a CARD predicate is already protected; this row is the file-predicate half. |
109
+ | `LP-04` | target | gate | A loop making no progress stops instead of thrashing. | goblin-verify --only LP-04 | Builtin: it measures a CHANGED EVIDENCE POINTER, which is a proxy for progress, not progress. Three consecutive verdict rows with an identical non-empty pointer and a non-`predicate:green` result is a FAIL that names the row numbers. A loop that edits a file each turn to keep the pointer moving is not caught, which is why LP-05 and the budget sit beside it. Nothing in the Hermes kanban goal loop detects a lack of progress at all - `run_kanban_goal_loop` carries no progress state. |
110
+ | `LP-05` | target | script | A loop that ended without its predicate green carries a written-up reason. | goblin-verify --only LP-05 | Builtin: the three-non-blank-line threshold is arbitrary and stated as such - it proves a write-up EXISTS, the way `SP-03` proves a bullet LOOKS checkable. Nothing can make the write-up true, and the row cannot see whether it names the right dead end or whether the loop stopped too early. A loop whose last row IS `predicate:green` needs no write-up and the row passes without reading one. |
111
+ | `RC-01` | target | gate | No file in the shipping tree matches a reference-corpus hash. | goblin-verify --only RC-01 | Four clauses: (1) `reference_manifest:` empty -> SKIP with that reason (the `FM-01`/`VA-01` shape - not born RED); (2) declared but missing or unparseable -> FAIL, never a SKIP; (3) any file under the declared `security: build_output:` whose sha256 appears in an entry's sha256 -> FAIL, naming path + entry; (4) the manifest itself must not sit inside the build output, at the declared path or as a byte-identical copy - it is a listing of every corpus hash, so shipping it ships the corpus's shape. Cannot see: a re-encoded/resized/recoloured asset (level 2), copied text in a shipped string (level 3), or a manifest authored weak - that is `RC-02`. |
112
+ | `RC-02` | target | lint | The reference manifest is shaped so `RC-01` cannot pass vacuously. | goblin-verify --only RC-02 | Schema `reference-manifest/1`; `generated_from` non-empty; `reference_app.package`/`.version`/`apk_sha256` (64 hex); `entries` non-empty; every entry carries `path`, 64-hex `sha256`, numeric `bytes`; `entry_count` equals `len(entries)`. SKIPs with `RC-01`'s reason when the key is empty. Exists because `RC-01` alone carries the `bans.tsv` weakness: a weakened input passes the check it feeds (`LIMITS.md` #28). It cannot see whether the entries are the *right* hashes - that is as strong as tamper hashing, and `installed.json` is unsigned (`LIMITS.md` #18). |
113
+ | `RC-03` | target | script | A lab repo tracks no extracted byte: scripts, notes, manifests, docs only. | goblin-verify --only RC-03 | Two clauses: every tracked path falls under a declared allowlist (`scripts/`, `notes/`, `manifests/`, root docs - the harness's own `.gob/` and `.hermes/` files and the `.gitignore` block it appends are the install, not the lab's content, and are excluded the way `FM-02` excludes them); and no tracked file's sha256 equals any hash in a `manifests/*.sha256`. The manifest itself lists paths and hashes, which the lab repo's own README explicitly permits - the check is about *bytes*, and it must not read the manifest as a violation. SKIPs with a reason when no `manifests/` exists. Cannot see a payload renamed and re-encoded - which is why the allowlist is a second, independent trap. |
114
+ | `RC-04` | target | lint | The acquisition record exists, and the manifest names the target and its source. | goblin-verify --only RC-04 | The header block must name a target slug + version, a source store, and a checksum (md5 or sha256 hex); the manifest must hold a `*.apk` row, the header's named apk must be that row, and a 64-hex token in the header, if present, must equal the row's sha256 (a header carrying only the md5 passes - the comparison is conditional in the engine). Weakest of the four: it proves a record EXISTS, not that the number came from the store (`HP-03`'s defect; the `JG-01` shape). It is the first to cut if the matrix gets heavy. |
115
+ | `PR-01` | source | test | The installer never writes outside its target. | tests/run-tests.sh | — |
116
+ | `PR-02` | source | test | A second install is a no-op, and an upgrade reports created/updated/unchanged. | tests/run-tests.sh | — |
117
+ | `PR-03` | source | test | Every target-scope check goes RED under its own violation. | tests/run-tests.sh | — (the negative control the verifier re-runs) |
118
+ | `PR-04` | source | lint | The repo is portable: no personal path in any reusable rule. | tests/run-tests.sh (the PT-01 body over the source tree, plus tests/) | — |
119
+ | `PR-05` | source | test | The automation producer is silent when there is nothing to report. | tests/run-tests.sh | — (the mutation is the control: the same producer, on the same fixture, with one installed file edited, must go from an empty stdout to a record and exit 1. A producer that stays quiet after the mutation is not silent, it is broken.) |
120
120
 
121
121
  ## Advisory rows, named
122
122
 
@@ -175,23 +175,22 @@ instead of silently passing, and `CL-01` fails if a forbidden part's artifact ex
175
175
  | review-panel | O | - | R | - | O |
176
176
  | playbooks | R | R | R | R | R |
177
177
  | tokens | O | - | - | - | O |
178
- | ci-gate | R | - | O | - | O |
179
178
 
180
179
  The five columns carry the domain names; the letters `A`..`E` and the older names `app` (software)
181
180
  and `agent` (fleet) are read-time aliases. The old `F` class — the Electron shell — was merged into
182
181
  `software`: the merge measured the two need columns identical on all ten parts, so what it added
183
182
  lives in config, not in this table. The **electron opt-in** (`electron: true`) turns the electron
184
183
  bans `BN-06`..`BN-09` on even when a hand-edited `bans:` list omits them, and requires a declared
185
- **host gate** — a number measured on a machine with a display; `docs/CI.md` §3 is the contract for
186
- it. `ci-gate` is the one part added at W4: when it is required or optional the installer renders
187
- `templates/ci/goblin-gate.yml.tmpl` into `.github/workflows/goblin-gate.yml`, and when it is `-` the
188
- artifact must be **absent** (which is why `CL-01` keys off that exact path, not the `.github/`
189
- directory — a repo is still allowed CI of its own). `docs/CI.md` is the contract for what that file
190
- does and does not make true.
184
+ **host gate** — a number measured on a machine with a display. The `ci-gate` rows in
185
+ `manifest/classes.tsv` are a W4 remnant kept for record: **v2 installs no CI** — the part carries
186
+ `-` for every class, the installer renders nothing under `.github/`, and no flag configures it
187
+ (so a reader who meets `ci-gate` in the tsv reads `off everywhere`, not a missing column here).
188
+ The CI story that used to live at `docs/CI.md` moved to `docs/LIMITS.md` (CI is out of the v2
189
+ product, not a promise this repo is behind on).
191
190
 
192
191
  ## The ban list (G5)
193
192
 
194
- `BN-00`..`BN-09` are not ordinary rows: they read `.goblin/manifest/bans.tsv`, a table whose
193
+ `BN-00`..`BN-09` are not ordinary rows: they read `.gob/manifest/bans.tsv`, a table whose
195
194
  every row carries a real command. A ban with no mechanism is a wish, so `manifest/bans.tsv`
196
195
  holds `id`, the ban, the globs, the `detect` command, the replacement code, the escape hatch,
197
196
  the reviewer and the source — and `BN-00` fails the whole list if any ban has no enforcement
package/docs/FLOWS.md CHANGED
@@ -80,7 +80,7 @@ cannot see. The router that picks one is the `goblin-mode` skill.
80
80
 
81
81
  - **When:** an unattended run over a predicate
82
82
  - **Steps:** 1 the exit condition is a checkable predicate written before iteration 1<br>- 2 it never gets relaxed<br>- 3 an escape hatch: a genuine dead end writes up why and stops<br>- 4 the morning audit reads the Attention section first
83
- - **Verification:** the predicate is a command, and its first run is recorded before iteration 1 (`LP-01`); the predicate is pinned and never relaxed (`LP-02`); `goal_max_turns` is set and at or under `loop_max_turns_ceiling` (`LP-03`); no three consecutive rows share an evidence pointer without reaching `predicate:green` (`LP-04`); a run that ends without its predicate green carries `.goblin/loop/stuck.md` naming it (`LP-05`); every landed change has a P7 verdict row; a verdict that says `done` cites a handle the repo resolves (`JG-01`) and comes from a lane disjoint from the author's (`JG-02`)
83
+ - **Verification:** the predicate is a command, and its first run is recorded before iteration 1 (`LP-01`); the predicate is pinned and never relaxed (`LP-02`); `goal_max_turns` is set and at or under `loop_max_turns_ceiling` (`LP-03`); no three consecutive rows share an evidence pointer without reaching `predicate:green` (`LP-04`); a run that ends without its predicate green carries `.gob/loop/stuck.md` naming it (`LP-05`); every landed change has a P7 verdict row; a verdict that says `done` cites a handle the repo resolves (`JG-01`) and comes from a lane disjoint from the author's (`JG-02`)
84
84
  - **Profiles:** default + coder
85
85
  - **Role:** code + judge
86
86
 
package/docs/GLOSSARY.md CHANGED
@@ -10,7 +10,7 @@ sentence). A term a new reader might trip on belongs here, not in a footnote.
10
10
  | archive | A class-D flag: verify requires no HANDOFF and no gates, and the summary says so. | R7 sec.2.4 |
11
11
  | class | A project category A-E that selects which parts are required, optional or off. See manifest/classes.tsv. | R7 sec.4 |
12
12
  | drift | A file whose current bytes no longer match the hash recorded at install time. IN-02 reports it; the remedy column says how to recover. | UX-review-2026-10-06 |
13
- | engine | The bash programs plus the manifest a verify run actually resolved: per-repo (.goblin/bin) or global (engine_dir:). Named in every run's footer. | UX-review-2026-10-06 |
13
+ | engine | The bash programs plus the manifest a verify run actually resolved: per-repo (.gob/bin) or global (engine_dir:). Named in every run's footer. | UX-review-2026-10-06 |
14
14
  | forge | The hosting platform a repo pushes to (GitHub, GitLab, ...). PG-05 and PG-06 read the workflow text but can never see what the forge itself enforces. | UX-review-2026-10-06 |
15
15
  | foreman | The orchestrating agent that delegates work to the fleet; the role that reads HANDOFF.md first and writes it last. | UX-review-2026-10-06 |
16
16
  | gate | A declared command that must exit 0. Declared per project, never inferred from the stack. | PP sec.4 |
@@ -18,8 +18,8 @@ sentence). A term a new reader might trip on belongs here, not in a footnote.
18
18
  | harness | An asserting check file: a failures counter, an assert(name, ok, detail) printer, and a non-zero exit on failure. | PP sec.3 |
19
19
  | judge | A role that decides whether a PROCESS met its own predicate, from a command's output and a pointer it can resolve - never from a report. One lane per verdict; never the author's lane. | docs/LOOP.md |
20
20
  | lane | A family of rules that share a mechanism and a blind spot: the ban lane, the judge/loop lane, the CI lane, the reference lane. The cannot-see footer reports per lane. | UX-review-2026-10-06 |
21
- | loop | One unattended run over a predicate, recorded in the committed .goblin/loop/ (predicate, pin, first-run, budget, one decisions.tsv row per iteration). | docs/LOOP.md |
22
- | never-relax | The predicate's digest is recorded at loop start and never updates itself; relaxing it is closing this loop and opening another, with the old predicate archived under .goblin/loop/closed-<date>/. | docs/LOOP.md |
21
+ | loop | One unattended run over a predicate, recorded in the committed .gob/loop/ (predicate, pin, first-run, budget, one decisions.tsv row per iteration). | docs/LOOP.md |
22
+ | never-relax | The predicate's digest is recorded at loop start and never updates itself; relaxing it is closing this loop and opening another, with the old predicate archived under .gob/loop/closed-<date>/. | docs/LOOP.md |
23
23
  | opt-out | A part recorded in disabled: so its required checks report SKIP (opt-out) instead of failing. | R6 sec.6.3 |
24
24
  | overnight | P10: an unattended run over a checkable predicate written before iteration 1. | R6 sec.3 |
25
25
  | part | One installable unit a class requires or forbids: handoff, spec, gate, replay, ratchet, pr-gate, review-panel, playbooks, tokens. | R7 sec.5 |
@@ -2,7 +2,7 @@
2
2
 
3
3
  `manifest/enforcement.tsv` is the machine-readable form; this is the prose for the ten rows it
4
4
  gained in v0.2 (`SC-01`..`SC-09`, `PF-01`). Every one of them is **declared** in
5
- `.goblin/goblin.yaml` under `security:` and `perf:` - a stack-specific rule guessed from the
5
+ `.gob/goblin.yaml` under `security:` and `perf:` - a stack-specific rule guessed from the
6
6
  files on disk is how a matrix starts lying, so nothing here infers a stack.
7
7
 
8
8
  ## The rung ladder, and the rule for choosing a rung
@@ -15,7 +15,7 @@ rung below must be *demonstrably unable* to see it - the reason is recorded in t
15
15
 
16
16
  1. **No network at verify time.** `docs/RISKS.md` K4: a network call at verify time breaks the
17
17
  offline dependency contract. So `SC-07` reads a **recorded** audit; recording is a separate,
18
- deliberate command (`.goblin/bin/goblin-audit`). A row that needs the network is not a
18
+ deliberate command (`.gob/bin/goblin-audit`). A row that needs the network is not a
19
19
  verify-time row.
20
20
  2. **`enforced_by` is a closed enum** (`script`, `lint`, `gate`, `advisory`) and `check` is one of:
21
21
  a real command, the literal `advisory`, or `goblin-verify --only <ID>` for a multi-line body.
@@ -57,14 +57,14 @@ about the thing it was measuring.
57
57
  unwaived`), reusing `DS-02`'s annotation pattern, so **the debt is visible on every run** and the
58
58
  row can pass while the debt stays loud.
59
59
 
60
- `.goblin/bin/goblin-audit` is the one tool in the toolchain allowed to touch the network, and it
61
- is not a check: a human runs it, once, deliberately, and commits `.goblin/audit.tsv`. Its exit
60
+ `.gob/bin/goblin-audit` is the one tool in the toolchain allowed to touch the network, and it
61
+ is not a check: a human runs it, once, deliberately, and commits `.gob/audit.tsv`. Its exit
62
62
  codes are part of the contract:
63
63
 
64
64
  | exit | meaning |
65
65
  |---|---|
66
66
  | 0 | the record was written (clean or not) |
67
- | 2 | usage, or no `.goblin/goblin.yaml` to read `security.audit_cmd` from |
67
+ | 2 | usage, or no `.gob/goblin.yaml` to read `security.audit_cmd` from |
68
68
  | 3 | the class declares no audit command - nothing to run |
69
69
  | 4 | the declared command could not run |
70
70
  | 5 | the output could not be parsed as an audit report, and it **refuses to write a record**: an empty record reads to `SC-07` as "clean", which would be a fabricated pass |