@techgoblin/gobstack 0.0.0-stage → 0.4.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/LICENSE +21 -0
  3. package/README.md +217 -2
  4. package/VERSION +1 -0
  5. package/adapters/_template/adapter.tsv +16 -0
  6. package/adapters/_template/detect.sh +10 -0
  7. package/adapters/_template/emit.sh +5 -0
  8. package/adapters/_template/verify.sh +4 -0
  9. package/adapters/claude/adapter.tsv +8 -0
  10. package/adapters/claude/detect.sh +8 -0
  11. package/adapters/claude/verify.sh +47 -0
  12. package/adapters/codex/adapter.tsv +12 -0
  13. package/adapters/codex/detect.sh +9 -0
  14. package/adapters/codex/verify.sh +45 -0
  15. package/adapters/copilot/adapter.tsv +10 -0
  16. package/adapters/copilot/detect.sh +8 -0
  17. package/adapters/copilot/verify.sh +45 -0
  18. package/adapters/cursor/adapter.tsv +11 -0
  19. package/adapters/cursor/detect.sh +10 -0
  20. package/adapters/cursor/verify.sh +45 -0
  21. package/adapters/gemini/adapter.tsv +15 -0
  22. package/adapters/gemini/detect.sh +11 -0
  23. package/adapters/gemini/verify.sh +49 -0
  24. package/adapters/hermes/adapter.tsv +9 -0
  25. package/adapters/hermes/detect.sh +8 -0
  26. package/adapters/hermes/verify.sh +27 -0
  27. package/adapters/opencode/adapter.tsv +14 -0
  28. package/adapters/opencode/detect.sh +9 -0
  29. package/adapters/opencode/verify.sh +45 -0
  30. package/automations/README.md +53 -0
  31. package/automations/bugreporter-intake.sh +145 -0
  32. package/automations/drift-audit.sh +139 -0
  33. package/automations/report.schema.tsv +10 -0
  34. package/bans/README.md +82 -0
  35. package/bans/grep-ban.sh +84 -0
  36. package/bans/layer-check.sh +57 -0
  37. package/bin/goblin +119 -0
  38. package/bin/goblin-audit +145 -0
  39. package/bin/goblin-bans +178 -0
  40. package/bin/goblin-doctor +233 -0
  41. package/bin/goblin-emit +484 -0
  42. package/bin/goblin-init +519 -0
  43. package/bin/goblin-install +720 -0
  44. package/bin/goblin-lib.sh +289 -0
  45. package/bin/goblin-model +105 -0
  46. package/bin/goblin-upgrade +572 -0
  47. package/bin/goblin-verify +2798 -0
  48. package/bin/goblin.js +103 -0
  49. package/docs/ADOPTION.md +168 -0
  50. package/docs/CI.md +187 -0
  51. package/docs/CONTRACTS.md +197 -0
  52. package/docs/DESIGN.md +92 -0
  53. package/docs/ENFORCEMENT.md +225 -0
  54. package/docs/FLOWS.md +164 -0
  55. package/docs/GUARDRAILS.md +126 -0
  56. package/docs/GUIDE.md +610 -0
  57. package/docs/INTEGRATION.md +92 -0
  58. package/docs/LIMITS.md +591 -0
  59. package/docs/LOOP.md +165 -0
  60. package/docs/RE-PLAYBOOK.md +183 -0
  61. package/docs/RISKS.md +70 -0
  62. package/docs/ROLES.md +105 -0
  63. package/manifest/bans.tsv +9 -0
  64. package/manifest/classes.tsv +61 -0
  65. package/manifest/enforcement.tsv +88 -0
  66. package/manifest/glossary.tsv +25 -0
  67. package/manifest/playbooks.tsv +16 -0
  68. package/package.json +37 -4
  69. package/presets/A-shipped-software.yaml +48 -0
  70. package/presets/B-service-config.yaml +40 -0
  71. package/presets/C-game.yaml +38 -0
  72. package/presets/D-knowledge.yaml +41 -0
  73. package/presets/E-fleet-config.yaml +42 -0
  74. package/presets/F-electron.yaml +67 -0
  75. package/roles.yaml +54 -0
  76. package/skills/goblin-bootstrap/SKILL.md +51 -0
  77. package/skills/goblin-bugfix/SKILL.md +26 -0
  78. package/skills/goblin-bugreporter/SKILL.md +52 -0
  79. package/skills/goblin-drift-audit/SKILL.md +43 -0
  80. package/skills/goblin-eval/SKILL.md +68 -0
  81. package/skills/goblin-feature/SKILL.md +26 -0
  82. package/skills/goblin-feature-map/SKILL.md +140 -0
  83. package/skills/goblin-handoff/SKILL.md +28 -0
  84. package/skills/goblin-investigation/SKILL.md +26 -0
  85. package/skills/goblin-judge/SKILL.md +74 -0
  86. package/skills/goblin-loop/SKILL.md +88 -0
  87. package/skills/goblin-mode/SKILL.md +70 -0
  88. package/skills/goblin-overnight/SKILL.md +42 -0
  89. package/skills/goblin-pr-gate/SKILL.md +42 -0
  90. package/skills/goblin-re-mobile/SKILL.md +51 -0
  91. package/skills/goblin-refactor/SKILL.md +23 -0
  92. package/skills/goblin-sweep/SKILL.md +23 -0
  93. package/skills/goblin-tdd-repro/SKILL.md +27 -0
  94. package/skills/goblin-verify-author/SKILL.md +50 -0
  95. package/skills/practice/SKILL.md +37 -0
  96. package/templates/AGENTS.md.tmpl +23 -0
  97. package/templates/HANDOFF.md.tmpl +43 -0
  98. package/templates/SPEC.md.tmpl +34 -0
  99. package/templates/audit-waiver.tsv.tmpl +10 -0
  100. package/templates/boundary-waivers.tmpl +8 -0
  101. package/templates/checks/assert.mjs.tmpl +60 -0
  102. package/templates/checks/gate.sh.tmpl +29 -0
  103. package/templates/ci/goblin-gate.yml.tmpl +46 -0
  104. package/templates/goblin.yaml.tmpl +138 -0
  105. package/templates/install-hooks.allowlist.tmpl +9 -0
  106. package/templates/loop/decisions.tsv.tmpl +1 -0
  107. package/templates/loop/predicate.tmpl +16 -0
  108. package/templates/report.yaml.tmpl +16 -0
package/bin/goblin.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // bin/goblin.js — the node shim over the bash engine (W2, PLAN-V1 §4.3).
3
+ //
4
+ // The shebang is load-bearing: the §4.3 body always calls bash with the payload
5
+ // explicitly, but npm's bin symlink executes THIS file directly (npx ./ --version),
6
+ // and a marketplace packager stripping the executable bit breaks the symlink, not
7
+ // the spawnSync path below.
8
+ //
9
+ // goblin verify [--only <id,...>] ... -> bin/goblin-verify
10
+ // goblin bans [...] -> bin/goblin-bans
11
+ // goblin audit [...] -> bin/goblin-audit
12
+ // goblin upgrade [...] -> bin/goblin-upgrade (W3)
13
+ // goblin doctor [...] -> bin/goblin-doctor (W4a)
14
+ // goblin emit [...] -> bin/goblin-emit (W4a)
15
+ // goblin init [...] -> bin/goblin-init (W6, the first-run wizard)
16
+ // goblin uninstall [--target <dir>] -> bin/goblin-install --uninstall
17
+ // goblin install [...] -> bin/goblin-install (the one legacy fallback)
18
+ // no args | -h/--help | any other unrecognized first arg
19
+ // -> this file's short usage, exit 2. A bare `goblin`
20
+ // used to fall through into the installer; a typo
21
+ // (`goblin inti`) silently installed too. Both now
22
+ // print the usage and stop.
23
+ //
24
+ // Non-negotiables (§4.3): args are passed as an ARRAY, never a shell string (no
25
+ // injection surface); `bash` is named explicitly (a packager stripping the
26
+ // executable bit must not break every command); the exit status is propagated
27
+ // VERBATIM so the four-value verify contract survives the shim. No dependencies,
28
+ // no async, CommonJS — this repo has no node tooling by design.
29
+ //
30
+ // The payload/ re-point happens with packaging (W3/W5): the npm tarball moves the
31
+ // bash payload under payload/, so this line becomes path.join(__dirname, "..", "payload", "bin", target).
32
+ // Until then the shim runs straight out of the checkout layout.
33
+ "use strict";
34
+
35
+ const { spawnSync } = require("node:child_process");
36
+ const fs = require("node:fs");
37
+ const path = require("node:path");
38
+
39
+ // --version prints the ONE source (VERSION) directly: a shim that routed --version to a
40
+ // subcommand would print a constant's copy of the number, the exact drift V6 exists for.
41
+ const arg0 = process.argv[2];
42
+ if (arg0 === "--version" || arg0 === "-V" || arg0 === "-v") {
43
+ process.stdout.write(fs.readFileSync(path.join(__dirname, "..", "VERSION"), "utf8"));
44
+ process.exit(0);
45
+ }
46
+
47
+ const SCRIPT = { verify: "goblin-verify", bans: "goblin-bans", audit: "goblin-audit", upgrade: "goblin-upgrade", doctor: "goblin-doctor", emit: "goblin-emit", init: "goblin-init" };
48
+ const [cmd, ...rest] = process.argv.slice(2);
49
+
50
+ // No args, a help flag, or an unrecognized first arg: short usage, exit 2. The one survivor of
51
+ // the old catch-all fallback is the literal `install` first arg — bare `goblin` mapped to the
52
+ // installer through npm's bin default, and a typo (`goblin inti`) silently installed into
53
+ // whatever directory the shell sat in. A bare subcommand-less `goblin install ...` keeps the
54
+ // installer; everything else stops here and names the word it did not know.
55
+ function usage() {
56
+ process.stderr.write(
57
+ [
58
+ "goblin <command>",
59
+ "",
60
+ " goblin init start here — the guided first step (detect, class, emit, first verify)",
61
+ " goblin verify run the rule matrix against the current repo",
62
+ " goblin bans run the ban list (per-pattern red lines over the source tree)",
63
+ " goblin audit check recorded dependency claims against live advisory feeds",
64
+ " goblin upgrade migrate a repo to the shared global engine at ~/.goblin/engine",
65
+ " goblin doctor one detection/drift run across the agent platforms",
66
+ " goblin emit write the skills + context block for one platform",
67
+ " goblin uninstall --target . remove exactly what an install wrote (preimages)",
68
+ "",
69
+ "start here: goblin init",
70
+ "uninstall: npm uninstall -g @techgoblin/gobstack",
71
+ "",
72
+ ].join("\n"));
73
+ }
74
+
75
+ if (cmd === undefined || cmd.startsWith("-")) {
76
+ usage();
77
+ process.exit(2);
78
+ }
79
+ if (!SCRIPT[cmd] && cmd !== "install" && cmd !== "uninstall") {
80
+ process.stderr.write(`goblin: unrecognized command: ${cmd}\n\n`);
81
+ usage();
82
+ process.exit(2);
83
+ }
84
+
85
+ let target;
86
+ let extra = [];
87
+ if (cmd === "install") {
88
+ target = "goblin-install"; // the one legacy fallback, kept verbatim
89
+ } else if (cmd === "uninstall") {
90
+ // `goblin uninstall --target <dir>` routes into the installer's uninstall job — the shape
91
+ // docs/GUIDE.md and README already promise. `--uninstall` is appended FIRST so the user's
92
+ // own `--target <dir>` and options still parse, and a stray literal `--uninstall` cannot
93
+ // appear twice.
94
+ target = "goblin-install";
95
+ extra = ["--uninstall"];
96
+ } else {
97
+ target = SCRIPT[cmd];
98
+ }
99
+ const file = path.join(__dirname, "..", "bin", target);
100
+ // execPath-independent: call bash explicitly so Windows-WSL/Git-Bash works and no
101
+ // shebang resolution is needed.
102
+ const r = spawnSync("bash", [file, ...extra, ...rest], { stdio: "inherit" });
103
+ process.exit(r.status ?? 2);
@@ -0,0 +1,168 @@
1
+ # Adoption — classes, presets, and the order
2
+
3
+ ## The six classes
4
+
5
+ A class is **not** a stringency level. It selects which parts are required, optional or off, and
6
+ it supplies the default gate and ratchet shape. The gate vocabulary differs by class; the
7
+ harness does not.
8
+
9
+ | Class | What "done" means |
10
+ |---|---|
11
+ | **A. Shipped software** | a gate set reports measured numbers, a round lands, the artifact deploys or publishes |
12
+ | **B. Service / configuration** | a contract (schema, route, API) is unchanged, or the change is intentional and migrated |
13
+ | **C. Game** | a suite green in the Editor **and** a human feel verdict — the verdict is a first-class deliverable |
14
+ | **D. Knowledge / research** | a question is answered with sources and the answer is findable |
15
+ | **E. Agent-fleet config** | a config change is applied, verified against the **artifact**, and versioned |
16
+ | **F. Desktop shell** | the renderer is isolated from Node, the main process is not busy, and the packaged bundle ships no dev dependency |
17
+
18
+ Two placements worth arguing about:
19
+
20
+ - A repo whose code is small and lives elsewhere, while the repo holds *output*, belongs in
21
+ **D**, not A — gating it like an application gates the wrong artifact; its gate is freshness,
22
+ not compilation.
23
+ - An input directory that is not a build target at all belongs in **D** with **`--archive`**.
24
+ Without the flag the installer keeps producing HANDOFFs for a directory whose own design
25
+ folders are empty.
26
+
27
+ ## The preset matrix
28
+
29
+ `R` = required · `O` = optional (installed, reported) · `—` = off. The same data is in
30
+ `manifest/classes.tsv`, and `CL-01` checks it against the repo.
31
+
32
+ | Part | A | B | C | D | E | F |
33
+ |---|---|---|---|---|---|---|
34
+ | HANDOFF | R | R | R | R | R | R |
35
+ | SPEC before change | R | R | R | — | R | R |
36
+ | Verification gate | R | R | R | O | R | R |
37
+ | Pinned-commit REPLAY | R | — | R | — | O | R |
38
+ | Ratchet | R | O | O | — | O | R |
39
+ | PR gate | O | — | O | — | O | O |
40
+ | Review panel | O | — | R | — | O | O |
41
+ | Playbooks (the skills) | R | R | R | R | R | R |
42
+ | Design tokens | O | — | — | — | — | O |
43
+ | CI lane | R | — | O | — | O | R |
44
+
45
+ **F is the desktop shell**, added at W4: it declares the Electron failure surface as bans
46
+ (`BN-06`..`BN-09`) and declares its FPS number as a **host gate** rather than a ratchet, because
47
+ the probe that measures it needs Playwright or Electron plus a display — neither of which a
48
+ shipped rule may depend on. Its ratchet carries `app_bundle_bytes` instead. `docs/CI.md` §3 argues
49
+ that in full, including why frame time is the wrong number (measured flat at 16.70 ms while the
50
+ main thread went from 1.8 % to 54.5 % busy).
51
+
52
+ **`—` is a real, enforced option.** The installer records every off part in `disabled:`, so its
53
+ rows report `SKIP (opt-out)`; `CL-01` fails if a forbidden part's artifact exists. A repo with
54
+ 1,599 test files or 27 research notes is not broken by a forced harness directory — it is
55
+ switched off for that class.
56
+
57
+ Four consequences that follow from measurement, not taste:
58
+
59
+ 1. A forced harness directory breaks a repo whose tests are not `checks/*.mjs`.
60
+ 2. Two gate *kinds* must exist in the schema: hermetic (repo-local) and host (touches paths
61
+ outside the repo). A host gate reported as hermetic reports environment differences as
62
+ failures.
63
+ 3. **The gate command must be declarable per project, never inferred from the stack.** One
64
+ inferred command is wrong for a repo with no runner, a repo that cannot run its own typecheck
65
+ read-only, and a repo whose runner lives in a skill — all at once. The shipped gate is a
66
+ **floor**, and `P8` step 3 is "replace it".
67
+ 4. **The default branch is not always `main`.** It is declared in `.goblin/goblin.yaml` and
68
+ compared against the real branch by `PT-02`; a preset that assumes `main` silently skips a
69
+ repo on `master`.
70
+
71
+ ## The adoption order
72
+
73
+ Each step is independently useful and the later ones build on the earlier:
74
+
75
+ 1. **A repo already at the standard, first — to prove the installer, not to improve the
76
+ project.** The only honest test of an installer is that it is idempotent against a repo that
77
+ needs nothing. Success = install, then verify reproduces the harness count and the ratchet
78
+ number with `git status` unchanged.
79
+ 2. **The highest-risk gap second.** A large repo with client deliverables and **no version
80
+ control**: everything else is recoverable from a working tree, that one has no "before".
81
+ Gate on: `git init` done, `.gitignore` verified against local env and build-info files, first
82
+ commit exists, HANDOFF exists, the real typecheck recorded.
83
+ 3. **The fleet-config repo third — highest value per minute.** A non-code preset that cannot
84
+ fix its own home is not publishable.
85
+ 4. **The cheapest correctness win fourth.** Content types untracked inside a repo that looks
86
+ protected: commit them, write a HANDOFF, then fix the branch name.
87
+ 5. **The small ones fifth** — they exercise the `—` switches. One needs a single commit; another
88
+ needs its `.gitignore` fixed **before** `git init` because a credentials file sits in-tree.
89
+ 6. **The two small repairs sixth** that make a fleet self-consistent: a stale HEAD, a stale
90
+ number, and the REPLAY added to each.
91
+ 7. **The rest seventh — real work, no emergency.** Each is blocked on a *decision* more than on
92
+ effort.
93
+ 8. **An archived input directory — never adopted.** It is the regression test for the off
94
+ switch.
95
+
96
+ ## What a first install actually gives you
97
+
98
+ `goblin-install` exits 0 and creates: `.goblin/` (the verifier, the manifest, the config),
99
+ `.hermes/skills/` (the flows), `HANDOFF.md`, `AGENTS.md`, and — for classes that need them —
100
+ `ROUND-000-SPEC.md`, `reviews/`, and the harness scaffold in `checks/`.
101
+
102
+ Then, in order:
103
+
104
+ git add -A && git commit # the install is a change like any other
105
+ .goblin/bin/goblin-verify # 42 passed, 1 failed - HP-05, until HANDOFF names a commit
106
+ hermes skills trust <target> # one-time, so the project-tier skills load
107
+
108
+ A class-A install is **green** — `43 passed, 0 failed, 11 advisory, 28 skipped`, exit 0 — once
109
+ `HANDOFF.md` names a commit that exists; before that edit the scaffold's `0000000` placeholder is
110
+ the one expected red (`42 passed, 1 failed`). Both numbers are measured, not assumed
111
+ (`docs/CONTRACTS.md`; step 2 of `docs/GUIDE.md`). Twenty-eight rows skip with a reason: `HS-02` (no
112
+ pinned pre-change commit yet), `AU-02`/`AU-03` (no report has been filed, so there is nothing to
113
+ dedup and no reporter run to audit), `SC-06`/`SC-07`/`SC-08` (no dependency manifest, no lockfile,
114
+ no audit record), `PF-01` (no measured perf baseline), `BN-01`/`BN-02`/`BN-05` (no `src/` for a ban
115
+ to read) and `BN-03` with the four
116
+ electron bans `BN-06`/`BN-07`/`BN-08`/`BN-09` (not in this class's `bans: [BN-01, BN-02, BN-05]`, so
117
+ they skip as *not enabled* rather than as *unread*), `FM-01`/`FM-02`/`VA-01`
118
+ (no feature map and no declared `verify_doctor:` yet), `RC-01`..`RC-04` (no reference corpus
119
+ declared: `reference_manifest:` ships empty and a fresh repo has no lab `manifests/`) and `JG-01`
120
+ with `LP-01`..`LP-05` (no
121
+ `.goblin/loop/` record, because no loop has run here yet). The class's required parts that only a
122
+ round can produce pass *vacuously* (zero `reviews/*.md` to check; the declared gate is still the
123
+ shipped floor), so the first-step list is a list of work, not a list of FAILs.
124
+
125
+ ## Adopting into a repo that already has a `HANDOFF.md`
126
+
127
+ The installer never overwrites a `HANDOFF.md` that existed before the install. It prints
128
+ `refused to overwrite: HANDOFF.md` and exits 1. **That exit is correct, and `--force` is not the
129
+ remedy**: `--force` replaces the project's own record with the scaffold, which is the act the
130
+ refusal exists to prevent. `--force` is for a scaffold copy with nothing to keep.
131
+
132
+ The remedy is a reconciliation. The project's file stays the file of record; nothing is deleted
133
+ (a stale sentence is corrected in place with a dated parenthetical, never removed).
134
+
135
+ 1. **Keep the file.** Do not re-install with `--force`, and do not delete it.
136
+ 2. **Give it the five sections it is missing**, each as an H2 or H3 heading whose *first word* is
137
+ the slot name — a marker or number prefix (`## ▶ START HERE`) is fine:
138
+
139
+ | section | accepted first words |
140
+ |---|---|
141
+ | orientation | `START HERE` |
142
+ | current state | `State` · `Status` |
143
+ | gates | `Gate` · `Gates` |
144
+ | next steps | `Next steps` · `Next` |
145
+ | what is not verified | `Not verified` · `Unverified` · `Not proven` · `Unproven` · `Pending <x> device test` |
146
+
147
+ `HP-02` is satisfied by the heading, not by the words appearing inside one: a `## BOARD STATE`
148
+ heading is not a `State` section.
149
+ 3. **Put the current gate numbers inside the `Gates` section, each with its date.** Every
150
+ gate-bearing line in that section must read `measured YYYY-MM-DD`. Gate numbers elsewhere (in
151
+ `State`, in a round block) are not the current gate line, and historical round gate lines are
152
+ exempt — they are the record and are kept:
153
+
154
+ - `tsc`=0 · `build`=0 · hex **144** (ceiling 160) · 38/38 harnesses green · measured 2026-09-24
155
+
156
+ 4. **Name the HEAD in `State`** (`HEAD when this file was written: <short sha>`) — `HP-05` wants a
157
+ commit that exists in this repo and is an ancestor of HEAD.
158
+ 5. **Commit, then verify:**
159
+
160
+ git add -A && git commit
161
+ .goblin/bin/goblin-verify # HP-02, HP-03, HP-05 go green
162
+
163
+ Success is the class's full green path (`43 passed, 0 failed, 11 advisory, 28 skipped`, exit 0 for
164
+ class A) with `git status --short` empty.
165
+
166
+ The edit is additive and small — measured on the model repo (§1's exemplar, 2450 lines): three
167
+ headings plus a `State` block, one dated gate line and a `Not verified` block, 15 lines, no line
168
+ removed.
package/docs/CI.md ADDED
@@ -0,0 +1,187 @@
1
+ # The CI lane — what makes a workflow a gate
2
+
3
+ `goblin-verify` runs on a machine you control, at a moment you choose. A workflow runs on a machine
4
+ you do not, at a moment you do not. The two must not be able to disagree about one SHA: that
5
+ disagreement is the failure mode *"review is a prompt and a report, never a gate"* (R5 §1.1), and one
6
+ level up it is a **required check that goes green while the gate never ran**.
7
+
8
+ This file is the contract for the three things that close it: the part `ci-gate` (CL-01), the workflow
9
+ the installer writes, and the two rows — `PG-05` and `PG-06` — that read it.
10
+
11
+ ---
12
+
13
+ ## 1. A workflow file is not a gate
14
+
15
+ Four settings separate the two. None of them lives in the repository, which is why no check in this
16
+ harness can make them true — they are **forge** changes, and `PG-04` is the row that records that.
17
+
18
+ | # | Setting | Why it is load-bearing |
19
+ |---|---|---|
20
+ | 1 | **Required check** — Settings → Branches (or a ruleset) requires the gate job | A workflow nobody waits on cannot block anything. It reports and is ignored. |
21
+ | 2 | **`Do not allow bypassing the above settings`** | *"By default, the restrictions of a branch protection rule don't apply to people with admin permissions"*, and *"People and apps with admin permissions to a repository are always able to push to a protected branch"*. Without this switch the rule binds everyone except the person who wrote it. It is per-rule, and it applies to a single branch at a time. |
22
+ | 3 | **A push identity that is not the repo's sole admin** | A required check armed under your own identity binds nobody. This is the honest blocker in the estate today: the sole admin of every repo is the only committer to it, so a required check is advisory for exactly the pushes that matter. Changing it is a forge/account change, not a repository change. |
23
+ | 4 | **Never conditional — not the job, not the step** | *"A job that is skipped will report its status as 'Success'. It will not prevent a pull request from merging, even if it is a required check."* GitHub reports a **skipped** job as Success, so a `if:` anywhere above the gate step is a green light for a commit whose gate never ran. `PG-05` enforces this mechanically. |
24
+
25
+ **Where a private repo's minutes go.** GitHub Free gives 2,000 Actions minutes/month (S12); a
26
+ typecheck + build is 3–5 minutes, so five repos on every push fits comfortably. A Unity job does not
27
+ (30-minute timeout, image-cache churn), which is why the one existing C# workflow is the outlier and
28
+ not the model.
29
+
30
+ **Ground truth, measured at the W4 revision** — `find ~/projects -maxdepth 6 -type d -name workflows
31
+ -path '*/.github/*'`:
32
+
33
+ - `~/projects/supreme-unity/.github/workflows/unity-compile-check.yml` — the estate's **only**
34
+ first-party CI, and it self-skips (6 of its 7 steps are guarded by
35
+ `if: steps.check.outputs.ready == 'true'`, and the step that sets `ready=false` is not guarded).
36
+ - `~/projects/pstack-upstream/.github/workflows/validate-plugins.yml` — an upstream clone of someone
37
+ else's project, not part of the estate.
38
+ - Everything else: none. Of the repos with a remote (`goblin-stack`, `goblin-ui`, `open-door`,
39
+ `tech-goblin/frontend`, `tech-goblin/lab/diagram-studio`, `supreme-unity`) five have no CI at all.
40
+ A research vault, a notes repo and a no-remote directory are not candidates.
41
+
42
+ ---
43
+
44
+ ## 2. What goblin-stack places, and what it checks
45
+
46
+ The `ci-gate` part is `R` for classes A and F, `O` for C and E, and `-` for B and D
47
+ (`manifest/classes.tsv`). When it is installed, the installer renders
48
+ `templates/ci/goblin-gate.yml.tmpl` into `.github/workflows/goblin-gate.yml` — one job, no `if:` at
49
+ any level, whose only step runs `.goblin/bin/goblin-verify`. It is `owned`, so a second install is a
50
+ no-op and a hand-edited copy is never overwritten.
51
+
52
+ Three rows read it:
53
+
54
+ - **`CL-01`** — the part's presence/absence is a class contract. Its artifact is
55
+ `.github/workflows/goblin-gate.yml`, **not** the `.github/` directory: a class that forbids
56
+ `ci-gate` forbids *goblin-stack writing a workflow there*, never the repo having CI of its own.
57
+ - **`PG-05`** — no job-level and no step-level `if:` anywhere in any workflow under
58
+ `.github/workflows/`, and every job must declare at least one `run:`/`uses:` step. A job that
59
+ declares nothing, a workflow with no `jobs:`, and the measured real shape (one *unguarded* step
60
+ deciding whether the guarded gate step runs) all FAIL.
61
+ - **`PG-06`** — the declared gate set is the gate CI runs. The declared set comes from
62
+ `g_yaml_gates` — the same reader `GT-01` uses, so a gate cannot vanish from the comparison in
63
+ silence. A workflow satisfies the row when the whole declared set is **run**: the verifier with no
64
+ `--only` (a full run executes every declared gate through `GT-02`), or every declared gate command
65
+ appearing verbatim.
66
+
67
+ Comments are blanked before either row reads the file, so a commented-out verifier call is not a run.
68
+ `#` inside a quoted string is the one reading error this costs: the rest of the line is treated as a
69
+ comment and cannot count toward a pass.
70
+
71
+ ---
72
+
73
+ ## 3. The Electron / desktop-shell class (F)
74
+
75
+ Class F exists because a desktop shell needs a part no other class has — a **host gate**, a number
76
+ measured on a machine with a display — and forbids a thing the others allow: a **renderer that
77
+ reaches Node or the filesystem directly**. That is a new row-set, not a flag on class A.
78
+
79
+ ### 3.1 The failure surface, and the check for each
80
+
81
+ | Failure mode | What the harness does | Row |
82
+ |---|---|---|
83
+ | Renderer has Node access (`nodeIntegration: true`) | ban, text probe over the class's globs | `BN-06` |
84
+ | Context isolation / process sandbox off | ban (one rule: Electron's docs make them one property) | `BN-07` |
85
+ | `webSecurity: false`, `allowRunningInsecureContent`, `enableBlinkFeatures`, `allowpopups` | one ban, four one-line patterns | `BN-08` |
86
+ | `sendSync(` / `@electron/remote` in a hot path | ban | `BN-09` |
87
+ | Blocking the main process | **host gate** — see §3.2 | declared, `PF-01` |
88
+ | Renderer idle-CPU spin (`idleWakeupsPerSecond`) | host gate, sampled over a 60 s window | declared |
89
+ | A leak that grows over hours | host gate; an 8-hour property a 30-minute gate cannot see | declared |
90
+ | The bundle ships dev dependencies | `SC-08`/`SC-03` read the manifest and the build output | existing |
91
+ | A renderer importing main (or the reverse) | **not mechanised** — needs a dependency graph; see §4 | — |
92
+ | IPC without `event.senderFrame` validation | **not mechanised** — semantic; see §4 | — |
93
+ | Fuses left at defaults (`runAsNode`) | **not mechanised** — a package-time read of the built binary | — |
94
+
95
+ ### 3.2 The perf lane: one ratchet, and a host gate beside it
96
+
97
+ There is **one** mechanism, and class F reuses it unchanged: `ratchet: {name, cmd, ceiling}`,
98
+ enforced by `GT-04`/`GT-05` and pinned to a commit by `PF-01`. A second perf mechanism is not
99
+ introduced.
100
+
101
+ What the ratchet measures here is `app_bundle_bytes` — the packaged bundle's byte count. It is
102
+ hermetic, it needs no browser, no display and no dependency, and a fat bundle is a slow cold start on
103
+ every machine. **The FPS number is declared as a host gate instead** (`perf_host_gate:` in
104
+ `presets/F-electron.yaml`), and carried in the HANDOFF with the date it was measured.
105
+
106
+ This is a **deviation from G6 §B.3**, which put `main_thread_busy_pct` in the ratchet, and the reason
107
+ is measured: the instrument that produces it — CDP `Performance.getMetrics`, or
108
+ `app.getAppMetrics()[i].cpu.percentCPUUsage` inside a real Electron — needs Playwright or Electron
109
+ plus a GUI, and the dependency contract (`docs/CONTRACTS.md`) allows a shipped rule nothing but
110
+ bash/git/awk/sed/grep/python3. A `ratchet.cmd` that cannot run makes a fresh install **born RED**,
111
+ which is the one thing the install path must not produce. The probe belongs to the project, next to
112
+ the code it measures; a project whose CI needs npm runs it in **its own** workflow, never in
113
+ goblin-stack's shipped contract.
114
+
115
+ ### 3.3 Why the FPS metric is `main_thread_busy_pct` and not frame time
116
+
117
+ Measured on a real Chromium (Playwright, CDP `Performance.getMetrics`), sweeping the main-thread
118
+ workload from 0 to 32 ms per frame:
119
+
120
+ ```
121
+ burn= 0 (0ms/frame) frames=194 p50=16.70ms p95=16.80ms p99=16.80ms >2xp50=0 main-thread-busy= 1.8%
122
+ burn= 1 (2ms/frame) frames=187 p50=16.70ms p95=16.80ms p99=16.80ms >2xp50=0 main-thread-busy=13.5%
123
+ burn= 2 (4ms/frame) frames=188 p50=16.70ms p95=16.70ms p99=16.80ms >2xp50=0 main-thread-busy=26.0%
124
+ burn= 4 (8ms/frame) frames=191 p50=16.70ms p95=16.80ms p99=33.40ms >2xp50=1 main-thread-busy=54.5%
125
+ burn= 6 (12ms/frame) frames= 7 p50=366.70ms p95=2550.00ms p99=2550ms >2xp50=3 main-thread-busy=76.9%
126
+ burn=10 (20ms/frame) frames=157 p50=16.70ms p95=33.40ms p99=33.40ms >2xp50=11 main-thread-busy=99.5%
127
+ burn=16 (32ms/frame) frames= 92 p50=33.30ms p95=33.50ms p99=50.00ms >2xp50=0 main-thread-busy=96.6%
128
+ ```
129
+
130
+ - `main_thread_busy` is the **only monotone instrument**: 1.8 → 13.5 → 26.0 → 54.5 → 76.9 → 99.5 %.
131
+ It moves with the workload across the whole range, and it is a *ratio* (CDP `TaskDuration` ÷ wall
132
+ time), so it needs no per-run calibration.
133
+ - **`p50` frame time is FLAT at 16.70 ms from burn=0 through burn=4** — while the main thread goes
134
+ from 1.8 % to 54.5 % busy. A frame-time gate at any threshold near 16.7 ms is **green on the idle
135
+ tree and green on the loaded tree** — `PROJECT-PRACTICE.md` §3, reproduced live in the exact metric
136
+ the note proposed.
137
+ - **Dropped frames (`>2×p50`) are non-monotone**: 0, 0, 0, 1, 3, 11, 0. At burn=16, the worst
138
+ workload in the sweep, the counter reads **0** — at 33.3 ms pacing every frame is equally slow and
139
+ nothing is 2× anything. A dropped-frame gate would pass the worst case.
140
+ - The sampler **starves** at burn=6 (7 frames in a 3.2 s window): any gate that reports "frames
141
+ counted" must treat a collapsed sample count as failure, not as data.
142
+
143
+ The same number exists on both sides of the process boundary. In the renderer it is CDP
144
+ `Performance.getMetrics` (`TaskDuration`/wall). In the main process it is
145
+ `app.getAppMetrics()[i].cpu.percentCPUUsage`, whose own docs note it *"starts a new measurement
146
+ interval"* per call — so the probe's sample window **is** the interval, and two probes in one run
147
+ interfere. `idleWakeupsPerSecond` is the idle-spin half, an Electron-specific field, and it is an
148
+ average over the time since the previous call, which makes a 60-second idle sample the natural unit.
149
+
150
+ **A perf number measured on one box is not a user's experience.** This box is an LXC with no display
151
+ and no system Chromium, so the numbers above come from a *bundled headless* Chromium with no
152
+ compositor and no vsync. Relative comparisons within one machine are meaningful — which is why
153
+ `main_thread_busy_pct` works as a gate at all — and an absolute FPS claim is not. The gate can catch
154
+ *"this commit made the main thread 3× busier than the SHA we measured"*; it can never answer *"does it
155
+ feel smooth on a laptop"*.
156
+
157
+ ---
158
+
159
+ ## 4. What is not mechanised, and why
160
+
161
+ Recorded rather than shipped, so a later session does not re-derive them:
162
+
163
+ - **A dependency-graph run over the renderer/main boundary.** Mechanisable *once the directory
164
+ convention is declared*, but it needs a graph tool, and no declaration exists yet.
165
+ - **`ipcMain` handler sender validation.** Semantic: proving `event.senderFrame` was checked requires
166
+ reading a control-flow path, not a line.
167
+ - **Fuses at package time.** The honest form reads the *built binary*, which needs a package step.
168
+ - **A real user's device, GPU, compositor and thermal state.** Not observable from any machine but
169
+ theirs.
170
+ - **An 8-hour leak.** A gate gets 30 minutes; a slope extrapolated from 30 minutes is a hypothesis.
171
+ - **Packaging, signing, notarisation, installers, auto-update, OS integration.** OS-specific; none of
172
+ it observable here.
173
+ - **Whether the app is *right*.** As with every gate: green means the declared checks ran.
174
+
175
+ ## 5. What this lane cannot see
176
+
177
+ The workflow rows read a **file**. They cannot see branch protection, the required-check list, or
178
+ whether the forge ever ran the job (`PG-04` is the row that records that, and it is advisory by
179
+ design). `PG-06` proves a declared gate is **invoked** in a file under `.github/workflows/` — never
180
+ that the forge marks that job required, never that it is the job the forge waits on, and never that
181
+ the workflow *can fail* (`PG-05` is the row for that). A command sitting inside a `name:`, `env:` or
182
+ `with:` value is dropped by the reader, but a command inside a quoted string that happens to match
183
+ the verifier's name is not. And nothing here can arm a check: a workflow file, a required check and a
184
+ push identity are three different things, and only the first one is in the repository.
185
+
186
+ See `docs/LIMITS.md` #34 for the Electron perf gap, #13 for the `PG-05` reading, and #18 for the
187
+ record every drift check trusts.
@@ -0,0 +1,197 @@
1
+ # Contracts — installer and verifier
2
+
3
+ Dependencies: **`bash`, `git`, `awk`, `sed`, `grep`, `python3`.** No npm, no jq, no yq, no
4
+ network. `python3` is used for one thing only: reading the two-level model mapping file, the
5
+ same way the fleet's own tool reads it. Everything else is line-oriented shell.
6
+
7
+ ## Install
8
+
9
+ goblin-install --target <dir> [options]
10
+
11
+ --target <dir> required; the repo root to install into
12
+ --class A|B|C|D|E|F required unless --uninstall or --re-pin
13
+ --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
14
+ --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md)
15
+ --parts <list> comma list to install; default = every part the class requires
16
+ --archive mark the project archive: verify requires no HANDOFF and no gates
17
+ --skills yes|no install .hermes/skills (default yes; needs the one-time trust step)
18
+ --dry-run print the plan; write nothing
19
+ --upgrade re-install at the current version; report created/updated/unchanged/skipped
20
+ --opt-out <part> record the part in disabled: so its required checks are skipped
21
+ --uninstall remove exactly the files in installed.json
22
+ --re-pin re-record practice_sha256: for an edited standard; nothing else changes
23
+ --force allow overwriting a file goblin-stack did not create
24
+ --yes non-interactive; take the defaults above
25
+
26
+ Exit codes: `0` success or no-op · `1` a refusal, with the path and the fix · `2` bad input or a
27
+ missing dependency.
28
+
29
+ ### Idempotency
30
+
31
+ The installer writes only paths it records, and it **hash-compares before writing**, so running
32
+ it twice with the same arguments and the same `VERSION` prints `no-op: N files unchanged` and
33
+ exits 0 **without touching a byte**. A different `VERSION` is an upgrade: it rewrites only the
34
+ files whose hash changed and prints `created C · updated U · unchanged N · skipped S`.
35
+
36
+ ### Three kinds of file, and why the distinction matters
37
+
38
+ | kind | recorded as | overwritten? | hash-checked? | removed by `--uninstall`? |
39
+ |---|---|---|---|---|
40
+ | installed artifact (bin, manifest, roles, skills, harness scaffold) | `files` | yes, on upgrade | yes — IN-02, SK-02 | yes |
41
+ | created once, then yours (`.goblin/goblin.yaml`, `HANDOFF.md`, `AGENTS.md`, `*-SPEC.md`, `reviews/.gitkeep`) | `owned` | never | no — you are meant to edit them | no, except the config |
42
+ | pre-existing, left alone | `refused` | never | no — IN-04 only proves it was not taken over | no |
43
+
44
+ A `refused` path is not a dead end. For `HANDOFF.md` the remedy is the reconciliation in
45
+ `docs/ADOPTION.md` ("Adopting into a repo that already has a `HANDOFF.md`"): keep the project's
46
+ file, merge the five required sections and a dated gate line in, then verify. `--force` overwrites
47
+ it and exists for a scaffold copy with nothing to keep.
48
+
49
+ An **edited standard is not a dead end** either. `practice_sha256:` pins the referenced standard
50
+ and `IN-02` re-checks it, so one intended edit to the standard reds `IN-02` in every installed
51
+ repo. The remedy is the explicit re-pin below — not a hand-edit of the hash, and never an
52
+ automatic one.
53
+
54
+ `.gitignore` is not a file goblin-stack owns: it appends **one marked block** and never rewrites
55
+ the rest. `.goblin/goblin.yaml` is generated once and is goblin-stack's own config, so
56
+ `--uninstall` removes it; `HANDOFF.md`, `AGENTS.md`, `ROUND-000-SPEC.md` and `reviews/` are the
57
+ project's record, not the harness's, and are left in place.
58
+
59
+ ### An edited standard is not a dead end
60
+
61
+ `.goblin/goblin.yaml` records `practice:` and `practice_sha256:`, and `IN-02` re-checks that hash.
62
+ The pin has exactly one purpose: to make a **silently** edited standard visible rather than
63
+ assumed. The standard itself is a living document, corrected in place, so an edit that is
64
+ *intended* needs a deliberate way to re-record the pin. That is all `--re-pin` is:
65
+
66
+ goblin-install --target <dir> --re-pin
67
+
68
+ It rewrites one line of the config — nothing else — and prints both hashes:
69
+
70
+ practice re-pinned: /path/to/PROJECT-PRACTICE.md
71
+ recorded 81612b17ac3483b9d613aeb86e236539fc17403e38bf7903af708d369cf7918f
72
+ now 8334ac24f056c94c35fa97831253e7e12bace68ae5747c81ae3b696c42ddd33a
73
+
74
+ `.goblin/bin/goblin-verify --only IN-02` then reports `practice pin ok`. Every other line of
75
+ `goblin.yaml`, comments included, is untouched, so the `owned` contract holds for everything
76
+ except the one value you just asked to re-record. Commit the config like any other change.
77
+ `--dry-run` prints the plan and writes nothing; when there is nothing to do it prints
78
+ `practice pin already current` and exits 0.
79
+
80
+ Four things it deliberately is not:
81
+
82
+ - **not automatic.** `goblin-verify` never re-pins, and neither does a plain `--upgrade`: a pin
83
+ that updated itself would be the very silent edit the pin exists to catch. The `practice EDITED`
84
+ failure detail names this command, so the remedy is printed where the operator meets the problem.
85
+ - **not a hand-edit.** Editing `practice_sha256:` by hand does work, but nothing then checks that
86
+ you pasted the right hash — which is the one thing this command does for you.
87
+ - **not `--force`.** `--force` is about overwriting a file goblin-stack did not create; it has no
88
+ opinion about the pin.
89
+ - **not a re-point.** It re-records the hash of the path already in `practice:`. Pointing the repo
90
+ at a *different* standard is a deliberate config edit, not a re-pin.
91
+
92
+ Refusals are exit `2`, each naming the path: no `.goblin/goblin.yaml`, no `practice:` recorded, the
93
+ recorded path absent, or no `practice_sha256:` line to rewrite. A failed write is exit `1`, with
94
+ the path — the command never reports a re-pin that did not land. One refusal guards the mode
95
+ itself: `--uninstall --re-pin` is exit `2` with `--uninstall and --re-pin are different jobs; run
96
+ them one at a time` — the two modes run one at a time, and the pair is refused before anything is
97
+ removed.
98
+
99
+ ### Never a half-state
100
+
101
+ If any write fails the installer prints the file and exits 1. It does not roll back, because
102
+ every created file is either a whole file or absent, and the record is written last.
103
+
104
+ ## Verify
105
+
106
+ goblin-verify [--only <id[,id...]>] [--json] [--list] [--source <goblin-stack path>]
107
+
108
+ Output is one line per executed row, in manifest order, plus a summary line at the end:
109
+
110
+ PASS HP-01 (test -f HANDOFF.md)
111
+ FAIL GT-02 gate commit: false -> exit 1
112
+ ADV MD-02 code lane and review lane both resolve to the same family
113
+ SKIP HS-02 no pinned pre-change commit yet - REPLAY not provable
114
+
115
+ Those four lines are one row of each marking. The summary line of a green class-A run is:
116
+
117
+ 43 passed, 0 failed, 11 advisory, 28 skipped
118
+
119
+ **Exit codes:** `0` every executed check passed (advisories and skips do not fail the run) ·
120
+ `1` at least one check FAILED · `2` verify could not run (not installed, a missing dependency,
121
+ an unparseable config) · `3` the manifest itself is broken (a row with no check and no
122
+ `advisory` label, or a duplicate id).
123
+
124
+ **What verify asserts, in one sentence:** that the files it installed are the files on disk,
125
+ that every rule in the manifest with a command still passes, and that the untestable remainder
126
+ is counted and capped.
127
+
128
+ **What it cannot see** (printed at the end of every run): whether a check in the harness dir
129
+ tests the right path rather than merely passing; whether the forge is bound by the workflow
130
+ `CL-01` found; whether a human
131
+ read the diff; whether the model mapping names a family that actually differs; and whether
132
+ `.goblin/installed.json` — the record every drift check trusts — was itself rewritten, since it
133
+ is not signed (`docs/LIMITS.md` #18). The CI lane adds its own, and `docs/CI.md` is where the four
134
+ settings that make a workflow a **gate** are written down.
135
+
136
+ ### A fresh install verifies green
137
+
138
+ Measured on a fresh class-A install, committed with no hand edit: **`43 passed, 0 failed,
139
+ 11 advisory, 28 skipped`, exit 0.** Twenty-eight rows skip with a reason: `HS-02` — no pre-change commit
140
+ is pinned yet, so the REPLAY is not provable (`docs/LIMITS.md` #11) — `AU-02` and `AU-03`, which
141
+ have no report to audit in a repo where no reporter has run — `SC-06`, `SC-07` and `SC-08`, which
142
+ have no dependency manifest, no lockfile and no audit record to read yet — `PF-01`, which has
143
+ no measured perf baseline — `BN-01`/`BN-02`/`BN-05`, which have no `src/` tree for a ban to read,
144
+ and `BN-03` with the four electron bans
145
+ `BN-06`..`BN-09`, which this class does not enable (`bans: [BN-01, BN-02, BN-05]`), so they skip as
146
+ *not enabled* rather than as *unread* — `FM-01`/`FM-02`/`VA-01`, which have no feature map and no declared
147
+ `verify_doctor:` yet (`feature_map:` and `verify_doctor:` ship empty on purpose: a fresh install
148
+ must not be born RED — G1, `docs/LIMITS.md` #30) — `RC-01`..`RC-04`, which read a declared
149
+ reference corpus and a lab `manifests/` directory a fresh install has neither of
150
+ (`reference_manifest:` and `quarantine_root:` ship empty on purpose: a repo with no corpus must
151
+ not be born RED) — and `JG-01` with `LP-01`..`LP-05`, which have
152
+ no `.goblin/loop/` record because no loop has run in this repo: the six judge/loop rows are
153
+ **absent-state** rows, and a fresh install must not be born RED either. **Two** rows do
154
+ **not** skip, both of them the CI lane's: `PG-05` and `PG-06` read the workflow this class installs.
155
+ Two, not four — the four electron bans named in the skip list above do skip here, and a tree without
156
+ a renderer would skip them the same way. Every skip above
157
+ is a *not yet*, not a pass.
158
+
159
+ Two of the eleven advisories arrive with the same lane. `JG-02` reports that the judge lane
160
+ resolves to **no profile** on this fleet — measured `bash bin/goblin-model judge` →
161
+ `judge unknown unknown unknown` — so the row prints the one-line remedy and reports ADV rather
162
+ than failing the repo for the fleet's routing (`docs/ROLES.md`, "the measured caveat"). `JG-03`
163
+ is the counted advisory row the ceiling had left free for G2 (`docs/ENFORCEMENT.md`).
164
+
165
+ The class's required parts that only a round can produce do **not** fail on a fresh install; they
166
+ pass **vacuously**, and that is the honest reading: `PG-01`..`PG-03` iterate over `reviews/*.md`
167
+ and there are none, and the declared gate *is* the shipped floor until step 3 replaces it. `P8`
168
+ (`goblin-bootstrap`) walks that first-step list because the work is not done, not because the
169
+ verifier is reporting FAILs.
170
+
171
+ ## Opting out, and uninstalling
172
+
173
+ - **Per part:** `--opt-out <part>` records the part in `disabled:`. `goblin-verify` then reports
174
+ the part's rows as `SKIP (opt-out)` in the summary, so the opt-out is **visible rather than
175
+ absent**. The same mechanism is what makes a class's `-` (off) real.
176
+ - **The opt-out numbers are pinned (V3-3).** A fresh class-A install with `--skills no` verifies
177
+ `39 passed, 0 failed, 11 advisory, 28 skipped`, exit 0, and `tests/t-install-off-switch.sh`
178
+ asserts that line: a silent drift in the opt-out path is caught rather than left as a number
179
+ nobody wrote down (the `--skills no` count moved from `37/0/9/11` at v0.2 to here when the ban
180
+ rows landed, and no file recorded the shift; **the skipped count moved 15 → 18 on 2026-09-25
181
+ (G1)** — the three new feature-map rows skip on the same path for the same reason, measured;
182
+ **and 18 → 24 on 2026-09-25 (W3)** — the six judge/loop rows skip there too, measured).
183
+ - **Whole harness:** `--uninstall` deletes the `files` list plus `.goblin/goblin.yaml`, removes
184
+ every directory that leaves empty (deepest first, after `installed.json` itself is gone — the
185
+ order that used to leave `.goblin/` and the sixteen `.hermes/skills/*` directories behind),
186
+ leaves `HANDOFF.md`, `AGENTS.md`, `ROUND-000-SPEC.md`, `reviews/` and the `.gitignore` block
187
+ (with a `# goblin-stack uninstalled <date>` marker inside it), and prints what it removed and
188
+ what it left.
189
+
190
+ ## The two commands, verbatim
191
+
192
+ bash bin/goblin-install --target /path/to/repo --class A
193
+ .goblin/bin/goblin-verify
194
+
195
+ From a checkout, without installing anything:
196
+
197
+ bash tests/run-tests.sh