compass

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.