@techgoblin/gobstack 0.6.0-alpha.3 → 0.6.0-alpha.4

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/README.md CHANGED
@@ -87,7 +87,7 @@ is unwired in this alpha: the shim refuses the verb by name and prints the usage
87
87
  | `gob bans` | run the ban list (per-pattern red lines over the source tree) |
88
88
  | `gob extras` | the curated extras catalogue: `list [category]` / `show <id>` / `install <id...> [­-target <dir>] [--platform <id>] [--with-mcp-config]` — init only SUGGESTS; installs come from the catalogue (the allowlist rule), never the web |
89
89
  | `gob mcp` | serve the harness to your coding agent over MCP stdio — three tools (`gob_verify`, `gob_map_status`, `gob_init_status`), local only, no SDK, no network |
90
- | `gob uninstall` | remove everything an install wrote, byte-exactly (`gob install --target <dir> --uninstall` is the same job) |
90
+ | `gob uninstall` | remove everything an install wrote, byte-exactly (the same job as `goblin-install --target <dir> --uninstall` from the checkout/install tree) |
91
91
 
92
92
  `goblin` remains as a legacy alias for every command above — existing scripts keep working, but
93
93
  new commands and docs use `gob`.
@@ -254,14 +254,17 @@ capability.
254
254
 
255
255
  gob uninstall --target .
256
256
 
257
- (equivalently `gob install --target . --uninstall` — through the legacy alias, spell it `goblin`
257
+ (the same job as `goblin-install --target . --uninstall` — through the legacy alias, spell it `goblin`
258
258
  instead of `gob`). The uninstall is **byte-exact**: it removes exactly the files
259
259
  `installed.json` records — hash-compared preimages, so a file you edited after install is
260
260
  reported and kept, never clobbered — then every directory that leaves empty, and it strips the
261
261
  `<!-- gob:begin --> … <!-- gob:end -->` block out of `AGENTS.md` (the body prose stays). After
262
262
  it, the repo has zero harness files; only the project's own record (`HANDOFF.md`, `AGENTS.md`'s
263
263
  prose, `reviews/`, the `.gitignore` block) survives, because that is the project's, not the
264
- harness's to delete. And because the engine is vendored, the repo needs no gobstack installed to
264
+ harness's to delete. Two residues are left **by design, and named in the output**: the
265
+ `.gitignore` ignore-rules block stays (removing it could eat a line the project added inside
266
+ it — delete that block by hand if you want it gone), and prose that named `.gob/` paths
267
+ (HANDOFF, the AGENTS.md body, SPECs) now points at removed files. And because the engine is vendored, the repo needs no gobstack installed to
265
268
  run this — it is self-contained until the moment you remove it.
266
269
 
267
270
  The short version, for a full removal from a machine and its repos: (b) in each initialized
@@ -269,7 +272,7 @@ repo, then (a).
269
272
 
270
273
  ## Re-pin the referenced standard
271
274
 
272
- gob install --target <dir> --re-pin
275
+ goblin-install --target <dir> --re-pin # from the gobstack checkout / install tree
273
276
 
274
277
  `practice_sha256:` in the AGENTS.md gob block pins the referenced standard and `IN-02`
275
278
  re-checks it, so editing that standard — a legitimate, intended edit — reds `IN-02` in every
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0-alpha.3
1
+ 0.6.0-alpha.4
package/bin/goblin-audit CHANGED
@@ -23,7 +23,7 @@
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.6.0-alpha.3"
26
+ GOBLIN_AUDIT_VERSION="0.6.0-alpha.4"
27
27
 
28
28
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
29
29
 
package/bin/goblin-bans CHANGED
@@ -28,7 +28,7 @@
28
28
 
29
29
  set -uo pipefail
30
30
 
31
- GOBLIN_BANS_VERSION="0.6.0-alpha.3"
31
+ GOBLIN_BANS_VERSION="0.6.0-alpha.4"
32
32
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
33
33
  # shellcheck source=goblin-lib.sh
34
34
  . "$SELF_DIR/goblin-lib.sh"
package/bin/goblin-extras CHANGED
@@ -54,7 +54,7 @@ usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
54
54
  CATALOGUE="${GOB_EXTRAS_CATALOGUE:-$SRC/extras-catalogue/catalogue.tsv}"
55
55
  PAYLOAD="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
56
56
 
57
- die() { g_err "extras: $*"; exit "${2:-2}"; }
57
+ die() { local msg="$1"; shift; g_err "extras: $msg"; exit "${1:-2}"; }
58
58
 
59
59
  # catalogue_row <id> — the whole data row (tab-separated), empty when absent.
60
60
  catalogue_row() {
@@ -250,7 +250,12 @@ install_skill() {
250
250
  [ -d "$d" ] || continue
251
251
  name=$(basename "$d")
252
252
  n=$((n + 1))
253
- g_info "extras: skill $name -> ${dst_root#"$TARGET"/}$name/"
253
+ # QA fix (v2-qa issue 9): the display path must carry exactly one separator between
254
+ # $dst_root and $name — the old single format printed `.gob/extras` + `taste` as
255
+ # `.gob/extrastaste/` (files landed in the right place, only the LOG lied). The
256
+ # platform branch normalizes $dst_root to end in /, the standalone branch does not,
257
+ # so the trailing slashes are stripped at print time and one / is always inserted.
258
+ printf 'extras: skill %s -> %s/%s/\n' "$name" "${dst_root%/}" "$name"
254
259
  copy_tree "$d" "$dst_root/$name"
255
260
  done
256
261
  [ "$n" -gt 0 ] || die "payload for '$id' holds no skill dirs ($src_root/*/ with SKILL.md each) — see extras-catalogue/payload/README.md" 2
package/bin/goblin-init CHANGED
@@ -133,6 +133,10 @@ back one proposal file. Do this now:
133
133
  found in step 1 — react, vue, playwright, godot, unity, node, postgres, …);
134
134
  - RECOMMEND rows are pre-ticked (write `- id` with `[x]`); MAYBE rows are
135
135
  listed but NOT pre-ticked (`[ ]`) — they are visible choices, never defaults;
136
+ - a row with NO vendored payload (no `extras-catalogue/payload/<id>/` directory)
137
+ must NOT be pre-ticked even when RECOMMEND — print it as `[ ]` instead;
138
+ `--write` installs through `gob extras install`, which refuses a
139
+ payload-missing id and fails the whole --write;
136
140
  - a row whose `conflicts:` names an id you also picked: pick at most one of them;
137
141
  - SKIP rows are never suggested and refuse install; leave them out entirely.
138
142
 
@@ -226,13 +230,21 @@ print_heuristic() {
226
230
  # files). The SAME matches column the agent brief reads — the fallback suggests from
227
231
  # the catalogue, never from its own private list. Verdicts are respected: only
228
232
  # RECOMMEND rows pre-tick; MAYBE rows print unticked; SKIP rows never appear.
233
+ # QA fix (v2-qa): a row pre-ticks ONLY when its vendored payload exists —
234
+ # extras-catalogue/payload/<id>/ — because --write installs through
235
+ # `gob extras install`, which refuses a payload-missing row and fails the whole
236
+ # --write after everything else was already written. A RECOMMEND row with no
237
+ # payload prints as a visible, unticked `[ ]` choice instead. (41 researched rows,
238
+ # 1 vendored payload: the old pre-tick turned a day-one init into a guaranteed
239
+ # exit 2 halfway through writing.)
229
240
  if [ -f "$CATALOGUE_TSV" ]; then
230
241
  local scan
231
242
  scan=$(cat "$TARGET/package.json" "$TARGET"/*.lock "$TARGET"/pnpm-lock.yaml \
232
243
  "$TARGET"/vue.config.* "$TARGET"/nuxt.config.* "$TARGET"/godot/project.godot \
233
244
  "$TARGET"/unity/ProjectSettings/ProjectVersion.txt 2>/dev/null | tr 'A-Z' 'a-z')
234
245
  if [ -n "$scan" ]; then
235
- local ticked="" maybe=""
246
+ local ticked="" maybe="" payload_root
247
+ payload_root="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
236
248
  while IFS=$'\t' read -r cid ccat ckind cname crepo cpath chmd clic cstars cpush \
237
249
  cmatches cconf creq chint cverdict cby cdate; do
238
250
  [ -n "$cid" ] || continue
@@ -242,6 +254,12 @@ print_heuristic() {
242
254
  case "$scan" in *"$tok"*) hit=1; break ;; esac
243
255
  done
244
256
  [ "$hit" -eq 1 ] || continue
257
+ if [ ! -d "$payload_root/$cid" ]; then
258
+ # no vendored payload: the row stays visible but never pre-ticks —
259
+ # a tick --write cannot honour is a refusal waiting to happen
260
+ maybe="$maybe - [ ] $cid # matches this repo, NO VENDORED PAYLOAD yet (gob extras install would refuse — vendor it under extras-catalogue/payload/$cid/ first)"$'\n'
261
+ continue
262
+ fi
245
263
  case "$cverdict" in
246
264
  RECOMMEND) ticked="$ticked - [x] $cid # matches this repo ($cmatches)"$'\n' ;;
247
265
  MAYBE) maybe="$maybe - [ ] $cid # matches, verdict MAYBE (not pre-ticked)"$'\n' ;;
@@ -326,6 +344,37 @@ SYNC_SEL=$(g_agents_list "$PROP" sync_platforms | tr '\n' ' ')
326
344
  # cannot install anything the catalogue does not carry.
327
345
  EXTRAS_SEL=$(sed -n '/catalogue_suggestions:/,/^[^ -]/{s/^[[:space:]]*-[[:space:]]*\[x\][[:space:]]*\([A-Za-z0-9_-]*\).*/\1/p;}' "$PROP" | sort -u | tr '\n' ' ')
328
346
 
347
+ # ---------------------------------------------------------------- pre-flight ---
348
+ # QA fix (v2-qa issue 1b): the TICKED extras are validated BEFORE anything is written.
349
+ # The old order installed the harness first and ran `gob extras install` last, so a
350
+ # missing vendored payload refused AFTER 23 files were already in the tree (the day-one
351
+ # exit 2 with the repo half-initialized). Here every ticked id is resolved through the
352
+ # same allowlist path goblin-extras uses (row exists, verdict installable, license gate,
353
+ # vendored payload present) while the target is still untouched; a refusal names the id
354
+ # and the fix and exits before the first write.
355
+ if [ -n "$EXTRAS_SEL" ] && [ "$DRYRUN" -eq 0 ]; then
356
+ PAYLOAD_ROOT="${GOB_EXTRAS_PAYLOAD:-$SRC/extras-catalogue/payload}"
357
+ for xid in $EXTRAS_SEL; do
358
+ row=$(awk -F'\t' -v id="$xid" 'NR>1 && $1==id { print; exit }' "$CATALOGUE_TSV" 2>/dev/null)
359
+ [ -n "$row" ] || { g_err "pre-flight: catalogue has no row '$xid' — fix the proposal's catalogue_suggestions (gob extras list names the allowlist)"; exit 2; }
360
+ verdict=$(printf '%s' "$row" | awk -F'\t' '{print $15}')
361
+ lic=$(printf '%s' "$row" | awk -F'\t' '{print $18}')
362
+ case "$verdict" in
363
+ RECOMMEND|MAYBE) ;;
364
+ *) g_err "pre-flight: '$xid' carries verdict '$verdict' — untick it in the proposal's catalogue_suggestions"; exit 2 ;;
365
+ esac
366
+ # the same license gate goblin-extras enforces, read before any write
367
+ case "$lic" in
368
+ verified) ;;
369
+ *) g_err "pre-flight: '$xid' refuses install: license pending verification — untick it in the proposal's catalogue_suggestions"; exit 2 ;;
370
+ esac
371
+ [ -d "$PAYLOAD_ROOT/$xid" ] || {
372
+ g_err "pre-flight: no vendored payload for '$xid' at $PAYLOAD_ROOT/$xid — untick it in the proposal's catalogue_suggestions, or vendor the payload there first";
373
+ exit 2;
374
+ }
375
+ done
376
+ fi
377
+
329
378
  if [ "$DRYRUN" -eq 1 ]; then
330
379
  g_info "gob init --write --dry-run: validated OK (class $CLASS, branch $BRANCH, owner $EMAIL, 1+ gate)"
331
380
  g_info " sync: ${SYNC_SEL:-none}"
@@ -1,19 +1,23 @@
1
1
  #!/usr/bin/env bash
2
2
  # goblin-install — drop the harness into a target repo, idempotently.
3
3
  #
4
- # Usage: gob install --target <dir> [options] (script: bin/goblin-install)
4
+ # Usage: bash bin/goblin-install --target <dir> [options] # checkout-only; `gob init --write` is the user surface
5
5
  # --target <dir> required; the repo root to install into
6
6
  # --class <name> required unless --uninstall or --re-pin; one of
7
7
  # software|service|game|research|fleet, or the aliases A-E / app / agent
8
8
  # / desktop (desktop/F => software + --electron); anything else is refused
9
9
  # --electron the electron opt-in overlay over the software class (electron: true):
10
10
  # the app_bundle_bytes ratchet, BN-06..09 bans, the host-gate declaration
11
- # --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
12
- # --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md); a named path that is absent is reported, never silently dropped
11
+ # --models <path> model mapping file (default: $GOBLIN_MODELS; unset = rendered as an
12
+ # empty models_file: the repo's machine-specific input, never a path
13
+ # from the machine the installer ran on)
14
+ # --practice <path> the referenced standard (default: $GOBLIN_PRACTICE; unset = empty
15
+ # practice: pin — a named path that is absent is reported, never
16
+ # silently dropped)
13
17
  # --parts <list> comma list to install; default = every part the class requires
14
18
  # --archive mark the project archive: verify requires no HANDOFF and no gates
15
19
  # --skills yes|no install agent skills under .hermes/skills (default no; opt in per
16
- # platform with: gob emit --platform <p>)
20
+ # catalogue row with: gob extras install <id>)
17
21
  # --dry-run print the plan; write nothing
18
22
  # --upgrade re-install at the current version; report created/updated/unchanged/skipped
19
23
  # --opt-out <part> record the part in disabled: so its required checks are skipped
@@ -31,7 +35,7 @@
31
35
 
32
36
  set -uo pipefail
33
37
 
34
- GOBLIN_INSTALL_VERSION="0.6.0-alpha.3"
38
+ GOBLIN_INSTALL_VERSION="0.6.0-alpha.4"
35
39
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
36
40
  SRC=$(cd "$SELF_DIR/.." && pwd)
37
41
  # shellcheck source=goblin-lib.sh
@@ -39,8 +43,16 @@ SRC=$(cd "$SELF_DIR/.." && pwd)
39
43
 
40
44
  TARGET=""
41
45
  CLASS=""
42
- MODELS="${GOBLIN_MODELS:-$HOME/projects/fleet-model.yaml}"
43
- PRACTICE="${GOBLIN_PRACTICE:-$HOME/projects/PROJECT-PRACTICE.md}"
46
+ # QA fix (v2-qa issue 4): the old defaults were $HOME/projects/fleet-model.yaml and
47
+ # $HOME/projects/PROJECT-PRACTICE.md - the MAINTAINER's home layout baked into every
48
+ # rendered config on every machine, including a fresh contributor's (their rendered
49
+ # AGENTS.md pointed at a file that does not exist on their machine). The neutral default
50
+ # is EMPTY: no --models / no GOBLIN_MODELS renders an empty models_file: line, and the
51
+ # practice pin stays empty (verify already treats both as the legitimate no-mapping /
52
+ # no-standard state). A machine that HAS a fleet mapping exports GOBLIN_MODELS (or
53
+ # passes --models) and gets the same rendering as before.
54
+ MODELS="${GOBLIN_MODELS:-}"
55
+ PRACTICE="${GOBLIN_PRACTICE:-}"
44
56
  # W5-3: `--practice <path>` naming a file that is not there used to be IGNORED IN SILENCE -
45
57
  # exit 0, `practice: ~`, `practice_sha256: ~`, and not one line about it in the install log.
46
58
  # The empty pin is a legitimate state (a repo may reference no standard, and the default path is
@@ -220,6 +232,15 @@ if [ "$UNINSTALL" -eq 1 ]; then
220
232
  printf '.goblin\n.gob/bin\n.gob/manifest\n.gob/bans\n.hermes\n.hermes/skills\n'
221
233
  } | awk 'NF' | awk -F/ '{print NF " " $0}' | sort -rn | cut -d' ' -f2- | awk '!seen[$0]++')
222
234
  g_info "removed $n file(s) and $nd empty director(y|ies). Left in place: HANDOFF.md, AGENTS.md, ROUND-000-SPEC.md, reviews/, the .gitignore block${KEPT:+, and the decision record(s) you edited:$KEPT}."
235
+ # QA fix (v2-qa issue 10): honesty about what the uninstall leaves UNTOUCHED. Two
236
+ # by-design residues a reader could otherwise read as a clean sweep:
237
+ # - the .gitignore block the install appended stays (removing it could eat a
238
+ # line the project added inside the block; the preimage is untracked text),
239
+ # - prose in kept files (HANDOFF.md, AGENTS.md body, SPECs) may still name
240
+ # now-deleted paths (.gob/bin/goblin-verify, checks/) — those references are
241
+ # dead after this command.
242
+ g_info " note: the .gitignore ignore-rules block stays (by design: it may carry lines the project added; remove it by hand if you want it gone)"
243
+ g_info " note: prose that names .gob/ paths (HANDOFF.md, AGENTS.md body, SPECs) now points at removed files — those references are dead"
223
244
  exit 0
224
245
  fi
225
246
 
@@ -852,7 +873,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
852
873
  if [ "$SKILLS" = "yes" ]; then
853
874
  g_info " 4. hermes skills trust $TARGET # one-time, so the project-tier skills load"
854
875
  else
855
- g_info " 4. agent skills are opt-in: gob emit --platform <p> # run gob doctor for the platform list"
876
+ g_info " 4. agent skills are opt-in: gob extras install <id...> # gob extras list shows the catalogue"
856
877
  fi
857
878
  g_info ""
858
879
  g_info "automations (optional; neither writes outside this repo, and A-02 has no agent in it):"
package/bin/goblin-lib.sh CHANGED
@@ -16,7 +16,7 @@
16
16
  # - name: typecheck four-space-indented second member of a list entry
17
17
  # cmd: npx tsc --noEmit
18
18
 
19
- GOBLIN_LIB_VERSION="0.6.0-alpha.3"
19
+ GOBLIN_LIB_VERSION="0.6.0-alpha.4"
20
20
 
21
21
  # ---------------------------------------------------------------- output -----
22
22
  # g_trunc <width> <text> — fold a long detail to one line at <width> columns, keeping the
@@ -31,6 +31,9 @@ g_trunc() {
31
31
  # <indent> (the cannot-see footer shape). Byte-safe: awk length on bytes approximates
32
32
  # columns for ASCII prose, which is all this text is.
33
33
  g_fold() {
34
+ # QA fix (v2-qa issue 9): the rest-assignment used to inline substr() as sub()'s third
35
+ # parameter — not a changeable lvalue, a hard error on gawk 5.2+ (the helper had never
36
+ # been exercised until the remedy fold called it). A temp variable is portable awk.
34
37
  awk -v w="$1" -v ind="$2" '
35
38
  {
36
39
  line = $0
@@ -39,10 +42,9 @@ g_fold() {
39
42
  while (cut > 1 && substr(line, cut, 1) != " ") cut--
40
43
  if (cut <= 1) cut = w
41
44
  print substr(line, 1, cut)
42
- sub(/^[ ]+/, "", substr(line, cut + 1))
43
- line = substr(line, cut + 1)
44
- sub(/^[ ]+/, "", line)
45
- line = ind line
45
+ rest = substr(line, cut + 1)
46
+ sub(/^[ ]+/, "", rest)
47
+ line = ind rest
46
48
  }
47
49
  print line
48
50
  }'
@@ -71,12 +73,16 @@ g_fail() {
71
73
  case "$rem" in
72
74
  —*) return 0 ;; # an em-dash cell: history and blindness, never a remedy
73
75
  esac
74
- # Whole in verbose/--only mode (the mode that reads, not scans), width-folded otherwise —
75
- # the same contract the row printers keep.
76
- if [ "${GOB_VERIFY_VERBOSE:-0}" -eq 1 ]; then
77
- [ -n "$rem" ] && printf 'remedy: %s\n' "$rem"
78
- else
79
- [ -n "$rem" ] && printf 'remedy: %s\n' "$(g_trunc "${GOB_REPORT_COLS:-100}" "$rem")"
76
+ # QA fix (v2-qa issue 9): a remedy is an instruction, not a detail line — truncating it
77
+ # with a ~ cut the command the operator had to type (measured: 'git -c user.ema~').
78
+ # The remedy is now WORD-FOLDED to continuation lines at the report width in every
79
+ # mode (whole text either way); --verbose keeps the single-line form it always had.
80
+ if [ -n "$rem" ]; then
81
+ if [ "${GOB_VERIFY_VERBOSE:-0}" -eq 1 ]; then
82
+ printf 'remedy: %s\n' "$rem"
83
+ else
84
+ printf 'remedy: %s\n' "$rem" | g_fold "${GOB_REPORT_COLS:-100}" " "
85
+ fi
80
86
  fi
81
87
  }
82
88
  g_remedy() {
package/bin/goblin-map CHANGED
@@ -45,7 +45,7 @@
45
45
  #
46
46
  # Never-clobber contract:
47
47
  # features/ absent (or empty of .md) -> generate README + one file per slug, exit 0
48
- # features/ already a map, no --force -> write NOTHING, name the path and --force, exit 1
48
+ # features/ already a map, no --force -> validate only; the index is kept (never rewritten)
49
49
  # --force -> regenerate ONLY README.md (indexing every existing
50
50
  # feature file too) and add files for NEW slugs;
51
51
  # existing feature files are never touched, exit 0
@@ -129,8 +129,9 @@ Notes:
129
129
  human pass has actually driven the feature once.
130
130
  - entry_paths tokens must be repo-relative paths that EXIST, and should occur in
131
131
  source text (a route literal, an import) so FM-02 can grep for them.
132
- - An existing map is never overwritten without --force (the index only); if a map
133
- already exists, hand-pass the files instead of regenerating them.
132
+ - --write validates and never rewrites: your hand-written README index is KEPT
133
+ (pass --force only if you want the index REGENERATED from the feature files);
134
+ existing feature files are never touched either way.
134
135
 
135
136
  == FEATURE-FILE SCHEMA =========================================================
136
137
 
@@ -201,13 +202,15 @@ if [ -n "$WRITE_DIR" ]; then
201
202
  WDIR=$(cd "$WRITE_DIR" && pwd) || { g_err "cannot enter: $WRITE_DIR"; exit 2; }
202
203
  # A map, for --write purposes, is the README index: --write never writes feature
203
204
  # files (they are its INPUT), so the only thing it could clobber is the index.
205
+ # QA fix (v2-qa issue 5): an existing index used to be a refusal ("pass --force") —
206
+ # but the AGENT BRIEF itself tells the agent to WRITE features/README.md before
207
+ # running --write, so the first --write of the brief's own prescribed flow demanded
208
+ # the scary flag. The coherent contract: --write VALIDATES and never rewrites the
209
+ # index without --force; an existing index is the author's own output and is kept,
210
+ # named in the done line. (The never-clobber promise is unchanged: --write still
211
+ # writes NOTHING without --force — the refusal was protecting an empty write.)
204
212
  HAVE_WMAP=0
205
213
  if [ -f "$WDIR/README.md" ]; then HAVE_WMAP=1; fi
206
- if [ "$HAVE_WMAP" -eq 1 ] && [ "$FORCE" -ne 1 ]; then
207
- g_err "refusing to overwrite an existing feature map: $WDIR"
208
- printf 'error: map: pass --force to regenerate the README index and add NEW files only; existing files are never rewritten\n' >&2
209
- exit 1
210
- fi
211
214
  # Validation: the frontmatter + H2 + verified contract, per file, fail closed.
212
215
  BAD=0; N=0
213
216
  for f in "$WDIR"/*.md; do
@@ -264,7 +267,10 @@ if [ -n "$WRITE_DIR" ]; then
264
267
  for f in "$WDIR"/*.md; do
265
268
  [ -f "$f" ] || continue
266
269
  slug=$(basename "$f"); slug=${slug%.md}
267
- case "$slug" in README.md) continue ;; esac
270
+ # QA fix (v2-qa issue 6): $slug is the STEM (README, not README.md) — the old
271
+ # pattern compared README against README.md, never matched, and the regenerated
272
+ # index listed the index itself as a feature.
273
+ case "$slug" in README) continue ;; esac
268
274
  printf '%s\t%s\n' "$slug" "$(printf '%s' "${f#"$WDIR"/}" | sed 's#^#./#')" >> "$SLUGS_TSV"
269
275
  done
270
276
  TODAY=$(date +%F)
@@ -509,7 +515,9 @@ MID
509
515
  for f in "$FEAT"/*.md; do
510
516
  [ -f "$f" ] || continue
511
517
  slug=$(basename "$f"); slug=${slug%.md}
512
- case "$slug" in README.md) continue ;; esac
518
+ # QA fix (v2-qa issue 6): stem comparison, same bug as the --write index loop —
519
+ # the slug is README after .md is stripped, so the old README.md pattern never fired.
520
+ case "$slug" in README) continue ;; esac
513
521
  awk -F'\t' -v s="$slug" '$1 == s { found=1 } END { exit !found }' "$SLUGS_TSV" && continue
514
522
  title=$(file_title "$f" "$slug")
515
523
  printf '%s\n' "- [${title}](./${slug}.md) covers TODO — existing feature file kept by --force (not re-described)"
package/bin/goblin-mcp.js CHANGED
@@ -211,7 +211,29 @@ function remedyMap(root) {
211
211
  }
212
212
 
213
213
  function toolGobVerify(args) {
214
- const root = process.cwd();
214
+ // QA fix (v2-qa issue 7): the tool honours an explicit `target` — an agent calling from
215
+ // an installed repo with a different repo named as target used to get the CWD's matrix
216
+ // with the bogus target silently dropped (wrong-repo-as-verified). Resolution rules:
217
+ // - target absent/empty -> process.cwd() (the documented default)
218
+ // - target relative -> resolved against process.cwd()
219
+ // - resolved dir carries no .gob/engine and no AGENTS.md gob block -> REFUSAL (the
220
+ // exact resolved path is named), never a silent fall-back to the CWD's matrix
221
+ // The result's first line names the verified path, so the agent sees WHICH repo was judged.
222
+ const cwd = process.cwd();
223
+ let root = cwd;
224
+ const target = args && typeof args.target === "string" ? args.target.trim() : "";
225
+ if (target) {
226
+ root = path.resolve(cwd, target);
227
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
228
+ return errText("gob_verify: target is not a directory: " + root);
229
+ }
230
+ const blockText = readBlock(path.join(root, "AGENTS.md"));
231
+ if (!fs.existsSync(path.join(root, ".gob", "bin", "goblin-verify")) && !blockText) {
232
+ return errText(
233
+ "gob_verify: target is not a gobstack repo (no .gob/bin/goblin-verify, no AGENTS.md gob block): " + root
234
+ );
235
+ }
236
+ }
215
237
  const engine = path.join(root, ".gob", "bin", "goblin-verify");
216
238
  if (!fs.existsSync(engine)) {
217
239
  return errText(
@@ -235,7 +257,9 @@ function toolGobVerify(args) {
235
257
  const verdict = parsed.failed === 0 && r.status === 0 ? "PASS" : "FAIL";
236
258
  const lines = [];
237
259
  lines.push(
238
- "gate: " + verdict + " — " + parsed.passed + " passed, " + parsed.failed + " failed, " +
260
+ // the resolved repo is named first: the consumer sees WHICH tree was judged
261
+ "verified: " + root +
262
+ "\ngate: " + verdict + " — " + parsed.passed + " passed, " + parsed.failed + " failed, " +
239
263
  parsed.advisory + " advisory, " + parsed.skipped + " skipped (exit " + r.status + ")"
240
264
  );
241
265
  for (const row of parsed.rows) {
@@ -358,7 +382,9 @@ const TOOLS = [
358
382
  type: "object",
359
383
  properties: {
360
384
  only: { type: "string", description: "optional comma-separated rule ids to run (e.g. FM-01,FM-02)" },
385
+ target: { type: "string", description: "optional repo root to verify (absolute, or relative to the CWD); defaults to the current working directory" },
361
386
  },
387
+ additionalProperties: false,
362
388
  },
363
389
  },
364
390
  {
@@ -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.6.0-alpha.3"
33
+ GOBLIN_UPGRADE_VERSION="0.6.0-alpha.4"
34
34
 
35
35
  usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
36
36
 
package/bin/goblin-verify CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  set -uo pipefail
21
21
 
22
- GOBLIN_VERIFY_VERSION="0.6.0-alpha.3"
22
+ GOBLIN_VERIFY_VERSION="0.6.0-alpha.4"
23
23
  SELF_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
24
24
  # shellcheck source=goblin-lib.sh
25
25
  . "$SELF_DIR/goblin-lib.sh"
@@ -276,9 +276,9 @@ if [ ! -f "$CONFIG" ]; then
276
276
  if [ "$ROOT" != "$PWD" ]; then
277
277
  g_err "not installed: $CONFIG is absent — and $PWD is not the installed repo ($ROOT)."
278
278
  g_err "fix: cd to your project repo, then re-run gob verify."
279
- g_err " (installing here would initialize $PWD: goblin-install --target $PWD --class <software|service|game|research|fleet>)"
279
+ g_err " (installing here would initialize $PWD: npx @techgoblin/gobstack init)"
280
280
  else
281
- g_err "not installed: $CONFIG is absent. Run: goblin-install --target $ROOT --class <software|service|game|research|fleet>"
281
+ g_err "not installed: $CONFIG is absent. Run: npx @techgoblin/gobstack init # or: gob init, inside $ROOT"
282
282
  fi
283
283
  exit 2
284
284
  fi
@@ -484,7 +484,7 @@ check_in_01() {
484
484
  # IN-01 recovery line (review 1, scope 4): the record is corrupt or missing; name the two
485
485
  # restores. The matrix's remedy: line rides under the payload via g_fail.
486
486
  printf 'installed.json is absent or records no version\n'
487
- printf 'recover: gob install --upgrade, or git checkout -- .gob/installed.json\n'
487
+ printf 'recover: git checkout -- .gob/installed.json, or re-run npx @techgoblin/gobstack init --write to re-install\n'
488
488
  return 1
489
489
  }
490
490
 
@@ -609,7 +609,7 @@ check_in_02() {
609
609
  # catch a SILENT edit, so re-pinning is a deliberate act: one explicit command, which
610
610
  # prints the old and new hash (docs/CONTRACTS.md, "An edited standard is not a dead end").
611
611
  printf ' if the edit is yours and intended, re-pin it deliberately:\n'
612
- printf ' goblin-install --target %s --re-pin\n' "$ROOT"
612
+ printf ' goblin-install --target %s --re-pin # from the gobstack checkout/install tree\n' "$ROOT"
613
613
  bad=1; pin=" | practice EDITED"
614
614
  else
615
615
  pin=" | practice pin ok"
@@ -626,7 +626,7 @@ check_in_02() {
626
626
  # naming the drifted file the payload above reported. Sits between the row's own lines and
627
627
  # the summary; g_fail appends the matrix's remedy: line under the whole payload.
628
628
  if [ "$bad" -ne 0 ] && [ -n "$DRIFTED" ]; then
629
- printf 'recover: git checkout -- %s, or gob install --upgrade\n' "$DRIFTED"
629
+ printf 'recover: git checkout -- %s, or re-run npx @techgoblin/gobstack init --write to re-install\n' "$DRIFTED"
630
630
  fi
631
631
  [ "$bad" -eq 0 ]
632
632
  }
package/docs/CONTRACTS.md CHANGED
@@ -13,12 +13,15 @@ same way the fleet's own tool reads it. Everything else is line-oriented shell.
13
13
  software · service · game · research · fleet. The letters A-E and the
14
14
  older names app (software), agent (fleet) and desktop/F (software + the
15
15
  electron opt-in) are read-time aliases.
16
- --models <path> model mapping file (default: $GOBLIN_MODELS -> ~/projects/fleet-model.yaml)
17
- --practice <path> the referenced standard (default: $GOBLIN_PRACTICE -> ~/projects/PROJECT-PRACTICE.md)
16
+ --models <path> model mapping file (default: $GOBLIN_MODELS; unset = an empty
17
+ models_file: the repo's machine-specific input, set per machine)
18
+ --practice <path> the referenced standard (default: $GOBLIN_PRACTICE; unset = no
19
+ practice pin; a named path that is absent is reported, never
20
+ silently dropped)
18
21
  --parts <list> comma list to install; default = every part the class requires
19
22
  --archive mark the project archive: verify requires no HANDOFF and no gates
20
23
  --skills yes|no install agent skills under .hermes/skills (default no — the harness is
21
- neutral; opt in per platform with: gob emit --platform <p>). On a repo whose
24
+ neutral; opt in per catalogue row with: gob extras install <id...>). On a repo whose
22
25
  record already has skills installed, an OMITTED flag keeps them; an explicit
23
26
  --skills no removes them.
24
27
  --dry-run print the plan; write nothing
@@ -31,7 +31,7 @@ Measured shape of this table: **87 rows** - 82 target, 5 source; advisory 10, ga
31
31
  | id | scope | enforced by | rule | check | if it cannot be enforced, why |
32
32
  |---|---|---|---|---|---|
33
33
  | `IN-01` | target | script | The install exists and records its version + every file's hash. | goblin-verify --only IN-01 | — (W1: the check is a builtin so the engine.mode=global clause can run — a global, declaration-only repo has no install record and SKIPs with `global engine mode — no per-repo install record`; the vendored clauses are the old one-liner: the record exists and names its version) |
34
- | `IN-02` | target | script | Every installed file still matches its recorded hash. | goblin-verify --only IN-02 | recover: gob install --upgrade, or git checkout -- .gob/installed.json; for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: gob install --target <dir> --re-pin) |
34
+ | `IN-02` | target | script | Every installed file still matches its recorded hash. | goblin-verify --only IN-02 | recover: git checkout -- .gob/installed.json (or re-run `npx @techgoblin/gobstack init --write` to re-install); for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: `goblin-install --target <dir> --re-pin` from the checkout/install tree) |
35
35
  | `IN-03` | target | script | The verifier's own manifest is complete: every rule has a check or is advisory. | goblin-verify --only IN-03 | — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script\|lint\|gate\|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in bin/goblin-verify), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine) |
36
36
  | `IN-04` | target | script | No file goblin-stack did not create has been overwritten. | goblin-verify --only IN-04 | Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch. |
37
37
  | `HP-01` | target | gate | HANDOFF.md exists at the root. | test -f HANDOFF.md | — |
package/docs/GUIDE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A step-by-step guide for your first week. **Read this before the README.** The README tells you
4
4
  what the pieces are; this tells you what to *do*, in order, and what you should see when it works.
5
5
 
6
- Version: `0.6.0-alpha.2` · Last measured: 2026-10-09 · Every command and every output below was run on a
6
+ Version: `0.6.0-alpha.4` · Last measured: 2026-10-09 · Every command and every output below was run on a
7
7
  real repository while writing this guide.
8
8
 
9
9
  ---
@@ -267,7 +267,8 @@ Open it. The keys that matter on day one:
267
267
  branch: main # DECLARED, never assumed
268
268
  owner_email: you@example.com # the commit identity this repo expects
269
269
  practice: /path/to/your-standard.md # optional: your own house rules, hash-pinned
270
- models_file: /path/to/fleet-model.yaml # the ONE machine-specific input
270
+ models_file: /path/to/fleet-model.yaml # the ONE machine-specific input (per machine; a
271
+ # fresh install without --models leaves it empty)
271
272
  gate_<name>_cmd: <one command> # YOUR real commands, one line each
272
273
 
273
274
  **The single most valuable edit you will make:** replace the gate line(s) with the commands you
@@ -290,7 +291,7 @@ repos follow an old version.
290
291
 
291
292
  When *you* legitimately edit your own standard:
292
293
 
293
- gob install --target . --re-pin
294
+ goblin-install --target . --re-pin # from the gobstack checkout / install tree
294
295
 
295
296
  It re-records the hash and prints the old and new value. Nothing re-pins automatically — not even
296
297
  a re-install — an edited standard is never a silent no-op.
@@ -528,8 +529,8 @@ same commands are available outside any repo through the npm CLI: `gob init` / `
528
529
 
529
530
  .gob/bin/goblin-verify [--only <id[,id...]>] [--json] [--list]
530
531
  .gob/bin/goblin-bans # run the ban list
531
- gob install --target <dir> --uninstall # the uninstall job
532
- gob install --target <dir> --re-pin # the deliberate re-pin
532
+ gob uninstall --target <dir> # the uninstall job
533
+ goblin-install --target <dir> --re-pin # the deliberate re-pin (from the checkout/install tree)
533
534
  bin/goblin-model <role> # checkout-only; resolve a role to a profile (docs/ROLES.md)
534
535
 
535
536
  ### Register the harness with your agent (MCP)
@@ -1,6 +1,6 @@
1
1
  id scope rule enforced_by artifact check if_not_why
2
2
  IN-01 target The install exists and records its version + every file's hash. script .gob/installed.json goblin-verify --only IN-01 — (W1: the check is a builtin so the engine.mode=global clause can run — a global, declaration-only repo has no install record and SKIPs with `global engine mode — no per-repo install record`; the vendored clauses are the old one-liner: the record exists and names its version)
3
- IN-02 target Every installed file still matches its recorded hash. script .gob/installed.json goblin-verify --only IN-02 recover: gob install --upgrade, or git checkout -- .gob/installed.json; for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: gob install --target <dir> --re-pin)
3
+ IN-02 target Every installed file still matches its recorded hash. script .gob/installed.json goblin-verify --only IN-02 recover: git checkout -- .gob/installed.json (or re-run npx @techgoblin/gobstack init --write to re-install); for a file it names drifted: git checkout -- <path> (if the edit is yours and intended, re-pin it: goblin-install --target <dir> --re-pin from the checkout/install tree)
4
4
  IN-03 target The verifier's own manifest is complete: every rule has a check or is advisory. script manifest/enforcement.tsv goblin-verify --only IN-03 — (this row is the reason the matrix cannot rot; the second clause is D6's shape in general: a row that carries no check must be labelled advisory, or it claims verification it does not perform. The third clause is Z1-5: `enforced_by` is documented as a closed enum in docs/ENFORCEMENT.md and was read by NOTHING, so a typo in that cell changed nothing - `script|lint|gate|advisory` plus `test`, the source-scope value whose check is tests/run-tests.sh. W1: the check runs against whichever manifest the engine actually resolved (the chain in bin/goblin-verify), no longer the hardcoded per-repo path — the rule's meaning is untouched, only the path input follows the engine)
5
5
  IN-04 target No file goblin-stack did not create has been overwritten. script .gob/installed.json goblin-verify --only IN-04 Detects a file the installer recorded as pre-existing (a `refused` entry) that has since vanished, or that is listed as installed anyway. The second clause is an internal-consistency guard: with correct code a refused path is never written, so it fires only if the installer regresses. The negative control exercises the vanished branch.
6
6
  HP-01 target HANDOFF.md exists at the root. gate HANDOFF.md test -f HANDOFF.md —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techgoblin/gobstack",
3
- "version": "0.6.0-alpha.3",
3
+ "version": "0.6.0-alpha.4",
4
4
  "description": "Agent-discipline toolkit: one verify command, an enforcement matrix, and LIMITS. bash engine, npm shim.",
5
5
  "bin": {
6
6
  "goblin": "bin/goblin.js",
@@ -13,7 +13,7 @@ Use when adopting goblin-stack in a repo, or starting one.
13
13
  with the electron bans and a host gate, not a sixth class. The letters `A`-`E` and the older
14
14
  names are read-time aliases.
15
15
  2. **`goblin-install --target <dir> --class <x>`** — the default install is a NEUTRAL harness:
16
- no agent skills. Opt in per platform afterwards with `gob emit --platform <p>` (or vendor the
16
+ no agent skills. Opt in per catalogue row afterwards with `gob extras install <id>` (or vendor the
17
17
  Hermes project tier with `--skills yes`).
18
18
  3. **`goblin-verify`** — a default software-class install (no agent skills) verifies green:
19
19
  `37 passed, 0 failed, 11 advisory, 34 skipped`, exit 0, once `HANDOFF.md` names a commit that