|
| 1 | +#!/usr/bin/env bash |
| 2 | +# polylane-promptlint.sh — deterministic gate on a GENERATED lane prompt before launch. |
| 3 | +# The orchestrator writes each .polylane/lanes/<lane>.txt with an LLM, so a block can be |
| 4 | +# dropped (the marker-drift + missing-OWN/FORBIDDEN bugs were exactly this). This lints |
| 5 | +# for the empirically-validated structure — objective, tool/file boundaries, the nonce |
| 6 | +# DONE contract, verify evidence — the way DSPy/promptfoo enforce prompt quality, but |
| 7 | +# cheap and offline (the prompts are generated fresh per lane, so there's nothing to |
| 8 | +# compile). Reports PROMPT-LINT: <lane> missing <what>; exit 6 if any lane fails. |
| 9 | +# lint <lane-prompt-file> [<lane-name>] one prompt |
| 10 | +# lint-run <manifest> every lane's prompt in a run.json |
| 11 | +# Pure bash-3.2 + jq (jq only for lint-run); main-guarded. |
| 12 | +set -euo pipefail |
| 13 | + |
| 14 | +# required token -> human label. A prompt must contain each (case-insensitive). |
| 15 | +lint_one() { |
| 16 | + local f="$1" lane="${2:-$(basename "$1" .txt)}" miss="" |
| 17 | + [ -s "$f" ] || { echo "PROMPT-LINT: $lane empty-or-missing $f"; return 6; } |
| 18 | + grep -qiE 'GOAL|/goal' "$f" || miss="$miss objective(GOAL)" |
| 19 | + grep -qi 'OWN' "$f" || miss="$miss ownership(OWN)" |
| 20 | + grep -qi 'FORBIDDEN' "$f" || miss="$miss boundaries(FORBIDDEN)" |
| 21 | + grep -qE 'STATUS:.*DONE' "$f" || miss="$miss done-marker(STATUS:..DONE)" |
| 22 | + grep -q 'run=' "$f" || miss="$miss nonce(run=<RUN_ID>)" |
| 23 | + grep -qi 'verify' "$f" || miss="$miss verify-evidence" |
| 24 | + if [ -n "$miss" ]; then echo "PROMPT-LINT: $lane missing$miss"; return 6; fi |
| 25 | + return 0 |
| 26 | +} |
| 27 | + |
| 28 | +lint_run() { |
| 29 | + local mf="$1" rc=0 lane pf dir |
| 30 | + command -v jq >/dev/null 2>&1 || { echo "polylane-promptlint: jq required for lint-run" >&2; return 2; } |
| 31 | + dir=$(cd "$(dirname "$mf")/.." && pwd) # .polylane/ -> project root |
| 32 | + for lane in $(jq -r '.lanes[].name, .integrator.name' "$mf"); do |
| 33 | + pf=$(jq -r --arg n "$lane" '(.lanes[],.integrator) | select(.name==$n) | .prompt_file' "$mf" | head -1) |
| 34 | + case "$pf" in /*) : ;; *) pf="$dir/$pf" ;; esac |
| 35 | + lint_one "$pf" "$lane" || rc=6 |
| 36 | + done |
| 37 | + return $rc |
| 38 | +} |
| 39 | + |
| 40 | +if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then |
| 41 | + case "${1:-}" in |
| 42 | + lint) shift; lint_one "$@" ;; |
| 43 | + lint-run) shift; lint_run "${1:?usage: lint-run <manifest>}" ;; |
| 44 | + *) echo "usage: polylane-promptlint.sh lint <prompt-file> [lane] | lint-run <manifest>" >&2; exit 2 ;; |
| 45 | + esac |
| 46 | +fi |
0 commit comments