@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
@@ -8,7 +8,9 @@
8
8
  # are rejected before the engine ever sees them).
9
9
  # --tool NAME --json '{...}' passthrough to any read-only engine tool
10
10
  # (get_code_snippet, search_graph, get_graph_schema,
11
- # trace_path, …). Destructive tools are rejected.
11
+ # trace_path, …). Destructive tools are rejected, and
12
+ # a `query` field in --json is scanned for write
13
+ # verbs exactly like --cypher.
12
14
  #
13
15
  # Dialect limits (engine v0.8.x — see _graph_queries.sh for the full list):
14
16
  # SAFE : fixed-length patterns, `=`, `<`, `STARTS WITH`, `NOT x STARTS WITH`,
@@ -46,7 +48,8 @@ Flags:
46
48
  --cypher STR Read-only openCypher query (write verbs CREATE/MERGE/DELETE/SET/
47
49
  REMOVE/DROP/DETACH are rejected). The {project} is injected.
48
50
  --tool NAME Engine tool to call (read-only allowlist). Combine with --json.
49
- --json STR JSON args for --tool (the project is injected if absent).
51
+ --json STR JSON args for --tool (the project is injected if absent). A
52
+ `query` field is write-verb checked like --cypher.
50
53
  --help Show this help.
51
54
 
52
55
  Dialect: avoid coalesce(), <>, NOT EXISTS, NOT(pattern), WITH-aggregation,
@@ -59,10 +62,10 @@ EOF
59
62
 
60
63
  while [[ $# -gt 0 ]]; do
61
64
  case "$1" in
62
- --repo) REPO="$2"; shift 2;;
63
- --cypher) CYPHER="$2"; shift 2;;
64
- --tool) TOOL="$2"; shift 2;;
65
- --json) TOOL_JSON="$2"; shift 2;;
65
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
66
+ --cypher) CYPHER="${2:?--cypher requires a value}"; shift 2;;
67
+ --tool) TOOL="${2:?--tool requires a value}"; shift 2;;
68
+ --json) TOOL_JSON="${2:?--json requires a value}"; shift 2;;
66
69
  --help|-h) usage; exit 0;;
67
70
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
68
71
  esac
@@ -89,25 +92,79 @@ if [[ -n "$TOOL" ]]; then
89
92
  }
90
93
  fi
91
94
 
92
- # Reject write verbs in --cypher BEFORE the engine sees the query.
93
- if [[ -n "$CYPHER" ]]; then
94
- UPPER="$(printf '%s' "$CYPHER" | tr '[:lower:]' '[:upper:]')"
95
- if printf '%s' "$UPPER" | grep -Eqw 'CREATE|MERGE|DELETE|SET|REMOVE|DROP|DETACH'; then
95
+ # Drop the contents of every quoted span before scanning for write verbs below.
96
+ # A raw substring grep can't tell a CREATE/SET/DELETE clause from a symbol
97
+ # literally named 'create', 'set', 'delete', etc. — exactly the method names
98
+ # real codebases are full of so quoted spans must be blanked out first or
99
+ # every query about one of them gets wrongly rejected. Cypher quotes strings
100
+ # with either ' or " and identifiers with `, and the engine accepts all three,
101
+ # so all three must be handled. Escaped quotes (`\'`, produced by gq_escape in
102
+ # _graph_queries.sh) must not be read as closing a span early, and an
103
+ # unterminated span fails closed (rejected) rather than letting the rest of the
104
+ # query — write verbs included — go unscanned.
105
+ strip_quoted_spans() {
106
+ local s="$1" out="" i=0 len ch quote
107
+ len="${#s}"
108
+ while (( i < len )); do
109
+ ch="${s:i:1}"
110
+ if [[ "$ch" != "'" && "$ch" != '"' && "$ch" != '`' ]]; then
111
+ out+="$ch"
112
+ i=$((i + 1))
113
+ continue
114
+ fi
115
+ quote="$ch"
116
+ out+="$quote"
117
+ i=$((i + 1))
118
+ while :; do
119
+ (( i < len )) || return 1
120
+ ch="${s:i:1}"
121
+ if [[ "$ch" == '\' ]]; then
122
+ i=$((i + 2))
123
+ continue
124
+ fi
125
+ i=$((i + 1))
126
+ if [[ "$ch" == "$quote" ]]; then
127
+ out+="$quote"
128
+ break
129
+ fi
130
+ done
131
+ done
132
+ printf '%s' "$out"
133
+ }
134
+
135
+ # Reject write verbs BEFORE the engine sees the query.
136
+ reject_write_verbs() {
137
+ local stripped upper
138
+ stripped="$(strip_quoted_spans "$1")" || {
139
+ echo "ERROR: write verbs are not allowed (read-only passthrough)" >&2
140
+ exit 1
141
+ }
142
+ upper="$(printf '%s' "$stripped" | tr '[:lower:]' '[:upper:]')"
143
+ if printf '%s' "$upper" | grep -Eqw 'CREATE|MERGE|DELETE|SET|REMOVE|DROP|DETACH'; then
96
144
  echo "ERROR: write verbs are not allowed (read-only passthrough)" >&2
97
145
  exit 1
98
146
  fi
147
+ }
148
+
149
+ if [[ -n "$CYPHER" ]]; then
150
+ reject_write_verbs "$CYPHER"
99
151
  fi
100
152
 
101
- REPO_ABS="$(cd "$REPO" && pwd)"
102
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
153
+ # --tool carries Cypher too: query_graph takes it in the payload's `query` field,
154
+ # so guarding only --cypher left the read-only contract fully bypassable via
155
+ # `--tool query_graph --json '{"query":"MATCH (n) DETACH DELETE n"}'`. Scan any
156
+ # `query` a tool payload carries, not just query_graph's, so a future
157
+ # query-bearing tool is covered by construction. jq-gated: without jq nothing can
158
+ # reach the engine anyway (graph_bootstrap fails first).
159
+ if [[ -n "$TOOL" ]] && command -v jq >/dev/null 2>&1; then
160
+ echo "$TOOL_JSON" | jq -e . >/dev/null 2>&1 || { echo "ERROR: --json is not valid JSON" >&2; exit 1; }
161
+ TOOL_QUERY="$(echo "$TOOL_JSON" | jq -r '.query // empty' 2>/dev/null || true)"
162
+ [[ -z "$TOOL_QUERY" ]] || reject_write_verbs "$TOOL_QUERY"
163
+ fi
103
164
 
104
165
  unavailable() { echo '{"source":"unavailable"}'; exit 2; }
105
166
 
106
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
107
- command -v jq >/dev/null 2>&1 || unavailable
108
-
109
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
110
- [[ -n "$PROJECT" ]] || unavailable
167
+ graph_bootstrap "$REPO" || unavailable
111
168
 
112
169
  if [[ -n "$CYPHER" ]]; then
113
170
  RES="$(gq_run "$PROJECT" "$CYPHER" || true)"
@@ -115,7 +172,6 @@ if [[ -n "$CYPHER" ]]; then
115
172
  printf '%s\n' "$RES"
116
173
  else
117
174
  # Inject the resolved project into the tool args unless the caller set one.
118
- echo "$TOOL_JSON" | jq -e . >/dev/null 2>&1 || { echo "ERROR: --json is not valid JSON" >&2; exit 1; }
119
175
  ARGS="$(echo "$TOOL_JSON" | jq -c --arg p "$PROJECT" 'if has("project") then . else . + {project:$p} end')"
120
176
  RES="$(memory_cli "$TOOL" "$ARGS" 2>/dev/null || true)"
121
177
  [[ -n "$RES" ]] || unavailable
@@ -42,8 +42,8 @@ EOF
42
42
 
43
43
  while [[ $# -gt 0 ]]; do
44
44
  case "$1" in
45
- --repo) REPO="$2"; shift 2;;
46
- --min-complexity) MIN_COMPLEXITY="$2"; shift 2;;
45
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
46
+ --min-complexity) MIN_COMPLEXITY="${2:?--min-complexity requires a value}"; shift 2;;
47
47
  --help|-h) usage; exit 0;;
48
48
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
49
49
  esac
@@ -52,16 +52,9 @@ done
52
52
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
53
53
  [[ "$MIN_COMPLEXITY" =~ ^[0-9]+$ ]] || { echo "ERROR: --min-complexity must be a non-negative integer" >&2; exit 1; }
54
54
 
55
- REPO_ABS="$(cd "$REPO" && pwd)"
56
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
57
-
58
55
  unavailable() { echo '{"risky":[],"total":0,"source":"unavailable"}'; exit 2; }
59
56
 
60
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
61
- command -v jq >/dev/null 2>&1 || unavailable
62
-
63
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
64
- [[ -n "$PROJECT" ]] || unavailable
57
+ graph_bootstrap "$REPO" || unavailable
65
58
 
66
59
  RES="$(gq_run "$PROJECT" "$(gq_q_risk)" || true)"
67
60
  [[ -n "$RES" ]] || unavailable
@@ -40,9 +40,9 @@ EOF
40
40
 
41
41
  while [[ $# -gt 0 ]]; do
42
42
  case "$1" in
43
- --repo) REPO="$2"; shift 2;;
44
- --query) QUERY="$2"; shift 2;;
45
- --limit) LIMIT="$2"; shift 2;;
43
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
44
+ --query) QUERY="${2:?--query requires a value}"; shift 2;;
45
+ --limit) LIMIT="${2:?--limit requires a value}"; shift 2;;
46
46
  --help|-h) usage; exit 0;;
47
47
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
48
48
  esac
@@ -52,20 +52,13 @@ done
52
52
  [[ -n "$QUERY" ]] || { echo "ERROR: --query is required" >&2; usage >&2; exit 1; }
53
53
  [[ "$LIMIT" =~ ^[0-9]+$ ]] || { echo "ERROR: --limit must be a non-negative integer" >&2; exit 1; }
54
54
 
55
- REPO_ABS="$(cd "$REPO" && pwd)"
56
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
57
-
58
55
  unavailable() {
59
56
  jq -n --arg q "$QUERY" '{query:$q, results:[], source:"unavailable"}' 2>/dev/null \
60
57
  || echo '{"results":[],"source":"unavailable"}'
61
58
  exit 2
62
59
  }
63
60
 
64
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
65
- command -v jq >/dev/null 2>&1 || unavailable
66
-
67
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
68
- [[ -n "$PROJECT" ]] || unavailable
61
+ graph_bootstrap "$REPO" || unavailable
69
62
 
70
63
  ARGS="$(jq -n --arg p "$PROJECT" --arg q "$QUERY" --argjson n "$LIMIT" \
71
64
  '{project:$p, query:$q, limit:$n}')"
@@ -52,8 +52,8 @@ EOF
52
52
 
53
53
  while [[ $# -gt 0 ]]; do
54
54
  case "$1" in
55
- --repo) REPO="$2"; shift 2;;
56
- --out) OUT_DIR="$2"; shift 2;;
55
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
56
+ --out) OUT_DIR="${2:?--out requires a value}"; shift 2;;
57
57
  --help|-h) usage; exit 0;;
58
58
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
59
59
  esac
@@ -73,17 +73,41 @@ command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 2; }
73
73
  PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
74
74
  [[ -n "$PROJECT" ]] || { echo "could not index repo — nothing written" >&2; exit 2; }
75
75
 
76
+ # ...then ALWAYS re-index. memory_ensure_index calls index_repository only when the
77
+ # project is ABSENT — correct for the graph-*.sh query wrappers, which must stay
78
+ # cheap — so on an already-indexed repo this tool used to write a gate marker with a
79
+ # fresh `generated_at` over a frozen index: a deleted symbol stayed resolvable, a new
80
+ # one never appeared, and nothing in the output said so. Refreshing is this tool's
81
+ # entire job. The engine indexes incrementally, so the repeat call is cheap.
82
+ REFRESHED="$(memory_index_bounded "$REPO_ABS" 2>/dev/null | jq -r '.project // empty' 2>/dev/null || true)"
83
+ [[ -n "$REFRESHED" ]] && PROJECT="$REFRESHED"
84
+
76
85
  mkdir -p "$OUT"
77
86
 
78
87
  # Prune any stale fat-snapshot artifacts from a prior (pre-engine-only) run so a
79
88
  # re-index migrates the repo to the thin model.
80
- rm -f "$OUT/architecture.json" "$OUT/hotspots.jsonl" \
81
- "$OUT/module-deps.mermaid" "$OUT/proto-map.mermaid" 2>/dev/null || true
82
- rm -rf "$OUT/okf" 2>/dev/null || true
89
+ #
90
+ # Gated on $OUT carrying positive evidence that Draft owns it. --out is
91
+ # caller-supplied and the mkdir -p above will happily create a typo'd path, so an
92
+ # ungated `rm -rf "$OUT/okf"` turns a mistyped flag into data loss. Evidence is
93
+ # the default location, a marker from a previous run, or a prior fat snapshot
94
+ # (which always carried architecture.json / hotspots.jsonl).
95
+ draft_owns_out_dir() {
96
+ [[ "$OUT" == "$REPO_ABS/draft/graph" ]] && return 0
97
+ [[ -f "$OUT/schema.yaml" ]] && return 0
98
+ [[ -f "$OUT/architecture.json" || -f "$OUT/hotspots.jsonl" ]] && return 0
99
+ return 1
100
+ }
101
+
102
+ if draft_owns_out_dir; then
103
+ rm -f "$OUT/architecture.json" "$OUT/hotspots.jsonl" \
104
+ "$OUT/module-deps.mermaid" "$OUT/proto-map.mermaid" 2>/dev/null || true
105
+ rm -rf "$OUT/okf" 2>/dev/null || true
106
+ fi
83
107
 
84
108
  # schema.yaml — provenance + gate. Counts are point-of-index provenance only;
85
109
  # the live engine is authoritative.
86
- STATUS_JSON="$(memory_cli index_status "{\"project\":\"$PROJECT\"}" || echo '{}')"
110
+ STATUS_JSON="$(memory_cli index_status "$(jq -n --arg p "$PROJECT" '{project:$p}')" || echo '{}')"
87
111
  # Tolerate field-name variation AND non-JSON output across engine versions;
88
112
  # counts are provenance only and must never abort the gate-marker write.
89
113
  NODES="$(echo "$STATUS_JSON" | jq -r '.nodes // .node_count // .total_nodes // 0' 2>/dev/null || echo 0)"
@@ -94,11 +118,16 @@ VER="$("$MEMORY_BIN" --version 2>/dev/null | awk '{print $NF}' || echo unknown)"
94
118
  # incrementally (content-based, git-aware), so re-indexing only touches changed
95
119
  # files. detect_changes reports that working-tree delta — recorded as provenance
96
120
  # and echoed so a refresh shows what moved. Best-effort: never aborts the write.
97
- CHANGES_JSON="$(memory_cli detect_changes "{\"project\":\"$PROJECT\"}" 2>/dev/null || echo '{}')"
121
+ CHANGES_JSON="$(memory_cli detect_changes "$(jq -n --arg p "$PROJECT" '{project:$p}')" 2>/dev/null || echo '{}')"
98
122
  echo "$CHANGES_JSON" | jq -e . >/dev/null 2>&1 || CHANGES_JSON='{}'
99
123
  CHANGED_FILES="$(echo "$CHANGES_JSON" | jq -r '.changed_count // (.changed_files | length?) // 0' 2>/dev/null || echo 0)"
100
124
  IMPACTED="$(echo "$CHANGES_JSON" | jq -r '(.impacted_symbols | length?) // 0' 2>/dev/null || echo 0)"
101
125
 
126
+ # YAML double-quoted scalars: escape backslashes then quotes so an unusual
127
+ # project name or engine version string can never corrupt the marker.
128
+ PROJECT_Y="${PROJECT//\\/\\\\}"; PROJECT_Y="${PROJECT_Y//\"/\\\"}"
129
+ VER_Y="${VER//\\/\\\\}"; VER_Y="${VER_Y//\"/\\\"}"
130
+
102
131
  cat > "$OUT/schema.yaml" <<EOF
103
132
  # Draft graph gate marker — written by scripts/tools/graph-snapshot.sh
104
133
  # Draft is engine-only: this file carries NO graph data. Its presence signals that
@@ -106,8 +135,8 @@ cat > "$OUT/schema.yaml" <<EOF
106
135
  # live via the graph-*.sh wrappers (or \`codebase-memory-mcp cli <tool>\`).
107
136
  # Counts below are point-of-index provenance; the live engine is authoritative.
108
137
  engine: codebase-memory-mcp
109
- engine_version: "$VER"
110
- project: "$PROJECT"
138
+ engine_version: "$VER_Y"
139
+ project: "$PROJECT_Y"
111
140
  generated_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
112
141
  indexed_nodes: $NODES
113
142
  indexed_edges: $EDGES
@@ -44,8 +44,8 @@ EOF
44
44
 
45
45
  while [[ $# -gt 0 ]]; do
46
46
  case "$1" in
47
- --repo) REPO="$2"; shift 2;;
48
- --qualified|--symbol) QNAME="$2"; shift 2;;
47
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
48
+ --qualified|--symbol) QNAME="${2:?--qualified|--symbol requires a value}"; shift 2;;
49
49
  --help|-h) usage; exit 0;;
50
50
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
51
51
  esac
@@ -54,20 +54,13 @@ done
54
54
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
55
55
  [[ -n "$QNAME" ]] || { echo "ERROR: --qualified is required" >&2; usage >&2; exit 1; }
56
56
 
57
- REPO_ABS="$(cd "$REPO" && pwd)"
58
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
59
-
60
57
  unavailable() {
61
58
  jq -n --arg q "$QNAME" '{qualified_name:$q, status:"unavailable", source:"unavailable"}' 2>/dev/null \
62
59
  || echo '{"status":"unavailable","source":"unavailable"}'
63
60
  exit 2
64
61
  }
65
62
 
66
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
67
- command -v jq >/dev/null 2>&1 || unavailable
68
-
69
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
70
- [[ -n "$PROJECT" ]] || unavailable
63
+ graph_bootstrap "$REPO" || unavailable
71
64
 
72
65
  ARGS="$(jq -n --arg p "$PROJECT" --arg q "$QNAME" '{project:$p, qualified_name:$q}')"
73
66
  RES="$(memory_cli get_code_snippet "$ARGS" 2>/dev/null || true)"
@@ -48,8 +48,8 @@ EOF
48
48
 
49
49
  while [[ $# -gt 0 ]]; do
50
50
  case "$1" in
51
- --repo) REPO="$2"; shift 2;;
52
- --symbol) SYMBOL="$2"; shift 2;;
51
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
52
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
53
53
  --untested) UNTESTED=1; shift;;
54
54
  --help|-h) usage; exit 0;;
55
55
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
@@ -59,9 +59,6 @@ done
59
59
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
60
60
  [[ -n "$SYMBOL" || "$UNTESTED" -eq 1 ]] || { echo "ERROR: provide --symbol or --untested" >&2; usage >&2; exit 1; }
61
61
 
62
- REPO_ABS="$(cd "$REPO" && pwd)"
63
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
64
-
65
62
  unavailable() {
66
63
  if [[ "$UNTESTED" -eq 1 ]]; then
67
64
  echo '{"untested":[],"total":0,"source":"unavailable"}'
@@ -72,11 +69,7 @@ unavailable() {
72
69
  exit 2
73
70
  }
74
71
 
75
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
76
- command -v jq >/dev/null 2>&1 || unavailable
77
-
78
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
79
- [[ -n "$PROJECT" ]] || unavailable
72
+ graph_bootstrap "$REPO" || unavailable
80
73
 
81
74
  if [[ "$UNTESTED" -eq 1 ]]; then
82
75
  EXP="$(gq_run "$PROJECT" "$(gq_q_exported)" || true)"
@@ -49,8 +49,8 @@ EOF
49
49
  while [[ $# -gt 0 ]]; do
50
50
  case "$1" in
51
51
  ingest) ACTION="ingest"; shift;;
52
- --repo) REPO="$2"; shift 2;;
53
- --file) FILE="$2"; shift 2;;
52
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
53
+ --file) FILE="${2:?--file requires a value}"; shift 2;;
54
54
  --experimental) EXPERIMENTAL=1; shift;;
55
55
  --help|-h) usage; exit 0;;
56
56
  *) echo "Unknown argument: $1" >&2; usage >&2; exit 1;;
@@ -63,19 +63,12 @@ done
63
63
  [[ -n "$FILE" ]] || { echo "ERROR: --file is required" >&2; usage >&2; exit 1; }
64
64
  [[ -f "$FILE" ]] || { echo "ERROR: --file '$FILE' does not exist" >&2; exit 1; }
65
65
 
66
- REPO_ABS="$(cd "$REPO" && pwd)"
67
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
68
-
69
66
  unavailable() { echo '{"source":"unavailable"}'; exit 2; }
70
67
 
71
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
72
- command -v jq >/dev/null 2>&1 || unavailable
68
+ graph_bootstrap "$REPO" || unavailable
73
69
 
74
70
  jq -e . "$FILE" >/dev/null 2>&1 || { echo "ERROR: --file is not valid JSON" >&2; exit 1; }
75
71
 
76
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
77
- [[ -n "$PROJECT" ]] || unavailable
78
-
79
72
  ARGS="$(jq -n --arg p "$PROJECT" --slurpfile t "$FILE" '{project:$p, traces:($t[0])}')"
80
73
  RES="$(memory_cli ingest_traces "$ARGS" 2>/dev/null || true)"
81
74
  [[ -n "$RES" ]] || unavailable
@@ -47,8 +47,8 @@ EOF
47
47
 
48
48
  while [[ $# -gt 0 ]]; do
49
49
  case "$1" in
50
- --repo) REPO="$2"; shift 2;;
51
- --top) TOP="$2"; shift 2;;
50
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
51
+ --top) TOP="${2:?--top requires a value}"; shift 2;;
52
52
  --help|-h) usage; exit 0;;
53
53
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
54
54
  esac
@@ -61,18 +61,12 @@ if [[ ! -d "$REPO" ]]; then
61
61
  exit 1
62
62
  fi
63
63
 
64
- REPO_ABS="$(cd "$REPO" && pwd)"
65
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
66
-
67
64
  unavailable() { echo '{"hotspots":[],"source":"unavailable"}'; exit 2; }
68
65
 
69
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
70
- command -v jq >/dev/null 2>&1 || unavailable
71
-
72
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
73
- [[ -n "$PROJECT" ]] || unavailable
66
+ graph_bootstrap "$REPO" || unavailable
74
67
 
75
- ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"hotspots\"]}" || true)"
68
+ ARCH_JSON="$(memory_cli get_architecture \
69
+ "$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["hotspots"]}')" || true)"
76
70
  [[ -n "$ARCH_JSON" ]] || unavailable
77
71
  echo "$ARCH_JSON" | jq -e . >/dev/null 2>&1 || unavailable
78
72
 
@@ -87,7 +81,7 @@ printf '%s' "$ARCH_JSON" > "$TMP_ARCH"
87
81
  printf '%s' "$PROPS_JSON" > "$TMP_PROPS"
88
82
 
89
83
  jq -n --slurpfile arch "$TMP_ARCH" --slurpfile props "$TMP_PROPS" --argjson top "$TOP" '
90
- (($props[0].rows) // []) as $prows
84
+ ((($props[0].rows) // []) | map(select(.[0] != null))) as $prows
91
85
  | (reduce $prows[] as $r ({};
92
86
  .[$r[0]] = {c:((($r[1]) // "0") | tonumber? // 0),
93
87
  cog:((($r[2]) // "0") | tonumber? // 0),
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env bash
2
+ # install-smoke-test.sh
3
+ #
4
+ # Exercises the real first-install path on a throwaway clone.
5
+ #
6
+ # The v2.8.3 install hang was invisible to `make test` because every suite runs
7
+ # against the working tree, and the working tree is never what a new user gets.
8
+ # A new user gets `git clone --depth 1` followed by manifest discovery and a
9
+ # writer pass over an empty HOME. This reproduces exactly that, in that order,
10
+ # so a regression in any of those steps fails a build instead of a user.
11
+ #
12
+ # Requires: git, node 18+. Does NOT require the `claude` CLI and never touches
13
+ # the network — the graph-engine fetch is stubbed out via --no-graph.
14
+ #
15
+ # Usage:
16
+ # scripts/tools/install-smoke-test.sh # clone this repo, run all checks
17
+ # scripts/tools/install-smoke-test.sh --repo <path> # smoke-test another checkout
18
+ # scripts/tools/install-smoke-test.sh --keep # leave the sandbox for inspection
19
+ # scripts/tools/install-smoke-test.sh --json
20
+ #
21
+ # Exit codes:
22
+ # 0 install path is healthy
23
+ # 1 a check failed
24
+ # 2 usage / runtime error
25
+
26
+ set -euo pipefail
27
+
28
+ usage() {
29
+ cat <<'EOF'
30
+ install-smoke-test.sh — clean-clone install smoke test
31
+
32
+ Options:
33
+ --repo <path> Repository to clone (default: this repo)
34
+ --keep Do not delete the sandbox on exit
35
+ --json Emit JSON instead of human-readable output
36
+ --help, -h Show this message
37
+ EOF
38
+ }
39
+
40
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
41
+ usage
42
+ exit 0
43
+ fi
44
+
45
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ # shellcheck source=_lib.sh
47
+ source "$SCRIPT_DIR/_lib.sh"
48
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
49
+ KEEP=0
50
+ EMIT_JSON=0
51
+
52
+ while [[ $# -gt 0 ]]; do
53
+ case "$1" in
54
+ --repo)
55
+ [[ -n "${2:-}" ]] || { echo "install-smoke-test: --repo requires a value" >&2; exit 2; }
56
+ REPO_ROOT="$2"; shift 2 ;;
57
+ --keep) KEEP=1; shift ;;
58
+ --json) EMIT_JSON=1; shift ;;
59
+ --help|-h) usage; exit 0 ;;
60
+ *) echo "install-smoke-test: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
61
+ esac
62
+ done
63
+
64
+ [[ -d "$REPO_ROOT" ]] || { echo "install-smoke-test: no such directory: $REPO_ROOT" >&2; exit 2; }
65
+ command -v git >/dev/null || { echo "install-smoke-test: git not found" >&2; exit 2; }
66
+ command -v node >/dev/null || { echo "install-smoke-test: node not found" >&2; exit 2; }
67
+
68
+ SANDBOX="$(mktemp -d "${TMPDIR:-/tmp}/draft-install-smoke.XXXXXX")"
69
+ cleanup() { [[ "$KEEP" -eq 1 ]] || rm -rf "$SANDBOX"; }
70
+ trap cleanup EXIT
71
+
72
+ CLONE="$SANDBOX/clone"
73
+ FAKE_HOME="$SANDBOX/home"
74
+ PROJECT="$SANDBOX/project"
75
+ mkdir -p "$FAKE_HOME" "$PROJECT"
76
+
77
+ FAILURES=()
78
+ RESULTS=()
79
+
80
+ # Collapse a file to one truncated line. Done in-shell rather than with
81
+ # `| cut -c1-200` because an early-closing reader plus `pipefail` turns a
82
+ # harmless truncation into a SIGPIPE failure of the whole script.
83
+ oneline() {
84
+ local text
85
+ text="$(tr '\n' ' ' < "$1")"
86
+ printf '%s' "${text:0:200}"
87
+ }
88
+
89
+ lastlines() {
90
+ local text
91
+ text="$(tail -n "$2" "$1" | tr '\n' ' ')"
92
+ printf '%s' "${text:0:200}"
93
+ }
94
+
95
+ record() {
96
+ local name="$1" ok="$2" detail="${3:-}"
97
+ RESULTS+=("$name|$ok|$detail")
98
+ if [[ "$ok" == "true" ]]; then
99
+ [[ "$EMIT_JSON" -eq 1 ]] || echo " PASS: $name${detail:+ — $detail}"
100
+ else
101
+ [[ "$EMIT_JSON" -eq 1 ]] || echo " FAIL: $name${detail:+ — $detail}"
102
+ FAILURES+=("$name")
103
+ fi
104
+ }
105
+
106
+ [[ "$EMIT_JSON" -eq 1 ]] || echo "=== Install smoke test (sandbox: $SANDBOX) ==="
107
+
108
+ # 1. Shallow clone — the literal first step of `plugin marketplace add`.
109
+ # file:// forces the real transfer path; a plain local path would hardlink
110
+ # and hide size problems.
111
+ if git clone --depth 1 --quiet "file://$REPO_ROOT" "$CLONE" 2>"$SANDBOX/clone.err"; then
112
+ record "shallow clone succeeds" "true"
113
+ else
114
+ record "shallow clone succeeds" "false" "$(oneline "$SANDBOX/clone.err")"
115
+ fi
116
+
117
+ if [[ -d "$CLONE" ]]; then
118
+ clone_mb="$(du -sm "$CLONE" | cut -f1)"
119
+ record "clone size recorded" "true" "${clone_mb} MB on disk"
120
+
121
+ # 2. Manifest discovery — install fails here if plugin.json is malformed or
122
+ # points at a skills directory the clone does not contain.
123
+ if node -e '
124
+ const fs = require("fs"), path = require("path");
125
+ const root = process.argv[1];
126
+ const plugin = JSON.parse(fs.readFileSync(path.join(root, ".claude-plugin/plugin.json"), "utf8"));
127
+ const market = JSON.parse(fs.readFileSync(path.join(root, ".claude-plugin/marketplace.json"), "utf8"));
128
+ if (!plugin.name) throw new Error("plugin.json has no name");
129
+ if (!Array.isArray(market.plugins) || market.plugins.length === 0) throw new Error("marketplace.json lists no plugins");
130
+ const skills = typeof plugin.skills === "string" ? plugin.skills : "./skills/";
131
+ const dir = path.join(root, skills);
132
+ if (!fs.existsSync(dir)) throw new Error("skills dir missing from clone: " + skills);
133
+ if (fs.readdirSync(dir).length === 0) throw new Error("skills dir is empty in clone");
134
+ ' "$CLONE" 2>"$SANDBOX/manifest.err"; then
135
+ record "plugin manifests resolve in the clone" "true"
136
+ else
137
+ record "plugin manifests resolve in the clone" "false" "$(oneline "$SANDBOX/manifest.err")"
138
+ fi
139
+
140
+ # 3. Every shipped skill must carry name+description frontmatter, or the
141
+ # host silently drops it at discovery time.
142
+ bad_skills=0
143
+ skill_count=0
144
+ while IFS= read -r skill; do
145
+ skill_count=$((skill_count + 1))
146
+ head -n 1 "$skill" | grep -q '^---$' || { bad_skills=$((bad_skills + 1)); continue; }
147
+ fm="$(sed -n '2,/^---$/p' "$skill")"
148
+ grep -q '^name:' <<< "$fm" || { bad_skills=$((bad_skills + 1)); continue; }
149
+ grep -q '^description:' <<< "$fm" || bad_skills=$((bad_skills + 1))
150
+ done < <(find "$CLONE/skills" -name SKILL.md -type f 2>/dev/null | sort)
151
+
152
+ if [[ "$skill_count" -eq 0 ]]; then
153
+ record "shipped skills discoverable" "false" "no SKILL.md files in clone"
154
+ else
155
+ record "shipped skills discoverable" \
156
+ "$([[ "$bad_skills" -eq 0 ]] && echo true || echo false)" \
157
+ "$skill_count skills, $bad_skills malformed"
158
+ fi
159
+
160
+ # 4. Writer pass against an empty HOME, per host. --dry-run plans every
161
+ # write without touching disk; --no-graph keeps it offline.
162
+ hosts="$(cd "$CLONE" && node -e '
163
+ const { hosts } = require("./cli/src/hosts");
164
+ console.log(hosts.map((h) => h.id).join(" "));
165
+ ' 2>/dev/null || true)"
166
+
167
+ if [[ -z "$hosts" ]]; then
168
+ record "host list enumerable" "false" "cli/src/hosts did not load"
169
+ else
170
+ record "host list enumerable" "true" "$hosts"
171
+ for host in $hosts; do
172
+ if (cd "$PROJECT" && HOME="$FAKE_HOME" node "$CLONE/cli/bin/draft.js" \
173
+ install "$host" --dry-run --no-graph >"$SANDBOX/$host.log" 2>&1); then
174
+ record "install --dry-run: $host" "true"
175
+ else
176
+ record "install --dry-run: $host" "false" "$(lastlines "$SANDBOX/$host.log" 3)"
177
+ fi
178
+ done
179
+ # A dry run that writes is worse than one that fails — it means the real
180
+ # installer's plan/apply split leaks. Both destinations must be checked:
181
+ # codex and opencode default to project scope and write AGENTS.md into the
182
+ # cwd, so a HOME-only assertion misses the likeliest leak.
183
+ record "dry run left HOME untouched" \
184
+ "$([[ -z "$(ls -A "$FAKE_HOME")" ]] && echo true || echo false)" \
185
+ "$(ls -A "$FAKE_HOME" | tr '\n' ' ')"
186
+ record "dry run left the project dir untouched" \
187
+ "$([[ -z "$(ls -A "$PROJECT")" ]] && echo true || echo false)" \
188
+ "$(ls -A "$PROJECT" | tr '\n' ' ')"
189
+ fi
190
+
191
+ # 5. Engine fetcher is present and self-documenting (never invoked here —
192
+ # it downloads).
193
+ if [[ -x "$CLONE/scripts/fetch-memory-engine.sh" ]]; then
194
+ record "graph engine fetcher shipped and executable" "true"
195
+ else
196
+ record "graph engine fetcher shipped and executable" "false" "scripts/fetch-memory-engine.sh missing or not executable"
197
+ fi
198
+ fi
199
+
200
+ if [[ "$EMIT_JSON" -eq 1 ]]; then
201
+ printf '{"sandbox":"%s","failures":%d,"checks":[' "$SANDBOX" "${#FAILURES[@]}"
202
+ sep=""
203
+ for r in "${RESULTS[@]}"; do
204
+ IFS='|' read -r name ok detail <<< "$r"
205
+ # json_escape (from _lib.sh) also handles backslashes and control chars —
206
+ # a Windows path or a stack trace in a captured stderr line used to emit
207
+ # invalid JSON.
208
+ printf '%s{"name":"%s","ok":%s,"detail":"%s"}' "$sep" "$(json_escape "$name")" "$ok" "$(json_escape "$detail")"
209
+ sep=","
210
+ done
211
+ printf ']}\n'
212
+ else
213
+ echo ""
214
+ if [[ "${#FAILURES[@]}" -eq 0 ]]; then
215
+ echo "Install path healthy — ${#RESULTS[@]} checks passed."
216
+ else
217
+ echo "Install path BROKEN — ${#FAILURES[@]} of ${#RESULTS[@]} checks failed:"
218
+ printf ' - %s\n' "${FAILURES[@]}"
219
+ fi
220
+ fi
221
+
222
+ [[ "${#FAILURES[@]}" -eq 0 ]] || exit 1
223
+ exit 0