@drafthq/draft 3.5.3 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) 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/hosts/cursor.js +3 -0
  7. package/cli/src/installer.js +8 -4
  8. package/cli/src/lib/cursor-registry.js +10 -0
  9. package/cli/src/lib/fsx.js +3 -0
  10. package/cli/src/lib/marker.js +8 -3
  11. package/core/agents/architect.md +21 -9
  12. package/core/agents/debugger.md +5 -0
  13. package/core/agents/ops.md +7 -2
  14. package/core/agents/planner.md +7 -0
  15. package/core/agents/rca.md +14 -3
  16. package/core/agents/reviewer.md +1 -0
  17. package/core/agents/writer.md +6 -0
  18. package/core/knowledge-base.md +22 -3
  19. package/core/methodology.md +30 -5
  20. package/core/shared/condensation.md +28 -18
  21. package/core/shared/context-verify.md +3 -1
  22. package/core/shared/cross-skill-dispatch.md +7 -3
  23. package/core/shared/draft-context-loading.md +3 -0
  24. package/core/shared/git-report-metadata.md +2 -1
  25. package/core/shared/graph-query.md +24 -26
  26. package/core/shared/graph-usage-report.md +1 -1
  27. package/core/shared/jira-sync.md +10 -6
  28. package/core/shared/okf-retrieval.md +1 -1
  29. package/core/shared/parallel-analysis.md +14 -7
  30. package/core/shared/pattern-learning.md +2 -0
  31. package/core/shared/template-contract.md +1 -1
  32. package/core/shared/tool-resolver.md +20 -15
  33. package/core/shared/vcs-commands.md +5 -2
  34. package/core/templates/ai-context.md +3 -2
  35. package/core/templates/ai-profile.md +7 -0
  36. package/core/templates/architecture.md +6 -2
  37. package/core/templates/discovery.md +1 -1
  38. package/core/templates/guardrails.md +9 -0
  39. package/core/templates/hld.md +5 -4
  40. package/core/templates/intake-questions.md +45 -0
  41. package/core/templates/jira.md +11 -2
  42. package/core/templates/lld.md +8 -5
  43. package/core/templates/okf/concept.md +0 -1
  44. package/core/templates/plan.md +1 -1
  45. package/core/templates/product.md +11 -2
  46. package/core/templates/rca.md +6 -0
  47. package/core/templates/service-index.md +1 -0
  48. package/core/templates/spec.md +15 -0
  49. package/core/templates/tech-stack.md +12 -0
  50. package/core/templates/workflow.md +20 -0
  51. package/integrations/agents/AGENTS.md +1449 -817
  52. package/integrations/copilot/.github/copilot-instructions.md +1449 -817
  53. package/package.json +1 -1
  54. package/scripts/fetch-memory-engine.sh +25 -7
  55. package/scripts/lib.sh +90 -6
  56. package/scripts/tools/_graph_queries.sh +22 -8
  57. package/scripts/tools/_lib.sh +81 -11
  58. package/scripts/tools/adr-index.sh +1 -1
  59. package/scripts/tools/check-repo-size.sh +125 -0
  60. package/scripts/tools/check-skill-line-caps.sh +3 -4
  61. package/scripts/tools/check-template-noop.sh +0 -1
  62. package/scripts/tools/check-track-hygiene.sh +41 -21
  63. package/scripts/tools/classify-files.sh +3 -3
  64. package/scripts/tools/cycle-detect.sh +18 -17
  65. package/scripts/tools/detect-test-framework.sh +1 -1
  66. package/scripts/tools/emit-skill-metrics.sh +7 -2
  67. package/scripts/tools/fix-whitespace.sh +3 -0
  68. package/scripts/tools/freshness-check.sh +2 -2
  69. package/scripts/tools/git-metadata.sh +12 -6
  70. package/scripts/tools/graph-arch.sh +5 -11
  71. package/scripts/tools/graph-callers.sh +10 -15
  72. package/scripts/tools/graph-deps.sh +3 -10
  73. package/scripts/tools/graph-errors.sh +4 -11
  74. package/scripts/tools/graph-hierarchy.sh +4 -11
  75. package/scripts/tools/graph-impact.sh +21 -19
  76. package/scripts/tools/graph-init.sh +13 -9
  77. package/scripts/tools/graph-preflight.sh +4 -1
  78. package/scripts/tools/graph-query.sh +74 -18
  79. package/scripts/tools/graph-risk.sh +3 -10
  80. package/scripts/tools/graph-search.sh +4 -11
  81. package/scripts/tools/graph-snapshot.sh +38 -9
  82. package/scripts/tools/graph-snippet.sh +3 -10
  83. package/scripts/tools/graph-tests.sh +3 -10
  84. package/scripts/tools/graph-traces.sh +3 -10
  85. package/scripts/tools/hotspot-rank.sh +6 -12
  86. package/scripts/tools/install-smoke-test.sh +223 -0
  87. package/scripts/tools/mermaid-from-graph.sh +5 -11
  88. package/scripts/tools/migrate-track-frontmatter.sh +11 -10
  89. package/scripts/tools/okf-coverage-check.sh +8 -7
  90. package/scripts/tools/okf-emit-catalog.sh +256 -0
  91. package/scripts/tools/okf-fix-links.sh +251 -0
  92. package/scripts/tools/okf-plan-concepts.sh +242 -14
  93. package/scripts/tools/okf-render-views.sh +63 -21
  94. package/scripts/tools/okf-validate-all.sh +20 -9
  95. package/scripts/tools/okf-validate-quality.sh +15 -10
  96. package/scripts/tools/okf-validate.sh +20 -6
  97. package/scripts/tools/parse-git-log.sh +23 -5
  98. package/scripts/tools/parse-reports.sh +1 -1
  99. package/scripts/tools/render-track.sh +1 -1
  100. package/scripts/tools/resolve-tools.sh +24 -8
  101. package/scripts/tools/run-coverage.sh +1 -1
  102. package/scripts/tools/scan-markers.sh +4 -4
  103. package/scripts/tools/validate-frontmatter.sh +2 -2
  104. package/scripts/tools/verify-citations.sh +3 -4
  105. package/scripts/tools/verify-doc-anchors.sh +14 -34
  106. package/scripts/tools/verify-graph-binary.sh +15 -5
  107. package/skills/GRAPH.md +21 -10
  108. package/skills/adr/SKILL.md +25 -7
  109. package/skills/assist-review/SKILL.md +2 -1
  110. package/skills/bughunt/SKILL.md +55 -12
  111. package/skills/bughunt/references/regression-tests.md +14 -4
  112. package/skills/change/SKILL.md +19 -9
  113. package/skills/coverage/SKILL.md +28 -12
  114. package/skills/debug/SKILL.md +13 -3
  115. package/skills/decompose/SKILL.md +42 -12
  116. package/skills/deep-review/SKILL.md +19 -6
  117. package/skills/deploy-checklist/SKILL.md +10 -3
  118. package/skills/documentation/SKILL.md +12 -1
  119. package/skills/draft/SKILL.md +40 -29
  120. package/skills/draft/context-files.md +8 -1
  121. package/skills/draft/quality-guide.md +10 -2
  122. package/skills/graph/SKILL.md +2 -2
  123. package/skills/impact/SKILL.md +4 -2
  124. package/skills/implement/SKILL.md +62 -20
  125. package/skills/incident-response/SKILL.md +5 -2
  126. package/skills/init/SKILL.md +112 -30
  127. package/skills/init/references/architecture-spec.md +32 -8
  128. package/skills/init/references/okf-emitter.md +42 -78
  129. package/skills/jira/SKILL.md +33 -13
  130. package/skills/jira/references/review.md +61 -19
  131. package/skills/learn/SKILL.md +30 -7
  132. package/skills/new-track/SKILL.md +73 -18
  133. package/skills/quick-review/SKILL.md +8 -3
  134. package/skills/revert/SKILL.md +11 -5
  135. package/skills/review/SKILL.md +126 -27
  136. package/skills/review/references/zero-setup-mode.md +117 -0
  137. package/skills/standup/SKILL.md +10 -2
  138. package/skills/status/SKILL.md +7 -7
  139. package/skills/tech-debt/SKILL.md +9 -3
  140. package/skills/testing-strategy/SKILL.md +10 -2
  141. package/skills/tour/SKILL.md +2 -1
  142. package/skills/upload/SKILL.md +4 -3
  143. package/core/templates/track-architecture.md +0 -311
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drafthq/draft",
3
- "version": "3.5.3",
3
+ "version": "3.7.1",
4
4
  "description": "Context-Driven Development for AI coding agents — install Draft into Claude Code, Cursor, Codex, or opencode.",
5
5
  "bin": {
6
6
  "draft": "cli/bin/draft.js"
@@ -8,7 +8,7 @@
8
8
  # which scripts/tools/_lib.sh:find_memory_bin resolves.
9
9
  #
10
10
  # Pinned by default for reproducibility; override with CMM_VERSION (a tag, e.g.
11
- # "v0.8.1", or "latest").
11
+ # "v0.9.0", or "latest").
12
12
  #
13
13
  # Usage:
14
14
  # scripts/fetch-memory-engine.sh [--dest DIR] [--force]
@@ -16,17 +16,23 @@
16
16
  # Env:
17
17
  # CMM_VERSION Release tag to fetch (default: pinned DEFAULT_VERSION).
18
18
  # CMM_DOWNLOAD_URL Override the release base URL (testing).
19
+ # DRAFT_STRICT_VERIFY 1 = refuse to install when the checksum cannot be
20
+ # verified (missing checksums.txt or unlisted archive).
21
+ # Default is warn-and-continue, which keeps installs working
22
+ # on releases that ship no checksums file — acceptable for
23
+ # individuals, not for anyone who must attest to what runs
24
+ # on their machine.
19
25
  #
20
26
  # Exit codes: 0 installed/already-present, 1 invocation error, 2 fetch/verify failure.
21
27
  set -euo pipefail
22
28
 
23
29
  REPO="DeusData/codebase-memory-mcp"
24
- DEFAULT_VERSION="v0.8.1" # pinned; bump deliberately. NOTE: tag must carry the leading "v" AND have published assets (0.7.0 had none → 404).
30
+ DEFAULT_VERSION="v0.9.0" # pinned; bump deliberately. NOTE: tag must carry the leading "v" AND have published assets (0.7.0 had none → 404).
25
31
  VERSION="${CMM_VERSION:-$DEFAULT_VERSION}"
26
32
  DEST="$HOME/.cache/draft/bin"
27
33
  FORCE=0
28
34
 
29
- usage() { sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; }
35
+ usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; }
30
36
 
31
37
  while [[ $# -gt 0 ]]; do
32
38
  case "$1" in
@@ -79,7 +85,19 @@ if ! curl -fSL --max-time 300 -o "$TMP/$ARCHIVE" "$BASE/$ARCHIVE"; then
79
85
  exit 2
80
86
  fi
81
87
 
82
- # --- Verify checksum (best-effort: hard-fail only if the archive is listed) ---
88
+ # --- Verify checksum ---
89
+ # A mismatch is always fatal. An *absent* checksum is fatal only under
90
+ # DRAFT_STRICT_VERIFY=1 — otherwise it warns, so that a release without a
91
+ # checksums.txt does not brick the install for everyone.
92
+ STRICT="${DRAFT_STRICT_VERIFY:-0}"
93
+ unverified() {
94
+ if [[ "$STRICT" == "1" ]]; then
95
+ echo "error: $1 (DRAFT_STRICT_VERIFY=1 refuses unverified binaries)" >&2
96
+ exit 2
97
+ fi
98
+ echo " warning: $1 — skipping verification (set DRAFT_STRICT_VERIFY=1 to make this fatal)" >&2
99
+ }
100
+
83
101
  if curl -fsSL --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev/null; then
84
102
  expected="$(grep " $ARCHIVE\$" "$TMP/checksums.txt" 2>/dev/null | awk '{print $1}' | head -1 || true)"
85
103
  if [[ -n "$expected" ]]; then
@@ -92,12 +110,12 @@ if curl -fsSL --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev
92
110
  echo "error: checksum mismatch for $ARCHIVE (expected $expected, got $actual)" >&2
93
111
  exit 2
94
112
  fi
95
- echo " checksum OK"
113
+ echo " checksum OK ($expected)"
96
114
  else
97
- echo " warning: $ARCHIVE not found in checksums.txt — skipping verification" >&2
115
+ unverified "$ARCHIVE not listed in checksums.txt"
98
116
  fi
99
117
  else
100
- echo " warning: checksums.txt unavailable skipping verification" >&2
118
+ unverified "checksums.txt unavailable at $BASE"
101
119
  fi
102
120
 
103
121
  # --- Extract and install ---
package/scripts/lib.sh CHANGED
@@ -2,21 +2,26 @@
2
2
  #
3
3
  # Shared validation library for Draft skill files.
4
4
  #
5
- # Sourced by test suites. Defines constants and validation functions
6
- # but does not execute anything when sourced.
5
+ # Sourced by test suites and the build script. Defines constants and validation
6
+ # functions only no side effects at source time, shell options included. Every
7
+ # consumer sets its own `set -euo pipefail`; doing it here would silently change
8
+ # the behaviour of whatever sourced us.
7
9
  #
8
10
  # Usage:
9
11
  # source scripts/lib.sh
10
12
  #
11
13
 
12
- set -euo pipefail
13
-
14
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
  ROOT_DIR="$(dirname "$SCRIPT_DIR")"
16
16
  SKILLS_DIR="$ROOT_DIR/skills"
17
17
  CORE_DIR="$ROOT_DIR/core"
18
18
  TOOLS_DIR="$ROOT_DIR/scripts/tools"
19
19
 
20
+ # Shared shell helpers (json_escape, apply_dest_mode, …). Side-effect free at
21
+ # source time, so this is safe for every consumer of lib.sh.
22
+ # shellcheck source=tools/_lib.sh
23
+ source "$TOOLS_DIR/_lib.sh"
24
+
20
25
  # ─────────────────────────────────────────────────────────
21
26
  # Skill ordering (canonical order for all references)
22
27
  # ─────────────────────────────────────────────────────────
@@ -57,6 +62,76 @@ SKILL_ORDER=(
57
62
  assist-review
58
63
  )
59
64
 
65
+ # ─────────────────────────────────────────────────────────
66
+ # Skill metadata: one row per skill — "<name>|<display header>|<copilot trigger>"
67
+ # The name and header never contain "|"; the trigger may (e.g. "[file|pr N]"),
68
+ # so lookups split on the FIRST TWO pipes only. Coverage against SKILL_ORDER is
69
+ # enforced by tests/test-trigger-functions.sh.
70
+ # ─────────────────────────────────────────────────────────
71
+
72
+ SKILL_META=(
73
+ 'draft|Draft Overview|"help" or "draft"'
74
+ 'init|Init Command|"init draft", "build the code graph", or "draft init [refresh] [--graph-only] [--module-only]"'
75
+ 'graph|Graph Command|"build graph", "refresh graph", or "draft graph [path]"'
76
+ 'new-track|New Track Command|"new feature" or "draft new-track <description>"'
77
+ 'decompose|Decompose Command|"break into modules" or "draft decompose"'
78
+ 'implement|Implement Command|"implement" or "draft implement"'
79
+ 'coverage|Coverage Command|"check coverage" or "draft coverage"'
80
+ 'deploy-checklist|Deploy Checklist Command|"deploy checklist" or "draft deploy-checklist [track <id>]"'
81
+ 'bughunt|Bug Hunt Command|"hunt bugs" or "draft bughunt [--track <id>]"'
82
+ 'review|Review Command|"review code" or "draft review [--track <id>] [--full]"'
83
+ 'upload|Upload Command|"upload for review" or "draft upload [track <id>]"'
84
+ 'plan|Plan Router|"plan feature" or "draft plan <intent>" (new-track, decompose, adr, tech-debt, change)'
85
+ 'ops|Ops Router|"ops deploy" or "draft ops <intent>" (deploy-checklist, incident, standup, status, revert)'
86
+ 'docs|Docs Router|"write docs" or "draft docs <intent>" (documentation)'
87
+ 'discover|Discover Router|"discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, index, etc.)'
88
+ 'jira|Jira Router|"jira preview", "jira create", or "jira review <ID>"'
89
+ 'integrations|Integrations Router|"integrations", "integrations jira-preview", or "integrations jira-create"'
90
+ 'quick-review|Quick Review Command|"quick review" or "draft quick-review [file|pr <number>]"'
91
+ 'deep-review|Deep Review Command|"deep review" or "draft deep-review [module]"'
92
+ 'testing-strategy|Testing Strategy Command|"test strategy" or "draft testing-strategy [track <id>|path]"'
93
+ 'learn|Learn Command|"learn patterns" or "draft learn [promote|migrate|path]"'
94
+ 'adr|ADR Command|"document decision" or "draft adr [title]"'
95
+ 'debug|Debug Command|"debug bug" or "draft debug [description|track <id>]"'
96
+ 'standup|Standup Command|"standup" or "draft standup [date|week|save]"'
97
+ 'tech-debt|Tech Debt Command|"tech debt" or "draft tech-debt [path|track <id>]"'
98
+ 'incident-response|Incident Response Command|"incident" or "draft incident-response [new|update|postmortem]"'
99
+ 'documentation|Documentation Command|"write docs" or "draft documentation [readme|runbook|api|onboarding]"'
100
+ 'status|Status Command|"status" or "draft status"'
101
+ 'revert|Revert Command|"revert" or "draft revert"'
102
+ 'change|Change Command|"handle change" or "draft change <description>"'
103
+ 'tour|Tour Command|"tour" or "draft tour"'
104
+ 'impact|Impact Command|"impact" or "draft impact"'
105
+ 'assist-review|Assist Review Command|"assist review" or "draft assist-review"'
106
+ )
107
+
108
+ # get_skill_header <skill> — display header for integration sections.
109
+ get_skill_header() {
110
+ local skill="$1" row rest
111
+ for row in "${SKILL_META[@]}"; do
112
+ if [[ "${row%%|*}" == "$skill" ]]; then
113
+ rest="${row#*|}"
114
+ printf '%s\n' "${rest%%|*}"
115
+ return 0
116
+ fi
117
+ done
118
+ printf '%s\n' "$(echo "${skill:0:1}" | tr '[:lower:]' '[:upper:]')${skill:1} Command"
119
+ }
120
+
121
+ # get_copilot_trigger <skill> — natural-language trigger for the Copilot header.
122
+ get_copilot_trigger() {
123
+ local skill="$1" row rest
124
+ for row in "${SKILL_META[@]}"; do
125
+ if [[ "${row%%|*}" == "$skill" ]]; then
126
+ rest="${row#*|}"
127
+ printf '%s\n' "${rest#*|}"
128
+ return 0
129
+ fi
130
+ done
131
+ printf '"draft %s"\n' "$skill"
132
+ }
133
+
134
+
60
135
  # ─────────────────────────────────────────────────────────
61
136
  # Core reference files (inlined by Claude plugin at runtime)
62
137
  # ─────────────────────────────────────────────────────────
@@ -91,7 +166,6 @@ CORE_FILES=(
91
166
  "templates/ai-context.md"
92
167
  "templates/ai-profile.md"
93
168
  "templates/architecture.md"
94
- "templates/track-architecture.md"
95
169
  "templates/jira.md"
96
170
  "templates/product.md"
97
171
  "templates/tech-stack.md"
@@ -181,6 +255,7 @@ TOOLS=(
181
255
  "validate-frontmatter.sh"
182
256
  # Foundations hygiene/verification tools
183
257
  "check-graph-usage-report.sh"
258
+ "check-repo-size.sh"
184
259
  "check-scope-conflicts.sh"
185
260
  "check-skill-line-caps.sh"
186
261
  "check-template-noop.sh"
@@ -188,6 +263,7 @@ TOOLS=(
188
263
  "diff-templates-vs-tracks.sh"
189
264
  "emit-skill-metrics.sh"
190
265
  "fix-whitespace.sh"
266
+ "install-smoke-test.sh"
191
267
  "migrate-track-frontmatter.sh"
192
268
  "render-track.sh"
193
269
  "verify-citations.sh"
@@ -202,6 +278,8 @@ TOOLS=(
202
278
  "okf-validate-quality.sh"
203
279
  "okf-coverage-check.sh"
204
280
  "okf-validate-all.sh"
281
+ "okf-emit-catalog.sh"
282
+ "okf-fix-links.sh"
205
283
  )
206
284
 
207
285
  # ─────────────────────────────────────────────────────────
@@ -264,12 +342,18 @@ extract_body() {
264
342
  }
265
343
 
266
344
  # Validate body format: line 1 blank, line 2 starts with #, line 3 blank.
345
+ # $3 (optional): pre-extracted body — skips a second extract_body parse when
346
+ # the caller already has it.
267
347
  validate_skill_body_format() {
268
348
  local skill="$1"
269
349
  local skill_file="$2"
270
350
 
271
351
  local body_head line1 line2 line3
272
- body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
352
+ if (( $# >= 3 )); then
353
+ body_head=$(printf '%s\n' "$3" | sed -n '1,3p')
354
+ else
355
+ body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
356
+ fi
273
357
  line1=$(echo "$body_head" | sed -n '1p')
274
358
  line2=$(echo "$body_head" | sed -n '2p')
275
359
  line3=$(echo "$body_head" | sed -n '3p')
@@ -12,7 +12,10 @@
12
12
  # `STARTS WITH`, `NOT x STARTS WITH`, `AND`, `OR`, relationship-type
13
13
  # alternation `[:A|B]`, simple `count(x)`.
14
14
  # UNSAFE : coalesce(), `<>` / `!=` / `<=` / `>=`, `NOT EXISTS(...)`,
15
- # `NOT (pattern)`, `WITH`-grouping aggregation, multi-pattern joins.
15
+ # `NOT (pattern)`, `WITH`-grouping aggregation, multi-pattern joins,
16
+ # and comparing one property against another (`a.x < b.x` — the
17
+ # parser wants a literal on the right and fails with "expected value
18
+ # at pos N"). `<` against a literal is fine.
16
19
  # Every builder below stays inside the SAFE set.
17
20
  #
18
21
  # Label-agnostic on name matches: code units are :Method ⪢ :Function in OO repos;
@@ -28,9 +31,12 @@ if [[ "$(type -t memory_cli 2>/dev/null)" != function ]]; then
28
31
  source "$_GQ_DIR/_lib.sh"
29
32
  fi
30
33
 
31
- # Escape single quotes for embedding inside a Cypher single-quoted string literal.
34
+ # Escape for embedding inside a Cypher single-quoted string literal.
35
+ # Backslashes must be doubled BEFORE quotes are escaped, otherwise an input
36
+ # ending in `\` yields `\'` — an escaped quote that never closes the literal.
32
37
  gq_escape() {
33
38
  local s="$1"
39
+ s="${s//\\/\\\\}"
34
40
  printf '%s' "${s//\'/\\\'}"
35
41
  }
36
42
 
@@ -40,7 +46,12 @@ gq_q_exists() { printf "MATCH (f {name:'%s'}) RETURN f.name AS name L
40
46
  gq_q_callers() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
41
47
  gq_q_callers_prod() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) WHERE c.is_test=false AND NOT c.file_path STARTS WITH 'tests/' RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
42
48
  gq_q_callers_qualified() { printf "MATCH (c)-[:CALLS]->(f {qualified_name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
43
- gq_q_cycles2() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(a) WHERE a.qualified_name < b.qualified_name RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 100"; }
49
+ # The `WHERE a.qualified_name < b.qualified_name` this used to carry was doing
50
+ # two jobs — drop self-loops, and emit each mutual pair once instead of twice —
51
+ # but the engine cannot parse a property-to-property comparison, so the query
52
+ # failed on every call and cycle-detect swallowed the error. Both jobs now happen
53
+ # in jq downstream; the limit is doubled because the raw rows come in pairs.
54
+ gq_q_cycles2() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 200"; }
44
55
  gq_q_cycles3() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(c)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b, c.qualified_name AS c LIMIT 100"; }
45
56
  gq_q_tests() { printf "MATCH (t)-[:TESTS]->(f {name:'%s'}) RETURN t.qualified_name AS test, t.file_path AS file LIMIT 200" "$1"; }
46
57
  gq_q_tested_all() { printf "MATCH (t)-[:TESTS]->(f) RETURN f.qualified_name AS symbol LIMIT 2000"; }
@@ -50,7 +61,6 @@ gq_q_co_change() { printf "MATCH (a:File)-[r:FILE_CHANGES_WITH]->(b:File
50
61
  gq_q_inherits() { printf "MATCH (c)-[:INHERITS]->(p) RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 500"; }
51
62
  gq_q_inherits_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE c.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
52
63
  gq_q_derived_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE p.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
53
- gq_q_writes() { printf "MATCH (f {name:'%s'})-[:WRITES]->(v) RETURN v.name AS target, v.file_path AS file LIMIT 200" "$1"; }
54
64
  gq_q_raises() { printf "MATCH (f {name:'%s'})-[:RAISES|THROWS]->(e) RETURN e.name AS error, e.qualified_name AS qualified LIMIT 200" "$1"; }
55
65
  gq_q_raisers() { printf "MATCH (f)-[:RAISES|THROWS]->(e {name:'%s'}) RETURN f.qualified_name AS raiser, f.file_path AS file LIMIT 200" "$1"; }
56
66
  gq_q_node_props() { printf "MATCH (f) RETURN f.qualified_name AS q, f.complexity AS c, f.cognitive AS cog, f.is_entry_point AS ep LIMIT 10000"; }
@@ -82,19 +92,23 @@ gq_rows_len() {
82
92
  printf '%s' "$1" | jq -r '(.rows // []) | length' 2>/dev/null || printf '0'
83
93
  }
84
94
 
85
- # gq_symbol_status <project> <sym_esc> <result_json> -> ok | no-edges | no-match
95
+ # gq_symbol_status <project> <sym_esc> <result_json>
96
+ # -> ok | no-edges | no-match | probe-failed
86
97
  # Fail-loud disambiguation (Guardrail 4): an empty result is only a true negative
87
98
  # ("no-edges") when the node actually exists; otherwise it is "no-match". An
88
99
  # existence probe runs only when the primary result is empty (hot path stays one
89
- # query).
100
+ # query). If the probe itself errors, that is NOT a true negative — report
101
+ # "probe-failed" so callers never mistake an engine failure for absence.
90
102
  gq_symbol_status() {
91
103
  local project="$1" sym="$2" result="$3"
92
104
  if [[ "$(gq_rows_len "$result")" -gt 0 ]]; then
93
105
  printf 'ok'; return
94
106
  fi
95
107
  local ex
96
- ex="$(gq_run "$project" "$(gq_q_exists "$sym")" || true)"
97
- if [[ -n "$ex" && "$(gq_rows_len "$ex")" -gt 0 ]]; then
108
+ if ! ex="$(gq_run "$project" "$(gq_q_exists "$sym")")"; then
109
+ printf 'probe-failed'; return
110
+ fi
111
+ if [[ "$(gq_rows_len "$ex")" -gt 0 ]]; then
98
112
  printf 'no-edges'
99
113
  else
100
114
  printf 'no-match'
@@ -5,6 +5,21 @@
5
5
 
6
6
  # shellcheck shell=bash
7
7
 
8
+ # Give a mktemp-created replacement file the mode its destination should carry.
9
+ # mktemp creates 0600 and `mv` swaps the inode, so an atomic rewrite silently
10
+ # strips the destination's permissions (a 0644 doc came back 0600). Call this on
11
+ # the temp file BEFORE the mv. Uses the destination's current mode when it
12
+ # exists, else the mode a plain `>` redirect would have produced under the
13
+ # caller's umask.
14
+ apply_dest_mode() {
15
+ local tmp="$1" dest="${2:-}" mode=""
16
+ if [[ -n "$dest" && -e "$dest" ]]; then
17
+ mode="$(stat -c '%a' "$dest" 2>/dev/null || stat -f '%Lp' "$dest" 2>/dev/null || true)"
18
+ fi
19
+ [[ -n "$mode" ]] || mode="$(printf '%o' "$(( 0666 & ~0$(umask) ))")"
20
+ chmod "$mode" "$tmp" 2>/dev/null || true
21
+ }
22
+
8
23
  json_escape() {
9
24
  local s="$1"
10
25
  s="${s//\\/\\\\}"
@@ -15,7 +30,6 @@ json_escape() {
15
30
  printf '%s' "$s"
16
31
  }
17
32
 
18
- # Extract a top-level YAML frontmatter field value from a Markdown file.
19
33
  # Discover track directories under a repo root (default: caller's Draft repo).
20
34
  discover_track_dirs() {
21
35
  local repo_root="${1:-}"
@@ -90,6 +104,7 @@ skill_line_cap() {
90
104
  printf '%s' "$global_cap"
91
105
  }
92
106
 
107
+ # Extract a top-level YAML frontmatter field value from a Markdown file.
93
108
  get_yaml_field() {
94
109
  local file="$1"
95
110
  local key="$2"
@@ -109,9 +124,53 @@ get_yaml_field() {
109
124
  ' "$file"
110
125
  }
111
126
 
127
+ # GitHub-flavored-markdown-ish heading id (shared by TOC, anchors, link fixers).
128
+ # Keeps underscores; strips other punctuation; collapses whitespace to '-'.
129
+ gfm_slug() {
130
+ local s="${1:-}"
131
+ # shellcheck disable=SC2001
132
+ s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]')"
133
+ s="$(printf '%s' "$s" | sed -E 's/[^[:alnum:]_[:space:]-]+//g; s/[[:space:]]+/-/g; s/-+/-/g; s/^-+//; s/-+$//')"
134
+ printf '%s' "$s"
135
+ }
136
+
137
+ # Count entries in an x-grounded-paths frontmatter field. Always prints an
138
+ # integer — a page with no such field counts 0, not "" (callers embed the value
139
+ # in diagnostics, and an empty string rendered as "x-grounded-paths < 2").
140
+ # Supports both inline (`x-grounded-paths: ["a", "b"]`) and block lists:
141
+ # x-grounded-paths:
142
+ # - "a"
143
+ # - "b"
144
+ grounded_paths_count() {
145
+ local file="$1"
146
+ [[ -f "$file" ]] || { echo 0; return; }
147
+ awk '
148
+ NR==1 && /^---$/ { fm=1; next }
149
+ fm && /^---$/ { exit }
150
+ fm && /^x-grounded-paths:[[:space:]]*\[/ {
151
+ line=$0
152
+ sub(/^x-grounded-paths:[[:space:]]*\[/, "", line)
153
+ sub(/\].*$/, "", line)
154
+ gsub(/[[:space:]]/, "", line)
155
+ if (line=="") { print 0; printed=1; exit }
156
+ n=split(line, a, ",")
157
+ print n
158
+ printed=1
159
+ exit
160
+ }
161
+ fm && /^x-grounded-paths:[[:space:]]*$/ { collect=1; next }
162
+ fm && collect {
163
+ if ($0 ~ /^[[:space:]]*-[[:space:]]*/) { n++; next }
164
+ if ($0 ~ /^[A-Za-z0-9_-]+:/) { print n+0; printed=1; exit }
165
+ }
166
+ END { if (!printed) print n+0 }
167
+ ' "$file"
168
+ }
169
+
112
170
  # Locate the `codebase-memory-mcp` binary (Draft knowledge-graph engine).
113
171
  # Sets MEMORY_BIN globally; returns 0 if found, 1 otherwise.
114
- # Preference: PATH > Draft-managed install (~/.cache/draft/bin) > vendored bin/<arch> under known roots.
172
+ # Precedence: DRAFT_MEMORY_DISABLE (hard off) > DRAFT_MEMORY_BIN > PATH >
173
+ # Draft-managed install (~/.cache/draft/bin) > vendored bin/<arch> under known roots.
115
174
  # No legacy fallbacks: the Aether `graph`/`graph-clang` binaries are retired.
116
175
  find_memory_bin() {
117
176
  local repo_abs="$1"
@@ -157,14 +216,6 @@ find_memory_bin() {
157
216
  local roots=()
158
217
  [[ -n "$repo_abs" && -d "$repo_abs" ]] && roots+=("$repo_abs")
159
218
  [[ -n "$self_repo" && -d "$self_repo" ]] && roots+=("$self_repo")
160
- for bc in \
161
- "$HOME/.cursor/plugins/local/draft/.draft-install-path" \
162
- "$HOME/.claude/plugins/draft/.draft-install-path"; do
163
- if [[ -f "$bc" ]]; then
164
- local pr; pr="$(cat "$bc" 2>/dev/null || true)"
165
- [[ -n "$pr" && -d "$pr" ]] && roots+=("$pr")
166
- fi
167
- done
168
219
 
169
220
  for pr in "${roots[@]}"; do
170
221
  local cand="$pr/bin/$ARCH/$bin_name"
@@ -177,6 +228,21 @@ find_memory_bin() {
177
228
  return 1
178
229
  }
179
230
 
231
+ # graph_bootstrap <repo-dir>
232
+ # Shared engine bootstrap for the graph-*.sh wrappers: resolves the repo path,
233
+ # locates the engine binary, checks jq, and ensures the index. Sets the globals
234
+ # REPO_ABS and PROJECT. Returns 1 on any failure so each wrapper routes it to
235
+ # its own unavailable-JSON shape (the shapes differ per tool).
236
+ graph_bootstrap() {
237
+ local repo="$1" self_repo
238
+ REPO_ABS="$(cd "$repo" 2>/dev/null && pwd)" || return 1
239
+ self_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
240
+ find_memory_bin "$REPO_ABS" "$self_repo" || return 1
241
+ command -v jq >/dev/null 2>&1 || return 1
242
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
243
+ [[ -n "$PROJECT" ]] || return 1
244
+ }
245
+
180
246
  # Run a codebase-memory-mcp CLI tool. Echoes the JSON result (stdout); the engine's
181
247
  # `level=...` log lines go to stderr and are discarded unless DRAFT_MEMORY_DEBUG is set.
182
248
  # Usage: memory_cli <tool> [json-args]
@@ -250,7 +316,11 @@ _can_cgroup_bound() {
250
316
  # Echoes the engine's JSON result on stdout (same contract as memory_cli).
251
317
  memory_index_bounded() {
252
318
  local repo_abs="$1"
253
- local json="{\"repo_path\":\"$repo_abs\"}"
319
+ # Payload built with jq (never string concatenation) so a repo path
320
+ # containing a `"` or `\` can never corrupt the JSON sent to the engine.
321
+ command -v jq >/dev/null 2>&1 || return 1
322
+ local json
323
+ json="$(jq -n --arg r "$repo_abs" '{repo_path:$r}')" || return 1
254
324
  export CBM_WORKERS="${CBM_WORKERS:-4}"
255
325
  local total pct
256
326
  total="$(_total_ram_mb)"
@@ -32,7 +32,7 @@ EOF
32
32
 
33
33
  while [[ $# -gt 0 ]]; do
34
34
  case "$1" in
35
- --root) ROOT="$2"; shift 2;;
35
+ --root) ROOT="${2:?--root requires a value}"; shift 2;;
36
36
  --help|-h) usage; exit 0;;
37
37
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
38
38
  esac
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # check-repo-size.sh
3
+ #
4
+ # Guards the size of the tree at HEAD.
5
+ #
6
+ # `claude plugin marketplace add <owner>/<repo>` git-clones this repository, so
7
+ # every byte tracked at HEAD is downloaded before the plugin can be installed.
8
+ # In v2.8.3 ~670 MB of audiobook .m4a files in HEAD stalled that first install
9
+ # step with no error message — an install-breaking regression that no test
10
+ # caught. This is that test.
11
+ #
12
+ # The threshold is deliberately loose (default 10 MB against a ~6 MB HEAD): it
13
+ # exists to catch binary assets landing in HEAD, not to police prose growth.
14
+ #
15
+ # Usage:
16
+ # scripts/tools/check-repo-size.sh # default cap, human output
17
+ # scripts/tools/check-repo-size.sh --max-mb 5 # custom cap
18
+ # scripts/tools/check-repo-size.sh --json # machine-readable
19
+ # scripts/tools/check-repo-size.sh --rev v3.6.0 # audit a past revision
20
+ #
21
+ # Exit codes:
22
+ # 0 tree at HEAD is within the cap
23
+ # 1 cap exceeded
24
+ # 2 usage / runtime error
25
+
26
+ set -euo pipefail
27
+
28
+ usage() {
29
+ cat <<'EOF'
30
+ check-repo-size.sh — fail if the tree at HEAD exceeds a size cap
31
+
32
+ Options:
33
+ --max-mb <n> Cap in megabytes (default: 10)
34
+ --rev <rev> Revision to measure (default: HEAD)
35
+ --repo <path> Repository root (default: this repo)
36
+ --top <n> Largest blobs to list (default: 10)
37
+ --json Emit JSON instead of human-readable output
38
+ --help, -h Show this message
39
+ EOF
40
+ }
41
+
42
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
43
+ usage
44
+ exit 0
45
+ fi
46
+
47
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
48
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
49
+
50
+ MAX_MB=10
51
+ REV="HEAD"
52
+ TOP=10
53
+ EMIT_JSON=0
54
+
55
+ while [[ $# -gt 0 ]]; do
56
+ case "$1" in
57
+ --max-mb)
58
+ [[ -n "${2:-}" ]] || { echo "check-repo-size: --max-mb requires a value" >&2; exit 2; }
59
+ MAX_MB="$2"; shift 2 ;;
60
+ --rev)
61
+ [[ -n "${2:-}" ]] || { echo "check-repo-size: --rev requires a value" >&2; exit 2; }
62
+ REV="$2"; shift 2 ;;
63
+ --repo)
64
+ [[ -n "${2:-}" ]] || { echo "check-repo-size: --repo requires a value" >&2; exit 2; }
65
+ REPO_ROOT="$2"; shift 2 ;;
66
+ --top)
67
+ [[ -n "${2:-}" ]] || { echo "check-repo-size: --top requires a value" >&2; exit 2; }
68
+ TOP="$2"; shift 2 ;;
69
+ --json) EMIT_JSON=1; shift ;;
70
+ --help|-h) usage; exit 0 ;;
71
+ *) echo "check-repo-size: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
72
+ esac
73
+ done
74
+
75
+ [[ "$MAX_MB" =~ ^[0-9]+$ ]] || { echo "check-repo-size: --max-mb must be an integer, got '$MAX_MB'" >&2; exit 2; }
76
+ [[ "$TOP" =~ ^[0-9]+$ ]] || { echo "check-repo-size: --top must be an integer, got '$TOP'" >&2; exit 2; }
77
+ [[ -d "$REPO_ROOT" ]] || { echo "check-repo-size: no such directory: $REPO_ROOT" >&2; exit 2; }
78
+
79
+ cd "$REPO_ROOT"
80
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "check-repo-size: $REPO_ROOT is not a git repository" >&2; exit 2; }
81
+ git rev-parse --verify --quiet "$REV" >/dev/null || { echo "check-repo-size: unknown revision '$REV'" >&2; exit 2; }
82
+
83
+ # `ls-tree -l` gives per-blob sizes; symlinks/submodules report '-' and are skipped.
84
+ listing="$(git ls-tree -r -l "$REV")"
85
+
86
+ total_bytes=0
87
+ file_count=0
88
+ while read -r _mode _type _sha size _path; do
89
+ [[ "$size" =~ ^[0-9]+$ ]] || continue
90
+ total_bytes=$((total_bytes + size))
91
+ file_count=$((file_count + 1))
92
+ done <<< "$listing"
93
+
94
+ max_bytes=$((MAX_MB * 1024 * 1024))
95
+ status="ok"
96
+ [[ "$total_bytes" -gt "$max_bytes" ]] && status="over"
97
+
98
+ total_mb="$(awk -v b="$total_bytes" 'BEGIN { printf "%.2f", b / 1048576 }')"
99
+
100
+ if [[ "$EMIT_JSON" -eq 1 ]]; then
101
+ printf '{"rev":"%s","status":"%s","total_bytes":%d,"total_mb":%s,"max_mb":%d,"files":%d}\n' \
102
+ "$REV" "$status" "$total_bytes" "$total_mb" "$MAX_MB" "$file_count"
103
+ else
104
+ echo "Tree at $REV: ${total_mb} MB across $file_count files (cap: ${MAX_MB} MB)"
105
+ if [[ "$status" == "over" ]]; then
106
+ echo ""
107
+ echo "Largest tracked blobs:"
108
+ # Materialize the sorted list before slicing it. Piping straight into
109
+ # `head` lets head close the pipe early, which under `pipefail` surfaces
110
+ # sort's SIGPIPE as a hard failure — non-deterministically, depending on
111
+ # whether the output fit in the pipe buffer.
112
+ largest="$(awk '$4 ~ /^[0-9]+$/ { printf " %8.2f MB %s\n", $4 / 1048576, $5 }' <<< "$listing" | sort -rn)"
113
+ head -n "$TOP" <<< "$largest"
114
+ fi
115
+ fi
116
+
117
+ if [[ "$status" == "over" ]]; then
118
+ echo "" >&2
119
+ echo "FAIL: HEAD exceeds ${MAX_MB} MB. 'plugin marketplace add' clones this tree —" >&2
120
+ echo " oversized blobs stall install with no error. Ship large assets as" >&2
121
+ echo " GitHub Release attachments instead of committing them." >&2
122
+ exit 1
123
+ fi
124
+
125
+ exit 0
@@ -51,8 +51,8 @@ while (($#)); do
51
51
  -h|--help) USAGE_HELP_MODE=1 usage ;;
52
52
  --enforce) ENFORCE=1; shift ;;
53
53
  --json) EMIT_JSON=1; shift ;;
54
- --caps) CAPS_CONF="$2"; shift 2 ;;
55
- --skills-dir) SKILLS_DIR="$2"; shift 2 ;;
54
+ --caps) CAPS_CONF="${2:?--caps requires a value}"; shift 2 ;;
55
+ --skills-dir) SKILLS_DIR="${2:?--skills-dir requires a value}"; shift 2 ;;
56
56
  -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
57
57
  *) printf 'Unexpected arg: %s\n' "$1" >&2; usage ;;
58
58
  esac
@@ -64,7 +64,6 @@ declare -a findings=()
64
64
  record() { findings+=("$1|$2|$3"); over_count=$((over_count + 1)); }
65
65
 
66
66
  while IFS= read -r path; do
67
- rel="${path#"$REPO_ROOT/"}"
68
67
  name="$(basename "$(dirname "$path")")"
69
68
  lines="$(wc -l < "$path" | tr -d ' ')"
70
69
  cap="$(skill_line_cap "$name" "$CAPS_CONF" "$GLOBAL_CAP")"
@@ -90,7 +89,7 @@ emit() {
90
89
  printf 'OK: all skills within cap (default %d).\n' "$GLOBAL_CAP"
91
90
  return
92
91
  fi
93
- local mode="warn-only" stream=stderr
92
+ local mode="warn-only"
94
93
  ((ENFORCE)) && mode="enforced"
95
94
  if ((ENFORCE)); then
96
95
  printf 'SKILL-CAPS: %d skill(s) over cap (%s):\n' \
@@ -51,7 +51,6 @@ while IFS= read -r f; do
51
51
  [[ -z "$f" ]] && continue
52
52
  case "$f" in
53
53
  skills/*|scripts/tools/*)
54
- # Skip changes to the templates themselves living under scripts.
55
54
  touches_skills_or_tools=1 ;;
56
55
  core/templates/*)
57
56
  touches_templates=1 ;;