add orient-entrypoints: Makefile target detection
commit a4ee40002bf21a228cde34daf1008e8c7e6286c6 · parent 0dbc57fe4e
· browse this tree
Ximianth, 2026-06-24 13:26:51 UTC
Detects an entrypoint system at the project root (currently just Makefile) and lists its targets so an agent can act without an exploratory roundtrip. Hard rule: parse only, never invoke. Repos may carry untrusted content, so the existence of files must not translate into code execution. Makefile parsing has two passes: - annotated: 'target: ## description' (a widely cargo-culted convention), treated as the author's curated entrypoint surface when present. - bare: fallback regex over 'name:' lines, skipping pattern rules and dotted internals. Both passes are pure regex; we never expand variables or follow includes. A Makefile that defines targets via include/eval will look empty here, and that's the correct safe behavior. The skeleton is set up so other systems (justfile, Taskfile, scripts/) can be added later as independent blocks following the same rules.
diff --git a/README.md b/README.md index fd9ed3b..aff2618 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ or compose into larger summaries (e.g. an agent's system prompt). - `orient-cwd` -- describes the current working directory. - `orient-git` -- describes the surrounding git repo, if any. - `orient-project` -- inlines root orientation docs and detects a meta repo. +- `orient-entrypoints` -- detects entrypoint systems (Makefile etc.) and lists their targets. - `summarize-md-toc` -- emits a markdown file's heading structure as a bullet list. Scripts share verb-prefix conventions (`orient-*`, `summarize-*`, ...) diff --git a/orient-entrypoints b/orient-entrypoints new file mode 100755 index 0000000..87e555f --- /dev/null +++ b/orient-entrypoints @@ -0,0 +1,119 @@ +#!/bin/bash +# Detects project entrypoint systems (Makefile etc.) and surfaces their +# targets so an agent can act without an exploratory roundtrip. +# +# Usage: orient-entrypoints [PATH] +# PATH defaults to the git toplevel of cwd, or cwd itself if not in a repo. +# +# Silent (no output, exit 0) when no entrypoint system is detected. +# Each detected system emits an h4 heading; intended to sit under an h3. +# +# Hard rule: parse only, never invoke. +# Repos can be untrusted content (e.g. freshly cloned from elsewhere); +# the existence of files in the repo must not translate into code execution. +# Any future addition here must follow the same rule. + +set -euo pipefail + +# Resolve the project root (mirrors orient-project). +if [ $# -ge 1 ]; then + root=$(cd "$1" && pwd) +elif root=$(git rev-parse --show-toplevel 2>/dev/null); then + : +else + root="$PWD" +fi + +# Cap on how many targets we'll list per system. +# Generous; mirrors orient-project's listing cap. +MAX_TARGETS=50 + +# --- Makefile ------------------------------------------------------------- +# +# Search order matches GNU make's own (GNUmakefile, makefile, Makefile). +# We only consider the first one found, the same as make would. +makefile="" +for candidate in GNUmakefile makefile Makefile; do + if [ -f "$root/$candidate" ]; then + makefile="$root/$candidate" + break + fi +done + +if [ -n "$makefile" ]; then + rel="${makefile#$root/}" + echo "#### \`${rel}\`" + echo + + # Two-pass parse, in order of preference: + # + # 1. Annotated targets: `name: [deps] ## description`. + # A widespread cargo-culted convention (often paired with a `help` target + # that greps for the same pattern). When present, treat it as the + # author's curated entrypoint surface. + # + # 2. Bare targets: `name:` or `name: deps`. + # Fallback when no annotations are found. Best-effort: filters out + # pattern rules (`%`), variable assignments (`:=`, `=`, etc.), and + # dotted internal targets (`.PHONY`, `.SUFFIXES`, ...) via the leading + # char class. + # + # Both passes are pure regex over the file; we never expand variables or + # follow includes. That means a Makefile that defines targets via include + # or eval will look empty here -- and that's the correct, safe behavior. + + annotated=$(grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*:[^=]*##' "$makefile" || true) + + if [ -n "$annotated" ]; then + echo "Annotated targets (\`name: ## description\` convention):" + echo + printf '%s\n' "$annotated" \ + | awk -F'##' '{ + split($1, a, ":"); + name = a[1]; + gsub(/^[ \t]+|[ \t]+$/, "", name); + desc = $2; + gsub(/^[ \t]+|[ \t]+$/, "", desc); + printf "- `%s` -- %s\n", name, desc; + }' \ + | head -n "$MAX_TARGETS" + total=$(printf '%s\n' "$annotated" | wc -l) + if [ "$total" -gt "$MAX_TARGETS" ]; then + echo "- ... ($((total - MAX_TARGETS)) more not shown)" + fi + else + # Bare-target parse. + # Regex requires `:` (or `::`) followed by space or end-of-line, which + # naturally excludes `:=` and other assignment forms. + mapfile -t targets < <( + grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*::?([[:space:]]|$)' "$makefile" \ + | grep -v '%' \ + | sed -E 's/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*).*/\1/' \ + | awk '!seen[$0]++' \ + || true + ) + if [ ${#targets[@]} -gt 0 ]; then + echo "Targets (parsed; no \`## description\` annotations found):" + echo + n=0 + for t in "${targets[@]}"; do + if [ "$n" -ge "$MAX_TARGETS" ]; then + echo "- ... ($((${#targets[@]} - MAX_TARGETS)) more not shown)" + break + fi + echo "- \`$t\`" + n=$((n + 1)) + done + else + # Makefile present but yielded no targets we recognize. + # Useful signal in itself: tells the agent "go read it". + echo "(No targets parsed; the Makefile may define targets via include or eval.)" + fi + fi +fi + +# --- Future entrypoint systems -------------------------------------------- +# +# Add detection blocks here for justfile, Taskfile.yml, scripts/, etc. +# Same rules apply: parse only, never invoke. Each block stays silent if +# its system isn't present, and emits an h4 heading when it is.