@drafthq/draft 3.7.2 → 4.0.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.
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "draft-plugins",
3
3
  "owner": {
4
- "name": "mayurpise",
4
+ "name": "Mayur Pise",
5
+ "email": "mayur@getdraft.dev",
5
6
  "url": "https://github.com/mayurpise"
6
7
  },
7
8
  "metadata": {
@@ -12,9 +13,11 @@
12
13
  "name": "draft",
13
14
  "source": "./",
14
15
  "description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
15
- "version": "3.7.2",
16
+ "version": "4.0.0",
16
17
  "author": {
17
- "name": "mayurpise"
18
+ "name": "Mayur Pise",
19
+ "email": "mayur@getdraft.dev",
20
+ "url": "https://github.com/mayurpise"
18
21
  },
19
22
  "homepage": "https://github.com/drafthq/draft",
20
23
  "license": "MIT",
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "draft",
3
3
  "description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
4
- "version": "3.7.2",
4
+ "version": "4.0.0",
5
5
  "author": {
6
- "name": "mayurpise"
6
+ "name": "Mayur Pise",
7
+ "email": "mayur@getdraft.dev",
8
+ "url": "https://github.com/mayurpise"
7
9
  },
8
10
  "homepage": "https://github.com/drafthq/draft",
9
11
  "license": "MIT",
@@ -2,11 +2,13 @@
2
2
  "name": "draft",
3
3
  "displayName": "Draft",
4
4
  "description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
5
- "version": "3.7.2",
5
+ "version": "4.0.0",
6
6
  "skills": "./skills/",
7
7
  "agents": "./core/agents/",
8
8
  "author": {
9
- "name": "mayurpise"
9
+ "name": "Mayur Pise",
10
+ "email": "mayur@getdraft.dev",
11
+ "url": "https://github.com/mayurpise"
10
12
  },
11
13
  "homepage": "https://github.com/drafthq/draft",
12
14
  "license": "MIT",
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Mayur Pise
3
+ Copyright (c) 2025-2026 Mayur Pise
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -68,12 +68,12 @@ Each host installs the way that host actually loads extensions — no manual ste
68
68
 
69
69
  | Host | `draft install …` | What it does |
70
70
  |------|-------------------|--------------|
71
- | **Claude Code** | `claude-code` | Registers the plugin via `claude plugin marketplace add` + `claude plugin install` (user scope). Restart Claude Code. |
72
- | **Cursor** | `cursor` | Copies the plugin into `~/.cursor/plugins/local/draft/`, writes `.cursor-plugin/plugin.json`, registers `draft@draft-plugins` in Cursor's plugin registry, and enables it. Restart Cursor (or Developer: Reload Window). Existing installs upgrade with `draft install cursor --force`. |
73
- | **Codex** | `codex` | Writes `./AGENTS.md`, which Codex reads automatically. |
74
- | **opencode** | `opencode` | Writes `./AGENTS.md` + `~/.agents/skills/draft/`, both auto-discovered. |
71
+ | **Claude Code** | `claude-code` | Registers the plugin via `claude plugin marketplace add` + `claude plugin install` (default `--global` / user scope). Restart Claude Code. |
72
+ | **Cursor** | `cursor` | Default `--global`. Copies the plugin into `~/.cursor/plugins/local/draft/`, writes `.cursor-plugin/plugin.json`, registers `draft@draft-plugins` in Cursor's plugin registry, and enables it. Restart Cursor (or Developer: Reload Window). Existing installs upgrade with `draft install cursor --force`. |
73
+ | **Codex** | `codex` | Default `--project`. Writes `./AGENTS.md`, which Codex reads automatically. |
74
+ | **opencode** | `opencode` | Default `--project`. Writes `./AGENTS.md` + `~/.agents/skills/draft/`, both auto-discovered. |
75
75
 
76
- Flags: `--global` / `--project` to pick scope, `--dry-run` to preview, `--force` to overwrite, `--no-graph` to skip the graph-engine fetch.
76
+ Flags: `--global` / `--project` to pick scope (`--global` is the default for `claude-code` and `cursor`; `--project` is the default for `codex` and `opencode`), `--dry-run` to preview, `--force` to overwrite, `--no-graph` to skip the graph-engine fetch.
77
77
 
78
78
  Then, in Claude Code (after restarting):
79
79
 
@@ -164,7 +164,7 @@ The graph powers `/draft:graph` and `/draft:impact`, enriches `/draft:bughunt` a
164
164
 
165
165
  ### Deterministic helper tools
166
166
 
167
- Skills also call into **shell helpers** under `scripts/tools/` for mechanical work — git metadata, file classification, test-framework detection, hotspot ranking, freshness checks, ADR indexing, and live graph queries (`graph-callers.sh`, `graph-impact.sh`, `hotspot-rank.sh`, `cycle-detect.sh`, `mermaid-from-graph.sh`). All emit JSON or markdown, follow a uniform exit-code contract, and degrade gracefully when their input source is unavailable.
167
+ Skills also call into **shell helpers** under `scripts/tools/` for mechanical work — git metadata, file classification, test-framework detection, hotspot ranking, freshness checks, ADR indexing, and live graph queries (`graph-callers.sh`, `graph-impact.sh`, `hotspot-rank.sh`, `cycle-detect.sh`, `mermaid-from-graph.sh`). All emit JSON or markdown and follow a uniform exit-code contract. Graph wrappers fail loud: shapeless `{}` (no `.rows` array) is `source:"unavailable"` with a non-zero exit, not a measured empty result.
168
168
 
169
169
  ---
170
170
 
@@ -270,7 +270,7 @@ Each layer narrows the solution space. By the time AI writes code, decisions are
270
270
 
271
271
  ---
272
272
 
273
- <p align="center">MIT License &middot; Graph engine: <a href="https://github.com/DeusData/codebase-memory-mcp" target="_blank" rel="noopener noreferrer">codebase-memory-mcp</a> by <a href="https://github.com/DeusData" target="_blank" rel="noopener noreferrer">DeusData</a></p>
273
+ <p align="center">MIT License &middot; Created &amp; maintained by <a href="https://www.linkedin.com/in/mayurpise/" target="_blank" rel="noopener noreferrer">Mayur Pise</a> &middot; Graph engine: <a href="https://github.com/DeusData/codebase-memory-mcp" target="_blank" rel="noopener noreferrer">codebase-memory-mcp</a> by <a href="https://github.com/DeusData" target="_blank" rel="noopener noreferrer">DeusData</a></p>
274
274
 
275
275
  <p align="center">
276
276
  <strong>Credits:</strong> Inspired by <a href="https://github.com/gemini-cli-extensions/conductor">gemini-cli-extensions/conductor</a>
package/bin/README.md CHANGED
@@ -17,7 +17,7 @@ This installs the binary to the **Draft-managed location**:
17
17
  ~/.cache/draft/bin/codebase-memory-mcp
18
18
  ```
19
19
 
20
- The fetch script picks the right release archive for the host OS/arch, verifies its SHA-256 against the published `checksums.txt`, extracts it, and installs it there. `draft install claude-code` / `draft install cursor` run this automatically (best-effort, network-gated); skip it with `--no-graph`.
20
+ The fetch script picks the right release archive for the host OS/arch, verifies its SHA-256 against the published `checksums.txt`, extracts it, and installs it there. Downloads pin `curl --proto '=https' --proto-redir '=https'` so a compromised redirect cannot downgrade to HTTP. `draft install claude-code` / `draft install cursor` run this automatically (best-effort, network-gated); skip it with `--no-graph`.
21
21
 
22
22
  ## Resolution order
23
23
 
package/cli/src/cli.js CHANGED
@@ -47,8 +47,8 @@ Usage:
47
47
  Hosts: ${hosts.map((h) => h.id).join(', ')}
48
48
 
49
49
  Flags:
50
- --global Install to the user-level location (default for cursor)
51
- --project Install into the current project (default for claude-code, codex, opencode)
50
+ --global Install to the user-level location (default for claude-code, cursor)
51
+ --project Install into the current project (default for codex, opencode)
52
52
  --dry-run Print planned writes without touching disk
53
53
  --no-graph Skip the knowledge-graph engine fetch
54
54
  --force Overwrite an existing target
@@ -15,9 +15,11 @@ const CHECK_TIMEOUT_MS = 10000;
15
15
  const USE_SHELL = process.platform === 'win32';
16
16
 
17
17
  function hasBinary(name) {
18
- // ENOENT on the error means the binary is not on PATH.
18
+ // Windows `.cmd` shims (shell: true) report a missing binary as status 127
19
+ // with no `error`. Treat any spawn error or non-zero status as absent.
19
20
  const r = spawnSync(name, ['--version'], { stdio: 'ignore', timeout: CHECK_TIMEOUT_MS, shell: USE_SHELL });
20
- return !(r.error && r.error.code === 'ENOENT');
21
+ if (r.error) return false;
22
+ return r.status === 0;
21
23
  }
22
24
 
23
25
  // Per-step ceiling so a stalled network op (e.g. the `git clone` behind
@@ -78,7 +78,7 @@ function registerCursorPlugin(opts) {
78
78
  // --- installed_plugins.json: merge our key, preserve installedAt on upgrade. ---
79
79
  const ip = readJson(paths.ipPath, { version: 2, plugins: {} });
80
80
  if (typeof ip.version !== 'number') ip.version = 2;
81
- if (!ip.plugins || typeof ip.plugins !== 'object') ip.plugins = {};
81
+ if (!ip.plugins || typeof ip.plugins !== 'object' || Array.isArray(ip.plugins)) ip.plugins = {};
82
82
  const existing = Array.isArray(ip.plugins[PLUGIN_KEY]) ? ip.plugins[PLUGIN_KEY][0] : null;
83
83
  const installedAt = existing && existing.installedAt ? existing.installedAt : now;
84
84
  ip.plugins[PLUGIN_KEY] = [
@@ -93,7 +93,7 @@ function registerCursorPlugin(opts) {
93
93
 
94
94
  // --- settings.json: flip our enabledPlugins flag, preserve everything else. ---
95
95
  const settings = readJson(paths.settingsPath, {});
96
- if (!settings.enabledPlugins || typeof settings.enabledPlugins !== 'object') {
96
+ if (!settings.enabledPlugins || typeof settings.enabledPlugins !== 'object' || Array.isArray(settings.enabledPlugins)) {
97
97
  settings.enabledPlugins = {};
98
98
  }
99
99
  settings.enabledPlugins[PLUGIN_KEY] = true;
@@ -14,7 +14,8 @@ function fetchGraph() {
14
14
  return;
15
15
  }
16
16
  log.note('Fetching knowledge-graph engine (best-effort)...');
17
- const result = spawnSync('bash', [script], { stdio: 'inherit' });
17
+ const timeout = Number(process.env.DRAFT_INSTALL_TIMEOUT_MS) || 300000;
18
+ const result = spawnSync('bash', [script], { stdio: 'inherit', timeout });
18
19
  if (result.status !== 0) {
19
20
  log.warn('Graph engine fetch skipped (offline or unsupported platform) — features degrade gracefully.');
20
21
  }
@@ -103,7 +103,7 @@ DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}
103
103
  |---|---|---|
104
104
  | `bash "$DRAFT_TOOLS/hotspot-rank.sh" [--top N]` | complexity-weighted hotspots | Emits `{hotspots:[],source:"unavailable"}` and exits 2 |
105
105
  | `bash "$DRAFT_TOOLS/cycle-detect.sh"` | call cycles | Emits `{cycles:[],source:"unavailable"}` and exits 2 |
106
- | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Emits an empty mermaid block and exits 2 |
106
+ | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Unavailable stub (`graph not built`) vs empty-graph stub (`no edges`); both exit 2 |
107
107
  | `bash "$DRAFT_TOOLS/graph-callers.sh" --symbol N [--transitive[=N]] [--prod-only] [--qualified]` | callers | `{callers:[],status:"unavailable",source:"unavailable"}`, exit 2 |
108
108
  | `bash "$DRAFT_TOOLS/graph-snippet.sh" --qualified N` | verified source + caller/callee counts | `{status:"unavailable",source:"unavailable"}`, exit 2 |
109
109
  | `bash "$DRAFT_TOOLS/graph-search.sh" --query "STR" [--limit N]` | semantic/ranked search | `{results:[],source:"unavailable"}`, exit 2 |
@@ -133,7 +133,9 @@ bare `[]` as a confirmed true negative:
133
133
  | `ok` | node found, edges returned |
134
134
  | `no-edges` | node exists but has no matching edge (a *real* negative) |
135
135
  | `no-match` | the named symbol was not found at all (check the name / try `--qualified`) |
136
- | `unavailable` | engine could not be resolved (exit 2) |
136
+ | `unavailable` | engine could not be resolved, or engine returned shapeless/non-row JSON (exit 2) |
137
+
138
+ **Shapeless JSON is unavailable.** `gq_run` requires `has("rows") and (.rows|type=="array")`. A bare `{}` (or any object without a `.rows` array) is not a measured empty result — wrappers emit `source:"unavailable"` and a non-zero exit. Do not read `{}` as "no callers / no cycles / no edges". `graph-impact`, `graph-callers`, and `mermaid-from-graph` also require their tool-shaped object; a failed snapshot refresh does not rewrite `schema.yaml`.
137
139
 
138
140
  **Verified engine param shapes** (engine v0.8.x — the runtime source of truth is
139
141
  `get_graph_schema`; do not hardcode a property set):
@@ -204,7 +206,7 @@ The engine uses a **unified, language-agnostic** node model — `Function`, `Met
204
206
 
205
207
  ## Query Tools
206
208
 
207
- Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved. Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade gracefully.
209
+ Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved **or returns shapeless JSON** (missing `.rows` array). Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade to `unavailable` rather than inventing empty results.
208
210
 
209
211
  ### Callers — who calls this function?
210
212
 
@@ -371,6 +373,6 @@ The engine indexes C/C++, Go, Python, TypeScript/JS, and more (tree-sitter, 159
371
373
  | Scenario | Behavior |
372
374
  |----------|----------|
373
375
  | No engine resolvable (or `DRAFT_MEMORY_DISABLE=1`) | Skip graph indexing in init; all skills proceed without graph data; tools emit `source: unavailable` |
374
- | Engine present but a query fails | Warn and proceed; skills work without graph data |
376
+ | Engine present but a query fails, or returns shapeless `{}` | Treat as unavailable — never as a true-negative empty result; skills proceed without graph data |
375
377
  | `draft/graph/schema.yaml` exists | Engine is wired — use live query tools as needed during the run |
376
378
  | Engine index out of date | The engine indexes incrementally (content-based, git-aware) on each query, so it self-freshens. Re-run `graph-snapshot.sh` (or init) to force a reindex and refresh the marker. |
@@ -9604,7 +9604,7 @@ Prefer `draft docs` going forward for all authoring requests. The legacy direct
9604
9604
 
9605
9605
  ## Discover Router
9606
9606
 
9607
- When user says "discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, index, etc.):
9607
+ When user says "discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, tour, impact, etc.):
9608
9608
 
9609
9609
  `draft discover` is the single front door for all investigation, auditing, pattern learning, and quality exploration activities.
9610
9610
 
@@ -18204,7 +18204,7 @@ DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}
18204
18204
  |---|---|---|
18205
18205
  | `bash "$DRAFT_TOOLS/hotspot-rank.sh" [--top N]` | complexity-weighted hotspots | Emits `{hotspots:[],source:"unavailable"}` and exits 2 |
18206
18206
  | `bash "$DRAFT_TOOLS/cycle-detect.sh"` | call cycles | Emits `{cycles:[],source:"unavailable"}` and exits 2 |
18207
- | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Emits an empty mermaid block and exits 2 |
18207
+ | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Unavailable stub (`graph not built`) vs empty-graph stub (`no edges`); both exit 2 |
18208
18208
  | `bash "$DRAFT_TOOLS/graph-callers.sh" --symbol N [--transitive[=N]] [--prod-only] [--qualified]` | callers | `{callers:[],status:"unavailable",source:"unavailable"}`, exit 2 |
18209
18209
  | `bash "$DRAFT_TOOLS/graph-snippet.sh" --qualified N` | verified source + caller/callee counts | `{status:"unavailable",source:"unavailable"}`, exit 2 |
18210
18210
  | `bash "$DRAFT_TOOLS/graph-search.sh" --query "STR" [--limit N]` | semantic/ranked search | `{results:[],source:"unavailable"}`, exit 2 |
@@ -18234,7 +18234,9 @@ bare `[]` as a confirmed true negative:
18234
18234
  | `ok` | node found, edges returned |
18235
18235
  | `no-edges` | node exists but has no matching edge (a *real* negative) |
18236
18236
  | `no-match` | the named symbol was not found at all (check the name / try `--qualified`) |
18237
- | `unavailable` | engine could not be resolved (exit 2) |
18237
+ | `unavailable` | engine could not be resolved, or engine returned shapeless/non-row JSON (exit 2) |
18238
+
18239
+ **Shapeless JSON is unavailable.** `gq_run` requires `has("rows") and (.rows|type=="array")`. A bare `{}` (or any object without a `.rows` array) is not a measured empty result — wrappers emit `source:"unavailable"` and a non-zero exit. Do not read `{}` as "no callers / no cycles / no edges". `graph-impact`, `graph-callers`, and `mermaid-from-graph` also require their tool-shaped object; a failed snapshot refresh does not rewrite `schema.yaml`.
18238
18240
 
18239
18241
  **Verified engine param shapes** (engine v0.8.x — the runtime source of truth is
18240
18242
  `get_graph_schema`; do not hardcode a property set):
@@ -18305,7 +18307,7 @@ The engine uses a **unified, language-agnostic** node model — `Function`, `Met
18305
18307
 
18306
18308
  ## Query Tools
18307
18309
 
18308
- Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved. Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade gracefully.
18310
+ Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved **or returns shapeless JSON** (missing `.rows` array). Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade to `unavailable` rather than inventing empty results.
18309
18311
 
18310
18312
  ### Callers — who calls this function?
18311
18313
 
@@ -18472,7 +18474,7 @@ The engine indexes C/C++, Go, Python, TypeScript/JS, and more (tree-sitter, 159
18472
18474
  | Scenario | Behavior |
18473
18475
  |----------|----------|
18474
18476
  | No engine resolvable (or `DRAFT_MEMORY_DISABLE=1`) | Skip graph indexing in init; all skills proceed without graph data; tools emit `source: unavailable` |
18475
- | Engine present but a query fails | Warn and proceed; skills work without graph data |
18477
+ | Engine present but a query fails, or returns shapeless `{}` | Treat as unavailable — never as a true-negative empty result; skills proceed without graph data |
18476
18478
  | `draft/graph/schema.yaml` exists | Engine is wired — use live query tools as needed during the run |
18477
18479
  | Engine index out of date | The engine indexes incrementally (content-based, git-aware) on each query, so it self-freshens. Re-run `graph-snapshot.sh` (or init) to force a reindex and refresh the marker. |
18478
18480
 
@@ -9604,7 +9604,7 @@ Prefer `draft docs` going forward for all authoring requests. The legacy direct
9604
9604
 
9605
9605
  ## Discover Router
9606
9606
 
9607
- When user says "discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, index, etc.):
9607
+ When user says "discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, tour, impact, etc.):
9608
9608
 
9609
9609
  `draft discover` is the single front door for all investigation, auditing, pattern learning, and quality exploration activities.
9610
9610
 
@@ -18204,7 +18204,7 @@ DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}
18204
18204
  |---|---|---|
18205
18205
  | `bash "$DRAFT_TOOLS/hotspot-rank.sh" [--top N]` | complexity-weighted hotspots | Emits `{hotspots:[],source:"unavailable"}` and exits 2 |
18206
18206
  | `bash "$DRAFT_TOOLS/cycle-detect.sh"` | call cycles | Emits `{cycles:[],source:"unavailable"}` and exits 2 |
18207
- | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Emits an empty mermaid block and exits 2 |
18207
+ | `bash "$DRAFT_TOOLS/mermaid-from-graph.sh" [--diagram module-deps\|co-change\|proto-map]` | diagram text | Unavailable stub (`graph not built`) vs empty-graph stub (`no edges`); both exit 2 |
18208
18208
  | `bash "$DRAFT_TOOLS/graph-callers.sh" --symbol N [--transitive[=N]] [--prod-only] [--qualified]` | callers | `{callers:[],status:"unavailable",source:"unavailable"}`, exit 2 |
18209
18209
  | `bash "$DRAFT_TOOLS/graph-snippet.sh" --qualified N` | verified source + caller/callee counts | `{status:"unavailable",source:"unavailable"}`, exit 2 |
18210
18210
  | `bash "$DRAFT_TOOLS/graph-search.sh" --query "STR" [--limit N]` | semantic/ranked search | `{results:[],source:"unavailable"}`, exit 2 |
@@ -18234,7 +18234,9 @@ bare `[]` as a confirmed true negative:
18234
18234
  | `ok` | node found, edges returned |
18235
18235
  | `no-edges` | node exists but has no matching edge (a *real* negative) |
18236
18236
  | `no-match` | the named symbol was not found at all (check the name / try `--qualified`) |
18237
- | `unavailable` | engine could not be resolved (exit 2) |
18237
+ | `unavailable` | engine could not be resolved, or engine returned shapeless/non-row JSON (exit 2) |
18238
+
18239
+ **Shapeless JSON is unavailable.** `gq_run` requires `has("rows") and (.rows|type=="array")`. A bare `{}` (or any object without a `.rows` array) is not a measured empty result — wrappers emit `source:"unavailable"` and a non-zero exit. Do not read `{}` as "no callers / no cycles / no edges". `graph-impact`, `graph-callers`, and `mermaid-from-graph` also require their tool-shaped object; a failed snapshot refresh does not rewrite `schema.yaml`.
18238
18240
 
18239
18241
  **Verified engine param shapes** (engine v0.8.x — the runtime source of truth is
18240
18242
  `get_graph_schema`; do not hardcode a property set):
@@ -18305,7 +18307,7 @@ The engine uses a **unified, language-agnostic** node model — `Function`, `Met
18305
18307
 
18306
18308
  ## Query Tools
18307
18309
 
18308
- Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved. Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade gracefully.
18310
+ Live queries go through the shell tools under `scripts/tools/`, which drive the engine and shape results into stable JSON. Each tool resolves the engine (see Finding the Engine), indexes the repo on demand, and emits `source: "memory-graph"` on success or `source: "unavailable"` (non-zero exit) when the engine cannot be resolved **or returns shapeless JSON** (missing `.rows` array). Set `DRAFT_MEMORY_DISABLE=1` to force the engine off; all tools then degrade to `unavailable` rather than inventing empty results.
18309
18311
 
18310
18312
  ### Callers — who calls this function?
18311
18313
 
@@ -18472,7 +18474,7 @@ The engine indexes C/C++, Go, Python, TypeScript/JS, and more (tree-sitter, 159
18472
18474
  | Scenario | Behavior |
18473
18475
  |----------|----------|
18474
18476
  | No engine resolvable (or `DRAFT_MEMORY_DISABLE=1`) | Skip graph indexing in init; all skills proceed without graph data; tools emit `source: unavailable` |
18475
- | Engine present but a query fails | Warn and proceed; skills work without graph data |
18477
+ | Engine present but a query fails, or returns shapeless `{}` | Treat as unavailable — never as a true-negative empty result; skills proceed without graph data |
18476
18478
  | `draft/graph/schema.yaml` exists | Engine is wired — use live query tools as needed during the run |
18477
18479
  | Engine index out of date | The engine indexes incrementally (content-based, git-aware) on each query, so it self-freshens. Re-run `graph-snapshot.sh` (or init) to force a reindex and refresh the marker. |
18478
18480
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drafthq/draft",
3
- "version": "3.7.2",
3
+ "version": "4.0.0",
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"
@@ -37,7 +37,11 @@
37
37
  "url": "https://github.com/drafthq/draft/issues"
38
38
  },
39
39
  "license": "MIT",
40
- "author": "mayurpise",
40
+ "author": {
41
+ "name": "Mayur Pise",
42
+ "email": "mayur@getdraft.dev",
43
+ "url": "https://github.com/mayurpise"
44
+ },
41
45
  "publishConfig": {
42
46
  "access": "public"
43
47
  },
@@ -80,7 +80,7 @@ TMP="$(mktemp -d)"
80
80
  trap 'rm -rf "$TMP"' EXIT
81
81
 
82
82
  echo "Fetching ${ARCHIVE} (${VERSION})..."
83
- if ! curl -fSL --max-time 300 -o "$TMP/$ARCHIVE" "$BASE/$ARCHIVE"; then
83
+ if ! curl -fSL --proto '=https' --proto-redir '=https' --max-time 300 -o "$TMP/$ARCHIVE" "$BASE/$ARCHIVE"; then
84
84
  echo "error: download failed: $BASE/$ARCHIVE" >&2
85
85
  exit 2
86
86
  fi
@@ -98,7 +98,7 @@ unverified() {
98
98
  echo " warning: $1 — skipping verification (set DRAFT_STRICT_VERIFY=1 to make this fatal)" >&2
99
99
  }
100
100
 
101
- if curl -fsSL --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev/null; then
101
+ if curl -fsSL --proto '=https' --proto-redir '=https' --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev/null; then
102
102
  expected="$(grep " $ARCHIVE\$" "$TMP/checksums.txt" 2>/dev/null | awk '{print $1}' | head -1 || true)"
103
103
  if [[ -n "$expected" ]]; then
104
104
  if command -v sha256sum >/dev/null 2>&1; then
package/scripts/lib.sh CHANGED
@@ -84,7 +84,7 @@ SKILL_META=(
84
84
  'plan|Plan Router|"plan feature" or "draft plan <intent>" (new-track, decompose, adr, tech-debt, change)'
85
85
  'ops|Ops Router|"ops deploy" or "draft ops <intent>" (deploy-checklist, incident, standup, status, revert)'
86
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.)'
87
+ 'discover|Discover Router|"discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, tour, impact, etc.)'
88
88
  'jira|Jira Router|"jira preview", "jira create", or "jira review <ID>"'
89
89
  'integrations|Integrations Router|"integrations", "integrations jira-preview", or "integrations jira-create"'
90
90
  'quick-review|Quick Review Command|"quick review" or "draft quick-review [file|pr <number>]"'
@@ -83,7 +83,9 @@ gq_run() {
83
83
  payload="$(jq -n --arg p "$project" --arg q "$query" '{project:$p, query:$q}')" || return 3
84
84
  res="$(memory_cli query_graph "$payload" 2>/dev/null || true)"
85
85
  [[ -n "$res" ]] || return 3
86
- printf '%s' "$res" | jq -e . >/dev/null 2>&1 || return 3
86
+ # Shapeless JSON (`{}`) is not an empty result it is a failed query.
87
+ # Callers must not treat it as a measured true-negative.
88
+ printf '%s' "$res" | jq -e 'has("rows") and (.rows | type == "array")' >/dev/null 2>&1 || return 3
87
89
  printf '%s' "$res"
88
90
  }
89
91
 
@@ -109,7 +109,11 @@ else
109
109
  # `head` lets head close the pipe early, which under `pipefail` surfaces
110
110
  # sort's SIGPIPE as a hard failure — non-deterministically, depending on
111
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)"
112
+ # git ls-tree -l: "mode type sha size<TAB>path" path may contain spaces.
113
+ largest="$(awk -F '\t' '$1 ~ / [0-9]+$/ {
114
+ n = split($1, a, " ")
115
+ printf " %8.2f MB %s\n", a[n] / 1048576, $2
116
+ }' <<< "$listing" | sort -rn)"
113
117
  head -n "$TOP" <<< "$largest"
114
118
  fi
115
119
  fi
@@ -64,8 +64,8 @@ graph_bootstrap "$REPO" || unavailable
64
64
  # derived from a query that never ran, which is exactly the true-negative
65
65
  # confusion Guardrail 4 exists to prevent, and the one failure mode a caller
66
66
  # cannot detect. Both queries must land for the sample to mean anything, so
67
- # either failure routes to `unavailable`. gq_run only ever echoes validated
68
- # JSON, so nothing downstream needs a second shape guard.
67
+ # either failure routes to `unavailable`. gq_run requires a `.rows` array, so
68
+ # shapeless JSON (`{}`) is unavailable, not an empty sample.
69
69
  R2="$(gq_run "$PROJECT" "$(gq_q_cycles2)")" || unavailable
70
70
  R3="$(gq_run "$PROJECT" "$(gq_q_cycles3)")" || unavailable
71
71
 
@@ -88,7 +88,7 @@ if [[ "$TRANSITIVE" -eq 1 ]]; then
88
88
  PAYLOAD="$(jq -n --arg p "$PROJECT" --arg f "$SYMBOL" --argjson d "$DEPTH" \
89
89
  '{project:$p, function_name:$f, depth:$d, direction:"both"}')"
90
90
  RES="$(memory_cli trace_path "$PAYLOAD" 2>/dev/null || true)"
91
- echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable
91
+ echo "$RES" | jq -e 'has("callers") and (.callers | type == "array")' >/dev/null 2>&1 || unavailable
92
92
  N="$(echo "$RES" | jq -r '(.callers // []) | length' 2>/dev/null || echo 0)"
93
93
  if [[ "$N" -gt 0 ]]; then STATUS="ok"; else
94
94
  STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" '{"rows":[]}')"
@@ -77,7 +77,8 @@ if [[ -n "$SYMBOL" ]]; then
77
77
  PAYLOAD="$(jq -n --arg p "$PROJECT" --arg f "$SYMBOL" --argjson d "$DEPTH" \
78
78
  '{project:$p, function_name:$f, depth:$d, direction:"both"}')"
79
79
  RES="$(memory_cli trace_path "$PAYLOAD" 2>/dev/null || true)"
80
- echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
80
+ echo "$RES" | jq -e 'has("callers") and (.callers | type == "array")' >/dev/null 2>&1 \
81
+ || unavailable "$TARGET" "$KIND"
81
82
  echo "$RES" | jq --arg t "$TARGET" '
82
83
  {target:$t, kind:"symbol",
83
84
  impacted: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
@@ -86,7 +87,8 @@ else
86
87
  # File impact: detect_changes maps the working-tree diff to impacted symbols.
87
88
  PAYLOAD="$(jq -n --arg p "$PROJECT" '{project:$p}')"
88
89
  RES="$(memory_cli detect_changes "$PAYLOAD" 2>/dev/null || true)"
89
- echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
90
+ echo "$RES" | jq -e 'has("impacted_symbols") and (.impacted_symbols | type == "array")' >/dev/null 2>&1 \
91
+ || unavailable "$TARGET" "$KIND"
90
92
  echo "$RES" | jq --arg t "$TARGET" '
91
93
  {target:$t, kind:"file",
92
94
  impacted: [ (.impacted_symbols // [])[]
@@ -80,7 +80,11 @@ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
80
80
  # one never appeared, and nothing in the output said so. Refreshing is this tool's
81
81
  # entire job. The engine indexes incrementally, so the repeat call is cheap.
82
82
  REFRESHED="$(memory_index_bounded "$REPO_ABS" 2>/dev/null | jq -r '.project // empty' 2>/dev/null || true)"
83
- [[ -n "$REFRESHED" ]] && PROJECT="$REFRESHED"
83
+ if [[ -z "$REFRESHED" ]]; then
84
+ echo "index refresh failed — nothing written" >&2
85
+ exit 2
86
+ fi
87
+ PROJECT="$REFRESHED"
84
88
 
85
89
  mkdir -p "$OUT"
86
90
 
@@ -108,7 +108,7 @@ render_co_change() {
108
108
  render_proto_map() {
109
109
  local res; res="$(memory_cli get_architecture \
110
110
  "$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["routes"]}')" || true)"
111
- [[ -n "$res" ]] && echo "$res" | jq -e . >/dev/null 2>&1 || return 2
111
+ [[ -n "$res" ]] && echo "$res" | jq -e 'has("routes") and (.routes | type == "array")' >/dev/null 2>&1 || return 2
112
112
  local edges; edges="$(echo "$res" | jq -r '(.routes // [])[] | " \"" + ((.method // "")|tostring) + " " + ((.path // "")|tostring) + "\" --> \"" + ((.handler // "?")|tostring) + "\""' 2>/dev/null || true)"
113
113
  if [[ -z "$edges" ]]; then return 1; fi
114
114
  printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
@@ -82,6 +82,9 @@ sniff_desc() {
82
82
 
83
83
  write_page() {
84
84
  local cid="$1" ctype="$2" resource="$3" fan_in="$4"
85
+ case "$cid" in
86
+ ''|/*|*..*) echo "ERROR: concept_id escapes bundle: $cid" >&2; return 1 ;;
87
+ esac
85
88
  local out="$BUNDLE/$cid"
86
89
  mkdir -p "$(dirname "$out")"
87
90
  if [[ -f "$out" && $FORCE -eq 0 ]]; then
@@ -226,7 +226,7 @@ render_architecture() {
226
226
  if [[ -x "$SCRIPT_DIR/okf-fix-links.sh" ]]; then
227
227
  draft_dir="$(cd "$(dirname "$out")" && pwd)"
228
228
  if [[ -d "$draft_dir/wiki" || "$(basename "$BUNDLE")" == "wiki" ]]; then
229
- "$SCRIPT_DIR/okf-fix-links.sh" --file "$out" --wiki "$BUNDLE" --fix >/dev/null 2>&1 || true
229
+ "$SCRIPT_DIR/okf-fix-links.sh" --file "$out" --wiki "$BUNDLE" --fix >/dev/null 2>&1 || exit 1
230
230
  fi
231
231
  fi
232
232
  echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
@@ -411,13 +411,15 @@ function resolve(base, href){
411
411
  if(p==='..') out.pop(); else if(p!=='.'&&p!=='') out.push(p); }
412
412
  return out.join('/');
413
413
  }
414
+ function escAttr(s){return esc(s).replace(/"/g,'&quot;');}
414
415
  function inline(s, base){
415
416
  s = s.replace(/`([^`]+)`/g, function(m,c){return '<code>'+esc(c)+'</code>';});
416
417
  s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(m,t,u){
417
- if(/^[a-z]+:\/\//.test(u)) return '<a href="'+u+'" target="_blank" rel="noopener">'+t+'</a>';
418
+ if(/^(https?|mailto):/i.test(u)) return '<a href="'+escAttr(u)+'" target="_blank" rel="noopener">'+t+'</a>';
419
+ if(/^[a-z][a-z0-9+.-]*:/i.test(u)) return t;
418
420
  var key=resolve(base,u);
419
421
  if(PAGES[key]) return '<a href="#'+key+'" data-nav="'+key+'">'+t+'</a>';
420
- return '<span title="'+esc(u)+'">'+t+'</span>';
422
+ return '<span title="'+escAttr(u)+'">'+t+'</span>';
421
423
  });
422
424
  s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
423
425
  s = s.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>');
@@ -438,7 +440,7 @@ function render(md, base){
438
440
  out+='<table>';
439
441
  rows.forEach(function(r,ri){
440
442
  var cells=r.replace(/^\||\|$/g,'').split('|');
441
- out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(c.trim(),base)+'</'+t+'>';}).join('')+'</tr>';
443
+ out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(esc(c.trim()),base)+'</'+t+'>';}).join('')+'</tr>';
442
444
  });
443
445
  out+='</table>'; tbl=[];
444
446
  }
@@ -252,6 +252,11 @@ if [[ -n "$PATH_INDEX" ]]; then
252
252
  # span lines and a line-oriented match would silently skip them.
253
253
  while IFS= read -r ref; do
254
254
  [[ -z "$ref" ]] && continue
255
+ case "$ref" in
256
+ /*|*..*)
257
+ add_error "path-index escapes the bundle: $ref"
258
+ continue ;;
259
+ esac
255
260
  if [[ ! -f "$BUNDLE/$ref" ]]; then
256
261
  add_error "path-index references missing concept page: $ref"
257
262
  fi
@@ -53,8 +53,10 @@ resolve() {
53
53
  # 2. Install marker written by `draft install` (authoritative).
54
54
  local marker="$HOME/.cache/draft/plugin-root"
55
55
  if [ -f "$marker" ]; then
56
- d="$(cat "$marker" 2>/dev/null)/scripts/tools"
57
- [ -d "$d" ] && { printf '%s' "$d"; return 0; }
56
+ local root
57
+ root="$(cat "$marker" 2>/dev/null || true)"
58
+ d="${root}/scripts/tools"
59
+ [ -n "$root" ] && [ -d "$d" ] && { printf '%s' "$d"; return 0; }
58
60
  fi
59
61
 
60
62
  # 3. ${CLAUDE_PLUGIN_ROOT} — set in hook/MCP contexts; harmless to probe.
@@ -66,7 +68,7 @@ resolve() {
66
68
  if command -v jq >/dev/null 2>&1 && [ -f "$reg" ]; then
67
69
  local ip
68
70
  ip="$(jq -r '.plugins | to_entries[] | select(.key|startswith("draft@")) | .value[0].installPath' \
69
- "$reg" 2>/dev/null | head -1)"
71
+ "$reg" 2>/dev/null | head -1 || true)"
70
72
  [ -n "$ip" ] && [ -d "$ip/scripts/tools" ] && { printf '%s' "$ip/scripts/tools"; return 0; }
71
73
  fi
72
74
 
@@ -78,7 +78,15 @@ emit_array() {
78
78
  local first=true
79
79
  printf '['
80
80
  while IFS= read -r _ln; do
81
- path="${_ln%%:*}"; rest="${_ln#*:}"; linenum="${rest%%:*}"; rest="${rest#*:}"
81
+ # Path may contain colons (e.g. src/foo:bar.js). The line number is the
82
+ # last :<digits>: separator git-grep/rg -n emit.
83
+ if [[ "$_ln" =~ ^(.+):([0-9]+):(.*)$ ]]; then
84
+ path="${BASH_REMATCH[1]}"
85
+ linenum="${BASH_REMATCH[2]}"
86
+ rest="${BASH_REMATCH[3]}"
87
+ else
88
+ continue
89
+ fi
82
90
  [[ -z "$path" || -z "$linenum" ]] && continue
83
91
  # Skip binaries or tool's own output directory.
84
92
  [[ "$path" == */.git/* ]] && continue
@@ -27,7 +27,7 @@ Draft commands can be invoked using natural language. If you describe your goal
27
27
  | "debug this issue", "investigate test failure", "fix crash" | `/draft:debug` | Structured 4-stage debugging workflow |
28
28
  | "deploy checklist", "release checks", "pre-flight checks" | `/draft:deploy-checklist` | Pre-deployment verification checklist |
29
29
  | "test strategy", "design test suite", "testing targets" | `/draft:testing-strategy` | Design standard testing plan |
30
- | "tech debt analysis", "catalog debt", "code debt" | `/draft:tech-debt` | Technical debt audit across 6 dimensions |
30
+ | "tech debt analysis", "catalog debt", "code debt" | `/draft:tech-debt` | Technical debt audit across 7 dimensions |
31
31
  | "weekly standup", "what did I do today", "activity summary" | `/draft:standup` | Summarize recent Git and file contributions |
32
32
  | "incident", "production outage", "mitigate bug" | `/draft:incident-response` | Triage, mitigation, and postmortem incident flow |
33
33
  | "write docs", "create readme", "api documentation" | `/draft:documentation` | Generate professional, structured docs |