@drafthq/draft 3.2.0 → 3.3.0

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +28 -0
  4. package/README.md +2 -2
  5. package/cli/src/hosts/cursor.js +35 -5
  6. package/cli/src/installer.js +20 -0
  7. package/cli/src/lib/cursor-registry.js +122 -0
  8. package/cli/src/lib/marker.js +93 -0
  9. package/cli/src/lib/plugin-manifest.js +20 -0
  10. package/core/methodology.md +1 -1
  11. package/core/shared/condensation.md +3 -2
  12. package/core/shared/git-report-metadata.md +3 -2
  13. package/core/shared/graph-query.md +4 -3
  14. package/core/shared/tool-resolver.md +71 -4
  15. package/core/templates/okf/ai-context-index.md +48 -0
  16. package/core/templates/okf/concept.md +54 -0
  17. package/core/templates/okf/index.md +40 -0
  18. package/core/templates/okf/section-index.md +25 -0
  19. package/core/templates/plan.md +3 -2
  20. package/integrations/agents/AGENTS.md +792 -102
  21. package/integrations/copilot/.github/copilot-instructions.md +792 -102
  22. package/package.json +3 -2
  23. package/scripts/lib.sh +10 -0
  24. package/scripts/tools/graph-preflight.sh +259 -0
  25. package/scripts/tools/okf-render-views.sh +373 -0
  26. package/scripts/tools/okf-validate.sh +204 -0
  27. package/scripts/tools/resolve-tools.sh +78 -0
  28. package/skills/adr/SKILL.md +3 -2
  29. package/skills/bughunt/SKILL.md +10 -1
  30. package/skills/coverage/SKILL.md +8 -3
  31. package/skills/debug/SKILL.md +16 -5
  32. package/skills/decompose/SKILL.md +29 -12
  33. package/skills/deep-review/SKILL.md +19 -6
  34. package/skills/deploy-checklist/SKILL.md +6 -5
  35. package/skills/graph/SKILL.md +15 -6
  36. package/skills/impact/SKILL.md +12 -1
  37. package/skills/implement/SKILL.md +20 -4
  38. package/skills/init/SKILL.md +36 -10
  39. package/skills/init/references/architecture-spec.md +17 -6
  40. package/skills/init/references/okf-emitter.md +223 -0
  41. package/skills/learn/SKILL.md +15 -4
  42. package/skills/quick-review/SKILL.md +13 -3
  43. package/skills/review/SKILL.md +32 -8
  44. package/skills/standup/SKILL.md +3 -2
  45. package/skills/status/SKILL.md +3 -2
  46. package/skills/tech-debt/SKILL.md +20 -6
  47. package/skills/upload/SKILL.md +3 -2
  48. package/integrations/copilot/.github/copilot-instructions.md.7iDz8X +0 -91
  49. package/integrations/copilot/.github/copilot-instructions.md.DoBdtd +0 -91
  50. package/integrations/copilot/.github/copilot-instructions.md.McGoBW +0 -122
  51. package/integrations/copilot/.github/copilot-instructions.md.VsPyLB +0 -91
  52. package/integrations/copilot/.github/copilot-instructions.md.XAVr7D +0 -91
  53. package/integrations/copilot/.github/copilot-instructions.md.YoFVFa +0 -91
  54. package/integrations/copilot/.github/copilot-instructions.md.a9DeW0 +0 -91
  55. package/integrations/copilot/.github/copilot-instructions.md.oxQs3B +0 -91
  56. package/integrations/copilot/.github/copilot-instructions.md.ww33Ly +0 -91
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env bash
2
+ # okf-render-views.sh — render the demoted views from an OKF taxonomy bundle.
3
+ #
4
+ # The wiki/ bundle is the source of truth. This produces the two derived,
5
+ # human-facing views deterministically (so they never drift from the bundle and
6
+ # carry zero extra maintenance):
7
+ # 1. architecture.md — a single linear concatenation of every concept page,
8
+ # frontmatter stripped, in canonical section order, with a banner + TOC.
9
+ # This is the onboarding "read one doc" view (demoted, not deleted).
10
+ # 2. Concept Map — a routing table injected between the
11
+ # <!-- CONCEPT-MAP:START --> / <!-- CONCEPT-MAP:END --> markers in
12
+ # wiki/index.md (and optionally another index-root file).
13
+ #
14
+ # Usage:
15
+ # okf-render-views.sh <BUNDLE_DIR> --arch-out <FILE> [--concept-map-into <FILE>]
16
+ #
17
+ # BUNDLE_DIR is the wiki/ directory. Exit 0 ok, 1 error, 2 bundle not found.
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ # shellcheck source=scripts/tools/_lib.sh
22
+ source "$SCRIPT_DIR/_lib.sh"
23
+
24
+ BUNDLE=""
25
+ ARCH_OUT=""
26
+ WEB_OUT=""
27
+ CMAP_INTO=()
28
+
29
+ usage() {
30
+ cat <<'EOF'
31
+ okf-render-views.sh — render architecture.md + Concept Map + HTML viewer from an OKF bundle.
32
+
33
+ Usage:
34
+ okf-render-views.sh <BUNDLE_DIR> [--arch-out FILE] [--concept-map-into FILE]... [--web FILE]
35
+
36
+ Flags:
37
+ --arch-out FILE Write the rendered linear architecture.md here.
38
+ --concept-map-into FILE Inject the Concept Map between the CONCEPT-MAP markers
39
+ in FILE (repeatable: e.g. wiki/index.md and ai-context.md).
40
+ --web FILE Write a self-contained, offline HTML viewer (single file:
41
+ all pages inlined, built-in markdown renderer, sidebar +
42
+ search). Double-click to open — no server, no internet.
43
+ --help Show this help.
44
+
45
+ Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found.
46
+ EOF
47
+ }
48
+
49
+ while [[ $# -gt 0 ]]; do
50
+ case "$1" in
51
+ --arch-out) ARCH_OUT="$2"; shift 2;;
52
+ --concept-map-into) CMAP_INTO+=("$2"); shift 2;;
53
+ --web) WEB_OUT="$2"; shift 2;;
54
+ --help|-h) usage; exit 0;;
55
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
56
+ *)
57
+ if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi
58
+ shift
59
+ ;;
60
+ esac
61
+ done
62
+
63
+ [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
64
+ [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
65
+ BUNDLE="${BUNDLE%/}"
66
+
67
+ # Canonical section order for the linear render. Sections not present are skipped.
68
+ SECTIONS=(overview systems features reference entrypoints)
69
+
70
+ # Emit bundle-relative page paths in canonical order: for each section, its
71
+ # index.md first, then the rest alphabetically. Pages outside these sections
72
+ # (e.g. log.md, the bundle root index.md) are excluded from the linear view.
73
+ ordered_pages() {
74
+ local sec dir f
75
+ for sec in "${SECTIONS[@]}"; do
76
+ dir="$BUNDLE/$sec"
77
+ [[ -d "$dir" ]] || continue
78
+ [[ -f "$dir/index.md" ]] && echo "$sec/index.md"
79
+ while IFS= read -r f; do
80
+ [[ "$(basename "$f")" == "index.md" ]] && continue
81
+ echo "$sec/${f##*/}"
82
+ done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | sort)
83
+ done
84
+ }
85
+
86
+ # Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
87
+ strip_frontmatter() {
88
+ awk '
89
+ NR==1 && /^---$/ { fm=1; next }
90
+ fm && /^---$/ { fm=0; next }
91
+ !fm { print }
92
+ ' "$1"
93
+ }
94
+
95
+ # --- 1. Render architecture.md ---
96
+ render_architecture() {
97
+ local out="$1"
98
+ local tmp; tmp="$(mktemp)"
99
+ {
100
+ echo "---"
101
+ echo "generated_by: \"draft:init (okf-render-views.sh)\""
102
+ echo "view: rendered"
103
+ echo "source_of_truth: \"wiki/\""
104
+ echo "---"
105
+ echo ""
106
+ echo "# Architecture (Rendered View)"
107
+ echo ""
108
+ echo "> **Generated** from the \`wiki/\` OKF bundle — do not edit by hand."
109
+ echo "> The bundle is the source of truth; this is the single-document linear"
110
+ echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`."
111
+ echo ""
112
+ echo "## Contents"
113
+ echo ""
114
+ # TOC from page titles.
115
+ local rel title sec last_sec=""
116
+ while IFS= read -r rel; do
117
+ [[ -z "$rel" ]] && continue
118
+ sec="${rel%%/*}"
119
+ if [[ "$sec" != "$last_sec" ]]; then
120
+ echo "- **${sec}/**"
121
+ last_sec="$sec"
122
+ fi
123
+ title="$(get_yaml_field "$BUNDLE/$rel" title)"
124
+ [[ -n "$title" ]] || title="$rel"
125
+ local anchor; anchor="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')"
126
+ anchor="${anchor#-}"; anchor="${anchor%-}"
127
+ echo " - [${title}](#${anchor})"
128
+ done < <(ordered_pages)
129
+ echo ""
130
+ # Body: each page, frontmatter stripped.
131
+ while IFS= read -r rel; do
132
+ [[ -z "$rel" ]] && continue
133
+ echo ""
134
+ echo "---"
135
+ echo ""
136
+ strip_frontmatter "$BUNDLE/$rel"
137
+ done < <(ordered_pages)
138
+ } >"$tmp"
139
+ mv "$tmp" "$out"
140
+ echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
141
+ }
142
+
143
+ # --- 2. Build the Concept Map table (stdout) ---
144
+ build_concept_map() {
145
+ echo "| Concept | Type | Open it when… |"
146
+ echo "|---------|------|---------------|"
147
+ local rel type title desc
148
+ while IFS= read -r -d '' page; do
149
+ rel="${page#"$BUNDLE/"}"
150
+ [[ "$(basename "$rel")" == "index.md" ]] && continue
151
+ type="$(get_yaml_field "$page" type)"
152
+ [[ -n "$type" ]] || continue
153
+ title="$(get_yaml_field "$page" title)"
154
+ [[ -n "$title" ]] || title="$rel"
155
+ # description may be a folded (>) block — take the first non-empty body line.
156
+ desc="$(awk '
157
+ NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
158
+ fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
159
+ fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
160
+ ' "$page")"
161
+ echo "| [${title}](${rel}) | ${type} | ${desc} |"
162
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
163
+ }
164
+
165
+ # Inject the Concept Map between markers in a target file (path may be relative
166
+ # to BUNDLE: links in the map are bundle-relative, so the target should resolve
167
+ # them — wiki/index.md works directly; an index root above wiki/ should prefix).
168
+ inject_concept_map() {
169
+ local target="$1" map="$2"
170
+ [[ -f "$target" ]] || { echo "WARN: concept-map target not found: $target" >&2; return 0; }
171
+ if ! grep -q 'CONCEPT-MAP:START' "$target" || ! grep -q 'CONCEPT-MAP:END' "$target"; then
172
+ echo "WARN: $target has no CONCEPT-MAP markers — skipping injection" >&2
173
+ return 0
174
+ fi
175
+ local tmp; tmp="$(mktemp)"
176
+ awk -v mapfile="$map" '
177
+ /<!-- CONCEPT-MAP:START -->/ { print; while ((getline line < mapfile) > 0) print line; close(mapfile); skip=1; next }
178
+ /<!-- CONCEPT-MAP:END -->/ { skip=0 }
179
+ !skip { print }
180
+ ' "$target" >"$tmp"
181
+ mv "$tmp" "$target"
182
+ echo "injected Concept Map → $target"
183
+ }
184
+
185
+ # --- 3. Render a self-contained offline HTML viewer (single file) ---
186
+ # All pages are inlined as JSON; a small built-in markdown renderer draws them in
187
+ # the browser. No server, no internet, no CDN. jq encodes page content safely
188
+ # (and we neutralize any literal </ so embedded "</script>" can't break parsing).
189
+ render_web() {
190
+ local out="$1"
191
+ command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; }
192
+ local tmp; tmp="$(mktemp)"
193
+
194
+ cat >"$tmp" <<'HTML_HEAD'
195
+ <!doctype html>
196
+ <html lang="en">
197
+ <head>
198
+ <meta charset="utf-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1">
200
+ <title>Knowledge Bundle</title>
201
+ <style>
202
+ :root { --bg:#0f1115; --panel:#161a22; --ink:#d7dce5; --muted:#8a93a6; --accent:#6ea8fe; --border:#262c38; --code:#1b2030; }
203
+ * { box-sizing: border-box; }
204
+ body { margin:0; font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:var(--ink); background:var(--bg); }
205
+ #app { display:flex; min-height:100vh; }
206
+ #side { width:300px; flex:0 0 300px; background:var(--panel); border-right:1px solid var(--border); height:100vh; overflow:auto; position:sticky; top:0; padding:14px; }
207
+ #side h1 { font-size:14px; margin:0 0 10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
208
+ #search { width:100%; padding:8px 10px; margin-bottom:12px; background:var(--code); border:1px solid var(--border); border-radius:6px; color:var(--ink); }
209
+ .sec { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:14px 0 4px; }
210
+ .nav a { display:block; padding:4px 8px; color:var(--ink); text-decoration:none; border-radius:5px; font-size:13.5px; }
211
+ .nav a:hover { background:var(--code); }
212
+ .nav a.active { background:var(--accent); color:#0b0e14; }
213
+ .nav a .ty { float:right; font-size:10px; color:var(--muted); }
214
+ .nav a.active .ty { color:#0b0e14; }
215
+ #main { flex:1; max-width:900px; padding:32px 44px; }
216
+ #content h1,#content h2,#content h3 { line-height:1.25; }
217
+ #content h1 { font-size:28px; border-bottom:1px solid var(--border); padding-bottom:8px; }
218
+ #content a { color:var(--accent); }
219
+ #content code { background:var(--code); padding:2px 5px; border-radius:4px; font-size:90%; }
220
+ #content pre { background:var(--code); border:1px solid var(--border); border-radius:8px; padding:12px 14px; overflow:auto; }
221
+ #content pre code { background:none; padding:0; }
222
+ #content pre.mermaid-src { border-left:3px solid var(--accent); }
223
+ #content pre.mermaid-src::before { content:"⬡ Mermaid diagram (source)"; display:block; color:var(--muted); font-size:11px; margin-bottom:6px; }
224
+ #content table { border-collapse:collapse; width:100%; margin:14px 0; font-size:13.5px; }
225
+ #content th,#content td { border:1px solid var(--border); padding:6px 9px; text-align:left; vertical-align:top; }
226
+ #content th { background:var(--code); }
227
+ #content blockquote { border-left:3px solid var(--border); margin:12px 0; padding:2px 14px; color:var(--muted); }
228
+ #content hr { border:none; border-top:1px solid var(--border); margin:22px 0; }
229
+ .crumb { color:var(--muted); font-size:12px; margin-bottom:8px; }
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div id="app">
234
+ <nav id="side">
235
+ <h1>Knowledge Bundle</h1>
236
+ <input id="search" placeholder="Search…" autocomplete="off">
237
+ <div id="nav" class="nav"></div>
238
+ </nav>
239
+ <main id="main"><div id="content"></div></main>
240
+ </div>
241
+ <script>
242
+ HTML_HEAD
243
+
244
+ # Inline page data: PAGES[rel] = {title, type, md}, plus ORDER (index first).
245
+ {
246
+ echo "const PAGES = {"
247
+ while IFS= read -r -d '' page; do
248
+ local rel title type
249
+ rel="${page#"$BUNDLE/"}"
250
+ title="$(get_yaml_field "$page" title)"; [[ -n "$title" ]] || title="$rel"
251
+ type="$(get_yaml_field "$page" type)"
252
+ printf '%s: {"title": %s, "type": %s, "md": %s},\n' \
253
+ "$(jq -Rn --arg v "$rel" '$v')" \
254
+ "$(jq -Rn --arg v "$title" '$v')" \
255
+ "$(jq -Rn --arg v "$type" '$v')" \
256
+ "$(strip_frontmatter "$page" | jq -Rs . | sed 's#</#<\\/#g')"
257
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
258
+ echo "};"
259
+ # ORDER: bundle root index.md first, then everything else sorted.
260
+ echo "const ORDER = Object.keys(PAGES).sort(function(a,b){"
261
+ echo " if(a==='index.md') return -1; if(b==='index.md') return 1;"
262
+ echo " return a<b?-1:a>b?1:0; });"
263
+ } >>"$tmp"
264
+
265
+ cat >>"$tmp" <<'HTML_TAIL'
266
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
267
+ function resolve(base, href){
268
+ if(/^[a-z]+:\/\//.test(href)||href[0]==='#') return href;
269
+ var dir = base.indexOf('/')<0 ? '' : base.replace(/\/[^/]*$/,'');
270
+ var parts = (dir? dir.split('/'):[]).concat(href.split('/')), out=[];
271
+ for(var i=0;i<parts.length;i++){ var p=parts[i];
272
+ if(p==='..') out.pop(); else if(p!=='.'&&p!=='') out.push(p); }
273
+ return out.join('/');
274
+ }
275
+ function inline(s, base){
276
+ s = s.replace(/`([^`]+)`/g, function(m,c){return '<code>'+esc(c)+'</code>';});
277
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(m,t,u){
278
+ if(/^[a-z]+:\/\//.test(u)) return '<a href="'+u+'" target="_blank" rel="noopener">'+t+'</a>';
279
+ var key=resolve(base,u);
280
+ if(PAGES[key]) return '<a href="#'+key+'" data-nav="'+key+'">'+t+'</a>';
281
+ return '<span title="'+esc(u)+'">'+t+'</span>';
282
+ });
283
+ s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
284
+ s = s.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>');
285
+ return s;
286
+ }
287
+ function render(md, base){
288
+ // Pull fenced code blocks out first so their contents aren't block-parsed.
289
+ var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
290
+ var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
291
+ blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
292
+ return 'BLOCK'+(blocks.length-1)+'';
293
+ });
294
+ var lines=src.split('\n'), out='', i=0, list='', tbl=[];
295
+ function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
296
+ function flushTbl(){
297
+ if(!tbl.length) return;
298
+ var rows=tbl.filter(function(r){return !/^\s*\|?[\s:|-]+\|?\s*$/.test(r);});
299
+ out+='<table>';
300
+ rows.forEach(function(r,ri){
301
+ var cells=r.replace(/^\||\|$/g,'').split('|');
302
+ out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(c.trim(),base)+'</'+t+'>';}).join('')+'</tr>';
303
+ });
304
+ out+='</table>'; tbl=[];
305
+ }
306
+ for(;i<lines.length;i++){
307
+ var ln=lines[i];
308
+ if(/^\s*\|.*\|\s*$/.test(ln)){ closeList(); tbl.push(ln); continue; } else flushTbl();
309
+ var h=ln.match(/^(#{1,6})\s+(.*)$/);
310
+ if(h){ closeList(); out+='<h'+h[1].length+'>'+inline(esc(h[2]),base)+'</h'+h[1].length+'>'; continue; }
311
+ if(/^\s*---\s*$/.test(ln)){ closeList(); out+='<hr>'; continue; }
312
+ if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
313
+ var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
314
+ if(li){ var want=/^\d/.test(li[1])?'ol':'ul'; if(list!==want){ closeList(); list=want; out+='<'+want+'>'; } out+='<li>'+inline(esc(li[2]),base)+'</li>'; continue; }
315
+ var b=ln.match(/^BLOCK(\d+)$/);
316
+ if(b){ closeList(); out+=blocks[+b[1]]; continue; }
317
+ if(/^\s*$/.test(ln)){ closeList(); continue; }
318
+ closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
319
+ }
320
+ flushTbl(); closeList();
321
+ return out;
322
+ }
323
+ var navEl=document.getElementById('nav'), contentEl=document.getElementById('content');
324
+ function section(k){ return k.indexOf('/')<0 ? '(root)' : k.split('/')[0]; }
325
+ function buildNav(filter){
326
+ navEl.innerHTML=''; var lastSec=null;
327
+ ORDER.forEach(function(k){
328
+ var p=PAGES[k];
329
+ if(filter && (p.title+' '+p.md).toLowerCase().indexOf(filter)<0) return;
330
+ var sec=section(k);
331
+ if(sec!==lastSec){ var s=document.createElement('div'); s.className='sec'; s.textContent=sec; navEl.appendChild(s); lastSec=sec; }
332
+ var a=document.createElement('a'); a.href='#'+k; a.dataset.nav=k;
333
+ a.innerHTML=esc(p.title)+(p.type?'<span class="ty">'+esc(p.type)+'</span>':'');
334
+ navEl.appendChild(a);
335
+ });
336
+ }
337
+ function show(k){
338
+ var p=PAGES[k]; if(!p){ k=ORDER[0]; p=PAGES[k]; }
339
+ contentEl.innerHTML='<div class="crumb">'+esc(k)+'</div>'+render(p.md,k);
340
+ document.querySelectorAll('#nav a').forEach(function(a){ a.classList.toggle('active', a.dataset.nav===k); });
341
+ if(location.hash.slice(1)!==k) history.replaceState(null,'','#'+k);
342
+ contentEl.parentElement.scrollTop=0; window.scrollTo(0,0);
343
+ }
344
+ document.addEventListener('click',function(e){ var a=e.target.closest('[data-nav]'); if(a){ e.preventDefault(); show(a.dataset.nav); } });
345
+ document.getElementById('search').addEventListener('input',function(e){ buildNav(e.target.value.toLowerCase().trim()); });
346
+ window.addEventListener('hashchange',function(){ var k=decodeURIComponent(location.hash.slice(1)); if(PAGES[k]) show(k); });
347
+ buildNav('');
348
+ show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
349
+ </script>
350
+ </body>
351
+ </html>
352
+ HTML_TAIL
353
+
354
+ mkdir -p "$(dirname "$out")"
355
+ mv "$tmp" "$out"
356
+ echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
357
+ }
358
+
359
+ [[ -n "$ARCH_OUT" ]] && render_architecture "$ARCH_OUT"
360
+
361
+ if [[ ${#CMAP_INTO[@]} -gt 0 ]]; then
362
+ MAP_TMP="$(mktemp)"
363
+ build_concept_map >"$MAP_TMP"
364
+ for tgt in "${CMAP_INTO[@]}"; do
365
+ inject_concept_map "$tgt" "$MAP_TMP"
366
+ done
367
+ rm -f "$MAP_TMP"
368
+ fi
369
+
370
+ [[ -n "$WEB_OUT" ]] && render_web "$WEB_OUT"
371
+
372
+ [[ -n "$ARCH_OUT" || -n "$WEB_OUT" || ${#CMAP_INTO[@]} -gt 0 ]] || { echo "ERROR: nothing to do (pass --arch-out, --web, and/or --concept-map-into)" >&2; exit 1; }
373
+ exit 0
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bash
2
+ # okf-validate.sh — validate an OKF (Open Knowledge Format) taxonomy bundle.
3
+ #
4
+ # This is the deterministic ground-truth verifier for the `/draft:init` OKF
5
+ # emitter (DRAFT_INIT_MODE=okf). It fails the build on dangling cross-links,
6
+ # missing/invalid frontmatter, and an incomplete path→concept index, so a
7
+ # page-by-page generation pass cannot ship a structurally broken bundle.
8
+ #
9
+ # Checks:
10
+ # 1. BUNDLE_DIR exists and contains a root index.md.
11
+ # 2. Every concept page (any *.md whose frontmatter declares `type:`) carries
12
+ # all required OKF frontmatter keys: type, title, description, resource.
13
+ # 3. Every declared `type` is in the frozen code-repo vocabulary (§4 of HLD).
14
+ # 4. Every relative markdown cross-link ( ](path.md) ) resolves to a file that
15
+ # exists inside the bundle. External (http/https/mailto) and pure-anchor
16
+ # (#frag) links are ignored.
17
+ # 5. (optional) --path-index FILE: every concept page referenced by the
18
+ # path→concept index exists in the bundle (no dangle, no stale rename).
19
+ #
20
+ # Usage:
21
+ # scripts/tools/okf-validate.sh <BUNDLE_DIR> [--path-index FILE] [--json]
22
+ #
23
+ # Exit codes: 0 valid, 1 invalid (diagnostics to stderr), 2 bundle not found.
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ # shellcheck source=scripts/tools/_lib.sh
28
+ source "$SCRIPT_DIR/_lib.sh"
29
+
30
+ # Frozen concept `type` vocabulary for code repos. Changing this churns every
31
+ # generated file, so it is versioned in the bundle (index.md: okf_types_version).
32
+ OKF_TYPES="Subsystem Module Feature Entrypoint API DataModel Dependency ADR Runbook"
33
+
34
+ BUNDLE=""
35
+ PATH_INDEX=""
36
+ JSON=0
37
+
38
+ usage() {
39
+ cat <<'EOF'
40
+ okf-validate.sh — validate an OKF taxonomy bundle (the /draft:init OKF emitter output).
41
+
42
+ Usage:
43
+ scripts/tools/okf-validate.sh <BUNDLE_DIR> [--path-index FILE] [--json]
44
+
45
+ Flags:
46
+ --path-index FILE Validate a path→concept index (JSON): every concept page it
47
+ names must exist in the bundle.
48
+ --json Emit a JSON summary instead of human diagnostics.
49
+ --help Show this help.
50
+
51
+ Exit 0 valid, 1 invalid, 2 bundle directory not found.
52
+ EOF
53
+ }
54
+
55
+ while [[ $# -gt 0 ]]; do
56
+ case "$1" in
57
+ --path-index) PATH_INDEX="$2"; shift 2;;
58
+ --json) JSON=1; shift;;
59
+ --help|-h) usage; exit 0;;
60
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
61
+ *)
62
+ if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"
63
+ else echo "Unexpected arg: $1" >&2; exit 1
64
+ fi
65
+ shift
66
+ ;;
67
+ esac
68
+ done
69
+
70
+ if [[ -z "$BUNDLE" ]]; then
71
+ usage >&2
72
+ exit 1
73
+ fi
74
+
75
+ if [[ ! -d "$BUNDLE" ]]; then
76
+ echo "ERROR: bundle directory not found: $BUNDLE" >&2
77
+ exit 2
78
+ fi
79
+
80
+ BUNDLE="${BUNDLE%/}"
81
+
82
+ ERRORS=()
83
+ PAGE_COUNT=0
84
+ CONCEPT_COUNT=0
85
+
86
+ add_error() { ERRORS+=("$1"); }
87
+
88
+ # Does the frozen vocabulary contain $1?
89
+ is_known_type() {
90
+ local t="$1"
91
+ [[ " $OKF_TYPES " == *" $t "* ]]
92
+ }
93
+
94
+ # --- 1. Root index ---
95
+ if [[ ! -f "$BUNDLE/index.md" ]]; then
96
+ add_error "missing bundle root: $BUNDLE/index.md"
97
+ fi
98
+
99
+ # --- 2/3. Per-page frontmatter + type vocabulary ---
100
+ while IFS= read -r -d '' page; do
101
+ PAGE_COUNT=$((PAGE_COUNT + 1))
102
+ rel="${page#"$BUNDLE/"}"
103
+
104
+ # A page is a "concept" only if its frontmatter declares a type.
105
+ type_val="$(get_yaml_field "$page" "type")"
106
+ [[ -z "$type_val" ]] && continue
107
+ CONCEPT_COUNT=$((CONCEPT_COUNT + 1))
108
+
109
+ for key in title description resource; do
110
+ if [[ -z "$(get_yaml_field "$page" "$key")" ]]; then
111
+ add_error "$rel: concept missing required frontmatter field '$key'"
112
+ fi
113
+ done
114
+
115
+ if ! is_known_type "$type_val"; then
116
+ add_error "$rel: unknown concept type '$type_val' (frozen vocab: $OKF_TYPES)"
117
+ fi
118
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
119
+
120
+ # --- 4. Cross-link resolution ---
121
+ # Scan every markdown page for relative links of the form ](target.md[#frag]).
122
+ # Resolve targets relative to the linking file's directory; flag dangles.
123
+ # Use a temp file (outside the bundle) because the scan runs in a pipeline
124
+ # subshell where add_error would not persist.
125
+ DANGLE_FILE="$(mktemp)"
126
+ trap 'rm -f "$DANGLE_FILE"' EXIT
127
+ while IFS= read -r -d '' page; do
128
+ pdir="$(dirname "$page")"
129
+ prel="${page#"$BUNDLE/"}"
130
+ # Extract link targets: text inside ]( ... ) up to a space or closing paren.
131
+ grep -oE '\]\([^) ]+\)' "$page" 2>/dev/null | sed -E 's/^\]\(//; s/\)$//' | while IFS= read -r target; do
132
+ [[ -z "$target" ]] && continue
133
+ # Skip external schemes and pure anchors.
134
+ case "$target" in
135
+ http://*|https://*|mailto:*|\#*) continue;;
136
+ esac
137
+ # Strip any #anchor and ?query.
138
+ target="${target%%#*}"
139
+ target="${target%%\?*}"
140
+ [[ -z "$target" ]] && continue
141
+ # Only resolve intra-bundle markdown/asset links (relative paths).
142
+ case "$target" in
143
+ /*) continue;; # absolute path — out of scope for bundle integrity
144
+ esac
145
+ resolved="$pdir/$target"
146
+ if [[ ! -e "$resolved" ]]; then
147
+ printf '%s\t%s\n' "$prel" "$target"
148
+ fi
149
+ done || true # inner pipeline returns non-zero at EOF; don't trip set -e
150
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z) >>"$DANGLE_FILE"
151
+
152
+ if [[ -s "$DANGLE_FILE" ]]; then
153
+ while IFS=$'\t' read -r prel target; do
154
+ add_error "$prel: dangling cross-link → '$target'"
155
+ done < "$DANGLE_FILE"
156
+ fi
157
+
158
+ # --- 5. path→concept index completeness (optional) ---
159
+ if [[ -n "$PATH_INDEX" ]]; then
160
+ if [[ ! -f "$PATH_INDEX" ]]; then
161
+ add_error "path-index not found: $PATH_INDEX"
162
+ else
163
+ # The index maps source path → array of concept page(s), bundle-relative:
164
+ # { "src/auth/login.go": ["systems/auth.md"], ... }
165
+ # Validate the array VALUES (the pages) only. Keys are source paths and may
166
+ # themselves end in .md (e.g. grounding to docs/INVARIANTS.md) — those are
167
+ # not bundle pages, so we extract strings *inside* the [ ... ] value arrays
168
+ # and ignore keys entirely. Each page must exist in the bundle.
169
+ while IFS= read -r ref; do
170
+ [[ -z "$ref" ]] && continue
171
+ if [[ ! -f "$BUNDLE/$ref" ]]; then
172
+ add_error "path-index references missing concept page: $ref"
173
+ fi
174
+ done < <(grep -oE '\[[^]]*\]' "$PATH_INDEX" 2>/dev/null \
175
+ | grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u)
176
+ fi
177
+ fi
178
+
179
+ # --- Report ---
180
+ if [[ $JSON -eq 1 ]]; then
181
+ valid=true
182
+ [[ ${#ERRORS[@]} -eq 0 ]] || valid=false
183
+ printf '{"valid":%s,"bundle":"%s","pages":%d,"concepts":%d,"errors":[' \
184
+ "$valid" "$(json_escape "$BUNDLE")" "$PAGE_COUNT" "$CONCEPT_COUNT"
185
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
186
+ for i in "${!ERRORS[@]}"; do
187
+ [[ $i -gt 0 ]] && printf ','
188
+ printf '"%s"' "$(json_escape "${ERRORS[$i]}")"
189
+ done
190
+ fi
191
+ printf ']}\n'
192
+ else
193
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
194
+ echo "OKF bundle INVALID: $BUNDLE ($PAGE_COUNT pages, $CONCEPT_COUNT concepts)" >&2
195
+ for e in "${ERRORS[@]}"; do
196
+ echo " - $e" >&2
197
+ done
198
+ else
199
+ echo "OKF bundle valid: $BUNDLE ($PAGE_COUNT pages, $CONCEPT_COUNT concepts)"
200
+ fi
201
+ fi
202
+
203
+ [[ ${#ERRORS[@]} -eq 0 ]] || exit 1
204
+ exit 0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # resolve-tools.sh — print the absolute path to Draft's bundled scripts/tools dir.
3
+ #
4
+ # Skills run with cwd = the user's project, and ${CLAUDE_PLUGIN_ROOT} is NOT exported
5
+ # into skill-driven Bash, so a bare `scripts/tools/foo.sh` invocation fails. This
6
+ # resolver finds the plugin's helper directory regardless of how Draft was installed.
7
+ # See core/shared/tool-resolver.md for the canonical procedure and the inline preamble
8
+ # skills embed (this script is the single source of truth for the resolution order).
9
+ #
10
+ # Usage:
11
+ # DRAFT_TOOLS="$(scripts/tools/resolve-tools.sh)" # prints the dir, exit 0 if found
12
+ # scripts/tools/resolve-tools.sh || echo "tools not found" # exit 1 if none exist
13
+ set -euo pipefail
14
+
15
+ case "${1:-}" in
16
+ -h|--help)
17
+ sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
18
+ exit 0
19
+ ;;
20
+ esac
21
+
22
+ newest() {
23
+ # Echo the lexically-newest existing match of a glob (by version sort), or nothing.
24
+ # shellcheck disable=SC2086
25
+ ls -d $1 2>/dev/null | sort -V | tail -1
26
+ }
27
+
28
+ resolve() {
29
+ local d
30
+
31
+ # 1. Explicit override (testing / pinned installs).
32
+ d="${DRAFT_PLUGIN_ROOT:-}/scripts/tools"
33
+ [ -n "${DRAFT_PLUGIN_ROOT:-}" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
34
+
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
+ # 2. Install marker written by `draft install` (authoritative).
40
+ local marker="$HOME/.cache/draft/plugin-root"
41
+ if [ -f "$marker" ]; then
42
+ d="$(cat "$marker" 2>/dev/null)/scripts/tools"
43
+ [ -d "$d" ] && { printf '%s' "$d"; return 0; }
44
+ fi
45
+
46
+ # 3. ${CLAUDE_PLUGIN_ROOT} — set in hook/MCP contexts; harmless to probe.
47
+ d="${CLAUDE_PLUGIN_ROOT:-}/scripts/tools"
48
+ [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
49
+
50
+ # 4. Claude Code's own registry (authoritative installPath; needs jq).
51
+ local reg="$HOME/.claude/plugins/installed_plugins.json"
52
+ if command -v jq >/dev/null 2>&1 && [ -f "$reg" ]; then
53
+ local ip
54
+ ip="$(jq -r '.plugins | to_entries[] | select(.key|startswith("draft@")) | .value[0].installPath' \
55
+ "$reg" 2>/dev/null | head -1)"
56
+ [ -n "$ip" ] && [ -d "$ip/scripts/tools" ] && { printf '%s' "$ip/scripts/tools"; return 0; }
57
+ fi
58
+
59
+ # 5. Newest cache install (glob).
60
+ d="$(newest "$HOME/.claude/plugins/cache/*/draft/*/scripts/tools")"
61
+ [ -n "$d" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
62
+
63
+ # 6. Marketplace clone.
64
+ d="$(newest "$HOME/.claude/plugins/marketplaces/*draft*/scripts/tools")"
65
+ [ -n "$d" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
66
+
67
+ # 7. Cursor local install.
68
+ d="$HOME/.cursor/plugins/local/draft/scripts/tools"
69
+ [ -d "$d" ] && { printf '%s' "$d"; return 0; }
70
+
71
+ # 8. Dev / dogfooding (running inside the draft repo itself).
72
+ d="$PWD/scripts/tools"
73
+ [ -d "$d" ] && { printf '%s' "$d"; return 0; }
74
+
75
+ return 1
76
+ }
77
+
78
+ resolve
@@ -54,8 +54,9 @@ Check for arguments:
54
54
  If argument is `list`:
55
55
  1. Prefer the deterministic `adr-index.sh` wrapper for the listing — it returns a structured JSON `{adrs:[{id,title,date,status,path,related_tracks}]}` derived from each ADR's frontmatter. Resolve via the canonical tool resolver (see [core/shared/tool-resolver.md](../../core/shared/tool-resolver.md)):
56
56
  ```bash
57
- DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$HOME/.claude/plugins/draft}/scripts/tools"
58
- [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$HOME/.cursor/plugins/local/draft/scripts/tools"
57
+ DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
58
+ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"
59
+ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"
59
60
  [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$PWD/scripts/tools"
60
61
  if [ -x "$DRAFT_TOOLS/adr-index.sh" ]; then
61
62
  bash "$DRAFT_TOOLS/adr-index.sh" --root draft/adrs