@drafthq/draft 3.6.0 → 3.7.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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +28 -49
  5. package/bin/README.md +47 -1
  6. package/cli/src/installer.js +1 -1
  7. package/cli/src/lib/cursor-registry.js +10 -0
  8. package/core/agents/architect.md +21 -9
  9. package/core/agents/debugger.md +5 -0
  10. package/core/agents/ops.md +7 -2
  11. package/core/agents/planner.md +7 -0
  12. package/core/agents/rca.md +14 -3
  13. package/core/agents/reviewer.md +1 -0
  14. package/core/agents/writer.md +6 -0
  15. package/core/knowledge-base.md +22 -3
  16. package/core/methodology.md +30 -5
  17. package/core/shared/condensation.md +27 -17
  18. package/core/shared/context-verify.md +3 -1
  19. package/core/shared/cross-skill-dispatch.md +7 -3
  20. package/core/shared/draft-context-loading.md +3 -0
  21. package/core/shared/git-report-metadata.md +1 -0
  22. package/core/shared/graph-query.md +0 -2
  23. package/core/shared/graph-usage-report.md +1 -1
  24. package/core/shared/jira-sync.md +10 -6
  25. package/core/shared/okf-retrieval.md +1 -1
  26. package/core/shared/parallel-analysis.md +14 -7
  27. package/core/shared/pattern-learning.md +2 -0
  28. package/core/shared/template-contract.md +1 -1
  29. package/core/shared/vcs-commands.md +5 -2
  30. package/core/templates/ai-context.md +3 -2
  31. package/core/templates/ai-profile.md +7 -0
  32. package/core/templates/architecture.md +6 -2
  33. package/core/templates/discovery.md +1 -1
  34. package/core/templates/guardrails.md +9 -0
  35. package/core/templates/hld.md +5 -4
  36. package/core/templates/intake-questions.md +45 -0
  37. package/core/templates/jira.md +11 -2
  38. package/core/templates/lld.md +8 -5
  39. package/core/templates/okf/concept.md +0 -1
  40. package/core/templates/product.md +11 -2
  41. package/core/templates/rca.md +6 -0
  42. package/core/templates/service-index.md +1 -0
  43. package/core/templates/spec.md +15 -0
  44. package/core/templates/tech-stack.md +12 -0
  45. package/core/templates/workflow.md +20 -0
  46. package/integrations/agents/AGENTS.md +1272 -292
  47. package/integrations/copilot/.github/copilot-instructions.md +1272 -292
  48. package/package.json +1 -1
  49. package/scripts/fetch-memory-engine.sh +24 -6
  50. package/scripts/lib.sh +11 -4
  51. package/scripts/tools/_graph_queries.sh +10 -3
  52. package/scripts/tools/_lib.sh +33 -11
  53. package/scripts/tools/check-repo-size.sh +125 -0
  54. package/scripts/tools/check-skill-line-caps.sh +1 -2
  55. package/scripts/tools/check-template-noop.sh +0 -1
  56. package/scripts/tools/classify-files.sh +2 -2
  57. package/scripts/tools/cycle-detect.sh +16 -8
  58. package/scripts/tools/fix-whitespace.sh +3 -0
  59. package/scripts/tools/git-metadata.sh +6 -0
  60. package/scripts/tools/graph-arch.sh +1 -1
  61. package/scripts/tools/graph-callers.sh +6 -3
  62. package/scripts/tools/graph-impact.sh +4 -2
  63. package/scripts/tools/graph-init.sh +12 -8
  64. package/scripts/tools/graph-preflight.sh +3 -0
  65. package/scripts/tools/graph-query.sh +70 -7
  66. package/scripts/tools/graph-snapshot.sh +29 -5
  67. package/scripts/tools/hotspot-rank.sh +24 -9
  68. package/scripts/tools/install-smoke-test.sh +223 -0
  69. package/scripts/tools/mermaid-from-graph.sh +28 -11
  70. package/scripts/tools/migrate-track-frontmatter.sh +9 -10
  71. package/scripts/tools/okf-coverage-check.sh +4 -3
  72. package/scripts/tools/okf-fix-links.sh +18 -93
  73. package/scripts/tools/okf-plan-concepts.sh +5 -8
  74. package/scripts/tools/okf-render-views.sh +19 -14
  75. package/scripts/tools/okf-validate-quality.sh +13 -2
  76. package/scripts/tools/okf-validate.sh +13 -2
  77. package/scripts/tools/parse-git-log.sh +19 -1
  78. package/scripts/tools/resolve-tools.sh +24 -8
  79. package/scripts/tools/verify-citations.sh +1 -2
  80. package/scripts/tools/verify-doc-anchors.sh +13 -33
  81. package/scripts/tools/verify-graph-binary.sh +13 -3
  82. package/skills/GRAPH.md +21 -10
  83. package/skills/adr/SKILL.md +24 -6
  84. package/skills/assist-review/SKILL.md +2 -1
  85. package/skills/bughunt/SKILL.md +54 -11
  86. package/skills/bughunt/references/regression-tests.md +14 -4
  87. package/skills/change/SKILL.md +19 -9
  88. package/skills/coverage/SKILL.md +26 -10
  89. package/skills/debug/SKILL.md +12 -2
  90. package/skills/decompose/SKILL.md +39 -9
  91. package/skills/deep-review/SKILL.md +17 -4
  92. package/skills/deploy-checklist/SKILL.md +9 -2
  93. package/skills/documentation/SKILL.md +12 -1
  94. package/skills/draft/SKILL.md +40 -29
  95. package/skills/draft/context-files.md +8 -1
  96. package/skills/draft/quality-guide.md +10 -2
  97. package/skills/graph/SKILL.md +1 -1
  98. package/skills/impact/SKILL.md +3 -1
  99. package/skills/implement/SKILL.md +60 -18
  100. package/skills/incident-response/SKILL.md +5 -2
  101. package/skills/init/SKILL.md +102 -23
  102. package/skills/init/references/architecture-spec.md +31 -7
  103. package/skills/init/references/okf-emitter.md +3 -3
  104. package/skills/jira/SKILL.md +33 -13
  105. package/skills/jira/references/review.md +61 -19
  106. package/skills/learn/SKILL.md +29 -6
  107. package/skills/new-track/SKILL.md +73 -18
  108. package/skills/quick-review/SKILL.md +7 -2
  109. package/skills/revert/SKILL.md +11 -5
  110. package/skills/review/SKILL.md +123 -24
  111. package/skills/review/references/zero-setup-mode.md +117 -0
  112. package/skills/standup/SKILL.md +9 -1
  113. package/skills/status/SKILL.md +6 -6
  114. package/skills/tech-debt/SKILL.md +7 -1
  115. package/skills/testing-strategy/SKILL.md +10 -2
  116. package/skills/tour/SKILL.md +2 -1
  117. package/skills/upload/SKILL.md +3 -2
@@ -20,7 +20,6 @@ DRAFT=""
20
20
  FILE=""
21
21
  WIKI=""
22
22
  DO_FIX=0
23
- DO_CHECK=1
24
23
 
25
24
  usage() {
26
25
  cat <<'EOF'
@@ -35,7 +34,7 @@ Flags:
35
34
  --file FILE single markdown file to rewrite/check
36
35
  --wiki DIR wiki bundle (required with --file for basename map)
37
36
  --fix rewrite broken/ambiguous links in place
38
- --check report broken links (default on)
37
+ --check accepted for symmetry with --fix; checking is unconditional
39
38
  --help
40
39
 
41
40
  Exit: 0 clean, 1 broken links remain, 2 bad invocation.
@@ -48,7 +47,7 @@ while [[ $# -gt 0 ]]; do
48
47
  --file) FILE="${2:?--file requires a value}"; shift 2;;
49
48
  --wiki) WIKI="${2:?--wiki requires a value}"; shift 2;;
50
49
  --fix) DO_FIX=1; shift;;
51
- --check) DO_CHECK=1; shift;;
50
+ --check) shift;; # checking always runs; accepted so callers can be explicit
52
51
  --help|-h) usage; exit 0;;
53
52
  -*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
54
53
  *) echo "Unexpected arg: $1" >&2; exit 2;;
@@ -65,83 +64,17 @@ fi
65
64
  [[ -n "$FILE" && -f "$FILE" ]] || { echo "ERROR: --file or --draft/architecture.md required" >&2; exit 2; }
66
65
  [[ -n "$WIKI" && -d "$WIKI" ]] || { echo "ERROR: wiki dir required" >&2; exit 2; }
67
66
 
68
- # Build basename → list of wiki-relative paths (relative to draft root as wiki/...)
69
- declare -A BASENAME_MAP_MULTI=()
70
- while IFS= read -r -d '' p; do
71
- rel="wiki/${p#"$WIKI"/}"
72
- base="$(basename "$p")"
73
- if [[ -n "${BASENAME_MAP_MULTI[$base]:-}" ]]; then
74
- BASENAME_MAP_MULTI[$base]="${BASENAME_MAP_MULTI[$base]}|$rel"
75
- else
76
- BASENAME_MAP_MULTI[$base]="$rel"
77
- fi
78
- done < <(find "$WIKI" -type f -name '*.md' -print0)
79
-
80
- pick_target() {
81
- local base="$1" label="$2"
82
- local cands="${BASENAME_MAP_MULTI[$base]:-}"
83
- [[ -n "$cands" ]] || { echo ""; return; }
84
- if [[ "$cands" != *"|"* ]]; then
85
- printf '%s' "$cands"
86
- return
87
- fi
88
- local lab; lab="$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')"
89
- IFS='|' read -ra arr <<< "$cands"
90
- local pref
91
- if [[ "$lab" == *product* || "$lab" == *feature* || "$lab" == *user*guide* || "$lab" == *install* ]]; then
92
- for pref in "${arr[@]}"; do [[ "$pref" == *"/features/"* ]] && { printf '%s' "$pref"; return; }; done
93
- fi
94
- if [[ "$lab" == *build* || "$lab" == *runbook* || "$lab" == *ops* || "$lab" == *source* ]]; then
95
- for pref in "${arr[@]}"; do [[ "$pref" == *"/overview/"* ]] && { printf '%s' "$pref"; return; }; done
96
- fi
97
- for order in /systems/ /features/ /overview/ /reference/ /entrypoints/; do
98
- for pref in "${arr[@]}"; do
99
- [[ "$pref" == *"$order"* ]] && { printf '%s' "$pref"; return; }
100
- done
101
- done
102
- printf '%s' "${arr[0]}"
67
+ command -v python3 >/dev/null 2>&1 || {
68
+ echo "ERROR: python3 is required by ${0##*/} (link rewriting and validation)" >&2
69
+ exit 2
103
70
  }
104
71
 
105
- # Collect heading slugs from a file for anchor checks
106
- build_slugs() {
107
- local f="$1"
108
- declare -gA SLUGS=()
109
- local counts=()
110
- # shellcheck disable=SC2034
111
- while IFS= read -r line; do
112
- [[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]] || continue
113
- local raw="${BASH_REMATCH[1]}"
114
- local s; s="$(gfm_slug "$raw")"
115
- local n="${SLUGS[_count_$s]:-0}"
116
- SLUGS[_count_$s]=$((n+1))
117
- if [[ $n -eq 0 ]]; then
118
- SLUGS[$s]=1
119
- else
120
- SLUGS["${s}-$n"]=1
121
- fi
122
- done < "$f"
123
- }
72
+ # Link resolution (basename disambiguation, section preference) lives in the
73
+ # Python pass inside fix_file — it builds its own basename map from the same
74
+ # wiki tree. The former bash twin, pick_target(), had no callers.
124
75
 
125
- resolve_ok() {
126
- local src="$1" href="$2"
127
- [[ "$href" =~ ^https?:// || "$href" =~ ^mailto: ]] && return 0
128
- if [[ "$href" == \#* ]]; then
129
- local a="${href#\#}"
130
- [[ -n "${SLUGS[$a]:-}" ]] && return 0
131
- return 1
132
- fi
133
- local path="${href%%\#*}"
134
- [[ -z "$path" ]] && return 0
135
- local dir; dir="$(cd "$(dirname "$src")" && pwd)"
136
- local draft_root=""
137
- if [[ -n "$DRAFT" ]]; then
138
- draft_root="$(cd "$DRAFT" && pwd)"
139
- else
140
- draft_root="$(cd "$(dirname "$src")" && pwd)"
141
- fi
142
- [[ -e "$dir/$path" || -e "$draft_root/$path" || -e "$path" ]] && return 0
143
- return 1
144
- }
76
+ # Anchor and link validation lives in the Python pass at the bottom of this
77
+ # file — it is the only checker, and it computes its own slug table.
145
78
 
146
79
  fix_file() {
147
80
  local src="$1"
@@ -239,11 +172,8 @@ def repl(m):
239
172
  new = re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text)
240
173
  out.write_text(new if new.endswith("\n") else new + "\n", encoding="utf-8")
241
174
  PY
242
- if [[ $DO_FIX -eq 1 ]]; then
243
- mv "$tmp" "$src"
244
- else
245
- rm -f "$tmp"
246
- fi
175
+ apply_dest_mode "$tmp" "$src" # mktemp is 0600; mv would strip the dest's mode
176
+ mv "$tmp" "$src"
247
177
  }
248
178
 
249
179
  if [[ $DO_FIX -eq 1 ]]; then
@@ -254,16 +184,7 @@ if [[ $DO_FIX -eq 1 ]]; then
254
184
  fi
255
185
  fi
256
186
 
257
- # Check
258
- build_slugs "$FILE"
259
- broken=0
260
- total=0
261
- while IFS= read -r line; do
262
- # crude extract — enough for validation
263
- :
264
- done < /dev/null
265
-
266
- # Python check for reliability
187
+ # Check — the single source of link/anchor truth
267
188
  broken_out="$(python3 - "$FILE" "${DRAFT:-$(dirname "$FILE")}" "$WIKI" <<'PY'
268
189
  import sys, re
269
190
  from pathlib import Path
@@ -304,7 +225,11 @@ for m in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", text):
304
225
  path = href.split("#",1)[0]
305
226
  if not path:
306
227
  ok += 1; continue
307
- cands = [src.parent/path, draft/path, Path(path)]
228
+ # Resolve only against the two bases a link inside this document can mean:
229
+ # the document's own directory and the draft/ root. Never against the
230
+ # process CWD — a same-named file in the caller's cwd used to make a
231
+ # genuinely dangling link report clean (exit flipped 1 -> 0).
232
+ cands = [src.parent/path, draft/path]
308
233
  if any(c.exists() for c in cands):
309
234
  ok += 1
310
235
  else:
@@ -175,7 +175,7 @@ plan_from_cargo_workspace() {
175
175
  [[ -f "$cargo" ]] || return 1
176
176
  # Only treat as workspace if [workspace] present with members.
177
177
  grep -qE '^\[workspace\]' "$cargo" || return 1
178
- grep -qE 'members\s*=' "$cargo" || return 1
178
+ grep -qE 'members[[:space:]]*=' "$cargo" || return 1
179
179
 
180
180
  local members=()
181
181
  # Extract quoted paths inside the members = [ ... ] array (possibly multi-line).
@@ -424,13 +424,10 @@ else
424
424
  plan_from_cargo_workspace || true
425
425
  plan_from_npm_workspaces || true
426
426
  plan_from_go_modules || true
427
- if plan_from_graph; then
428
- :
429
- elif [[ ${#E_ID[@]} -eq 0 ]]; then
430
- plan_from_heuristic
431
- DISCOVERY_META+=("heuristic")
432
- fi
433
- # If graph failed but we have cargo/npm/go, still success.
427
+ plan_from_graph || true
428
+ # Last resort only: any of cargo/npm/go/graph succeeding leaves E_ID non-empty
429
+ # and skips this. Re-running the heuristic a second time can never add what
430
+ # the first pass didn't find, so there is one call site, not two.
434
431
  if [[ ${#E_ID[@]} -eq 0 ]]; then
435
432
  plan_from_heuristic
436
433
  DISCOVERY_META+=("heuristic")
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # okf-render-views.sh — render the demoted views from an OKF taxonomy bundle.
3
3
  #
4
- # The wiki/ bundle is the source of truth. This produces the two derived,
4
+ # The wiki/ bundle is the source of truth. This produces the derived,
5
5
  # human-facing views deterministically (so they never drift from the bundle and
6
6
  # carry zero extra maintenance):
7
7
  # 1. architecture.md — a single linear concatenation of every concept page,
@@ -24,6 +24,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
24
  # shellcheck source=scripts/tools/_lib.sh
25
25
  source "$SCRIPT_DIR/_lib.sh"
26
26
 
27
+ command -v python3 >/dev/null 2>&1 || {
28
+ echo "ERROR: python3 is required by ${0##*/} (link rewriting in rendered views)" >&2
29
+ exit 2
30
+ }
31
+
27
32
  BUNDLE=""
28
33
  ARCH_OUT=""
29
34
  WEB_OUT=""
@@ -56,7 +61,7 @@ Flags:
56
61
  this tool has no clock dependency).
57
62
  --help Show this help.
58
63
 
59
- Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found.
64
+ Requires jq (for --web) and python3 (link rewriting). Exit 0 ok, 1 error, 2 bundle not found.
60
65
  EOF
61
66
  }
62
67
 
@@ -100,8 +105,6 @@ ordered_pages() {
100
105
  done
101
106
  }
102
107
 
103
- # Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
104
-
105
108
  # Rewrite markdown links so concatenated architecture.md (at draft/) resolves them.
106
109
  # stdin body; $1 = bundle-relative path e.g. systems/foo.md
107
110
  rewrite_body_links() {
@@ -133,6 +136,7 @@ sys.stdout.write(re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text))
133
136
  ' "$sec"
134
137
  }
135
138
 
139
+ # Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
136
140
  strip_frontmatter() {
137
141
  awk '
138
142
  NR==1 && /^---$/ { fm=1; next }
@@ -217,6 +221,7 @@ render_architecture() {
217
221
  strip_frontmatter "$BUNDLE/$rel" | rewrite_body_links "$rel"
218
222
  done < <(ordered_pages)
219
223
  } >"$tmp"
224
+ apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
220
225
  mv "$tmp" "$out"
221
226
  if [[ -x "$SCRIPT_DIR/okf-fix-links.sh" ]]; then
222
227
  draft_dir="$(cd "$(dirname "$out")" && pwd)"
@@ -243,12 +248,7 @@ build_concept_map() {
243
248
  [[ -n "$type" ]] || continue
244
249
  title="$(get_yaml_field "$page" title)"
245
250
  [[ -n "$title" ]] || title="$rel"
246
- # description may be a folded (>) block — take the first non-empty body line.
247
- desc="$(awk '
248
- NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
249
- fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
250
- fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
251
- ' "$page")"
251
+ desc="$(page_desc "$page")"
252
252
  echo "| [${title}](${rel}) | ${type} | ${desc} |"
253
253
  done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
254
254
  }
@@ -313,14 +313,18 @@ inject_concept_map() {
313
313
  /<!-- CONCEPT-MAP:END -->/ { skip=0 }
314
314
  !skip { print }
315
315
  ' "$target" >"$tmp"
316
+ apply_dest_mode "$tmp" "$target" # mktemp is 0600; mv would strip the dest's mode
316
317
  mv "$tmp" "$target"
317
318
  echo "injected Concept Map → $target"
318
319
  }
319
320
 
320
321
  # --- 3. Render a self-contained offline HTML viewer (single file) ---
321
322
  # All pages are inlined as JSON; a small built-in markdown renderer draws them in
322
- # the browser. No server, no internet, no CDN. jq encodes page content safely
323
- # (and we neutralize any literal </ so embedded "</script>" can't break parsing).
323
+ # the browser. No server, no internet, no CDN. jq encodes page content safely,
324
+ # and the whole emitted data block is passed through a `</` → `<\/` filter: JSON
325
+ # escaping does NOT stop a literal "</script>" from closing the <script> element,
326
+ # and page titles/types/paths come from repo-derived frontmatter, so every field
327
+ # has to be neutralized, not just the markdown body.
324
328
  render_web() {
325
329
  local out="$1"
326
330
  command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; }
@@ -388,14 +392,14 @@ HTML_HEAD
388
392
  "$(jq -Rn --arg v "$rel" '$v')" \
389
393
  "$(jq -Rn --arg v "$title" '$v')" \
390
394
  "$(jq -Rn --arg v "$type" '$v')" \
391
- "$(strip_frontmatter "$page" | jq -Rs . | sed 's#</#<\\/#g')"
395
+ "$(strip_frontmatter "$page" | jq -Rs .)"
392
396
  done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
393
397
  echo "};"
394
398
  # ORDER: bundle root index.md first, then everything else sorted.
395
399
  echo "const ORDER = Object.keys(PAGES).sort(function(a,b){"
396
400
  echo " if(a==='index.md') return -1; if(b==='index.md') return 1;"
397
401
  echo " return a<b?-1:a>b?1:0; });"
398
- } >>"$tmp"
402
+ } | sed 's#</#<\\/#g' >>"$tmp"
399
403
 
400
404
  cat >>"$tmp" <<'HTML_TAIL'
401
405
  function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
@@ -487,6 +491,7 @@ show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
487
491
  HTML_TAIL
488
492
 
489
493
  mkdir -p "$(dirname "$out")"
494
+ apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
490
495
  mv "$tmp" "$out"
491
496
  echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
492
497
  }
@@ -73,6 +73,17 @@ body_of() {
73
73
  awk 'NR==1&&/^---$/{fm=1;next} fm&&/^---$/{fm=0;next} !fm{print}' "$1"
74
74
  }
75
75
 
76
+ # Body text the Q-TEMPLATE scan is allowed to see: fenced code blocks and
77
+ # shell-style ${VAR} expansions removed. Both legitimately carry {ALL_CAPS}
78
+ # spans that are not unreplaced placeholders (a page quoting `${HOME}` or a CI
79
+ # snippet would otherwise fail).
80
+ template_scan_body() {
81
+ body_of "$1" | awk '
82
+ /^[[:space:]]*```/ { fence = !fence; next }
83
+ !fence { gsub(/\$\{[A-Za-z_][A-Za-z0-9_]*\}/, ""); print }
84
+ '
85
+ }
86
+
76
87
  # Count body lines after the frontmatter close (Q-LEN = "lines after frontmatter").
77
88
  body_lines() { body_of "$1" | wc -l | tr -d ' '; }
78
89
 
@@ -114,7 +125,7 @@ mermaid_lint() {
114
125
  echo "unicode arrow in mermaid (use --> not →)"; return 1
115
126
  fi
116
127
  # '&' node chaining (common breaker).
117
- if printf '%s' "$blocks" | grep -qE '[A-Za-z0-9_]\s*&\s*[A-Za-z0-9_]'; then
128
+ if printf '%s' "$blocks" | grep -qE '[A-Za-z0-9_][[:space:]]*&[[:space:]]*[A-Za-z0-9_]'; then
118
129
  echo "'&' node chaining in mermaid"; return 1
119
130
  fi
120
131
  # Reserved bareword node ids.
@@ -235,7 +246,7 @@ while IFS= read -r -d '' page; do
235
246
  fi
236
247
 
237
248
  # Q-TEMPLATE: unreplaced {TOKEN} placeholders.
238
- if body_of "$page" | grep -qE "$TOKEN_RE"; then
249
+ if template_scan_body "$page" | grep -qE "$TOKEN_RE"; then
239
250
  fail "$rel" "Q-TEMPLATE" "unreplaced template token {PLACEHOLDER}"
240
251
  fi
241
252
 
@@ -176,14 +176,25 @@ while IFS= read -r -d '' page; do
176
176
  fi
177
177
  done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
178
178
 
179
+ # Page text the {ALL_CAPS} template-token scan is allowed to see. Fenced code
180
+ # blocks and shell-style ${VAR} expansions are removed first: both legitimately
181
+ # contain {ALL_CAPS} spans that are NOT unreplaced placeholders, and a wiki page
182
+ # quoting `${HOME}` or a CI snippet would otherwise fail the whole bundle.
183
+ template_scan_text() {
184
+ awk '
185
+ /^[[:space:]]*```/ { fence = !fence; next }
186
+ !fence { gsub(/\$\{[A-Za-z_][A-Za-z0-9_]*\}/, ""); print }
187
+ ' "$1"
188
+ }
189
+
179
190
  # --- 3b. Unreplaced template tokens (any page, including index pages) ---
180
191
  # Quality checks skip index.md pages, so a leftover {SECTION_TITLE}/{PROJECT_NAME}
181
192
  # placeholder in a hand-seeded index would otherwise survive. Match {ALL_CAPS}
182
193
  # tokens (safe: real prose almost never contains them).
183
194
  while IFS= read -r -d '' page; do
184
195
  prel="${page#"$BUNDLE/"}"
185
- if grep -qE '\{[A-Z][A-Z0-9_]+\}' "$page" 2>/dev/null; then
186
- tok="$(grep -oE '\{[A-Z][A-Z0-9_]+\}' "$page" | head -1)"
196
+ tok="$(template_scan_text "$page" 2>/dev/null | grep -oE '\{[A-Z][A-Z0-9_]+\}' | head -1 || true)"
197
+ if [[ -n "$tok" ]]; then
187
198
  add_error "$prel: unreplaced template token '$tok'"
188
199
  fi
189
200
  done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
@@ -63,6 +63,13 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
63
63
  exit 1
64
64
  fi
65
65
 
66
+ # `git log -n <non-numeric>` parses the count as 0 and exits 0, so a typo'd
67
+ # --limit silently yields an empty stream. Reject it here instead.
68
+ if [[ -n "$LIMIT" && ! "$LIMIT" =~ ^[0-9]+$ ]]; then
69
+ echo "ERROR: --limit must be a non-negative integer (got '$LIMIT')" >&2
70
+ exit 1
71
+ fi
72
+
66
73
  # Format: delimiter-separated metadata line, then --name-only file list, blank line separator.
67
74
  GIT_ARGS=(log --pretty=tformat:'COMMIT%x1f%H%x1f%an%x1f%aI%x1f%s' --name-only --no-merges)
68
75
  [[ -n "$SINCE" ]] && GIT_ARGS+=(--since="$SINCE")
@@ -118,6 +125,17 @@ process_commit() {
118
125
 
119
126
  # Single git log stream: each commit is `COMMIT<US>sha<US>author<US>ts<US>subject`
120
127
  # followed by its file paths (one per line) and a blank separator.
128
+ #
129
+ # Staged through a temp file rather than `< <(git ...)`: a process substitution
130
+ # swallows git's exit status, so a bad --branch/--since/--limit produced an empty
131
+ # JSONL stream and exit 0 — indistinguishable from "this range has no commits".
132
+ GIT_OUT="$(mktemp)"
133
+ trap 'rm -f "$GIT_OUT"' EXIT
134
+ if ! git "${GIT_ARGS[@]}" >"$GIT_OUT"; then
135
+ echo "ERROR: git log failed (check --branch / --since / --limit)" >&2
136
+ exit 1
137
+ fi
138
+
121
139
  cur_sha=""; cur_author=""; cur_ts=""; cur_subject=""; cur_files=0
122
140
  while IFS= read -r line; do
123
141
  if [[ "$line" == COMMIT$'\x1f'* ]]; then
@@ -129,7 +147,7 @@ while IFS= read -r line; do
129
147
  elif [[ -n "$line" ]]; then
130
148
  cur_files=$((cur_files + 1))
131
149
  fi
132
- done < <(git "${GIT_ARGS[@]}")
150
+ done < "$GIT_OUT"
133
151
  if [[ -n "$cur_sha" ]]; then
134
152
  process_commit "$cur_sha" "$cur_author" "$cur_ts" "$cur_subject" "$cur_files"
135
153
  fi
@@ -7,6 +7,14 @@
7
7
  # See core/shared/tool-resolver.md for the canonical procedure and the inline preamble
8
8
  # skills embed (this script is the single source of truth for the resolution order).
9
9
  #
10
+ # Resolution order is deliberately "authoritative install first, cwd last":
11
+ # skills execute whatever this prints, and the cwd is the repository under review,
12
+ # which Draft routinely runs against untrusted code. A project that ships its own
13
+ # scripts/tools/ must never shadow the installed plugin.
14
+ #
15
+ # Working on Draft itself? Export DRAFT_PLUGIN_ROOT="$PWD" — the explicit override
16
+ # is step 1 and always wins.
17
+ #
10
18
  # Usage:
11
19
  # DRAFT_TOOLS="$(scripts/tools/resolve-tools.sh)" # prints the dir, exit 0 if found
12
20
  # scripts/tools/resolve-tools.sh || echo "tools not found" # exit 1 if none exist
@@ -21,8 +29,18 @@ esac
21
29
 
22
30
  newest() {
23
31
  # Echo the lexically-newest existing match of a glob (by version sort), or nothing.
32
+ # `|| true` is load-bearing: on a glob miss `ls` exits non-zero, pipefail
33
+ # propagates it, and errexit would abort the whole resolver instead of letting
34
+ # the remaining fallbacks run.
24
35
  # shellcheck disable=SC2086
25
- ls -d $1 2>/dev/null | sort -V | tail -1
36
+ ls -d $1 2>/dev/null | sort -V | tail -1 || true
37
+ }
38
+
39
+ # Is DIR Draft's own source checkout? Checked before trusting the cwd at all.
40
+ is_draft_checkout() {
41
+ [ -f "$1/scripts/tools/resolve-tools.sh" ] || return 1
42
+ [ -f "$1/.claude-plugin/plugin.json" ] || return 1
43
+ grep -q '"name"[[:space:]]*:[[:space:]]*"draft"' "$1/.claude-plugin/plugin.json" 2>/dev/null
26
44
  }
27
45
 
28
46
  resolve() {
@@ -32,10 +50,6 @@ resolve() {
32
50
  d="${DRAFT_PLUGIN_ROOT:-}/scripts/tools"
33
51
  [ -n "${DRAFT_PLUGIN_ROOT:-}" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
34
52
 
35
- # 1b. Dev / dogfooding: cwd IS the draft repo. Guarded by this script's own
36
- # presence so it can never misfire in a user project (which has no resolve-tools.sh).
37
- [ -f "$PWD/scripts/tools/resolve-tools.sh" ] && { printf '%s' "$PWD/scripts/tools"; return 0; }
38
-
39
53
  # 2. Install marker written by `draft install` (authoritative).
40
54
  local marker="$HOME/.cache/draft/plugin-root"
41
55
  if [ -f "$marker" ]; then
@@ -68,9 +82,11 @@ resolve() {
68
82
  d="$HOME/.cursor/plugins/local/draft/scripts/tools"
69
83
  [ -d "$d" ] && { printf '%s' "$d"; return 0; }
70
84
 
71
- # 8. Dev / dogfooding (running inside the draft repo itself).
72
- d="$PWD/scripts/tools"
73
- [ -d "$d" ] && { printf '%s' "$d"; return 0; }
85
+ # 8. Last resort — dev / dogfooding inside Draft's own checkout. Deliberately
86
+ # after every install location, and gated on the plugin manifest rather than on
87
+ # a same-named file: a bare `$PWD/scripts/tools` test matched any project using
88
+ # that very common layout and handed it script execution.
89
+ is_draft_checkout "$PWD" && { printf '%s' "$PWD/scripts/tools"; return 0; }
74
90
 
75
91
  return 1
76
92
  }
@@ -189,8 +189,7 @@ scan_one_track() {
189
189
 
190
190
  while IFS= read -r f; do
191
191
  local rel_md="${f#"$track_dir/"}"
192
- while IFS=$'\t' read -r md_file lineno cite; do
193
- local md_line="$md_file"
192
+ while IFS=$'\t' read -r _md_file lineno cite; do
194
193
  local result rc=0
195
194
  # set +e to capture rc; set -e would exit on a non-zero return.
196
195
  set +e
@@ -3,12 +3,16 @@
3
3
 
4
4
  #
5
5
  # Verify cross-document references in a track:
6
- # - §X.Y or §X numbered-section references → target document must contain
7
- # a heading whose text starts with that number.
8
6
  # - <doc>.md#<anchor> markdown anchors → resolve <anchor> against the
9
7
  # target file's header slugs.
10
8
  # - (planned) / [New file ...] annotations → file MUST NOT exist locally.
11
9
  #
10
+ # Plain-prose `§X.Y` references are deliberately NOT validated: authors use them
11
+ # as shorthand for an external doc (`architecture.md §20.2`) without naming the
12
+ # file on the same line, so attribution is guesswork. A track that wants a
13
+ # machine-checkable §-reference writes it as a markdown link to the heading,
14
+ # which the anchor check above already covers.
15
+ #
12
16
  # Skips content inside <!-- VERIFIER:IGNORE START --> ... END --> blocks.
13
17
  #
14
18
  # Usage:
@@ -35,7 +39,7 @@ TRACK_PATHS=()
35
39
  usage() {
36
40
  local stream=2 code=2
37
41
  if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
38
- sed -n '2,20p' "$0" >&$stream
42
+ sed -n '2,26p' "$0" >&$stream
39
43
  exit "$code"
40
44
  }
41
45
 
@@ -72,24 +76,15 @@ md_slugs() {
72
76
  s = tolower(s)
73
77
  # Replace spaces with dashes
74
78
  gsub(/ +/, "-", s)
75
- # Strip punctuation except dashes
76
- gsub(/[^a-z0-9-]/, "", s)
79
+ # Strip punctuation except dashes and underscores. GitHub keeps `_`
80
+ # in a heading slug (and so does _lib.sh:gfm_slug) — stripping it
81
+ # here reported every valid #foo_bar-style anchor as missing.
82
+ gsub(/[^a-z0-9_-]/, "", s)
77
83
  print s
78
84
  }
79
85
  ' "$1" | sort -u
80
86
  }
81
87
 
82
- # Extract numbered headers (e.g. "## 7 Parallel SST transfer" or "## 20.2 ...").
83
- md_numbered_headers() {
84
- awk '
85
- /^#{1,6} +[0-9]+(\.[0-9]+)*[ .]/ {
86
- s = $0
87
- sub(/^#+ +/, "", s)
88
- print s
89
- }
90
- ' "$1"
91
- }
92
-
93
88
  scan_md() {
94
89
  local track_dir="$1" md="$2"
95
90
  local rel_track="${track_dir#"$REPO_ROOT/"}"
@@ -101,22 +96,7 @@ scan_md() {
101
96
  if [[ "$line" == *"<!-- VERIFIER:IGNORE END -->"* ]]; then ignore=0; continue; fi
102
97
  (( ignore )) && continue
103
98
 
104
- # 1. §-references inside a markdown link.
105
- # Plain-prose `§X.Y` is too ambiguous in practice — authors commonly
106
- # use it as shorthand for an external doc (e.g. `architecture.md §20.2`)
107
- # without naming the file on the same line. To keep this validator
108
- # focused on machine-verifiable cross-references, we only check
109
- # §X.Y when it appears INSIDE a markdown link target — i.e. the
110
- # author has structurally committed to an in-track reference.
111
- # Example that IS validated:
112
- # [see §3.1](./hld.md#detailed-design) ← anchor check below
113
- # Example that is NOT validated (prose only):
114
- # The §20.2 layout matches architecture.md.
115
- # If a track wants stricter intra-track §-checks, it can use
116
- # markdown links pointing to numbered headers explicitly.
117
- : # §-ref prose validation disabled — see comment above.
118
-
119
- # 2. Markdown anchor references like ./hld.md#section-name
99
+ # 1. Markdown anchor references like ./hld.md#section-name
120
100
  if echo "$line" | grep -qE '\([^)]*\.md#[A-Za-z0-9_-]+\)'; then
121
101
  local m
122
102
  while IFS= read -r m; do
@@ -141,7 +121,7 @@ scan_md() {
141
121
  done < <(echo "$line" | grep -oE '\([^)]*\.md#[A-Za-z0-9_-]+\)')
142
122
  fi
143
123
 
144
- # 3. (planned) / [New file ...] annotations: the path mentioned on the
124
+ # 2. (planned) / [New file ...] annotations: the path mentioned on the
145
125
  # same line must NOT yet exist relative to the track.
146
126
  #
147
127
  # Only fire when the line has EXACTLY one path-looking token. Lines
@@ -2,6 +2,7 @@
2
2
  # verify-graph-binary.sh — validate and select the Draft knowledge-graph engine.
3
3
  #
4
4
  # The engine is the codebase-memory-mcp binary. Resolution order (see _lib.sh:find_memory_bin):
5
+ # 0. DRAFT_MEMORY_DISABLE — hard off; resolution stops here and reports "none"
5
6
  # 1. DRAFT_MEMORY_BIN override
6
7
  # 2. codebase-memory-mcp on $PATH
7
8
  # 3. Draft-managed install (~/.cache/draft/bin/)
@@ -32,7 +33,8 @@ usage() {
32
33
  verify-graph-binary.sh — Draft knowledge-graph engine resolver + verifier
33
34
 
34
35
  Engine: codebase-memory-mcp
35
- Resolution: DRAFT_MEMORY_BIN > PATH > ~/.cache/draft/bin > bin/<arch>/
36
+ Resolution: DRAFT_MEMORY_DISABLE (hard off) > DRAFT_MEMORY_BIN > PATH
37
+ > ~/.cache/draft/bin > bin/<arch>/
36
38
 
37
39
  Options:
38
40
  --repo DIR Repo root for context (default .)
@@ -95,15 +97,23 @@ classify_source() {
95
97
 
96
98
  MEMORY_BIN=""
97
99
  if ! find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
98
- if [[ $EMIT_JSON -eq 1 ]]; then
100
+ # DRAFT_MEMORY_DISABLE short-circuits resolution, so say so — "install it or
101
+ # put it on PATH" is the wrong remedy when an env var turned the engine off.
102
+ if [[ -n "${DRAFT_MEMORY_DISABLE:-}" ]]; then
103
+ local_msg="Graph engine disabled by DRAFT_MEMORY_DISABLE (unset it to re-enable resolution)"
104
+ else
99
105
  local_msg="No codebase-memory-mcp engine found in DRAFT_MEMORY_BIN, PATH, ~/.cache/draft/bin, or bin/<arch>/"
106
+ fi
107
+ if [[ $EMIT_JSON -eq 1 ]]; then
100
108
  if [[ $STRICT -eq 1 ]]; then
101
109
  printf '{"status":"none","engine_bin":null,"source":null,"arch":"%s","message":"strict mode: %s"}\n' "$ARCH" "$local_msg"
102
110
  else
103
111
  printf '{"status":"unavailable","engine_bin":null,"source":null,"arch":"%s","message":"%s"}\n' "$ARCH" "$local_msg"
104
112
  fi
105
113
  elif [[ $STRICT -eq 1 ]]; then
106
- echo "STRICT: No codebase-memory-mcp engine found." >&2
114
+ echo "STRICT: $local_msg" >&2
115
+ elif [[ -n "${DRAFT_MEMORY_DISABLE:-}" ]]; then
116
+ echo "ERROR: $local_msg" >&2
107
117
  else
108
118
  echo "ERROR: No Draft graph engine located (codebase-memory-mcp)." >&2
109
119
  echo " Install it (scripts/fetch-memory-engine.sh) or put it on PATH." >&2