@drafthq/draft 3.6.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +28 -49
- package/bin/README.md +47 -1
- package/cli/src/installer.js +1 -1
- package/cli/src/lib/cursor-registry.js +10 -0
- package/core/agents/architect.md +21 -9
- package/core/agents/debugger.md +5 -0
- package/core/agents/ops.md +7 -2
- package/core/agents/planner.md +7 -0
- package/core/agents/rca.md +14 -3
- package/core/agents/reviewer.md +1 -0
- package/core/agents/writer.md +6 -0
- package/core/knowledge-base.md +22 -3
- package/core/methodology.md +30 -5
- package/core/shared/condensation.md +27 -17
- package/core/shared/context-verify.md +3 -1
- package/core/shared/cross-skill-dispatch.md +7 -3
- package/core/shared/draft-context-loading.md +3 -0
- package/core/shared/git-report-metadata.md +1 -0
- package/core/shared/graph-query.md +0 -2
- package/core/shared/graph-usage-report.md +1 -1
- package/core/shared/jira-sync.md +10 -6
- package/core/shared/okf-retrieval.md +1 -1
- package/core/shared/parallel-analysis.md +14 -7
- package/core/shared/pattern-learning.md +2 -0
- package/core/shared/template-contract.md +1 -1
- package/core/shared/vcs-commands.md +5 -2
- package/core/templates/ai-context.md +3 -2
- package/core/templates/ai-profile.md +7 -0
- package/core/templates/architecture.md +6 -2
- package/core/templates/discovery.md +1 -1
- package/core/templates/guardrails.md +9 -0
- package/core/templates/hld.md +5 -4
- package/core/templates/intake-questions.md +45 -0
- package/core/templates/jira.md +11 -2
- package/core/templates/lld.md +8 -5
- package/core/templates/okf/concept.md +0 -1
- package/core/templates/product.md +11 -2
- package/core/templates/rca.md +6 -0
- package/core/templates/service-index.md +1 -0
- package/core/templates/spec.md +15 -0
- package/core/templates/tech-stack.md +12 -0
- package/core/templates/workflow.md +20 -0
- package/integrations/agents/AGENTS.md +1272 -292
- package/integrations/copilot/.github/copilot-instructions.md +1272 -292
- package/package.json +1 -1
- package/scripts/fetch-memory-engine.sh +24 -6
- package/scripts/lib.sh +11 -4
- package/scripts/tools/_graph_queries.sh +10 -3
- package/scripts/tools/_lib.sh +33 -11
- package/scripts/tools/check-repo-size.sh +125 -0
- package/scripts/tools/check-skill-line-caps.sh +1 -2
- package/scripts/tools/check-template-noop.sh +0 -1
- package/scripts/tools/classify-files.sh +2 -2
- package/scripts/tools/cycle-detect.sh +16 -8
- package/scripts/tools/fix-whitespace.sh +3 -0
- package/scripts/tools/git-metadata.sh +6 -0
- package/scripts/tools/graph-arch.sh +1 -1
- package/scripts/tools/graph-callers.sh +6 -3
- package/scripts/tools/graph-impact.sh +4 -2
- package/scripts/tools/graph-init.sh +12 -8
- package/scripts/tools/graph-preflight.sh +3 -0
- package/scripts/tools/graph-query.sh +70 -7
- package/scripts/tools/graph-snapshot.sh +29 -5
- package/scripts/tools/hotspot-rank.sh +24 -9
- package/scripts/tools/install-smoke-test.sh +223 -0
- package/scripts/tools/mermaid-from-graph.sh +28 -11
- package/scripts/tools/migrate-track-frontmatter.sh +9 -10
- package/scripts/tools/okf-coverage-check.sh +4 -3
- package/scripts/tools/okf-fix-links.sh +18 -93
- package/scripts/tools/okf-plan-concepts.sh +5 -8
- package/scripts/tools/okf-render-views.sh +19 -14
- package/scripts/tools/okf-validate-quality.sh +13 -2
- package/scripts/tools/okf-validate.sh +13 -2
- package/scripts/tools/parse-git-log.sh +19 -1
- package/scripts/tools/resolve-tools.sh +24 -8
- package/scripts/tools/verify-citations.sh +1 -2
- package/scripts/tools/verify-doc-anchors.sh +13 -33
- package/scripts/tools/verify-graph-binary.sh +13 -3
- package/skills/GRAPH.md +21 -10
- package/skills/adr/SKILL.md +24 -6
- package/skills/assist-review/SKILL.md +2 -1
- package/skills/bughunt/SKILL.md +54 -11
- package/skills/bughunt/references/regression-tests.md +14 -4
- package/skills/change/SKILL.md +19 -9
- package/skills/coverage/SKILL.md +26 -10
- package/skills/debug/SKILL.md +12 -2
- package/skills/decompose/SKILL.md +39 -9
- package/skills/deep-review/SKILL.md +17 -4
- package/skills/deploy-checklist/SKILL.md +9 -2
- package/skills/documentation/SKILL.md +12 -1
- package/skills/draft/SKILL.md +40 -29
- package/skills/draft/context-files.md +8 -1
- package/skills/draft/quality-guide.md +10 -2
- package/skills/graph/SKILL.md +1 -1
- package/skills/impact/SKILL.md +3 -1
- package/skills/implement/SKILL.md +60 -18
- package/skills/incident-response/SKILL.md +5 -2
- package/skills/init/SKILL.md +102 -23
- package/skills/init/references/architecture-spec.md +31 -7
- package/skills/init/references/okf-emitter.md +3 -3
- package/skills/jira/SKILL.md +33 -13
- package/skills/jira/references/review.md +61 -19
- package/skills/learn/SKILL.md +29 -6
- package/skills/new-track/SKILL.md +73 -18
- package/skills/quick-review/SKILL.md +7 -2
- package/skills/revert/SKILL.md +11 -5
- package/skills/review/SKILL.md +123 -24
- package/skills/review/references/zero-setup-mode.md +117 -0
- package/skills/standup/SKILL.md +9 -1
- package/skills/status/SKILL.md +6 -6
- package/skills/tech-debt/SKILL.md +7 -1
- package/skills/testing-strategy/SKILL.md +10 -2
- package/skills/tour/SKILL.md +2 -1
- package/skills/upload/SKILL.md +3 -2
|
@@ -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,
|
|
@@ -89,13 +92,74 @@ if [[ -n "$TOOL" ]]; then
|
|
|
89
92
|
}
|
|
90
93
|
fi
|
|
91
94
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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"
|
|
151
|
+
fi
|
|
152
|
+
|
|
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"
|
|
99
163
|
fi
|
|
100
164
|
|
|
101
165
|
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
|
|
@@ -108,7 +172,6 @@ if [[ -n "$CYPHER" ]]; then
|
|
|
108
172
|
printf '%s\n' "$RES"
|
|
109
173
|
else
|
|
110
174
|
# Inject the resolved project into the tool args unless the caller set one.
|
|
111
|
-
echo "$TOOL_JSON" | jq -e . >/dev/null 2>&1 || { echo "ERROR: --json is not valid JSON" >&2; exit 1; }
|
|
112
175
|
ARGS="$(echo "$TOOL_JSON" | jq -c --arg p "$PROJECT" 'if has("project") then . else . + {project:$p} end')"
|
|
113
176
|
RES="$(memory_cli "$TOOL" "$ARGS" 2>/dev/null || true)"
|
|
114
177
|
[[ -n "$RES" ]] || unavailable
|
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 "
|
|
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,7 +118,7 @@ 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 "
|
|
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)"
|
|
@@ -65,13 +65,26 @@ unavailable() { echo '{"hotspots":[],"source":"unavailable"}'; exit 2; }
|
|
|
65
65
|
|
|
66
66
|
graph_bootstrap "$REPO" || unavailable
|
|
67
67
|
|
|
68
|
-
ARCH_JSON="$(memory_cli get_architecture
|
|
68
|
+
ARCH_JSON="$(memory_cli get_architecture \
|
|
69
|
+
"$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["hotspots"]}')" || true)"
|
|
69
70
|
[[ -n "$ARCH_JSON" ]] || unavailable
|
|
70
71
|
echo "$ARCH_JSON" | jq -e . >/dev/null 2>&1 || unavailable
|
|
71
72
|
|
|
72
73
|
# Pre-computed complexity/cognitive/is_entry_point per symbol (merged onto fan-in).
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
#
|
|
75
|
+
# The hotspots themselves come from get_architecture above, which is fail-loud,
|
|
76
|
+
# so a failure here costs enrichment rather than the whole result — worth keeping
|
|
77
|
+
# the ranking for. But it must not pass silently: substituting an empty row set
|
|
78
|
+
# gave every symbol complexity 0 and cognitive 0, folded those zeros into `score`,
|
|
79
|
+
# and still reported source:"memory-graph", so a fan-in-only ranking was
|
|
80
|
+
# indistinguishable from a fully measured one. Record the outcome instead, and
|
|
81
|
+
# omit the fields rather than emitting measurements that were never taken.
|
|
82
|
+
if PROPS_JSON="$(gq_run "$PROJECT" "$(gq_q_node_props)")"; then
|
|
83
|
+
ENRICHMENT="ok"
|
|
84
|
+
else
|
|
85
|
+
PROPS_JSON='{"rows":[]}'
|
|
86
|
+
ENRICHMENT="unavailable"
|
|
87
|
+
fi
|
|
75
88
|
|
|
76
89
|
# Merge via temp files (the props row set can exceed argv limits on large repos).
|
|
77
90
|
TMP_ARCH="$(mktemp)"; TMP_PROPS="$(mktemp)"
|
|
@@ -79,7 +92,8 @@ trap 'rm -f "$TMP_ARCH" "$TMP_PROPS"' EXIT
|
|
|
79
92
|
printf '%s' "$ARCH_JSON" > "$TMP_ARCH"
|
|
80
93
|
printf '%s' "$PROPS_JSON" > "$TMP_PROPS"
|
|
81
94
|
|
|
82
|
-
jq -n --slurpfile arch "$TMP_ARCH" --slurpfile props "$TMP_PROPS" --argjson top "$TOP"
|
|
95
|
+
jq -n --slurpfile arch "$TMP_ARCH" --slurpfile props "$TMP_PROPS" --argjson top "$TOP" \
|
|
96
|
+
--arg enrichment "$ENRICHMENT" '
|
|
83
97
|
((($props[0].rows) // []) | map(select(.[0] != null))) as $prows
|
|
84
98
|
| (reduce $prows[] as $r ({};
|
|
85
99
|
.[$r[0]] = {c:((($r[1]) // "0") | tonumber? // 0),
|
|
@@ -88,10 +102,11 @@ jq -n --slurpfile arch "$TMP_ARCH" --slurpfile props "$TMP_PROPS" --argjson top
|
|
|
88
102
|
| [ (($arch[0].hotspots) // [])[]
|
|
89
103
|
| (.qualified_name) as $q
|
|
90
104
|
| ($pmap[$q] // {c:0, cog:0, ep:false}) as $p
|
|
91
|
-
| {id:$q, name:.name, fanIn:(.fan_in // 0),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
| ({id:$q, name:.name, fanIn:(.fan_in // 0),
|
|
106
|
+
score:((.fan_in // 0) + $p.c + $p.cog)}
|
|
107
|
+
+ (if $enrichment == "ok"
|
|
108
|
+
then {complexity:$p.c, cognitive:$p.cog, isEntryPoint:$p.ep}
|
|
109
|
+
else {} end)) ]
|
|
95
110
|
| sort_by(-.score)
|
|
96
111
|
| (if $top > 0 then .[0:$top] else . end) as $h
|
|
97
|
-
| {hotspots:$h, source:"memory-graph"}'
|
|
112
|
+
| {hotspots:$h, enrichment:$enrichment, source:"memory-graph"}'
|
|
@@ -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
|
|
@@ -57,25 +57,39 @@ if [[ ! -d "$REPO" ]]; then
|
|
|
57
57
|
exit 1
|
|
58
58
|
fi
|
|
59
59
|
|
|
60
|
+
# $1: "unavailable" (engine/query failed) or "empty" (query ran, no such edges).
|
|
61
|
+
# Swallowing a failed query with `{}` used to make both look identical, so an
|
|
62
|
+
# engine failure and a repo with genuinely no cross-file imports produced the
|
|
63
|
+
# same "graph not built" diagram — the reader could not tell which they had.
|
|
60
64
|
stub() {
|
|
61
|
-
|
|
65
|
+
if [[ "${1:-unavailable}" == "empty" ]]; then
|
|
66
|
+
cat <<'EOF'
|
|
67
|
+
```mermaid
|
|
68
|
+
%% graph is built, but holds no edges of this kind — nothing to draw
|
|
69
|
+
flowchart LR
|
|
70
|
+
empty["no edges"]
|
|
71
|
+
```
|
|
72
|
+
EOF
|
|
73
|
+
else
|
|
74
|
+
cat <<'EOF'
|
|
62
75
|
```mermaid
|
|
63
76
|
%% graph data unavailable — index the repo with the graph engine first
|
|
64
77
|
flowchart LR
|
|
65
78
|
empty["graph not built"]
|
|
66
79
|
```
|
|
67
80
|
EOF
|
|
81
|
+
fi
|
|
68
82
|
exit 2
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
graph_bootstrap "$REPO" || stub
|
|
85
|
+
graph_bootstrap "$REPO" || stub unavailable
|
|
72
86
|
|
|
73
87
|
# module-deps: real IMPORTS edges (the auto-derived dependency graph). Self-imports
|
|
74
88
|
# (src == dst) are dropped so the diagram is a true cross-file graph. Capped at 40
|
|
75
89
|
# edges for readability.
|
|
76
90
|
render_module_deps() {
|
|
77
|
-
local res; res="$(gq_run "$PROJECT" "$(gq_q_imports)" ||
|
|
78
|
-
local edges; edges="$(echo "$
|
|
91
|
+
local res; res="$(gq_run "$PROJECT" "$(gq_q_imports)")" || return 2
|
|
92
|
+
local edges; edges="$(echo "$res" | jq -r '
|
|
79
93
|
[ (.rows // [])[] | {s:(.[0]|tostring), d:(.[1]|tostring)}
|
|
80
94
|
| select(.s != "" and .d != "" and .s != .d) ]
|
|
81
95
|
| unique | .[0:40][] | " \"" + .s + "\" --> \"" + .d + "\""' 2>/dev/null || true)"
|
|
@@ -85,22 +99,25 @@ render_module_deps() {
|
|
|
85
99
|
|
|
86
100
|
# co-change: FILE_CHANGES_WITH coupling (the prior module-deps proxy).
|
|
87
101
|
render_co_change() {
|
|
88
|
-
local res; res="$(gq_run "$PROJECT" "$(gq_q_co_change)" ||
|
|
89
|
-
local edges; edges="$(echo "$
|
|
102
|
+
local res; res="$(gq_run "$PROJECT" "$(gq_q_co_change)")" || return 2
|
|
103
|
+
local edges; edges="$(echo "$res" | jq -r '(.rows // [])[] | " \"" + (.[0]|tostring) + "\" --> \"" + (.[1]|tostring) + "\""' 2>/dev/null || true)"
|
|
90
104
|
if [[ -z "$edges" ]]; then return 1; fi
|
|
91
105
|
printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
render_proto_map() {
|
|
95
|
-
local res; res="$(memory_cli get_architecture
|
|
96
|
-
|
|
109
|
+
local res; res="$(memory_cli get_architecture \
|
|
110
|
+
"$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["routes"]}')" || true)"
|
|
111
|
+
[[ -n "$res" ]] && echo "$res" | jq -e . >/dev/null 2>&1 || return 2
|
|
112
|
+
local edges; edges="$(echo "$res" | jq -r '(.routes // [])[] | " \"" + ((.method // "")|tostring) + " " + ((.path // "")|tostring) + "\" --> \"" + ((.handler // "?")|tostring) + "\""' 2>/dev/null || true)"
|
|
97
113
|
if [[ -z "$edges" ]]; then return 1; fi
|
|
98
114
|
printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
|
|
99
115
|
}
|
|
100
116
|
|
|
117
|
+
# rc 2 = the query failed; rc 1 = it ran and returned nothing. Different stubs.
|
|
101
118
|
case "$DIAGRAM" in
|
|
102
|
-
module-deps) render_module_deps || stub ;;
|
|
103
|
-
co-change) render_co_change || stub ;;
|
|
104
|
-
proto-map) render_proto_map || stub ;;
|
|
119
|
+
module-deps) render_module_deps || { rc=$?; [[ $rc -eq 1 ]] && stub empty || stub unavailable; } ;;
|
|
120
|
+
co-change) render_co_change || { rc=$?; [[ $rc -eq 1 ]] && stub empty || stub unavailable; } ;;
|
|
121
|
+
proto-map) render_proto_map || { rc=$?; [[ $rc -eq 1 ]] && stub empty || stub unavailable; } ;;
|
|
105
122
|
*) echo "Unknown --diagram '$DIAGRAM' (expected module-deps|co-change|proto-map)" >&2; exit 1 ;;
|
|
106
123
|
esac
|
|
@@ -69,15 +69,10 @@ EPHEMERAL_KEYS=(
|
|
|
69
69
|
"scope_excludes"
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"track_id"
|
|
77
|
-
"generated_by"
|
|
78
|
-
"generated_at"
|
|
79
|
-
"links"
|
|
80
|
-
)
|
|
72
|
+
# Keys that survive in markdown frontmatter: project, module, track_id,
|
|
73
|
+
# generated_by, generated_at, links. Recorded here for the reader only — the
|
|
74
|
+
# rewrite is driven entirely by EPHEMERAL_KEYS above (a drop-list, not an
|
|
75
|
+
# allow-list), so there is nothing to enumerate in code.
|
|
81
76
|
|
|
82
77
|
# Strip ephemeral blocks from a markdown file's YAML frontmatter.
|
|
83
78
|
# Approach: read the file; rewrite the frontmatter section so any line whose
|
|
@@ -225,7 +220,11 @@ EOF
|
|
|
225
220
|
(( BACKUP )) && cp "$path" "$path.bak"
|
|
226
221
|
# %s\n restores the EOF newline stripped by command substitution
|
|
227
222
|
# (same pattern as fix-whitespace.sh).
|
|
228
|
-
local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"
|
|
223
|
+
local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"
|
|
224
|
+
printf '%s\n' "$after" > "$_tmp"
|
|
225
|
+
# mktemp creates 0600 and `mv` swaps the inode — carry the mode across.
|
|
226
|
+
apply_dest_mode "$_tmp" "$path"
|
|
227
|
+
mv -f "$_tmp" "$path"
|
|
229
228
|
printf 'migrate: stripped ephemeral frontmatter from %s\n' "$path"
|
|
230
229
|
fi
|
|
231
230
|
fi
|
|
@@ -90,7 +90,7 @@ DEFERRED=() # concept_id\treason
|
|
|
90
90
|
EXPECTED_TOTAL=0; REQUIRED=0
|
|
91
91
|
|
|
92
92
|
# Iterate expected entries.
|
|
93
|
-
while IFS=$'\t' read -r cid required reason
|
|
93
|
+
while IFS=$'\t' read -r cid required reason _ftype fanin; do
|
|
94
94
|
[[ -z "$cid" ]] && continue
|
|
95
95
|
EXPECTED_TOTAL=$((EXPECTED_TOTAL + 1))
|
|
96
96
|
if [[ "$required" == "true" ]]; then
|
|
@@ -141,8 +141,8 @@ write_coverage_page() {
|
|
|
141
141
|
echo ""
|
|
142
142
|
echo "| Component | Wiki page | Status | Fan-in |"
|
|
143
143
|
echo "|-----------|-----------|--------|--------|"
|
|
144
|
-
local cid
|
|
145
|
-
while IFS=$'\t' read -r cid required reason
|
|
144
|
+
local cid fanin link
|
|
145
|
+
while IFS=$'\t' read -r cid required reason _ftype fanin; do
|
|
146
146
|
[[ -z "$cid" ]] && continue
|
|
147
147
|
if [[ "$required" == "true" ]]; then
|
|
148
148
|
if [[ -f "$BUNDLE/$cid" ]]; then
|
|
@@ -167,6 +167,7 @@ write_coverage_page() {
|
|
|
167
167
|
fi
|
|
168
168
|
done < <(jq -r '.expected[] | [.concept_id, (.required|tostring), (.reason_if_deferred // "-"), (.type // "Module"), (.fan_in // 0 | tostring)] | @tsv' "$PLAN")
|
|
169
169
|
} > "$tmp"
|
|
170
|
+
apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
|
|
170
171
|
mv "$tmp" "$out"
|
|
171
172
|
}
|
|
172
173
|
[[ $WRITE_PAGE -eq 1 ]] && write_coverage_page
|