@techgoblin/gobstack 0.5.0-beta.7 → 0.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +112 -123
  3. package/VERSION +1 -1
  4. package/automations/drift-audit.sh +4 -4
  5. package/bans/layer-check.sh +10 -8
  6. package/bin/goblin +61 -52
  7. package/bin/goblin-audit +11 -13
  8. package/bin/goblin-bans +11 -11
  9. package/bin/goblin-init +275 -713
  10. package/bin/goblin-install +160 -114
  11. package/bin/goblin-lib.sh +234 -1
  12. package/bin/goblin-map +607 -0
  13. package/bin/goblin-mcp.js +492 -0
  14. package/bin/goblin-model +4 -4
  15. package/bin/goblin-upgrade +1 -1
  16. package/bin/goblin-verify +159 -145
  17. package/bin/goblin.js +33 -49
  18. package/docs/ADOPTION.md +15 -15
  19. package/docs/CONTRACTS.md +16 -15
  20. package/docs/DESIGN.md +1 -1
  21. package/docs/ENFORCEMENT.md +89 -90
  22. package/docs/FLOWS.md +1 -1
  23. package/docs/GLOSSARY.md +3 -3
  24. package/docs/GUARDRAILS.md +5 -5
  25. package/docs/GUIDE.md +178 -169
  26. package/docs/INTEGRATION.md +1 -1
  27. package/docs/LIMITS.md +25 -0
  28. package/docs/LOOP.md +12 -12
  29. package/docs/RE-PLAYBOOK.md +3 -3
  30. package/docs/ROLES.md +5 -5
  31. package/manifest/bans.tsv +8 -8
  32. package/manifest/classes.tsv +3 -3
  33. package/manifest/enforcement.tsv +40 -40
  34. package/manifest/glossary.tsv +3 -3
  35. package/manifest/playbooks.tsv +1 -1
  36. package/package.json +1 -1
  37. package/presets/electron-overlay.yaml +2 -2
  38. package/presets/fleet.yaml +8 -7
  39. package/presets/game.yaml +1 -1
  40. package/presets/research.yaml +1 -1
  41. package/presets/service.yaml +1 -1
  42. package/presets/software.yaml +1 -1
  43. package/skills/goblin-bootstrap/SKILL.md +2 -2
  44. package/templates/AGENTS.md.tmpl +8 -18
  45. package/templates/HANDOFF.md.tmpl +5 -5
  46. package/templates/agents-block.tmpl +45 -0
  47. package/templates/audit-waiver.tsv.tmpl +2 -2
  48. package/templates/boundary-waivers.tmpl +1 -1
  49. package/templates/checks/gate.sh.tmpl +6 -6
  50. package/templates/install-hooks.allowlist.tmpl +1 -1
  51. package/templates/ci/goblin-gate.yml.tmpl +0 -46
  52. package/templates/goblin.yaml.tmpl +0 -146
  53. package/templates/loop/decisions.tsv.tmpl +0 -1
  54. package/templates/loop/predicate.tmpl +0 -16
package/bin/goblin CHANGED
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env bash
2
- # goblin — the global CLI dispatcher (W1, PLAN-V1 §2.3 / W1-SPEC §3).
2
+ # goblin — the CLI dispatcher.
3
3
  #
4
- # gob verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
5
- # gob bans [--only <id[,id...]>] [--list]
6
- # gob audit [--target <dir>] [--print]
7
- # gob doctor [--platform <p>] # W4a
8
- # gob emit --platform <p> [...] # W4a/W4b; `gob sync` is the same verb, renamed
9
- # gob init [...] # W6: the first-run wizard
10
- # gob upgrade [--target .] [...] # W3
4
+ # gob init [--heuristic | --write <proposal>] [--target <dir>] [...]
5
+ # gob map [target] [--agent | --heuristic] [--write <dir>] [--force]
6
+ # gob verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
7
+ # gob bans [--only <id[,id...]>] [--list]
8
+ # gob mcp serve the harness over MCP stdio
9
+ # gob uninstall --target <dir>
11
10
  # gob --version
12
11
  #
12
+ # v2 SURFACE (AI-driven development): the CLI calls no AI API. `gob init` and `gob map`
13
+ # EMIT a prompt + schema that the agent already running in this repo fulfils with its own
14
+ # tools; heuristic detection demotes to the --heuristic fallback.
15
+ #
16
+ # UNWIRED this session (code kept under bin/, deletion is session 3): doctor, audit,
17
+ # upgrade, emit/sync. `sync` becomes an init-internal step (hidden --resync flag).
18
+ #
13
19
  # Identity: the package is gobstack (npm @techgoblin/gobstack), the command is `gob`
14
- # (`goblin` remains as a legacy alias). W1 ships the DISPATCH
15
- # SHELL only — the node shim and npm packaging are W2, doctor/emit are W4a (exit-2
16
- # placeholders naming their workstream), upgrade is W3 (same). G3 forbids a runtime
17
- # rewrite: the engine stays bash, this file only routes and propagates.
20
+ # (`goblin` remains as a legacy alias).
18
21
  #
19
22
  # The four-value verify contract is unchanged and non-negotiable (bin/goblin-verify:5-9):
20
23
  # 0 every executed check passed
@@ -22,11 +25,10 @@
22
25
  # 2 could not run
23
26
  # 3 the manifest itself is broken
24
27
  # Every subcommand propagates the engine's exit code verbatim; no wrapper translates a
25
- # 1 into a 0. A placeholder that silently exited 0 would be a green build doing nothing.
28
+ # 1 into a 0.
26
29
  #
27
- # Root resolution: find_installed_root() at bin/goblin-verify:64-77 is preserved
28
- # BYTE-FOR-BYTE (walk up from $PWD to .goblin/goblin.yaml; fall back to git toplevel,
29
- # then $PWD — the D20 nested-repo fix). W1 changes nothing about its logic.
30
+ # Root resolution: find_installed_root() walks up from $PWD to an AGENTS.md carrying the
31
+ # gob marker block; fall back to git toplevel, then $PWD — the D20 nested-repo fix.
30
32
  #
31
33
  # No npm, no jq, no yq, no network. bash/awk/sed/grep only.
32
34
 
@@ -42,26 +44,30 @@ usage() {
42
44
  cat <<'USAGE'
43
45
  gob — the gobstack command line.
44
46
 
45
- gob verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
46
- gob bans [--only <id[,id...]>] [--list]
47
- gob audit [--target <dir>] [--print]
48
- gob doctor [--platform <p>] [--target <dir>]
49
- gob emit --platform <p> --scope project|global [...] # alias: gob sync
50
- gob sync --platform <p> --scope project|global [...] # the same verb, friendlier name
51
- gob init [--target <dir>] [--class app|A-F] [--dry-run]
52
- gob upgrade [--target .] [--dry-run] [--yes] [--engine-dir <path>]
47
+ gob init [--heuristic | --write <proposal>] [--target <dir>] [--class app|A-F] [--dry-run]
48
+ gob map [target] [--agent | --heuristic] [--write <dir>] [--force]
49
+ gob verify [--only <id[,id...]>] [--json] [--list] [--source <path>]
50
+ gob bans [--only <id[,id...]>] [--list]
51
+ gob mcp serve the harness to your agent over MCP stdio (verify/map/init tools)
52
+ gob uninstall --target <dir>
53
53
  gob --version
54
54
 
55
+ gob init and gob map are AI-DRIVEN: they print a prompt + schema for the agent already
56
+ running in this repo, which writes the proposal file; `--write` validates and installs
57
+ it. The heuristic detectors are the --heuristic fallback.
58
+
55
59
  Exit codes (verify): 0 pass | 1 a check failed | 2 could not run | 3 the manifest is
56
60
  broken. Every subcommand propagates the engine's exit code verbatim.
57
61
  USAGE
58
62
  }
59
63
 
60
- # ---- the repo-root locator, preserved byte-for-byte from bin/goblin-verify:64-77 ----
64
+ # ---- the repo-root locator (an AGENTS.md gob block is the install marker) ----
61
65
  find_installed_root() {
62
66
  local d="$PWD"
63
67
  while :; do
64
- if [ -f "$d/.goblin/goblin.yaml" ]; then printf '%s\n' "$d"; return 0; fi
68
+ if [ -f "$d/AGENTS.md" ] && grep -q '^<!-- gob:begin' "$d/AGENTS.md" 2>/dev/null; then
69
+ printf '%s\n' "$d"; return 0
70
+ fi
65
71
  [ "$d" = "/" ] && return 1
66
72
  d=$(dirname "$d")
67
73
  done
@@ -73,6 +79,15 @@ CMD="${1:-}"
73
79
  shift
74
80
 
75
81
  case "$CMD" in
82
+ init)
83
+ # The prompt+schema engine: prints the agent brief, or installs a proposal.
84
+ exec bash "$SRC/bin/goblin-init" "$@"
85
+ ;;
86
+ map)
87
+ # The standalone feature-map generator: `--agent` (default) prints prompt+schema;
88
+ # --heuristic runs the scanner. Routes and propagates its 0/1/2 contract verbatim.
89
+ exec bash "$SRC/bin/goblin-map" "$@"
90
+ ;;
76
91
  verify)
77
92
  # The engine is whatever the per-repo chain resolves (W1 §2.3): --source, vendored,
78
93
  # engine_dir:, GOBLIN_ENGINE_DIR, ~/.goblin/engine — all inside goblin-verify. The
@@ -82,33 +97,16 @@ case "$CMD" in
82
97
  bans)
83
98
  exec bash "$SRC/bin/goblin-bans" "$@"
84
99
  ;;
85
- audit)
86
- exec bash "$SRC/bin/goblin-audit" "$@"
87
- ;;
88
- doctor)
89
- # W4a: one run, three platforms (the §7 report); the dispatcher routes and
90
- # propagates the doctor's 0/1/2 verbatim, like every other subcommand.
91
- exec bash "$SRC/bin/goblin-doctor" "$@"
100
+ mcp)
101
+ # The MCP stdio server (bin/goblin-mcp.js): JSON-RPC 2.0 over stdin/stdout, a LOCAL
102
+ # tool server over the vendored harness — no API call, no socket. node is the same
103
+ # runtime bin/goblin.js already needs; it is exec'd directly, args pass through.
104
+ exec node "$SRC/bin/goblin-mcp.js" "$@"
92
105
  ;;
93
- emit)
94
- # W4a: per-platform emission (the §4 contract); the 0/1/2 exit contract is the
95
- # same three values verify's wrapper states, propagated verbatim.
96
- exec bash "$SRC/bin/goblin-emit" "$@"
97
- ;;
98
- sync)
99
- # Wizard v2: the friendlier name for the same emission engine. `emit` stays a
100
- # first-class verb; this case only adds the alias and propagates identically.
101
- exec bash "$SRC/bin/goblin-emit" "$@"
102
- ;;
103
- init)
104
- # W6: the first-run wizard. It drives install/emit/verify and propagates their
105
- # exit contract verbatim, like every other subcommand here.
106
- exec bash "$SRC/bin/goblin-init" "$@"
107
- ;;
108
- upgrade)
109
- # W3: the migration lives in its own checkout-level script (W3-SPEC §1.1);
110
- # the dispatcher routes and propagates the four-value contract verbatim.
111
- exec bash "$SRC/bin/goblin-upgrade" "$@"
106
+ uninstall)
107
+ # Routes into the installer's uninstall job. `--uninstall` is appended FIRST so the
108
+ # user's own `--target <dir>` and options still parse.
109
+ exec bash "$SRC/bin/goblin-install" --uninstall "$@"
112
110
  ;;
113
111
  --version|-V|-v)
114
112
  printf '%s\n' "$VERSION"
@@ -118,6 +116,17 @@ case "$CMD" in
118
116
  usage
119
117
  exit 0
120
118
  ;;
119
+ doctor|audit|upgrade|emit|sync|install)
120
+ # Unwired from usage in v2 (code kept; deletion is session 3). The refusal names
121
+ # what replaced each verb rather than silently executing the old path.
122
+ g_err "gob $CMD is unwired in v2."
123
+ case "$CMD" in
124
+ install|init-legacy) g_err "fix: gob init --write <proposal> installs a repo now." ;;
125
+ sync|emit) g_err "fix: gob init --write <proposal> runs the sync step internally." ;;
126
+ doctor|audit|upgrade) g_err "fix: gob verify (audit stays a deliberate manual step; see docs/)." ;;
127
+ esac
128
+ exit 2
129
+ ;;
121
130
  *)
122
131
  g_err "unknown subcommand: $CMD"
123
132
  usage >&2
package/bin/goblin-audit CHANGED
@@ -16,14 +16,14 @@
16
16
  #
17
17
  # Exit codes:
18
18
  # 0 the record was written (whether or not it is clean)
19
- # 2 usage, or no .goblin/goblin.yaml to read `security.audit_cmd` from
19
+ # 2 usage, or no AGENTS.md frontmatter to read `security.audit_cmd` from
20
20
  # 3 the class declares no audit command (security.audit_cmd is empty) - nothing to run
21
21
  # 4 the declared command could not run
22
22
  # 5 the output could not be parsed as an audit report - REFUSING to write an empty record,
23
23
  # because an empty record reads to SC-07 as "clean" and that would be a fabricated pass
24
24
  set -uo pipefail
25
25
 
26
- GOBLIN_AUDIT_VERSION="0.5.0"
26
+ GOBLIN_AUDIT_VERSION="0.6.0-alpha.1"
27
27
 
28
28
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
29
29
 
@@ -40,22 +40,20 @@ done
40
40
  [ -n "$TARGET" ] || { printf 'goblin-audit: --target needs a directory\n' >&2; exit 2; }
41
41
  cd "$TARGET" || { printf 'goblin-audit: no such directory: %s\n' "$TARGET" >&2; exit 2; }
42
42
 
43
- CONFIG=".goblin/goblin.yaml"
44
- [ -f "$CONFIG" ] || { printf 'goblin-audit: %s has no %s - run goblin-install first\n' "$TARGET" "$CONFIG" >&2; exit 2; }
43
+ CONFIG="AGENTS.md"
44
+ [ -f "$CONFIG" ] && grep -q "^$GOB_AGENTS_BEGIN" "$CONFIG" 2>/dev/null \
45
+ || { printf 'goblin-audit: %s has no %s gob block - run gob init first\n' "$TARGET" "$CONFIG" >&2; exit 2; }
45
46
 
46
47
  # Self-contained on purpose: the audit runner must work even if goblin-lib.sh was edited or is
47
48
  # missing, because it is the one tool a human runs by hand.
48
- g_yaml_block_scalar() { # <file> <block> <key>
49
- awk -v b="$2" -v k="$3" '
50
- $0 ~ ("^" b ":[[:space:]]*$") { inb = 1; next }
51
- inb && /^[^ ]/ { inb = 0 }
52
- inb && $0 ~ ("^ " k ":") {
53
- v = $0; sub("^ " k ":[[:space:]]*", "", v); print v; exit
54
- }
55
- ' "$1"
49
+ g_agents_read_inline() { # <file> <dotted-key> — the one reader the audit needs, inline
50
+ local f="$1" k="$2" line
51
+ while IFS= read -r line; do
52
+ case "$line" in "$k:"*) printf '%s\n' "${line#"$k":}"; return 0 ;; esac
53
+ done < "$f"
56
54
  }
57
55
 
58
- AUDIT_CMD=$(g_yaml_block_scalar "$CONFIG" security audit_cmd)
56
+ AUDIT_CMD=$(g_agents_read_inline "$CONFIG" security.audit_cmd)
59
57
  if [ -z "$AUDIT_CMD" ]; then
60
58
  printf 'goblin-audit: this class declares no audit command (security.audit_cmd is empty in\n'
61
59
  printf ' %s). Nothing to run, and no record to write.\n' "$CONFIG"
package/bin/goblin-bans CHANGED
@@ -28,7 +28,7 @@
28
28
 
29
29
  set -uo pipefail
30
30
 
31
- GOBLIN_BANS_VERSION="0.5.0"
31
+ GOBLIN_BANS_VERSION="0.6.0-alpha.1"
32
32
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
33
33
  # shellcheck source=goblin-lib.sh
34
34
  . "$SELF_DIR/goblin-lib.sh"
@@ -49,7 +49,7 @@ done
49
49
  find_installed_root() {
50
50
  local d="$PWD"
51
51
  while :; do
52
- if [ -f "$d/.goblin/goblin.yaml" ]; then printf '%s\n' "$d"; return 0; fi
52
+ if [ -f "$d/AGENTS.md" ] && grep -q "^$GOB_AGENTS_BEGIN" "$d/AGENTS.md" 2>/dev/null; then printf '%s\n' "$d"; return 0; fi
53
53
  [ "$d" = "/" ] && return 1
54
54
  d=$(dirname "$d")
55
55
  done
@@ -60,9 +60,9 @@ if [ -z "$ROOT" ]; then
60
60
  [ -n "$ROOT" ] || ROOT="$PWD"
61
61
  fi
62
62
 
63
- CONFIG="$ROOT/.goblin/goblin.yaml"
64
- if [ -f "$ROOT/.goblin/manifest/bans.tsv" ]; then
65
- BANS="$ROOT/.goblin/manifest/bans.tsv"
63
+ CONFIG="$ROOT/AGENTS.md"
64
+ if [ -f "$ROOT/.gob/manifest/bans.tsv" ]; then
65
+ BANS="$ROOT/.gob/manifest/bans.tsv"
66
66
  else
67
67
  BANS="$SELF_DIR/../manifest/bans.tsv"
68
68
  fi
@@ -85,13 +85,11 @@ fi
85
85
  # ---- the config: which bans this project turns on, and its narrow exceptions --
86
86
  ENABLED=""
87
87
  if [ -f "$CONFIG" ]; then
88
- ENABLED=$(g_yaml_scalar "$CONFIG" bans)
89
- ENABLED=${ENABLED#[}; ENABLED=${ENABLED%]}
90
- ENABLED=$(printf '%s' "$ENABLED" | tr ',' ' ' | tr -s ' ' ' ')
88
+ ENABLED=$(g_agents_list "$CONFIG" bans | tr '\n' ' ')
91
89
  # The electron opt-in (software class): `electron: true` turns the electron ban set ON even
92
90
  # when a hand-edited bans: list omits it — the declaration is the contract, and the engine
93
91
  # (not the LLM) is what makes a listed ban non-skippable. A repo without the key is unaffected.
94
- if [ "$(g_yaml_scalar "$CONFIG" electron)" = "true" ]; then
92
+ if [ "$(g_agents_read "$CONFIG" electron)" = "true" ]; then
95
93
  for e in BN-06 BN-07 BN-08 BN-09; do
96
94
  case " $ENABLED " in *" $e "*) ;; *) ENABLED="$ENABLED $e" ;; esac
97
95
  done
@@ -113,7 +111,9 @@ enabled() {
113
111
  # keeps the old behaviour, which fails CLOSED, never open.
114
112
  exempt_paths() { # exempt_paths <id> -> path prefixes, one per line
115
113
  [ -f "$CONFIG" ] || return 0
116
- g_yaml_list "$CONFIG" bans_exempt | awk -v id="$1" '$1==id {print $2}'
114
+ # v2 shape: one flat `[BN-01 src, BN-02 app]` array — item 1 is the ban id, item 2 its
115
+ # path prefix (g_agents_list splits on the commas, so an item keeps its inner space).
116
+ g_agents_list "$CONFIG" bans_exempt | awk -v id="$1" '$1==id {print $2}'
117
117
  }
118
118
 
119
119
  only_selected() {
@@ -131,7 +131,7 @@ while IFS=$'\t' read -r id ban globs detect replacement escape reviewer source;
131
131
  SEL=$((SEL + 1))
132
132
 
133
133
  if ! enabled "$id"; then
134
- SKIP=$((SKIP + 1)); g_skip "$id" "$ban (not enabled in bans: - add $id to .goblin/goblin.yaml to turn it on)"
134
+ SKIP=$((SKIP + 1)); g_skip "$id" "$ban (not enabled in bans: - add $id to the bans: array in AGENTS.md to turn it on)"
135
135
  continue
136
136
  fi
137
137
  # A ban whose globs match no file is a SKIP with a reason, not a pass and not a fail.