@techgoblin/gobstack 0.0.0-stage → 0.4.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +351 -0
- package/LICENSE +21 -0
- package/README.md +163 -2
- package/VERSION +1 -0
- package/adapters/_template/adapter.tsv +16 -0
- package/adapters/_template/detect.sh +10 -0
- package/adapters/_template/emit.sh +5 -0
- package/adapters/_template/verify.sh +4 -0
- package/adapters/claude/adapter.tsv +8 -0
- package/adapters/claude/detect.sh +8 -0
- package/adapters/claude/verify.sh +47 -0
- package/adapters/codex/adapter.tsv +12 -0
- package/adapters/codex/detect.sh +9 -0
- package/adapters/codex/verify.sh +45 -0
- package/adapters/copilot/adapter.tsv +10 -0
- package/adapters/copilot/detect.sh +8 -0
- package/adapters/copilot/verify.sh +45 -0
- package/adapters/cursor/adapter.tsv +11 -0
- package/adapters/cursor/detect.sh +10 -0
- package/adapters/cursor/verify.sh +45 -0
- package/adapters/gemini/adapter.tsv +15 -0
- package/adapters/gemini/detect.sh +11 -0
- package/adapters/gemini/verify.sh +49 -0
- package/adapters/hermes/adapter.tsv +9 -0
- package/adapters/hermes/detect.sh +8 -0
- package/adapters/hermes/verify.sh +27 -0
- package/adapters/opencode/adapter.tsv +14 -0
- package/adapters/opencode/detect.sh +9 -0
- package/adapters/opencode/verify.sh +45 -0
- package/automations/README.md +53 -0
- package/automations/bugreporter-intake.sh +145 -0
- package/automations/drift-audit.sh +139 -0
- package/automations/report.schema.tsv +10 -0
- package/bans/README.md +82 -0
- package/bans/grep-ban.sh +84 -0
- package/bans/layer-check.sh +57 -0
- package/bin/goblin +119 -0
- package/bin/goblin-audit +145 -0
- package/bin/goblin-bans +178 -0
- package/bin/goblin-doctor +233 -0
- package/bin/goblin-emit +482 -0
- package/bin/goblin-init +519 -0
- package/bin/goblin-install +720 -0
- package/bin/goblin-lib.sh +289 -0
- package/bin/goblin-model +105 -0
- package/bin/goblin-upgrade +572 -0
- package/bin/goblin-verify +2798 -0
- package/bin/goblin.js +48 -0
- package/docs/ADOPTION.md +168 -0
- package/docs/CI.md +187 -0
- package/docs/CONTRACTS.md +197 -0
- package/docs/DESIGN.md +92 -0
- package/docs/ENFORCEMENT.md +225 -0
- package/docs/FLOWS.md +164 -0
- package/docs/GUARDRAILS.md +126 -0
- package/docs/GUIDE.md +610 -0
- package/docs/INTEGRATION.md +92 -0
- package/docs/LIMITS.md +591 -0
- package/docs/LOOP.md +165 -0
- package/docs/RE-PLAYBOOK.md +183 -0
- package/docs/RISKS.md +70 -0
- package/docs/ROLES.md +105 -0
- package/manifest/bans.tsv +9 -0
- package/manifest/classes.tsv +61 -0
- package/manifest/enforcement.tsv +88 -0
- package/manifest/glossary.tsv +25 -0
- package/manifest/playbooks.tsv +16 -0
- package/package.json +36 -4
- package/presets/A-shipped-software.yaml +48 -0
- package/presets/B-service-config.yaml +40 -0
- package/presets/C-game.yaml +38 -0
- package/presets/D-knowledge.yaml +41 -0
- package/presets/E-fleet-config.yaml +42 -0
- package/presets/F-electron.yaml +67 -0
- package/roles.yaml +54 -0
- package/skills/goblin-bootstrap/SKILL.md +51 -0
- package/skills/goblin-bugfix/SKILL.md +26 -0
- package/skills/goblin-bugreporter/SKILL.md +52 -0
- package/skills/goblin-drift-audit/SKILL.md +43 -0
- package/skills/goblin-eval/SKILL.md +68 -0
- package/skills/goblin-feature/SKILL.md +26 -0
- package/skills/goblin-feature-map/SKILL.md +140 -0
- package/skills/goblin-handoff/SKILL.md +28 -0
- package/skills/goblin-investigation/SKILL.md +26 -0
- package/skills/goblin-judge/SKILL.md +74 -0
- package/skills/goblin-loop/SKILL.md +88 -0
- package/skills/goblin-mode/SKILL.md +70 -0
- package/skills/goblin-overnight/SKILL.md +42 -0
- package/skills/goblin-pr-gate/SKILL.md +42 -0
- package/skills/goblin-re-mobile/SKILL.md +51 -0
- package/skills/goblin-refactor/SKILL.md +23 -0
- package/skills/goblin-sweep/SKILL.md +23 -0
- package/skills/goblin-tdd-repro/SKILL.md +27 -0
- package/skills/goblin-verify-author/SKILL.md +50 -0
- package/skills/practice/SKILL.md +37 -0
- package/templates/AGENTS.md.tmpl +23 -0
- package/templates/HANDOFF.md.tmpl +43 -0
- package/templates/SPEC.md.tmpl +34 -0
- package/templates/audit-waiver.tsv.tmpl +10 -0
- package/templates/boundary-waivers.tmpl +8 -0
- package/templates/checks/assert.mjs.tmpl +60 -0
- package/templates/checks/gate.sh.tmpl +29 -0
- package/templates/ci/goblin-gate.yml.tmpl +46 -0
- package/templates/goblin.yaml.tmpl +138 -0
- package/templates/install-hooks.allowlist.tmpl +9 -0
- package/templates/loop/decisions.tsv.tmpl +1 -0
- package/templates/loop/predicate.tmpl +16 -0
- package/templates/report.yaml.tmpl +16 -0
package/bin/goblin.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
// anything else (install) -> bin/goblin-install
|
|
17
|
+
//
|
|
18
|
+
// Non-negotiables (§4.3): args are passed as an ARRAY, never a shell string (no
|
|
19
|
+
// injection surface); `bash` is named explicitly (a packager stripping the
|
|
20
|
+
// executable bit must not break every command); the exit status is propagated
|
|
21
|
+
// VERBATIM so the four-value verify contract survives the shim. No dependencies,
|
|
22
|
+
// no async, CommonJS — this repo has no node tooling by design.
|
|
23
|
+
//
|
|
24
|
+
// The payload/ re-point happens with packaging (W3/W5): the npm tarball moves the
|
|
25
|
+
// bash payload under payload/, so this line becomes path.join(__dirname, "..", "payload", "bin", target).
|
|
26
|
+
// Until then the shim runs straight out of the checkout layout.
|
|
27
|
+
"use strict";
|
|
28
|
+
|
|
29
|
+
const { spawnSync } = require("node:child_process");
|
|
30
|
+
const fs = require("node:fs");
|
|
31
|
+
const path = require("node:path");
|
|
32
|
+
|
|
33
|
+
// --version prints the ONE source (VERSION) directly: a shim that routed --version to a
|
|
34
|
+
// subcommand would print a constant's copy of the number, the exact drift V6 exists for.
|
|
35
|
+
const arg0 = process.argv[2];
|
|
36
|
+
if (arg0 === "--version" || arg0 === "-V" || arg0 === "-v") {
|
|
37
|
+
process.stdout.write(fs.readFileSync(path.join(__dirname, "..", "VERSION"), "utf8"));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SCRIPT = { verify: "goblin-verify", bans: "goblin-bans", audit: "goblin-audit", upgrade: "goblin-upgrade", doctor: "goblin-doctor", emit: "goblin-emit", init: "goblin-init" };
|
|
42
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
43
|
+
const target = SCRIPT[cmd] ?? "goblin-install"; // install → goblin-install (v1); init is a real subcommand since W6
|
|
44
|
+
const file = path.join(__dirname, "..", "bin", target);
|
|
45
|
+
// execPath-independent: call bash explicitly so Windows-WSL/Git-Bash works and no
|
|
46
|
+
// shebang resolution is needed.
|
|
47
|
+
const r = spawnSync("bash", [file, ...rest], { stdio: "inherit" });
|
|
48
|
+
process.exit(r.status ?? 2);
|
package/docs/ADOPTION.md
ADDED
|
@@ -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
|