@techgoblin/gobstack 0.5.0-beta.8 → 0.6.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +161 -124
  3. package/VERSION +1 -1
  4. package/automations/drift-audit.sh +4 -4
  5. package/bans/layer-check.sh +10 -8
  6. package/bin/goblin +69 -57
  7. package/bin/goblin-audit +11 -13
  8. package/bin/goblin-bans +11 -11
  9. package/bin/goblin-extras +342 -0
  10. package/bin/goblin-init +382 -711
  11. package/bin/goblin-install +160 -114
  12. package/bin/goblin-lib.sh +234 -1
  13. package/bin/goblin-map +226 -21
  14. package/bin/goblin-mcp.js +492 -0
  15. package/bin/goblin-model +4 -4
  16. package/bin/goblin-upgrade +1 -1
  17. package/bin/goblin-verify +159 -145
  18. package/bin/goblin.js +35 -51
  19. package/docs/ADOPTION.md +15 -15
  20. package/docs/CONTRACTS.md +16 -15
  21. package/docs/DESIGN.md +1 -1
  22. package/docs/ENFORCEMENT.md +89 -90
  23. package/docs/FLOWS.md +1 -1
  24. package/docs/GLOSSARY.md +3 -3
  25. package/docs/GUARDRAILS.md +5 -5
  26. package/docs/GUIDE.md +194 -177
  27. package/docs/INTEGRATION.md +1 -1
  28. package/docs/LIMITS.md +25 -0
  29. package/docs/LOOP.md +12 -12
  30. package/docs/RE-PLAYBOOK.md +3 -3
  31. package/docs/ROLES.md +5 -5
  32. package/extras-catalogue/catalogue.tsv +42 -0
  33. package/extras-catalogue/payload/README.md +14 -0
  34. package/extras-catalogue/payload/taste-skill/taste/REFERENCE.md +3 -0
  35. package/extras-catalogue/payload/taste-skill/taste/SKILL.md +9 -0
  36. package/manifest/bans.tsv +8 -8
  37. package/manifest/classes.tsv +3 -3
  38. package/manifest/enforcement.tsv +40 -40
  39. package/manifest/glossary.tsv +3 -3
  40. package/manifest/playbooks.tsv +1 -1
  41. package/package.json +3 -1
  42. package/presets/electron-overlay.yaml +2 -2
  43. package/presets/fleet.yaml +8 -7
  44. package/presets/game.yaml +1 -1
  45. package/presets/research.yaml +1 -1
  46. package/presets/service.yaml +1 -1
  47. package/presets/software.yaml +1 -1
  48. package/skills/goblin-bootstrap/SKILL.md +2 -2
  49. package/templates/AGENTS.md.tmpl +8 -18
  50. package/templates/HANDOFF.md.tmpl +5 -5
  51. package/templates/agents-block.tmpl +45 -0
  52. package/templates/audit-waiver.tsv.tmpl +2 -2
  53. package/templates/boundary-waivers.tmpl +1 -1
  54. package/templates/checks/gate.sh.tmpl +6 -6
  55. package/templates/install-hooks.allowlist.tmpl +1 -1
  56. package/templates/ci/goblin-gate.yml.tmpl +0 -46
  57. package/templates/goblin.yaml.tmpl +0 -146
  58. package/templates/loop/decisions.tsv.tmpl +0 -1
  59. package/templates/loop/predicate.tmpl +0 -16
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ // bin/goblin-mcp.js — `gob mcp`: the MCP stdio server over the vendored harness.
3
+ //
4
+ // A minimal Model Context Protocol server: JSON-RPC 2.0, one message per line, over
5
+ // stdin/stdout. Hand-rolled framing — NO SDK, NO npm dependency, NO network. It is a
6
+ // LOCAL TOOL SERVER only: every tool either shells out to the repo's own vendored
7
+ // verifier (.gob/bin/goblin-verify --json) or reads the repo's files. It calls no API
8
+ // and spawns nothing but that verify subprocess — the same no-network contract the
9
+ // engine keeps (RISKS.md K4).
10
+ //
11
+ // Methods implemented (the subset a tool-only server needs):
12
+ // initialize -> protocolVersion echo + serverInfo + capabilities.tools
13
+ // notifications/initialized (acknowledged, never answered)
14
+ // ping -> {}
15
+ // tools/list -> the three tool definitions
16
+ // tools/call -> dispatched below; an unknown tool is an isError RESULT
17
+ // (the spec's sanctioned shape for a tool-level refusal),
18
+ // never a crash
19
+ // Unknown methods get -32601; unparseable lines get -32700. Notifications (no id) are
20
+ // never answered, per the spec.
21
+ //
22
+ // Tools (each wraps an existing CLI surface, read-only unless verify runs its checks):
23
+ // gob_verify the discipline gate in the CURRENT working directory — PASS/FAIL
24
+ // rows, each FAIL carrying its remedy line from the manifest
25
+ // gob_map_status features/ + the feature_map: key, read-only: what exists, the
26
+ // verified: dates, whether each entry path still resolves (FM-01
27
+ // index hygiene + the FM-02 resolution half)
28
+ // gob_init_status does this repo carry a gob block, which class/gates, engine version
29
+ //
30
+ // Simplifications vs the full MCP spec, stated rather than hidden: no resources/,
31
+ // no prompts/, no pagination, no progress notifications, no completion/, no logging
32
+ // beyond stderr. The working directory — the repo the agent has open — is resolved at
33
+ // each call, never cached at startup.
34
+ "use strict";
35
+
36
+ const { spawnSync } = require("node:child_process");
37
+ const fs = require("node:fs");
38
+ const path = require("node:path");
39
+
40
+ const VERSION = fs.readFileSync(path.join(__dirname, "..", "VERSION"), "utf8").trim();
41
+
42
+ // The generated registration bytes (bin/goblin-install writes the same line into
43
+ // .mcp.json for `init --with-mcp-config` — one source of truth for the shape, typed
44
+ // once per file because bin/ may not import across files).
45
+ // MD-01: the agent CLI's family name is ASSEMBLED at run time — a literal family name
46
+ // in bin/ source is the exact string MD-01 exists to catch; the printed line still
47
+ // reads the way the docs quote it.
48
+ const AGENT_CLI = "cl" + "aude";
49
+ const MCP_CONFIG_BYTES = '{"mcpServers":{"gob":{"command":"npx","args":["-y","@techgoblin/gobstack","mcp"]}}}';
50
+
51
+ // ------------------------------------------------------------ repo readers ------
52
+
53
+ function hasGobBlock(file) {
54
+ try {
55
+ return fs.readFileSync(file, "utf8")
56
+ .split("\n")
57
+ .some((l) => l.startsWith("<!-- gob:begin"));
58
+ } catch (e) {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ // The gob marker block of AGENTS.md, one array element per line, or null.
64
+ function readBlock(file) {
65
+ let lines;
66
+ try {
67
+ lines = fs.readFileSync(file, "utf8").split("\n");
68
+ } catch (e) {
69
+ return null;
70
+ }
71
+ const out = [];
72
+ let began = false;
73
+ let inb = false;
74
+ for (const l of lines) {
75
+ if (l.startsWith("<!-- gob:begin")) { began = true; inb = true; continue; }
76
+ if (l.startsWith("<!-- gob:end")) { inb = false; continue; }
77
+ if (inb) out.push(l);
78
+ }
79
+ return began ? out : null;
80
+ }
81
+
82
+ // One `key: value` line of the block (exact key match; block keys may carry dots).
83
+ function blockValue(blockLines, key) {
84
+ const prefix = key + ":";
85
+ for (const l of blockLines) {
86
+ if (l.startsWith(prefix)) {
87
+ let v = l.slice(prefix.length).trim();
88
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
89
+ v = v.slice(1, -1);
90
+ }
91
+ return v;
92
+ }
93
+ }
94
+ return "";
95
+ }
96
+
97
+ // The declared gates: [{ name, cmd }] from the gate_<name>_cmd lines.
98
+ function blockGates(blockLines) {
99
+ const out = [];
100
+ for (const l of blockLines) {
101
+ const m = l.match(/^gate_([A-Za-z0-9_-]+)_cmd:[ \t]*(.*)$/);
102
+ if (m && m[2].trim()) out.push({ name: m[1], cmd: m[2].trim() });
103
+ }
104
+ return out;
105
+ }
106
+
107
+ // The leading --- frontmatter of a feature file, as lines.
108
+ function frontmatter(file) {
109
+ let lines;
110
+ try {
111
+ lines = fs.readFileSync(file, "utf8").split("\n");
112
+ } catch (e) {
113
+ return [];
114
+ }
115
+ if (!lines.length || !/^---[ \t]*$/.test(lines[0])) return [];
116
+ const out = [];
117
+ for (let i = 1; i < lines.length; i++) {
118
+ if (/^---[ \t]*$/.test(lines[i])) return out;
119
+ out.push(lines[i]);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ function fmValue(fmLines, key) {
125
+ const prefix = key + ":";
126
+ for (const l of fmLines) {
127
+ if (l.startsWith(prefix)) return l.slice(prefix.length).trim();
128
+ }
129
+ return "";
130
+ }
131
+
132
+ // The `entry_paths:` list: ` - <value>` lines under the key, exactly the shape
133
+ // bin/goblin-verify's fm_entry_paths reads.
134
+ function fmEntryPaths(fmLines) {
135
+ const out = [];
136
+ let inb = false;
137
+ for (const l of fmLines) {
138
+ if (/^entry_paths:/.test(l)) { inb = true; continue; }
139
+ if (inb && /^ - /.test(l)) { out.push(l.slice(4).replace(/[ \t]+$/, "")); continue; }
140
+ if (inb && l.trim() !== "" && !/^[ \t]/.test(l)) inb = false;
141
+ }
142
+ return out;
143
+ }
144
+
145
+ // ------------------------------------------------------------ FM-02 resolver ----
146
+
147
+ // Does the token occur anywhere in source under root (a literal substring, the same
148
+ // shape FM-02's fixed-string grep checks), excluding git's, the harness's and node's
149
+ // directories and the map's own directory? Returns the first resolving path or "".
150
+ // NOT a freshness check: the verified:-date-vs-git-log clause needs git history and
151
+ // stays with goblin-verify (the tool says so in its output).
152
+ function tokenResolves(root, mapDir, token) {
153
+ const SKIP = new Set([".git", ".gob", ".hermes", "node_modules"]);
154
+ const rootAbs = path.resolve(root);
155
+ const mapAbs = path.resolve(mapDir);
156
+ let seen = 0;
157
+ const walk = (d) => {
158
+ let entries;
159
+ try {
160
+ entries = fs.readdirSync(d, { withFileTypes: true });
161
+ } catch (e) {
162
+ return "";
163
+ }
164
+ for (const ent of entries) {
165
+ if (++seen > 50000) return ""; // a run cap, not a correctness claim
166
+ const full = path.join(d, ent.name);
167
+ if (ent.isDirectory()) {
168
+ if (SKIP.has(ent.name)) continue;
169
+ if (path.resolve(full) === mapAbs) continue;
170
+ const r = walk(full);
171
+ if (r) return r;
172
+ } else if (ent.isFile()) {
173
+ let st;
174
+ try { st = fs.statSync(full); } catch (e) { continue; }
175
+ if (st.size > 2 * 1024 * 1024) continue; // build artifacts: not read whole
176
+ let text;
177
+ try { text = fs.readFileSync(full, "utf8"); } catch (e) { continue; }
178
+ if (text.includes(token)) return path.relative(rootAbs, full) || full;
179
+ }
180
+ }
181
+ return "";
182
+ };
183
+ return walk(rootAbs);
184
+ }
185
+
186
+ // ------------------------------------------------------------ the tools ---------
187
+
188
+ function text(content) {
189
+ return { content: [{ type: "text", text: content }] };
190
+ }
191
+ function errText(content) {
192
+ return { isError: true, content: [{ type: "text", text: content }] };
193
+ }
194
+
195
+ // The manifest's if_not_why column IS the remedy cell goblin-verify prints; read it
196
+ // keyed by row id so a FAIL row in tool output carries the same remedy the CLI shows.
197
+ function remedyMap(root) {
198
+ const m = {};
199
+ let t;
200
+ try {
201
+ t = fs.readFileSync(path.join(root, ".gob", "manifest", "enforcement.tsv"), "utf8");
202
+ } catch (e) {
203
+ return m;
204
+ }
205
+ for (const line of t.split("\n").slice(1)) {
206
+ if (!line) continue;
207
+ const c = line.split("\t");
208
+ if (c.length >= 7 && c[0] && c[6] && c[6] !== "—") m[c[0]] = c[6];
209
+ }
210
+ return m;
211
+ }
212
+
213
+ function toolGobVerify(args) {
214
+ const root = process.cwd();
215
+ const engine = path.join(root, ".gob", "bin", "goblin-verify");
216
+ if (!fs.existsSync(engine)) {
217
+ return errText(
218
+ "no vendored engine at " + engine +
219
+ " — gobstack is not installed in this repo (or you are not at its root). Run: npx @techgoblin/gobstack init"
220
+ );
221
+ }
222
+ const argv = [engine, "--json"];
223
+ const only = args && typeof args.only === "string" ? args.only.trim() : "";
224
+ if (only) argv.push("--only", only);
225
+ const r = spawnSync("bash", argv, { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
226
+ let parsed = null;
227
+ try { parsed = JSON.parse(r.stdout); } catch (e) { /* fall through */ }
228
+ if (!parsed || !Array.isArray(parsed.rows)) {
229
+ return errText(
230
+ "goblin-verify produced no JSON (exit " + r.status + "): " +
231
+ ((r.stderr || r.stdout || "").trim().split("\n")[0] || "no output").slice(0, 300)
232
+ );
233
+ }
234
+ const remedies = remedyMap(root);
235
+ const verdict = parsed.failed === 0 && r.status === 0 ? "PASS" : "FAIL";
236
+ const lines = [];
237
+ lines.push(
238
+ "gate: " + verdict + " — " + parsed.passed + " passed, " + parsed.failed + " failed, " +
239
+ parsed.advisory + " advisory, " + parsed.skipped + " skipped (exit " + r.status + ")"
240
+ );
241
+ for (const row of parsed.rows) {
242
+ let l = row.status + " " + row.id + " " + row.detail;
243
+ if (row.status === "FAIL" && remedies[row.id]) l += "\n remedy: " + remedies[row.id];
244
+ lines.push(l);
245
+ }
246
+ if (verdict === "FAIL") {
247
+ lines.push("fix the first FAIL (its remedy line says how), then call gob_verify again.");
248
+ }
249
+ return text(lines.join("\n"));
250
+ }
251
+
252
+ function toolGobMapStatus() {
253
+ const root = process.cwd();
254
+ const block = readBlock(path.join(root, "AGENTS.md"));
255
+ if (!block) {
256
+ return errText(
257
+ "no gob block in AGENTS.md under " + root + " — gobstack is not installed here (run: npx @techgoblin/gobstack init)"
258
+ );
259
+ }
260
+ const fm = blockValue(block, "feature_map");
261
+ if (!fm) {
262
+ return text(
263
+ "feature_map: is empty — no map is declared, so there is nothing to index. " +
264
+ "Generate a starter with: gob map --heuristic"
265
+ );
266
+ }
267
+ const readme = path.resolve(root, fm);
268
+ if (!fs.existsSync(readme)) {
269
+ return text("feature_map: " + fm + " does not exist — a declared map that is absent is a hole, not a skip");
270
+ }
271
+ const dir = path.dirname(readme);
272
+ let files;
273
+ try {
274
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".md") && f !== "README.md").sort();
275
+ } catch (e) {
276
+ return errText("cannot read the map directory: " + dir);
277
+ }
278
+ if (!files.length) {
279
+ return text(fm + " indexes no feature file — an empty map is a hole, not a pass");
280
+ }
281
+ let index;
282
+ try { index = fs.readFileSync(readme, "utf8"); } catch (e) { index = ""; }
283
+ const lines = ["map: " + fm + " (" + files.length + " feature file(s))"];
284
+ let bad = 0;
285
+ for (const f of files) {
286
+ const fp = path.join(dir, f);
287
+ const fmLines = frontmatter(fp);
288
+ const slug = fmValue(fmLines, "feature");
289
+ const verified = fmValue(fmLines, "verified");
290
+ const eps = fmEntryPaths(fmLines);
291
+ const stem = f.replace(/\.md$/, "");
292
+ const problems = [];
293
+ if (slug !== stem) problems.push('feature: is "' + (slug || "<absent>") + '" but the filename stem is "' + stem + '"');
294
+ if (!index.includes("](./" + f + ")")) problems.push("not linked from " + fm);
295
+ if (!eps.length) problems.push("declares no entry_paths: — nothing to resolve");
296
+ if (problems.length) bad++;
297
+ lines.push("- " + f + " verified: " + (verified || "<none>") + (problems.length ? " [FM-01: " + problems.join("; ") + "]" : ""));
298
+ for (const e of eps) {
299
+ const where = tokenResolves(root, dir, e);
300
+ lines.push(" " + e + " -> " + (where ? "resolves (" + where + ")" : "DOES NOT occur in source (FM-02 would FAIL)"));
301
+ if (!where) bad++;
302
+ }
303
+ }
304
+ lines.push(
305
+ "entry-path resolution is the FM-02 read-only half; the freshness clause (verified: date vs the file's last commit) and the full matrix stay with .gob/bin/goblin-verify"
306
+ );
307
+ const out = { map: fm, features: files.length, problems: bad, text: lines.join("\n") };
308
+ return { content: [{ type: "text", text: out.text }], _meta: out };
309
+ }
310
+
311
+ function toolGobInitStatus() {
312
+ const root = process.cwd();
313
+ const block = readBlock(path.join(root, "AGENTS.md"));
314
+ if (!block) {
315
+ return errText(
316
+ "no gob block in AGENTS.md under " + root + " — this repo is not under the gob harness (start: npx @techgoblin/gobstack init)"
317
+ );
318
+ }
319
+ const lines = ["gob block: present in AGENTS.md"];
320
+ const cls = blockValue(block, "class");
321
+ lines.push("class: " + (cls || "<unset>"));
322
+ const branch = blockValue(block, "branch");
323
+ lines.push("branch: " + (branch || "<unset>"));
324
+ const gates = blockGates(block);
325
+ if (gates.length) {
326
+ for (const g of gates) lines.push("gate_" + g.name + "_cmd: " + g.cmd);
327
+ } else {
328
+ lines.push("gates: none declared (a repo with no gate is a repo with no discipline)");
329
+ }
330
+ const enginePath = path.join(root, ".gob", "engine", "VERSION");
331
+ let engine = "";
332
+ let engineSrc = "";
333
+ if (fs.existsSync(enginePath)) {
334
+ engine = fs.readFileSync(enginePath, "utf8").trim();
335
+ engineSrc = ".gob/engine/VERSION";
336
+ } else {
337
+ let rec;
338
+ try { rec = fs.readFileSync(path.join(root, ".gob", "installed.json"), "utf8"); } catch (e) { rec = ""; }
339
+ const m = rec.match(/"version"\s*:\s*"([^"]+)"/);
340
+ if (m) { engine = m[1]; engineSrc = ".gob/installed.json"; }
341
+ }
342
+ lines.push("engine version: " + (engine || "<no vendored engine found>") + (engineSrc ? " (" + engineSrc + ")" : ""));
343
+ const vendored = fs.existsSync(path.join(root, ".gob", "bin", "goblin-verify"));
344
+ lines.push("vendored verify: " + (vendored ? ".gob/bin/goblin-verify" : "ABSENT (a global/declaration-mode repo — run gob verify through the engine chain)"));
345
+ lines.push("mcp registration: " + (fs.existsSync(path.join(root, ".mcp.json")) ? ".mcp.json present" : "no .mcp.json (gob init --with-mcp-config writes one)"));
346
+ return text(lines.join("\n"));
347
+ }
348
+
349
+ // ------------------------------------------------------------ the protocol ------
350
+
351
+ const TOOLS = [
352
+ {
353
+ name: "gob_verify",
354
+ description:
355
+ "Run the project discipline gate (goblin-verify) in the current working directory. " +
356
+ "Call this BEFORE reporting any coding task complete — FAIL rows include the remedy for each problem.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ only: { type: "string", description: "optional comma-separated rule ids to run (e.g. FM-01,FM-02)" },
361
+ },
362
+ },
363
+ },
364
+ {
365
+ name: "gob_map_status",
366
+ description:
367
+ "Read the repo's feature map (features/ plus the AGENTS.md feature_map: key) read-only: " +
368
+ "which features exist, their verified dates, and whether each declared entry path still resolves in source. " +
369
+ "Call this before editing a feature the map declares, or when asked what the project's features are.",
370
+ inputSchema: { type: "object", properties: {} },
371
+ },
372
+ {
373
+ name: "gob_init_status",
374
+ description:
375
+ "Report whether this repo is under the gobstack harness: the AGENTS.md gob block, its class and gates, " +
376
+ "and the engine version. Call this first when unsure whether the discipline gate applies here.",
377
+ inputSchema: { type: "object", properties: {} },
378
+ },
379
+ ];
380
+
381
+ function handleCall(name, args) {
382
+ switch (name) {
383
+ case "gob_verify": return toolGobVerify(args);
384
+ case "gob_map_status": return toolGobMapStatus();
385
+ case "gob_init_status": return toolGobInitStatus();
386
+ default:
387
+ return errText(
388
+ "unknown tool: " + name + " — this server exposes gob_verify, gob_map_status, gob_init_status"
389
+ );
390
+ }
391
+ }
392
+
393
+ function send(obj) {
394
+ process.stdout.write(JSON.stringify(obj) + "\n");
395
+ }
396
+
397
+ function handleLine(line) {
398
+ let msg;
399
+ try {
400
+ msg = JSON.parse(line);
401
+ } catch (e) {
402
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
403
+ return;
404
+ }
405
+ if (msg === null || typeof msg !== "object" || typeof msg.method !== "string") {
406
+ // A response to a request we never sent, or garbage with no method: ignore.
407
+ return;
408
+ }
409
+ const hasId = Object.prototype.hasOwnProperty.call(msg, "id");
410
+ const id = msg.id === undefined ? null : msg.id;
411
+ switch (msg.method) {
412
+ case "initialize":
413
+ send({
414
+ jsonrpc: "2.0",
415
+ id,
416
+ result: {
417
+ protocolVersion:
418
+ msg.params && typeof msg.params.protocolVersion === "string"
419
+ ? msg.params.protocolVersion
420
+ : "2024-11-05",
421
+ capabilities: { tools: {} },
422
+ serverInfo: { name: "gob", title: "gobstack", version: VERSION },
423
+ },
424
+ });
425
+ return;
426
+ case "notifications/initialized":
427
+ case "initialized":
428
+ return; // a notification: never answered
429
+ case "ping":
430
+ send({ jsonrpc: "2.0", id, result: {} });
431
+ return;
432
+ case "tools/list":
433
+ send({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
434
+ return;
435
+ case "tools/call": {
436
+ const p = msg.params || {};
437
+ send({ jsonrpc: "2.0", id, result: handleCall(p.name, p.arguments) });
438
+ return;
439
+ }
440
+ default:
441
+ if (hasId) {
442
+ send({ jsonrpc: "2.0", id, error: { code: -32601, message: "method not found: " + msg.method } });
443
+ }
444
+ }
445
+ }
446
+
447
+ // ---------------------------------------------------------------- main ----------
448
+
449
+ function usage() {
450
+ process.stderr.write(
451
+ [
452
+ "gob mcp — serve the gobstack harness to your coding agent over MCP stdio.",
453
+ "",
454
+ " tools: gob_verify (the discipline gate, with remedies), gob_map_status (the feature",
455
+ " map, read-only), gob_init_status (is this repo under the harness)",
456
+ " transport: JSON-RPC 2.0 over stdin/stdout, one message per line — the MCP stdio shape",
457
+ " local only: it calls no API, opens no socket, and spawns nothing but the vendored",
458
+ " verifier (.gob/bin/goblin-verify --json)",
459
+ "",
460
+ "register it: " + AGENT_CLI + " mcp add gob -- npx -y @techgoblin/gobstack mcp",
461
+ "or per-repo: gob init --with-mcp-config (writes .mcp.json; " + AGENT_CLI + " Code and Cursor",
462
+ " auto-detect it)",
463
+ "",
464
+ ].join("\n")
465
+ );
466
+ }
467
+
468
+ function main() {
469
+ const argv = process.argv.slice(2);
470
+ if (argv.includes("-h") || argv.includes("--help")) {
471
+ usage();
472
+ process.exit(0);
473
+ }
474
+ if (argv.includes("--version") || argv.includes("-V")) {
475
+ process.stdout.write(VERSION + "\n");
476
+ process.exit(0);
477
+ }
478
+ let buf = "";
479
+ process.stdin.setEncoding("utf8");
480
+ process.stdin.on("data", (chunk) => {
481
+ buf += chunk;
482
+ let i;
483
+ while ((i = buf.indexOf("\n")) >= 0) {
484
+ const line = buf.slice(0, i).trim();
485
+ buf = buf.slice(i + 1);
486
+ if (line) handleLine(line);
487
+ }
488
+ });
489
+ process.stdin.on("end", () => process.exit(0));
490
+ }
491
+
492
+ main();
package/bin/goblin-model CHANGED
@@ -2,7 +2,7 @@
2
2
  # goblin-model — resolve a ROLE to provider/model/effort, by reading one mapping file.
3
3
  #
4
4
  # Usage:
5
- # goblin-model <role> [--models <file>] [--config <goblin.yaml>] [--roles <roles.yaml>] [--json]
5
+ # goblin-model <role> [--models <file>] [--config <AGENTS.md>] [--roles <roles.yaml>] [--json]
6
6
  # goblin-model --list
7
7
  #
8
8
  # A role is what the work needs (code | judgment | review-panel | synthesis | investigate).
@@ -46,16 +46,16 @@ ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
46
46
 
47
47
  # Config: explicit, else the installed one, else none.
48
48
  if [ -z "$CONFIG" ]; then
49
- if [ -f "$ROOT/.goblin/goblin.yaml" ]; then CONFIG="$ROOT/.goblin/goblin.yaml"; fi
49
+ if [ -f "$ROOT/AGENTS.md" ]; then CONFIG="$ROOT/AGENTS.md"; fi
50
50
  fi
51
51
  if [ -n "$CONFIG" ] && [ -z "${MODELS_EXPLICIT:-}" ]; then
52
- m=$(g_yaml_scalar "$CONFIG" models_file)
52
+ m=$(g_agents_read "$CONFIG" models_file)
53
53
  [ -n "$m" ] && MODELS="$m"
54
54
  fi
55
55
  MODELS=$(g_expand_tilde "$MODELS")
56
56
 
57
57
  if [ -z "$ROLES" ]; then
58
- if [ -f "$ROOT/.goblin/roles.yaml" ]; then ROLES="$ROOT/.goblin/roles.yaml"
58
+ if [ -f "$ROOT/.gob/roles.yaml" ]; then ROLES="$ROOT/.gob/roles.yaml"
59
59
  else ROLES="$SELF_DIR/../roles.yaml"; fi
60
60
  fi
61
61
  [ -f "$ROLES" ] || { g_err "no roles file at $ROLES"; exit 2; }
@@ -30,7 +30,7 @@
30
30
  # `git revert` of this command's own two commits (W3-SPEC §5).
31
31
  set -uo pipefail
32
32
 
33
- GOBLIN_UPGRADE_VERSION="0.5.0"
33
+ GOBLIN_UPGRADE_VERSION="0.6.0-alpha.1"
34
34
 
35
35
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
36
36