← Files LinchpinARCHIVED FILE
scripts/linchpin.sh
87.1 KB · Oct 2, 2026 · 00:29 UTC
#!/bin/sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
contract_reference="$repo_root/references/prd-contract.md"
intake_reference="$repo_root/references/intake.md"
runtime_reference="$repo_root/references/runtime.md"
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_file() {
[ -f "$1" ] || die "missing file: $1"
}
runtime_value() {
runtime_role="$1"
runtime_column="$2"
# Scoped to its own table. runtime.md holds several tables with the same
# column shape, so an unscoped scan makes every table a lookup source for
# every other one.
awk -F '|' -v target="$runtime_role" -v column="$runtime_column" '
/^## / { in_table = ($0 ~ /^## Role pins/); next }
!in_table { next }
{
role = $2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", role)
if (role == target) {
value = $column
gsub(/`/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
print value
exit
}
}
' "$runtime_reference"
}
alias_model() {
# Resolves an alias from the Model aliases table in runtime.md. Empty output
# means the alias has no row, which is a configuration failure, not a model
# request to pass along. Scoped to that one table: the Role pins table has the
# same column shape, so an unscoped scan would let `worker = "Manager"` resolve
# to a real model that the alias allowlist was supposed to reject.
awk -F '|' -v target="$1" '
/^## / { in_table = ($0 ~ /^## Model aliases/); next }
!in_table { next }
{
name = $2
gsub(/`/, "", name)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
if (name == target) {
value = $3
gsub(/`/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value ~ /^gpt-/) { print value; exit }
}
}
' "$runtime_reference"
}
runtime_metadata() {
require_file "$runtime_reference"
worker_model=$(runtime_value Worker 3)
worker_effort=$(runtime_value Worker 4)
worker_mechanism=$(runtime_value Worker 5)
reviewer_model=$(runtime_value Reviewer 3)
reviewer_effort=$(runtime_value Reviewer 4)
reviewer_mechanism=$(runtime_value Reviewer 5)
# A repo-local effort override, declared before the run starts, is the user's
# call. It is not the forbidden thing: what the delegation rules prohibit is
# the MANAGER changing tier mid-run to get past a gate that failed. The model
# itself stays pinned — preflight verifies the worker model's capability, and
# substituting one is how a run silently stops being the run that was checked.
[ -z "${cfg_worker_effort:-}" ] || worker_effort="$cfg_worker_effort"
[ -z "${cfg_reviewer_effort:-}" ] || reviewer_effort="$cfg_reviewer_effort"
if [ -n "${cfg_worker_model:-}" ]; then
worker_model=$(alias_model "$cfg_worker_model")
[ -n "$worker_model" ] || die "unknown worker alias: $cfg_worker_model (see the Model aliases table in references/runtime.md)"
fi
if [ -n "${cfg_reviewer_model:-}" ]; then
reviewer_model=$(alias_model "$cfg_reviewer_model")
[ -n "$reviewer_model" ] || die "unknown reviewer alias: $cfg_reviewer_model (see the Model aliases table in references/runtime.md)"
fi
[ -n "$worker_model" ] || die 'runtime.md has no Worker model pin'
[ -n "$worker_effort" ] || die 'runtime.md has no Worker effort pin'
[ "$worker_mechanism" = 'codex exec' ] || die 'runtime.md Worker mechanism is not codex exec'
[ -n "$reviewer_model" ] || die 'runtime.md has no Reviewer model pin'
[ -n "$reviewer_effort" ] || die 'runtime.md has no Reviewer effort pin'
[ "$reviewer_mechanism" = 'codex exec --sandbox read-only' ] || die 'runtime.md Reviewer mechanism is not read-only codex exec'
worker_runtime="model=$worker_model; effort=$worker_effort; mechanism=$worker_mechanism"
reviewer_runtime="model=$reviewer_model; effort=$reviewer_effort; mechanism=$reviewer_mechanism"
worker_invocation="$worker_mechanism --model $worker_model -c 'model_reasoning_effort=\"$worker_effort\"' -C <lane> <brief>"
reviewer_invocation="codex exec --model $reviewer_model -c 'model_reasoning_effort=\"$reviewer_effort\"' --sandbox read-only -C <lane> <review>"
}
marker_is_valid() {
# The marker lives in the opening front-matter block. Other front-matter keys
# are allowed; the exact `prd_contract: v1` line is not optional.
awk 'NR == 1 && $0 != "---" { exit 1 }
NR > 1 && $0 == "---" { if (found) ok = 1; exit }
NR > 1 && $0 == "prd_contract: v1" { found = 1 }
NR > 50 { exit }
END { if (!ok) exit 1 }' "$1"
}
section_pattern() {
# A required heading may carry a leading number and trailing context, but the
# named section itself is never renamed. `references/prd-contract.md` owns this
# rule; every reader below builds its regex here so they cannot drift.
printf '^## ([0-9]+[.] )?%s([[:space:]].*)?$\n' "$1"
}
section_line() {
# The first argument is a section name, not a regular expression.
grep -En "$(section_pattern "$1")" "$2" | sed -n '1s/:.*//p'
}
section_block() {
awk -v name="$2" '
BEGIN {
start = "^## ([0-9]+[.] )?" name "([[:space:]].*)?$"
prefix = "^## ([0-9]+[.] )?" name
}
$0 ~ start { active = 1 }
active && /^## / && $0 !~ prefix { exit }
active { print }
' "$1"
}
files_list() {
require_file "$1"
awk '
function clear_seen( item) {
for (item in seen) delete seen[item]
}
function finish( item) {
if (active && count != expected) bad = 1
for (item in seen) if (seen[item] > 1) bad = 1
active = 0
count = 0
expected = 0
clear_seen()
}
/^\*\*Files \([0-9][0-9]*\):\*\*$/ {
if (active) finish()
header = $0
sub(/^\*\*Files \(/, "", header)
sub(/\).*$/, "", header)
expected = header + 0
if (expected < 1) bad = 1
active = 1
count = 0
clear_seen()
next
}
active && /^- / {
entry = $0
if (entry !~ /^- `[^`]+`[[:space:]]+(- (NEW|EDIT|DELETE):|—[[:space:]]+(NEW|EDIT|DELETE))/) {
bad = 1
next
}
sub(/^- `/, "", entry)
sub(/`.*/, "", entry)
if (entry ~ /^\// || entry ~ /^\.\.\// || entry ~ /[[:space:]]/) bad = 1
seen[entry]++
count++
total++
print entry
next
}
active && NF == 0 { if (count > 0) finish(); next }
END {
if (active) finish()
if (bad || total == 0) exit 1
}
' "$1"
}
prose_files() {
# A user's PRD often declares its files in a prose `**Files:**` paragraph
# instead of a machine-readable `Files (N)` list. The author still named the
# paths, so read them rather than declaring the whole batch unknowable. This
# is a derived set: it is announced as advisory and never silently trusted as
# a conformance result.
require_file "$1"
awk '
/^\*\*Files:\*\*/ { buffer = buffer " " $0; active = 1; next }
active && NF == 0 { active = 0; next }
active { buffer = buffer " " $0; next }
{ next }
END { print buffer }
' "$1" |
tr '`' '\n' |
awk 'NR % 2 == 0' |
grep -E '^[A-Za-z0-9_.][A-Za-z0-9_./-]*\.[A-Za-z0-9]+$' |
sort -u
}
ledger_block() {
section_block "$1" 'Integration Ledger'
}
negative_block() {
section_block "$1" 'Negative Controls'
}
acceptance_block() {
section_block "$1" 'Acceptance Criteria'
}
checkpoint_block() {
section_block "$1" 'Checkpoint [Pp]rotocol'
}
brief_section() {
# A user's PRD is executed as written, so a section it never had is reported
# as absent instead of blocking the lane. Whatever is present is verbatim, and
# a legacy heading for the same content is transferred under its own name.
if [ -n "$(section_line "$2" "$1" || true)" ]; then
section_block "$1" "$2"
elif [ -n "${3:-}" ] && [ -n "$(section_line "$3" "$1" || true)" ]; then
printf 'The PRD declares this as `%s`, transferred verbatim:\n\n' "$3"
section_block "$1" "$3"
else
printf 'NOT DECLARED in this PRD. Follow the PRD as written; do not invent a %s section.\n' "$2"
fi
}
negative_data() {
negative_block "$1" | awk -F '|' '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
/^\|/ {
gate = trim($2)
if (gate != "" && gate != "Gate" && gate !~ /^-+$/) {
field = (NF == 6 ? trim($5) : "__MALFORMED__")
print gate "\t" field
}
}
'
}
clean_evidence_field() {
printf '%s\n' "$1" | sed 's/`//g'
}
command_from_field() {
clean_field=$(clean_evidence_field "$1")
# The command may stand alone (plan time) or precede a result/exit triple
# (delivery evidence). Both forms yield the same comparable command string.
printf '%s\n' "$clean_field" |
sed -n 's/^command:[[:space:]]*\([^;][^;]*\).*/\1/p' |
sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}
result_from_field() {
clean_field=$(clean_evidence_field "$1")
printf '%s\n' "$clean_field" |
sed -n 's/^command:[[:space:]]*[^;][^;]*;[[:space:]]*result:[[:space:]]*\(.*\);[[:space:]]*exit:[[:space:]]*[0-9][0-9]*$/\1/p' |
sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}
exit_from_field() {
clean_field=$(clean_evidence_field "$1")
printf '%s\n' "$clean_field" |
sed -n 's/^command:[[:space:]]*[^;][^;]*;[[:space:]]*result:.*;[[:space:]]*exit:[[:space:]]*\([0-9][0-9]*\)$/\1/p'
}
validate_negative_controls() {
prd="$1"
negative_block "$prd" | awk -F '|' '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
/^\|/ {
gate = trim($2)
if (gate == "Gate" || gate == "" || gate ~ /^-+$/) next
rows++
if (NF != 6 || trim($3) == "" || trim($4) == "" || trim($5) == "") bad = 1
}
END { if (rows == 0 || bad) exit 1 }
' || die "Negative Controls must have one exact command/result field per gate: $prd"
negative_rows=$(negative_data "$prd")
[ -n "$negative_rows" ] || die "Negative Controls has no data rows: $prd"
duplicate_gate=$(printf '%s\n' "$negative_rows" | cut -f1 | sort | uniq -d)
[ -z "$duplicate_gate" ] || die "Negative Controls has duplicate gate id: $duplicate_gate"
while IFS="$(printf '\t')" read -r gate field; do
[ -n "$gate" ] || continue
[ "$field" != '__MALFORMED__' ] || die "Negative Controls has a malformed row: $gate"
expected_command=$(command_from_field "$field")
expected_result=$(result_from_field "$field")
expected_exit=$(exit_from_field "$field")
[ -n "$expected_command" ] || die "Negative Controls has no exact command: $gate"
# The PRD declares the gate, its control, and the exact command. `RED
# observed` and the non-zero exit are what a worker observes; `gate` requires
# them in the report. A PRD that already carries them must still be honest.
if [ -n "$expected_result" ]; then
case "$expected_result" in
*'RED observed:'*) ;;
*) die "Negative Controls result lacks RED observed marker: $gate" ;;
esac
fi
if [ -n "$expected_exit" ]; then
case "$expected_exit" in
0|*[!0-9]*) die "Negative Controls result must declare a non-zero exit: $gate" ;;
esac
[ "$expected_exit" -gt 0 ] || die "Negative Controls result must declare a non-zero exit: $gate"
fi
done <<EOF
$negative_rows
EOF
}
table_row_count() {
awk -F '|' '
/^\|[[:space:]]*[0-9]+[[:space:]]*\|/ { count++ }
END { print count + 0 }
' "$1"
}
negative_row_count() {
negative_data "$1" | awk 'NF { count++ } END { print count + 0 }'
}
validate_ledger() {
# A PRD is written before its code exists, so the plan-time contract requires
# a named non-test caller file, not a line number that cannot exist yet. The
# `file:line` form is enforced at delivery by `gate`, where it is checkable.
prd="$1"
ledger_mode="${2:-plan}"
rows=$(table_row_count "$prd")
[ "$rows" -gt 0 ] || die "Integration Ledger has no data rows: $prd"
ledger_block "$prd" | awk -F '|' -v mode="$ledger_mode" '
/^\|[[:space:]]*[0-9]+[[:space:]]*\|/ {
for (i = 1; i <= NF; i++) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
gsub(/`/, "", $i)
}
caller = $4
if (caller == "" || caller ~ /TBD|pending/) bad = 1
else if (caller ~ /optional\/unbuilt/) { }
else if (caller ~ /(^|[[:space:],(\/])(__tests__|tests?|spec|specs)\//) bad = 1
else if (caller !~ /[A-Za-z0-9_.\/-]+\.[A-Za-z0-9]+/) bad = 1
else if (mode == "delivered" && caller !~ /:[0-9]+/) bad = 1
if ($7 == "" || $7 ~ /TBD|pending/) bad = 1
}
END { if (bad) exit 1 }
' || die "ledger row lacks a real caller or negative control: $prd"
}
problem() {
printf '%s\n' "$*" >> "$problem_file"
}
collect_problem() {
# Run a validator that dies on failure and record its message instead of
# exiting, so one contract run names every gap the author still has to close.
if ! validator_message=$("$@" 2>&1 >/dev/null); then
problem "$(printf '%s\n' "$validator_message" | sed 's/^ERROR: //' | sed -n '1p')"
fi
}
contract_problems() {
# Callers rely on this reporting every gap at once; it never exits early and
# never reuses the shared `prd` variable its callers still need.
contract_prd="$1"
problem_file=$(mktemp "${TMPDIR:-/tmp}/linchpin-contract.XXXXXX")
marker_is_valid "$contract_prd" || problem "missing or invalid prd_contract: v1 marker: $contract_prd"
integration_line=$(section_line 'Integration Ledger' "$contract_prd" || true)
phases_line=$(section_line 'Execution Phases' "$contract_prd" || true)
negative_line=$(section_line 'Negative Controls' "$contract_prd" || true)
acceptance_line=$(section_line 'Acceptance Criteria' "$contract_prd" || true)
checkpoint_line=$(section_line 'Checkpoint [Pp]rotocol' "$contract_prd" || true)
[ -n "$integration_line" ] || problem "missing Integration Ledger: $contract_prd"
[ -n "$phases_line" ] || problem "missing Execution Phases: $contract_prd"
[ -n "$negative_line" ] || problem "missing Negative Controls: $contract_prd"
[ -n "$acceptance_line" ] || problem "missing Acceptance Criteria: $contract_prd"
[ -n "$checkpoint_line" ] || problem "missing Checkpoint Protocol: $contract_prd"
if [ -n "$integration_line" ] && [ -n "$phases_line" ] && [ -n "$negative_line" ] &&
[ -n "$acceptance_line" ] && [ -n "$checkpoint_line" ]; then
if [ "$integration_line" -ge "$phases_line" ] ||
[ "$phases_line" -ge "$negative_line" ] ||
[ "$negative_line" -ge "$acceptance_line" ] ||
[ "$acceptance_line" -ge "$checkpoint_line" ]; then
problem "required sections are out of order: $contract_prd"
fi
fi
( files_list "$contract_prd" ) >/dev/null 2>&1 ||
problem "one or more Files (N) lists are malformed: $contract_prd"
collect_problem validate_ledger "$contract_prd"
collect_problem validate_negative_controls "$contract_prd"
cat "$problem_file"
problem_count=$(awk 'END { print NR + 0 }' "$problem_file")
rm -f -- "$problem_file"
[ "$problem_count" -eq 0 ]
}
contract_check() {
prd="$1"
require_file "$prd"
if ! reported=$(contract_problems "$prd"); then
printf '%s\n' "$reported" | sed 's/^/ERROR: /' >&2
exit 1
fi
printf 'CONFORMING %s\n' "$prd"
}
migrate_body() {
# Mechanical legacy -> v1 normalization. It renames the required headings,
# rewrites prose `**Files:**` paragraphs into parseable `Files (N)` lists, and
# scaffolds the sections a legacy artifact never had. Anything it cannot
# convert without inventing content is emitted as a MIGRATION-TODO line so the
# contract stays red until an author fills it in.
awk -v has_phases="$1" -v has_acceptance="$2" -v has_negative="$3" -v has_checkpoint="$4" '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
function emit_negative_controls() {
print "## Negative Controls"
print ""
print "<!-- MIGRATION-TODO: one row per gate named in the phases above."
print " The fourth column is machine-checked and must read exactly:"
print " `command: <cmd>`; result: RED observed: <what broke>; exit: <non-zero> -->"
print ""
print "| Gate | Negative control | Expected red | Exact command/result |"
print "|---|---|---|---|"
print "| MIGRATION-TODO | MIGRATION-TODO | MIGRATION-TODO | MIGRATION-TODO |"
print ""
}
function emit_checkpoint() {
print "## Checkpoint Protocol"
print ""
print "MIGRATION-TODO: name the automated and manual checks, the evidence"
print "format, and the condition that blocks delivery. A gate may not be"
print "declared passed from a green-only run."
print ""
}
function flush_files( parts, count, i, entry, path, kind, description, lines, total) {
if (files_buffer == "") return
total = split(files_buffer, parts, "·")
count = 0
lines = ""
for (i = 1; i <= total; i++) {
entry = trim(parts[i])
sub(/[.]$/, "", entry)
entry = trim(entry)
if (entry == "") continue
count++
path = ""
if (match(entry, /`[^`]+`/)) {
path = substr(entry, RSTART + 1, RLENGTH - 2)
}
kind = ""
if (entry ~ /(^|[^A-Za-z])DELETE([^A-Za-z]|$)/) kind = "DELETE"
else if (entry ~ /(^|[^A-Za-z])NEW([^A-Za-z]|$)/) kind = "NEW"
else if (entry ~ /(^|[^A-Za-z])EDIT([^A-Za-z]|$)/) kind = "EDIT"
if (path == "" || kind == "" || path ~ /[[:space:]]/) {
lines = lines "- MIGRATION-TODO: legacy file entry needs one backtick path and one NEW/EDIT/DELETE kind: " entry "\n"
continue
}
description = entry
sub(/`[^`]+`/, "", description)
gsub(/\*\*/, "", description)
sub(/(^|[^A-Za-z])(NEW|EDIT|DELETE)([^A-Za-z]|$)/, " ", description)
description = trim(description)
gsub(/^[(]|[)]$/, "", description)
description = trim(description)
if (description == "") description = "migrated from the legacy file list"
lines = lines "- `" path "` - " kind ": " description "\n"
}
if (count > 0) {
print "**Files (" count "):**"
print ""
printf "%s", lines
}
files_buffer = ""
in_files = 0
}
in_files {
if (trim($0) == "") { flush_files(); print ""; next }
files_buffer = files_buffer " " $0
next
}
/^\*\*Files:\*\*/ {
files_buffer = $0
sub(/^\*\*Files:\*\*/, "", files_buffer)
in_files = 1
next
}
/^## / {
heading = $0
body = heading
sub(/^##[[:space:]]+/, "", body)
number = ""
if (match(body, /^[0-9]+[.][[:space:]]+/)) {
number = substr(body, RSTART, RLENGTH)
body = substr(body, RSTART + RLENGTH)
}
if (!has_phases && body ~ /^Phases([[:space:]]|$)/) {
print "## " number "Execution Phases"
next
}
if (body ~ /^Acceptance([[:space:]]|$)/ || body ~ /^Acceptance Criteria([[:space:]]|$)/) {
if (!has_negative && !negative_done) { emit_negative_controls(); negative_done = 1 }
if (!has_acceptance) { print "## " number "Acceptance Criteria"; next }
}
print
next
}
{ print }
END {
flush_files()
if (!has_negative && !negative_done) { print ""; emit_negative_controls() }
if (!has_checkpoint) { print ""; emit_checkpoint() }
}
'
}
migrate() {
[ "$#" -ge 1 ] || die 'usage: linchpin.sh migrate PRD [--out PATH] [--force]'
prd="$1"
shift
migrate_out=
migrate_force=0
while [ "$#" -gt 0 ]; do
case "$1" in
--out) [ "$#" -ge 2 ] || die '--out needs a path'; migrate_out="$2"; shift 2 ;;
--out=*) migrate_out=${1#*=}; shift ;;
--force) migrate_force=1; shift ;;
*) die "unknown migrate option: $1" ;;
esac
done
require_file "$prd"
if contract_problems "$prd" >/dev/null 2>&1; then
printf 'ALREADY-CONFORMING %s\n' "$prd"
return 0
fi
[ -n "$migrate_out" ] || migrate_out=$(printf '%s' "$prd" | sed 's/\.md$//').v1.md
[ "$migrate_out" != "$prd" ] || die 'migrate never writes over the original PRD'
if [ -e "$migrate_out" ] && [ "$migrate_force" -eq 0 ]; then
die "migration target already exists: $migrate_out (pass --force to replace it)"
fi
migrate_dir=$(mktemp -d "${TMPDIR:-/tmp}/linchpin-migrate.XXXXXX")
has_phases=0
has_acceptance=0
has_negative=0
has_checkpoint=0
[ -z "$(section_line 'Execution Phases' "$prd" || true)" ] || has_phases=1
[ -z "$(section_line 'Acceptance Criteria' "$prd" || true)" ] || has_acceptance=1
[ -z "$(section_line 'Negative Controls' "$prd" || true)" ] || has_negative=1
[ -z "$(section_line 'Checkpoint [Pp]rotocol' "$prd" || true)" ] || has_checkpoint=1
migrate_body "$has_phases" "$has_acceptance" "$has_negative" "$has_checkpoint" \
< "$prd" > "$migrate_dir/body"
if [ "$(sed -n '1p' "$migrate_dir/body")" = '---' ]; then
# Keep an existing front-matter block and add the marker to it.
awk 'NR == 1 { print; print "prd_contract: v1"; next } { print }' \
"$migrate_dir/body" > "$migrate_dir/candidate"
else
{ printf -- '---\nprd_contract: v1\n---\n\n'; cat "$migrate_dir/body"; } > "$migrate_dir/candidate"
fi
printf 'ORIGINAL-PRESERVED %s\n' "$prd"
cp "$migrate_dir/candidate" "$migrate_out"
if migrate_problems=$(contract_problems "$migrate_out"); then
rm -rf -- "$migrate_dir"
printf 'MIGRATED %s -> %s\n' "$prd" "$migrate_out"
printf '%s\n' 'REVIEW: every converted Files (N) entry is a mechanical rewrite of legacy prose; confirm the paths before scheduling lanes.'
printf 'NEXT: sh scripts/linchpin.sh route execute %s\n' "$migrate_out"
return 0
fi
# A candidate that still needs an author must not claim conformance.
cp "$migrate_dir/body" "$migrate_out"
rm -rf -- "$migrate_dir"
printf 'MIGRATION-INCOMPLETE %s -> %s\n' "$prd" "$migrate_out"
printf '%s\n' 'The marker was withheld. Remaining gaps need an author, not a parser:'
printf '%s\n' "$migrate_problems" | sed 's/^/- /'
todo_lines=$(grep -n 'MIGRATION-TODO' "$migrate_out" | sed -n '1,20p' || true)
if [ -n "$todo_lines" ]; then
printf '%s\n' 'MIGRATION-TODO markers to resolve:'
printf '%s\n' "$todo_lines" | sed 's/^/- /'
fi
printf 'NEXT: fill the gaps above in %s, then rerun: sh scripts/linchpin.sh contract %s\n' \
"$migrate_out" "$migrate_out"
return 1
}
require_exact_line() {
expected_line="$1"
target_file="$2"
[ "$(grep -Fxc "$expected_line" "$target_file" || true)" -eq 1 ]
}
block_has_exact_sequence() {
expected_file="$1"
target_file="$2"
awk '
FNR == NR {
expected[FNR] = $0
expected_count = FNR
next
}
{
if (matched) next
if ($0 == expected[1]) position = 1
if (position > 0) {
if ($0 == expected[position]) {
position++
if (position > expected_count) {
matched = 1
exit
}
} else if ($0 == expected[1]) {
position = 2
} else {
position = 0
}
}
}
END { if (!matched) exit 1 }
' "$expected_file" "$target_file"
}
config_directory() {
if [ "$#" -gt 0 ] && [ -n "$1" ]; then
printf '%s\n' "$1"
elif [ -n "${LINCHPIN_CONFIG_DIR:-}" ]; then
printf '%s\n' "$LINCHPIN_CONFIG_DIR"
else
printf '%s\n' "$PWD"
fi
}
load_config() {
config_dir=$(config_directory "${1:-}")
resolved_config=$(config_values "$config_dir")
while IFS='=' read -r config_key config_value; do
case "$config_key" in
execution) execution="$config_value" ;;
delivery) delivery="$config_value" ;;
base) base="$config_value" ;;
review) review="$config_value" ;;
max_lanes) max_lanes="$config_value" ;;
prd_floor) prd_floor="$config_value" ;;
worker) cfg_worker_model="$config_value" ;;
reviewer) cfg_reviewer_model="$config_value" ;;
worker_effort) cfg_worker_effort="$config_value" ;;
reviewer_effort) cfg_reviewer_effort="$config_value" ;;
esac
done <<EOF
$resolved_config
EOF
}
brief() {
[ "$#" -gt 0 ] || die 'usage: linchpin.sh brief PRD [LANE_ID LANE_MODE DELIVERY_MODE] [--config-dir DIR]'
prd="$1"
shift
config_dir="${LINCHPIN_CONFIG_DIR:-$PWD}"
lane_id="${LINCHPIN_LANE_ID:-lane-1}"
lane_mode="${LINCHPIN_LANE_MODE:-}"
delivery_mode="${LINCHPIN_DELIVERY_MODE:-}"
brief_out=
positional_count=0
while [ "$#" -gt 0 ]; do
case "$1" in
--config-dir)
[ "$#" -ge 2 ] || die '--config-dir needs a directory'
config_dir="$2"
shift 2
;;
--config-dir=*)
config_dir=${1#*=}
shift
;;
--out)
[ "$#" -ge 2 ] || die '--out needs a path'
brief_out="$2"
shift 2
;;
--out=*)
brief_out=${1#*=}
shift
;;
--lane-id)
[ "$#" -ge 2 ] || die '--lane-id needs a value'
lane_id="$2"
shift 2
;;
--lane-id=*)
lane_id=${1#*=}
shift
;;
--lane-mode)
[ "$#" -ge 2 ] || die '--lane-mode needs a value'
lane_mode="$2"
shift 2
;;
--lane-mode=*)
lane_mode=${1#*=}
shift
;;
--delivery-mode)
[ "$#" -ge 2 ] || die '--delivery-mode needs a value'
delivery_mode="$2"
shift 2
;;
--delivery-mode=*)
delivery_mode=${1#*=}
shift
;;
--)
shift
while [ "$#" -gt 0 ]; do
positional_count=$((positional_count + 1))
case "$positional_count" in
1) lane_id="$1" ;;
2) lane_mode="$1" ;;
3) delivery_mode="$1" ;;
*) die 'brief accepts at most three metadata arguments' ;;
esac
shift
done
;;
*)
positional_count=$((positional_count + 1))
case "$positional_count" in
1) lane_id="$1" ;;
2) lane_mode="$1" ;;
3) delivery_mode="$1" ;;
*) die 'brief accepts at most three metadata arguments' ;;
esac
shift
;;
esac
done
load_config "$config_dir"
if [ -z "$lane_mode" ]; then
case "$execution" in
sequential) lane_mode=sequential ;;
auto|parallel) lane_mode=parallel ;;
esac
fi
[ -n "$delivery_mode" ] || delivery_mode="$delivery"
# An error that names no value cannot be acted on inside a batch loop: the
# caller cannot tell which lane failed, and abandons the loop for hand-unrolled
# invocations. Every rejection echoes the offending value.
[ -n "$lane_id" ] || die "lane identity is empty for $prd; pass LANE_ID as the first metadata argument"
printf '%s\n' "$lane_id" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]*$' ||
die "lane identity is malformed: '$lane_id' (allowed: alphanumeric start, then A-Z a-z 0-9 . _ / -)"
case "$lane_mode" in parallel|sequential) ;; *) die "lane mode must be parallel or sequential: $lane_mode" ;; esac
case "$delivery_mode" in pr|branch) ;; *) die "delivery mode must be pr or branch: $delivery_mode" ;; esac
require_file "$prd"
runtime_metadata
if [ -n "$brief_out" ]; then
# A brief the manager has to retype into a prompt is a brief that gets
# dropped. Write it to a file the worker invocation can read.
brief_emit "$prd" "$lane_id" "$lane_mode" "$delivery_mode" > "$brief_out"
printf 'BRIEF-WRITTEN %s\n' "$brief_out"
return
fi
brief_emit "$prd" "$lane_id" "$lane_mode" "$delivery_mode"
}
brief_rules() {
# One definition, read by both the emitter and the checker. Two copies of a
# rule string is how the check silently stops matching what ships.
# A lane worker inherits the same plugin the manager is running, so the word
# "PRD" in its brief is enough to make it open the linchpin router and re-run
# intake on the PRD it was already handed. One field run spent a Luna/max turn
# printing `ROUTE-EXECUTE-CONFORMING` for a decision the manager had already
# made. Worse, a worker that reaches the coordinator starts scheduling lanes of
# its own inside a lane.
rule_prohibited='Prohibited actions: native Luna spawning; runtime tier changes; unsafe external install/swap actions; re-entering linchpin (do not read the linchpin router or coordinator skills, and do not run linchpin.sh route, mode, schedule, or brief — routing already happened and this brief is its result)'
rule_scope='Scope rule: change only the files this PRD covers. Do not delete, move, or edit an unrelated file, do not bump an unrelated dependency, and do not bundle unrelated work into this lane. Something outside scope that looks wrong is a note in your report, not an edit. This rule bounds WHAT you change; it never forbids committing what you did change.'
rule_gate='Gate rule: every negative control this PRD declares needs observed-red evidence before delivery. A control the PRD never declared is not invented here.'
# Workers told only what to change left the result uncommitted in the working
# tree, and each of those lanes cost a second worker whose only task was `git
# commit`. Two things caused it: the requirement lived in the manager's skill
# and never reached the worker's prompt, and the only sentence containing the
# word "commit" was a prohibition, which workers read as "do not commit".
rule_commit='Commit rule: your lane is not done until your work is committed on this lane branch, inside your own working directory. An uncommitted working tree is PARTIAL, not a delivery, however complete the code is. Stage the in-scope files by explicit path (never `git add -A`), commit them, and report the resulting commit sha in your final summary. Do not push, open a PR, merge, rebase, or switch branches; the manager owns delivery.'
# One PRD in the field declared "Nothing was committed or pushed" as its own
# acceptance criterion. The worker correctly abstained and was handed a
# pointless repair round for it. The PRD outranks this default.
rule_commit_exception='Commit rule exception: if this PRD explicitly requires that nothing be committed or pushed, follow the PRD. Say plainly in your summary that you left the tree uncommitted and quote the criterion that required it. That is a completed lane, not a partial one.'
rule_environment='Environment rule: your working directory may be a fresh git worktree with no installed dependencies and no editor tooling. Before concluding that a declared gate cannot run, bootstrap what the repository already specifies (its lockfile install, its pinned runtime version) and name the exact command you ran. Report a gate blocked by a sandbox restriction or an unbuilt environment as a setup result, with the exact error, and never as a verification result. If the repository test config excludes the directory you are working in, say so and report the override you used.'
}
brief_emit() {
prd="$1"
lane_id="$2"
lane_mode="$3"
delivery_mode="$4"
printf '%s\n' 'WORKER BRIEF: contract-preserving lane'
printf 'Source PRD: %s\n' "$prd"
printf 'Lane identity: %s\n' "$lane_id"
printf '%s\n' 'Files (N) parsed:'
# `files_list` still prints paths on the way to a non-zero exit, so a non-empty
# `brief_files` does not mean the set resolved. Record which branch actually
# ran; anything downstream that asks "was there a file set?" must read this.
if brief_files=$(files_list "$prd" 2>/dev/null) && [ -n "$brief_files" ]; then
brief_files_resolved=yes
printf '%s\n' "$brief_files" | sed 's/^/- /'
else
brief_files_resolved=no
printf '%s\n' '- UNPARSED: this PRD does not use machine-readable `Files (N)` lists.' \
'- Read the phase file lists in the PRD itself and follow them as written.' \
'- Its lane is grouped from the paths it names in prose, or alone if it names none.'
if brief_derived=$(prose_files "$prd" 2>/dev/null) && [ -n "$brief_derived" ]; then
printf '%s\n' '- Derived from prose (grouping only, not a substitute for the PRD):'
printf '%s\n' "$brief_derived" | sed 's/^/ - /'
fi
fi
printf '%s\n' '' '## Integration Ledger (verbatim)'
brief_section "$prd" 'Integration Ledger'
printf '%s\n' '' '## Negative Controls (verbatim)'
brief_section "$prd" 'Negative Controls' 'Verification'
printf '%s\n' '' '## Acceptance Criteria (verbatim)'
brief_section "$prd" 'Acceptance Criteria' 'Acceptance'
printf '%s\n' '' '## Checkpoint Protocol (verbatim)'
brief_section "$prd" 'Checkpoint [Pp]rotocol'
printf '%s\n' '' '## Resolved Lane Metadata'
printf 'Worker runtime: %s\n' "$worker_runtime"
printf 'Reviewer runtime: %s\n' "$reviewer_runtime"
printf 'Runtime invocation: worker=%s; reviewer=%s\n' "$worker_invocation" "$reviewer_invocation"
printf 'Lane mode: %s\n' "$lane_mode"
printf 'Delivery mode: %s\n' "$delivery_mode"
brief_rules
printf '%s\n' "$rule_prohibited" "$rule_scope" "$rule_gate" "$rule_commit" "$rule_commit_exception" "$rule_environment"
if [ "$brief_files_resolved" = no ]; then
printf '%s\n' 'File-set rule: this PRD declares no machine-readable file set, so establish your own from the PRD prose before you start and list every path you touched in your final summary. Without a resolved file set there is nothing definite to stage, and lanes in that position have ended with correct work left uncommitted.'
fi
}
review_brief() {
prd=''
lane_id=''
commit_sha=''
gates_file=''
brief_out=''
positional_count=0
while [ "$#" -gt 0 ]; do
case "$1" in
--gates) [ "$#" -ge 2 ] || die 'review-brief --gates needs a path'; gates_file="$2"; shift 2 ;;
--commit) [ "$#" -ge 2 ] || die 'review-brief --commit needs a sha'; commit_sha="$2"; shift 2 ;;
--out) [ "$#" -ge 2 ] || die 'review-brief --out needs a path'; brief_out="$2"; shift 2 ;;
*)
positional_count=$((positional_count + 1))
case "$positional_count" in
1) prd="$1" ;;
2) lane_id="$1" ;;
*) die "review-brief accepts at most two positional arguments: got '$1'" ;;
esac
shift
;;
esac
done
[ -n "$prd" ] || die 'usage: linchpin.sh review-brief PRD LANE_ID --gates PATH --commit SHA [--out PATH]'
[ -n "$lane_id" ] || die "lane identity is empty for $prd; pass LANE_ID as the second argument"
printf '%s\n' "$lane_id" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]*$' ||
die "lane identity is malformed: '$lane_id' (allowed: alphanumeric start, then A-Z a-z 0-9 . _ / -)"
# The reviewer runs read-only and cannot install dependencies or run the
# repository's gates. A review launched without the manager's gate evidence
# can only report what it was unable to do, so the evidence is required here
# rather than left to the manager to remember.
[ -n "$gates_file" ] ||
die 'review-brief requires --gates PATH: run the gates yourself and pass the Gate Evidence table, or the read-only reviewer can only report what it could not run'
require_file "$gates_file"
# A reviewer cannot review a working tree it cannot see. An uncommitted lane
# is a worker-contract failure; it is not a question to put to the reviewer.
[ -n "$commit_sha" ] ||
die 'review-brief requires --commit SHA: an uncommitted lane is PARTIAL, not reviewable — get the worker commit first'
require_file "$prd"
if [ -n "$brief_out" ]; then
review_brief_emit "$prd" "$lane_id" "$commit_sha" "$gates_file" > "$brief_out"
printf 'REVIEW-BRIEF-WRITTEN %s\n' "$brief_out"
return
fi
review_brief_emit "$prd" "$lane_id" "$commit_sha" "$gates_file"
}
review_brief_emit() {
prd="$1"
lane_id="$2"
commit_sha="$3"
gates_file="$4"
printf '%s\n' 'REVIEW BRIEF: read-only lane review'
printf 'Source PRD: %s\n' "$prd"
printf 'Lane identity: %s\n' "$lane_id"
printf 'Lane commit under review: %s\n' "$commit_sha"
printf '%s\n' '' '## Acceptance Criteria (verbatim)'
brief_section "$prd" 'Acceptance Criteria' 'Acceptance'
printf '%s\n' '' '## Negative Controls (verbatim)'
brief_section "$prd" 'Negative Controls' 'Verification'
printf '%s\n' '' '## Manager Gate Evidence (already run; treat as established fact)'
cat "$gates_file"
printf '%s\n' '' '## Review rules'
printf '%s\n' 'You are read-only by design. You cannot install dependencies, write files, or run this repository'"'"'s gates. That is the expected condition of this role, not a finding. The gate results above were run by the manager in a writable tree; do not re-derive them and do not report their absence.'
printf '%s\n' 'Review the committed diff by reading it. The findings that matter here are the ones only a reader can reach: a negative control that stays green when the feature is removed, a field the code accepts and never maps, a document that claims behavior the code contradicts, an acceptance criterion nothing actually satisfies.'
printf '%s\n' 'Label every finding exactly one of DEFECT or EVIDENCE-GAP. DEFECT: something is wrong in the diff and you can name it with a file:line. EVIDENCE-GAP: the work may be correct but a result you would want was not supplied. Give every DEFECT a file:line and the concrete consequence.'
printf '%s\n' 'Verdict is APPROVE or REQUEST_CHANGES on the last line as `VERDICT: <value>`. REQUEST_CHANGES requires at least one DEFECT. An EVIDENCE-GAP never blocks delivery on its own; record it and APPROVE.'
printf '%s\n' 'Facts stated in this brief are context, not findings. Do not treat a fact you were handed as a defect you discovered, and do not open a finding merely to have one. APPROVE with zero findings is a valid and useful review.'
}
brief_check() {
prd=''
brief_file=''
config_dir=''
while [ "$#" -gt 0 ]; do
case "$1" in
--config-dir) [ "$#" -ge 2 ] || die 'brief-check --config-dir needs a path'; config_dir="$2"; shift 2 ;;
# `brief` accepts the joined form, so `brief-check` must too; otherwise the
# same flag typed the same way fails on one command and not the other.
--config-dir=*) config_dir="${1#--config-dir=}"; shift ;;
--*) die "brief-check does not accept the option: $1" ;;
*)
if [ -z "$prd" ]; then prd="$1"
elif [ -z "$brief_file" ]; then brief_file="$1"
else die "brief-check accepts at most two positional arguments: got '$1'"
fi
shift
;;
esac
done
[ -n "$prd" ] && [ -n "$brief_file" ] || die 'usage: linchpin.sh brief-check PRD BRIEF [--config-dir DIR]'
require_file "$prd"
require_file "$brief_file"
# The checker must resolve the same effort overrides the emitter used, or a
# brief built under a repo-local override fails its own verification.
load_config "$config_dir"
runtime_metadata
metadata_count=$(grep -Ec '^Lane identity:' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief lane identity is missing or duplicated'
metadata_count=$(grep -Ec '^Lane identity: [A-Za-z0-9][A-Za-z0-9._/-]*$' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief lane identity is malformed'
metadata_count=$(grep -Ec '^Lane mode:' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief lane mode is missing or duplicated'
metadata_count=$(grep -Ec '^Lane mode: (parallel|sequential)$' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief lane mode is malformed'
metadata_count=$(grep -Ec '^Delivery mode:' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief delivery mode is missing or duplicated'
metadata_count=$(grep -Ec '^Delivery mode: (pr|branch)$' "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die 'worker brief delivery mode is malformed'
for metadata_prefix in 'Worker runtime:' 'Reviewer runtime:' 'Runtime invocation:' 'Prohibited actions:' 'Scope rule:' 'Commit rule:' 'Commit rule exception:' 'Environment rule:'; do
metadata_count=$(grep -Fc "$metadata_prefix" "$brief_file" || true)
[ "$metadata_count" -eq 1 ] || die "worker brief metadata is missing or duplicated: $metadata_prefix"
done
require_exact_line "Worker runtime: $worker_runtime" "$brief_file" || die 'worker brief Worker runtime metadata is missing or stale'
require_exact_line "Reviewer runtime: $reviewer_runtime" "$brief_file" || die 'worker brief Reviewer runtime metadata is missing or stale'
require_exact_line "Runtime invocation: worker=$worker_invocation; reviewer=$reviewer_invocation" "$brief_file" || die 'worker brief runtime invocation is missing or stale'
brief_rules
require_exact_line "$rule_prohibited" "$brief_file" || die 'worker brief prohibited-actions metadata is missing or malformed'
require_exact_line "$rule_scope" "$brief_file" || die 'worker brief scope rule is missing or malformed'
require_exact_line "$rule_gate" "$brief_file" || die 'worker brief gate rule is missing or malformed'
require_exact_line "$rule_commit" "$brief_file" || die 'worker brief commit rule is missing or malformed'
require_exact_line "$rule_commit_exception" "$brief_file" || die 'worker brief commit-rule exception is missing or malformed'
require_exact_line "$rule_environment" "$brief_file" || die 'worker brief environment rule is missing or malformed'
brief_temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/linchpin-brief-check.XXXXXX")
trap 'rm -rf -- "$brief_temp_dir"' EXIT HUP INT TERM
brief_section "$prd" 'Integration Ledger' > "$brief_temp_dir/ledger"
brief_section "$prd" 'Negative Controls' 'Verification' > "$brief_temp_dir/negative"
brief_section "$prd" 'Acceptance Criteria' 'Acceptance' > "$brief_temp_dir/acceptance"
brief_section "$prd" 'Checkpoint [Pp]rotocol' > "$brief_temp_dir/checkpoint"
for block_name in ledger negative acceptance checkpoint; do
block_has_exact_sequence "$brief_temp_dir/$block_name" "$brief_file" || die "worker brief omitted or changed the verbatim $block_name block"
done
printf 'BRIEF-PASS metadata and verbatim contract blocks transferred\n'
}
config_values() {
config_dir=$(config_directory "${1:-}")
config_file="$config_dir/.linchpin.toml"
execution=auto
delivery=pr
base=auto
review=true
max_lanes=4
prd_floor=3
# Empty means "use the pin in runtime.md" — the zero-config default.
worker_effort_override=
reviewer_effort_override=
worker_override=
reviewer_override=
if [ -f "$config_file" ]; then
while IFS= read -r raw || [ -n "$raw" ]; do
line=$(printf '%s' "$raw" | sed 's/[[:space:]]*#.*$//')
key=$(printf '%s' "$line" | sed -n 's/^[[:space:]]*\([A-Za-z_][A-Za-z_0-9]*\)[[:space:]]*=.*/\1/p')
if [ -z "$key" ]; then
[ -z "$(printf '%s' "$line" | tr -d '[:space:]')" ] && continue
die "malformed .linchpin.toml line: $raw"
fi
value=$(printf '%s' "$line" | sed -n 's/^[^=]*=[[:space:]]*//p' | sed 's/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//')
case "$key" in
execution) execution="$value" ;;
delivery) delivery="$value" ;;
base) base="$value" ;;
review) review="$value" ;;
max_lanes) max_lanes="$value" ;;
prd_floor) prd_floor="$value" ;;
worker) worker_override="$value" ;;
reviewer) reviewer_override="$value" ;;
worker_effort) worker_effort_override="$value" ;;
reviewer_effort) reviewer_effort_override="$value" ;;
*) die "unknown .linchpin.toml key: $key" ;;
esac
done < "$config_file"
fi
case "$execution" in auto|parallel|sequential) ;; *) die "invalid execution: $execution" ;; esac
case "$delivery" in pr|branch) ;; *) die "invalid delivery: $delivery" ;; esac
case "$base" in
''|*[!A-Za-z0-9._/-]*) die "base must be auto or a branch name: $base" ;;
esac
case "$review" in true|false) ;; *) die "review must be true or false" ;; esac
case "$max_lanes" in *[!0-9]*|0|"") die "max_lanes must be a positive integer" ;; esac
case "$prd_floor" in *[!0-9]*|"") die "prd_floor must be a non-negative integer" ;; esac
# An unrecognized effort must fail here rather than reach a `codex exec` that
# rejects it once per lane, after the run is already underway.
case "$worker_effort_override" in
''|low|medium|high|max) ;;
*) die "worker_effort must be low, medium, high, or max: $worker_effort_override" ;;
esac
case "$reviewer_effort_override" in
''|low|medium|high|max) ;;
*) die "reviewer_effort must be low, medium, high, or max: $reviewer_effort_override" ;;
esac
# Validate the alias against the table that actually resolves it, so adding a
# model to runtime.md is one edit rather than two that can disagree.
for role_pair in "worker=$worker_override" "reviewer=$reviewer_override"; do
role_name=${role_pair%%=*}
role_alias=${role_pair#*=}
[ -n "$role_alias" ] || continue
[ -n "$(alias_model "$role_alias")" ] ||
die "$role_name must be an alias in the Model aliases table in references/runtime.md: $role_alias"
done
printf 'execution=%s\ndelivery=%s\nbase=%s\nreview=%s\nmax_lanes=%s\nprd_floor=%s\nworker=%s\nreviewer=%s\nworker_effort=%s\nreviewer_effort=%s\n' \
"$execution" "$delivery" "$base" "$review" "$max_lanes" "$prd_floor" \
"$worker_override" "$reviewer_override" \
"$worker_effort_override" "$reviewer_effort_override"
}
execute_intent='run|execute|start|begin|launch|resume|continue|kick off'
route() {
intent=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')
[ -n "$intent" ] || die 'route needs an intent'
shift
score=
if printf '%s' "$intent" | grep -Eq 'build|implement'; then
score="${1:-}"
[ "$#" -gt 0 ] && shift
elif printf '%s' "$intent" | grep -Eq "$execute_intent" &&
[ "$#" -gt 0 ] && printf '%s' "$1" | grep -Eq '^[0-9]+$'; then
# Keep compatibility with the old helper shape while allowing the
# documented execute form: route execute PRD [PRD ...].
score="$1"
shift
fi
config_dir="${LINCHPIN_CONFIG_DIR:-$PWD}"
prd_list=$(mktemp "${TMPDIR:-/tmp}/linchpin-route.XXXXXX")
trap 'rm -f -- "$prd_list"' EXIT HUP INT TERM
while [ "$#" -gt 0 ]; do
case "$1" in
--config-dir)
[ "$#" -ge 2 ] || die '--config-dir needs a directory'
config_dir="$2"
shift 2
;;
--config-dir=*)
config_dir=${1#*=}
shift
;;
*)
printf '%s\n' "$1" >> "$prd_list"
shift
;;
esac
done
load_config "$config_dir"
if printf '%s' "$intent" | grep -Eq '(writ(e|ing)|draft(ing)?|author(ing)?|creat(e|ing))([[:space:]]+[a-z]+){0,3}[[:space:]]+prd'; then
printf '%s\n' 'ROUTE-WRITE-PRD -> prd-creator'
return
fi
if printf '%s' "$intent" | grep -Eq 'build|implement'; then
case "$score" in ''|*[!0-9]*) die 'build intent requires a numeric complexity score' ;; esac
built_in_floor=3
floor="$prd_floor"
[ "$floor" -ge "$built_in_floor" ] || floor="$built_in_floor"
if [ "$score" -lt "$floor" ]; then
printf '%s\n' 'ROUTE-BUILD-SMALL -> direct-edit-refusal'
else
printf '%s\n' 'ROUTE-BUILD-LARGE -> prd-creator-confirm'
fi
return
fi
if printf '%s' "$intent" | grep -Eq "$execute_intent"; then
count=$(wc -l < "$prd_list" | tr -d ' ')
if [ "$count" -eq 0 ]; then
printf '%s\n' 'ROUTE-EXECUTE-NONE -> ask-once'
return
fi
# A PRD the user points at is executed as written. The contract is a creator
# standard for artifacts Linchpin authors, never an admission gate on the
# user's own document. The only execution blocker is a path that is not there
# — and it blocks that one path, not the batch beside it.
found_list=$(mktemp "${TMPDIR:-/tmp}/linchpin-route-found.XXXXXX")
missing=0
while IFS= read -r prd; do
[ -n "$prd" ] || continue
if [ -d "$prd" ]; then
# A directory in an execute argv is a target-repository hint, not a PRD.
printf 'ADVISORY %s is a directory, not a PRD; read as the target repository.\n' "$prd"
continue
fi
if [ -f "$prd" ]; then
printf '%s\n' "$prd" >> "$found_list"
else
printf 'MISSING-PRD-PATH %s (resolved from %s)\n' "$prd" "$PWD"
missing=1
fi
done < "$prd_list"
found_count=$(wc -l < "$found_list" | tr -d ' ')
if [ "$found_count" -eq 0 ]; then
rm -f -- "$found_list"
printf '%s\n' 'ROUTE-EXECUTE-NONE -> ask-once'
return
fi
printf '%s\n' 'ROUTE-EXECUTE-CONFORMING -> prd-swarm-coordinator'
if [ "$missing" -eq 1 ]; then
printf '%s\n' 'ADVISORY route the paths above that exist; ask once about the missing one instead of stopping the batch.'
fi
while IFS= read -r prd; do
[ -n "$prd" ] || continue
if ! sh "$script_dir/linchpin.sh" contract "$prd" >/dev/null 2>&1; then
printf 'ADVISORY %s does not carry prd_contract: v1; execute it as written. Do not rewrite it, and do not migrate it unless the user asks.\n' "$prd"
fi
done < "$found_list"
rm -f -- "$found_list"
return
fi
printf '%s\n' 'ROUTE-AMBIGUOUS -> ask-once'
}
mode_selection() {
requested_execution="$1"
shift
case "$requested_execution" in auto|parallel|sequential) ;; *) die "invalid execution: $requested_execution" ;; esac
config_dir="${LINCHPIN_CONFIG_DIR:-$PWD}"
temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/linchpin-mode.XXXXXX")
trap 'rm -rf -- "$temp_dir"' EXIT HUP INT TERM
: > "$temp_dir/prds"
while [ "$#" -gt 0 ]; do
case "$1" in
--config-dir)
[ "$#" -ge 2 ] || die '--config-dir needs a directory'
config_dir="$2"
shift 2
;;
--config-dir=*)
config_dir=${1#*=}
shift
;;
*)
printf '%s\n' "$1" >> "$temp_dir/prds"
shift
;;
esac
done
count=$(wc -l < "$temp_dir/prds" | tr -d ' ')
[ "$count" -gt 0 ] || die 'mode selection needs at least one PRD'
load_config "$config_dir"
if [ "$requested_execution" != auto ]; then
execution="$requested_execution"
fi
index=1
unparsed_any=0
while IFS= read -r prd; do
require_file "$prd"
if files_list "$prd" 2>/dev/null | sort -u > "$temp_dir/files-$index" &&
[ -s "$temp_dir/files-$index" ]; then
printf '0\n' > "$temp_dir/unparsed-$index"
elif prose_files "$prd" > "$temp_dir/files-$index" 2>/dev/null &&
[ -s "$temp_dir/files-$index" ]; then
# The author named the paths in prose instead of a `Files (N)` list. Read
# them for grouping only; the file on disk is never rewritten.
printf '0\n' > "$temp_dir/unparsed-$index"
printf 'ANNOUNCE: %s has no machine-readable Files (N) list; its file set was derived from its prose **Files:** paragraphs for grouping only.\n' "$prd"
else
# No declared file set at all. The lane cannot be proved disjoint from
# anything, so it takes its own group instead of dragging every other
# lane into one queue behind it.
: > "$temp_dir/files-$index"
printf '1\n' > "$temp_dir/unparsed-$index"
unparsed_any=1
printf 'ANNOUNCE: %s declares no file set; its lane runs alone in its own group and its isolation is unproven.\n' "$prd"
fi
printf '%s\n' "$prd" > "$temp_dir/label-$index"
index=$((index + 1))
done < "$temp_dir/prds"
: > "$temp_dir/edges"
i=1
while [ "$i" -le "$count" ]; do
j=$((i + 1))
while [ "$j" -le "$count" ]; do
if comm -12 "$temp_dir/files-$i" "$temp_dir/files-$j" | grep -q .; then
printf '%s %s\n' "$i" "$j" >> "$temp_dir/edges"
fi
j=$((j + 1))
done
i=$((i + 1))
done
if [ "$execution" = parallel ] && [ -s "$temp_dir/edges" ]; then
die 'execution=parallel forced parallelism but Files (N) lists intersect'
fi
if [ "$execution" = parallel ] && [ "$unparsed_any" -eq 1 ]; then
die 'execution=parallel forced parallelism but a PRD declares no file set, so disjointness cannot be proved'
fi
awk -v n="$count" '
BEGIN { for (i = 1; i <= n; i++) parent[i] = i }
function root(x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]
x = parent[x]
}
return x
}
{ left = root($1); right = root($2); if (left != right) parent[right] = left }
END { for (i = 1; i <= n; i++) print i, root(i) }
' "$temp_dir/edges" > "$temp_dir/groups"
if [ ! -s "$temp_dir/edges" ]; then
awk -v n="$count" 'BEGIN { for (i = 1; i <= n; i++) print i, i }' > "$temp_dir/groups"
fi
roots=$(awk '{ print $2 }' "$temp_dir/groups" | sort -nu)
active_count=0
if [ "$execution" = sequential ]; then
concurrency_limit=1
else
concurrency_limit="$max_lanes"
fi
for root in $roots; do
member_count=$(awk -v root="$root" '$2 == root { count++ } END { print count + 0 }' "$temp_dir/groups")
case "$execution" in
sequential) selected=sequential ;;
parallel) selected=parallel ;;
auto) if [ "$member_count" -gt 1 ]; then selected=sequential; else selected=parallel; fi ;;
esac
lanes=
active_lanes=
queued_lanes=
group_active_count=0
if [ "$selected" = sequential ]; then
group_limit=1
else
group_limit="$member_count"
fi
for member in $(awk -v root="$root" '$2 == root { print $1 }' "$temp_dir/groups" | sort -n); do
label=$(sed -n '1p' "$temp_dir/label-$member")
if [ -n "$lanes" ]; then lanes="$lanes,$label"; else lanes="$label"; fi
if [ "$active_count" -lt "$concurrency_limit" ] && [ "$group_active_count" -lt "$group_limit" ]; then
if [ -n "$active_lanes" ]; then active_lanes="$active_lanes,$label"; else active_lanes="$label"; fi
active_count=$((active_count + 1))
group_active_count=$((group_active_count + 1))
else
if [ -n "$queued_lanes" ]; then queued_lanes="$queued_lanes,$label"; else queued_lanes="$label"; fi
fi
done
[ -n "$active_lanes" ] || active_lanes=-
[ -n "$queued_lanes" ] || queued_lanes=-
printf 'group=%s mode=%s lanes=%s active=%s queued=%s\n' "$root" "$selected" "$lanes" "$active_lanes" "$queued_lanes"
done
}
schedule() {
requested_execution="$1"
worktree_status="$2"
shift 2
case "$requested_execution" in auto|parallel|sequential) ;; *) die "invalid execution: $requested_execution" ;; esac
# The announced reason must be the reason that actually happened. `fail` stays
# for the worktree case it has always meant; the named statuses cover the other
# degradations without claiming a `git worktree add` that was never attempted.
case "$worktree_status" in
ok) ;;
fail|worktree-fail) fallback_reason='git worktree add failed' ;;
dirty-tree) fallback_reason='the working tree could not be safely stashed' ;;
unparsed-files) fallback_reason='the lane group declares no separable file set' ;;
config) fallback_reason='execution = "sequential" was configured' ;;
*) die 'worktree status must be ok, worktree-fail, dirty-tree, unparsed-files, or config' ;;
esac
config_dir="${LINCHPIN_CONFIG_DIR:-$PWD}"
lane_file=$(mktemp "${TMPDIR:-/tmp}/linchpin-schedule.XXXXXX")
trap 'rm -f -- "$lane_file"' EXIT HUP INT TERM
while [ "$#" -gt 0 ]; do
case "$1" in
--config-dir)
[ "$#" -ge 2 ] || die '--config-dir needs a directory'
config_dir="$2"
shift 2
;;
--config-dir=*)
config_dir=${1#*=}
shift
;;
*)
printf '%s\n' "$1" >> "$lane_file"
shift
;;
esac
done
lane_count=$(wc -l < "$lane_file" | tr -d ' ')
[ "$lane_count" -gt 0 ] || die 'schedule needs at least one lane'
load_config "$config_dir"
forced_parallel=0
if [ "$requested_execution" = auto ]; then
selected="$execution"
[ "$execution" = parallel ] && forced_parallel=1
[ "$execution" = auto ] && selected=parallel
else
selected="$requested_execution"
[ "$requested_execution" = parallel ] && forced_parallel=1
fi
if [ "$worktree_status" != ok ]; then
if [ "$forced_parallel" -eq 1 ]; then
die "execution=parallel forced worktrees, but $fallback_reason"
fi
printf 'ANNOUNCE: %s; this lane group runs sequentially in the shared working tree.\n' "$fallback_reason"
selected=sequential
elif [ "$selected" = sequential ]; then
printf '%s\n' 'ANNOUNCE: sequential execution was selected by configuration or collision analysis.'
fi
if [ "$selected" = sequential ]; then
concurrency_limit=1
else
concurrency_limit="$max_lanes"
fi
lanes=
active_lanes=
queued_lanes=
active_count=0
while IFS= read -r lane; do
if [ -n "$lanes" ]; then lanes="$lanes,$lane"; else lanes="$lane"; fi
if [ "$active_count" -lt "$concurrency_limit" ]; then
if [ -n "$active_lanes" ]; then active_lanes="$active_lanes,$lane"; else active_lanes="$lane"; fi
active_count=$((active_count + 1))
else
if [ -n "$queued_lanes" ]; then queued_lanes="$queued_lanes,$lane"; else queued_lanes="$lane"; fi
fi
done < "$lane_file"
[ -n "$active_lanes" ] || active_lanes=-
[ -n "$queued_lanes" ] || queued_lanes=-
printf 'group=1 mode=%s lanes=%s active=%s queued=%s\n' "$selected" "$lanes" "$active_lanes" "$queued_lanes"
}
gate_evidence() {
prd="$1"
report="$2"
require_file "$prd"
# The contract governs artifacts Linchpin authored. A user's own PRD is
# executed as written, so conformance is never an admission gate on delivery.
# What a PRD declares is still binding; what it never declared is not.
if marker_is_valid "$prd"; then
contract_check "$prd" >/dev/null
require_file "$report"
# Delivery is the point where the planned caller must have become a real one.
validate_ledger "$prd" delivered
else
require_file "$report"
fi
expected_rows=$(negative_data "$prd")
if [ -z "$expected_rows" ] && ! marker_is_valid "$prd"; then
printf 'GATES-NOT-DECLARED %s declares no Negative Controls; deliver on the verification it does declare\n' "$prd"
return 0
fi
actual_rows=$(awk -F '|' '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
/^## Gate Evidence[[:space:]]*$/ { active = 1; next }
active && /^## / { exit }
active && /^\|/ {
gate = trim($2)
if (gate != "" && gate != "Gate" && gate !~ /^-+$/) {
result = (NF >= 3 ? trim($3) : "")
evidence = (NF >= 4 ? trim($4) : "")
exact = (NF == 6 ? trim($5) : "__MALFORMED__")
print gate "\t" result "\t" evidence "\t" exact
}
}
' "$report")
[ -n "$expected_rows" ] || die 'PRD has no expected gate identifiers'
[ -n "$actual_rows" ] || die 'gate evidence is absent, green-only, or malformed'
expected=$(printf '%s\n' "$expected_rows" | awk 'NF { count++ } END { print count + 0 }')
actual=$(printf '%s\n' "$actual_rows" | awk 'NF { count++ } END { print count + 0 }')
[ "$actual" -eq "$expected" ] || die "gate evidence count $actual does not match negative-control count $expected"
expected_ids=$(printf '%s\n' "$expected_rows" | cut -f1 | sort)
actual_ids=$(printf '%s\n' "$actual_rows" | cut -f1 | sort)
duplicate_gate=$(printf '%s\n' "$actual_ids" | uniq -d)
[ -z "$duplicate_gate" ] || die "gate evidence has duplicate gate id: $duplicate_gate"
[ "$actual_ids" = "$expected_ids" ] || die 'gate evidence gate identifiers do not exactly match the PRD'
while IFS="$(printf '\t')" read -r gate result evidence actual_field; do
[ -n "$gate" ] || continue
expected_field=$(printf '%s\n' "$expected_rows" | awk -F '\t' -v target="$gate" '$1 == target { print $2; exit }')
expected_command=$(command_from_field "$expected_field")
actual_command=$(command_from_field "$actual_field")
actual_result=$(result_from_field "$actual_field")
actual_exit=$(exit_from_field "$actual_field")
[ "$result" = PASS ] || die "gate evidence result is not PASS: $gate"
case "$evidence" in
*'RED observed:'*) ;;
*) die "gate evidence lacks an observed-red marker: $gate" ;;
esac
[ -n "$actual_command" ] || die "gate evidence lacks exact command/result field: $gate"
[ "$actual_command" = "$expected_command" ] || die "gate evidence command does not match PRD: $gate"
case "$actual_result" in
*'RED observed:'*) ;;
*) die "gate evidence result lacks observed-red evidence: $gate" ;;
esac
case "$actual_exit" in
''|0|*[!0-9]*) die "gate evidence exit is not non-zero: $gate" ;;
esac
[ "$actual_exit" -gt 0 ] || die "gate evidence exit is not non-zero: $gate"
done <<EOF
$actual_rows
EOF
printf 'GATES-PASS %s controls with exact observed-red evidence\n' "$actual"
}
preflight_model() {
# The configured worker may not be the shipped pin, so resolve config before
# deciding which model to verify. Verifying the default while the run uses
# another model is a preflight that proves nothing. A malformed config must
# fail here rather than be swallowed: preflight exists to refuse before any
# branch is created, and a PASS naming a model the config never asked for is
# worse than no preflight at all. An ABSENT config is the zero-config default
# and is not an error.
load_config "${LINCHPIN_CONFIG_DIR:-$PWD}"
runtime_metadata
cache_path="${1:-${LINCHPIN_MODELS_CACHE:-}}"
if [ -z "$cache_path" ]; then
codex_home="${CODEX_HOME:-${HOME:?HOME is required for model preflight}/.codex}"
cache_path="$codex_home/models_cache.json"
fi
require_file "$cache_path"
command -v jq >/dev/null 2>&1 || die 'jq is required for model preflight'
# Every `codex exec` child writes its own session state under CODEX_HOME
# before the model is ever contacted. When the manager itself runs under a
# sandbox that leaves CODEX_HOME read-only, the child dies with `failed to
# initialize in-process app-server client: Read-only file system` — and the
# run discovers it at the *reviewer*, after every lane has already been built
# and committed. That run ended "committed but review-gated" with no review at
# all. A directory write test costs nothing and moves the discovery here.
preflight_home=$(dirname -- "$cache_path")
preflight_probe="$preflight_home/.linchpin-preflight-write"
if ! (: > "$preflight_probe") 2>/dev/null; then
die "CODEX_HOME is not writable: $preflight_home — every codex exec worker and reviewer fails at app-server init before the model starts; run linchpin from a session that can write it, or set CODEX_HOME to a writable directory"
fi
rm -f "$preflight_probe"
# Every role that will actually run gets checked, not just the worker. A
# reviewer model missing from the cache fails at the first lane's review,
# after the run has already spent its worker time.
for preflight_model_slug in "$worker_model" "$reviewer_model"; do
jq -e --arg model "$preflight_model_slug" '
[.. | objects | select(.slug? == $model)] | length > 0
' "$cache_path" >/dev/null || die "model missing from $cache_path: $preflight_model_slug"
# A model that declares no multi-agent capability is not one this plugin
# knows how to drive. Absent is a refusal; the specific version is not.
jq -e --arg model "$preflight_model_slug" '
[.. | objects | select(.slug? == $model) | select(.multi_agent_version? != null)] | length > 0
' "$cache_path" >/dev/null || die "model declares no multi_agent_version in $cache_path: $preflight_model_slug"
done
# Luna speaks v1 while native spawning speaks v2, so it must never be started
# through a native subagent. Linchpin always uses `codex exec`, and
# `scripts/verify.sh` greps the skills for `agent_type`/`fork_turns` to keep
# it that way; this line records which model carries the constraint.
worker_multi_agent=$(jq -r --arg model "$worker_model" '
[.. | objects | select(.slug? == $model) | .multi_agent_version?] | map(select(. != null)) | first // "unknown"
' "$cache_path")
printf 'PREFLIGHT-PASS worker=%s (multi_agent=%s) mechanism=%s reviewer=%s reviewer_mechanism=%s cache=%s\n' \
"$worker_model" "$worker_multi_agent" "$worker_mechanism" "$reviewer_model" "$reviewer_mechanism" "$cache_path"
}
workspace_ignore_one() {
# `git check-ignore` is the only honest test: a repo that already ignores the
# path through any mechanism needs no second entry.
workspace_path="$1"
if git -C "$workspace_repo" check-ignore -q "$workspace_path" 2>/dev/null; then
printf 'WORKSPACE-ALREADY-IGNORED %s\n' "$workspace_path"
return
fi
# `.git/info/exclude`, not `.gitignore`. Ignoring our own scratch output must
# not itself show up as a modified tracked file in the user's `git status`,
# and must not ride along in a lane commit.
workspace_exclude="$workspace_git_dir/info/exclude"
mkdir -p "$workspace_git_dir/info"
[ -f "$workspace_exclude" ] || : > "$workspace_exclude"
if [ -s "$workspace_exclude" ] && [ "$(tail -c 1 "$workspace_exclude" | od -An -c | tr -d ' \n')" != '\n' ]; then
printf '\n' >> "$workspace_exclude"
fi
printf '%s\n' "$workspace_path" >> "$workspace_exclude"
printf 'WORKSPACE-IGNORED %s .git/info/exclude\n' "$workspace_path"
}
workspace() {
workspace_repo="${1:-${LINCHPIN_CONFIG_DIR:-$PWD}}"
[ -d "$workspace_repo" ] || die "workspace target is not a directory: $workspace_repo"
command -v git >/dev/null 2>&1 || die 'git is required to prepare a linchpin workspace'
git -C "$workspace_repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
die "workspace target is not a Git repository: $workspace_repo"
# The COMMON git dir, not the per-worktree one. Inside a linked worktree
# `--absolute-git-dir` resolves to `.git/worktrees/<name>`, whose `info/exclude`
# git never reads — the entry would be written to a file with no effect, which
# is worse than not writing it. Linchpin runs lanes in worktrees, so this is
# the common case, not the edge case.
workspace_git_dir=$(git -C "$workspace_repo" rev-parse --git-common-dir)
case "$workspace_git_dir" in
/*) ;;
# Older git returns this relative to the working directory.
*) workspace_git_dir=$(CDPATH= cd -- "$workspace_repo/$workspace_git_dir" && pwd) ;;
esac
# Claim the ignore entries BEFORE the first write. A run directory that
# appears in `git status` is leftover the user has to clean up by hand.
workspace_ignore_one '.linchpin/'
workspace_ignore_one '.worktrees/'
mkdir -p "$workspace_repo/.linchpin"
printf 'WORKSPACE-READY %s\n' "$workspace_repo/.linchpin"
}
lane_worktree() {
# Linchpin used to describe lane isolation and leave the mechanism to the
# manager. Managers filled the gap with whatever worktree helper the user
# happened to have installed. One of those helpers ran `git pull` in the
# source tree on the way to creating the worktree, left an unresolved merge
# in twenty modified files the user had not committed, and the run spent its
# first minutes on `git merge --abort` instead of on the PRD. Lane isolation
# is linchpin's job, so linchpin ships the command.
worktree_repo=''
worktree_slug=''
worktree_base=''
worktree_path=''
while [ "$#" -gt 0 ]; do
case "$1" in
--path) [ "$#" -ge 2 ] || die 'worktree --path needs a directory'; worktree_path="$2"; shift 2 ;;
--*) die "unknown worktree option: $1" ;;
*)
if [ -z "$worktree_repo" ]; then worktree_repo="$1"
elif [ -z "$worktree_slug" ]; then worktree_slug="$1"
elif [ -z "$worktree_base" ]; then worktree_base="$1"
else die "unexpected worktree argument: $1"
fi
shift ;;
esac
done
[ -n "$worktree_repo" ] && [ -n "$worktree_slug" ] && [ -n "$worktree_base" ] ||
die 'usage: linchpin.sh worktree REPO LANE_SLUG BASE_REF [--path DIR]'
printf '%s\n' "$worktree_slug" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]*$' ||
die "lane slug is malformed: '$worktree_slug' (allowed: alphanumeric start, then A-Z a-z 0-9 . _ -)"
command -v git >/dev/null 2>&1 || die 'git is required to create a lane worktree'
[ -d "$worktree_repo" ] || die "worktree target is not a directory: $worktree_repo"
git -C "$worktree_repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
die "worktree target is not a Git repository: $worktree_repo"
# Refuse to build a lane from inside another lane. A manager that had already
# moved into a worktree created the next lane relative to *that* directory and
# produced `.worktrees/<lane-a>/.worktrees/<lane-b>`, branched off lane A
# rather than off the base. Both nesting and lane-from-lane branching are
# forbidden by the coordinator; this is where they are actually prevented.
if [ "$(git -C "$worktree_repo" rev-parse --is-inside-work-tree 2>/dev/null)" = 'true' ] &&
[ "$(git -C "$worktree_repo" rev-parse --git-dir)" != "$(git -C "$worktree_repo" rev-parse --git-common-dir)" ]; then
printf 'WORKTREE-FAIL nested %s is itself a linked worktree; create lanes from the main worktree\n' "$worktree_repo" >&2
exit 1
fi
worktree_branch="linchpin/$worktree_slug"
if git -C "$worktree_repo" show-ref --verify --quiet "refs/heads/$worktree_branch"; then
printf 'WORKTREE-FAIL branch-exists %s already exists; resume it or pick another lane slug\n' "$worktree_branch" >&2
exit 1
fi
# Branch from the remote base after a fetch. A local base that sits ahead of
# its remote carries the user's unrelated committed work into every lane, and
# delivery then merges that work under a PR title that never mentions it.
# `git fetch` is safe in a dirty tree; `git pull` is not, and is never run.
worktree_resolved="$worktree_base"
if git -C "$worktree_repo" remote get-url origin >/dev/null 2>&1; then
git -C "$worktree_repo" fetch --quiet origin "$worktree_base" 2>/dev/null || true
if git -C "$worktree_repo" rev-parse --verify --quiet "origin/$worktree_base" >/dev/null; then
worktree_resolved="origin/$worktree_base"
fi
fi
if ! git -C "$worktree_repo" rev-parse --verify --quiet "$worktree_resolved" >/dev/null; then
printf 'WORKTREE-FAIL missing-base %s does not resolve in %s\n' "$worktree_resolved" "$worktree_repo" >&2
exit 1
fi
worktree_base_sha=$(git -C "$worktree_repo" rev-parse "$worktree_resolved")
[ -n "$worktree_path" ] || worktree_path="$worktree_repo/.worktrees/$worktree_slug"
if [ -e "$worktree_path" ]; then
printf 'WORKTREE-FAIL path-exists %s\n' "$worktree_path" >&2
exit 1
fi
# The failure text is the announcement the user reads, so it carries git's own
# reason rather than a summary of it. `schedule auto worktree-fail` is only
# honest when the attempt actually happened.
if ! worktree_error=$(git -C "$worktree_repo" worktree add -b "$worktree_branch" "$worktree_path" "$worktree_resolved" 2>&1); then
printf 'WORKTREE-FAIL add %s\n' "$(printf '%s' "$worktree_error" | tr '\n' ' ')" >&2
exit 1
fi
printf 'WORKTREE-READY path=%s branch=%s base=%s sha=%s\n' \
"$worktree_path" "$worktree_branch" "$worktree_resolved" "$worktree_base_sha"
}
lane_await() {
# A lane takes tens of minutes. Managers waited on them by polling a live
# subprocess every few seconds: one field batch spent 954 thirty-second polls
# and 739 one-second polls restating that lanes were still running. Polling
# once per lane per interval makes waiting cost turns in proportion to lane
# *duration*; this waits on a whole group in one call, so it costs turns in
# proportion to the number of groups.
await_interval=30
await_timeout=0
await_pidfiles=''
while [ "$#" -gt 0 ]; do
case "$1" in
--interval) [ "$#" -ge 2 ] || die 'await --interval needs seconds'; await_interval="$2"; shift 2 ;;
--timeout) [ "$#" -ge 2 ] || die 'await --timeout needs seconds'; await_timeout="$2"; shift 2 ;;
--*) die "unknown await option: $1" ;;
*) await_pidfiles="$await_pidfiles $1"; shift ;;
esac
done
[ -n "$await_pidfiles" ] || die 'usage: linchpin.sh await PIDFILE... [--interval SECONDS] [--timeout SECONDS]'
for await_arg in $await_interval $await_timeout; do
printf '%s\n' "$await_arg" | grep -Eq '^[0-9]+$' || die "await interval and timeout are whole seconds: $await_arg"
done
[ "$await_interval" -gt 0 ] || die 'await --interval must be greater than zero'
for await_file in $await_pidfiles; do
require_file "$await_file"
done
await_waited=0
while :; do
await_running=0
for await_file in $await_pidfiles; do
await_pid=$(tr -dc '0-9' < "$await_file")
[ -n "$await_pid" ] || continue
kill -0 "$await_pid" 2>/dev/null && await_running=$((await_running + 1))
done
[ "$await_running" -eq 0 ] && break
if [ "$await_timeout" -gt 0 ] && [ "$await_waited" -ge "$await_timeout" ]; then
# A timeout is not a delivery result. Say which lanes are still alive and
# leave them running: the manager inspects the real diff from here.
printf 'AWAIT-TIMEOUT running=%d waited=%ds\n' "$await_running" "$await_waited" >&2
exit 1
fi
sleep "$await_interval"
await_waited=$((await_waited + await_interval))
done
for await_file in $await_pidfiles; do
await_pid=$(tr -dc '0-9' < "$await_file")
await_status='exited'
[ -f "$await_file.exit" ] && await_status=$(tr -dc '0-9' < "$await_file.exit")
printf 'AWAIT-DONE lane=%s pid=%s exit=%s\n' \
"$(basename "$await_file" .pid)" "${await_pid:-unknown}" "$await_status"
done
printf 'AWAIT-COMPLETE lanes=%d waited=%ds\n' "$(printf '%s\n' $await_pidfiles | wc -l | tr -d ' ')" "$await_waited"
}
# Every other invariant in this plugin is enforced by a command: briefs are
# checked, gates are checked, worktrees are created, models are preflighted. The
# run ledger was the exception. The coordinator demands fifteen fields per lane,
# calls a run without one unresumable, and then leaves a manager model to type
# those fields from memory at the end of an eight-lane batch. That is exactly
# where a row reading `DELIVERED` against a sha nobody created gets written —
# a failure the skill names and had no way to catch. These two commands make the
# ledger the artifact it was always described as: written by a helper that
# refuses a claim it cannot verify, and read back by a command rather than
# recalled.
ledger_lane_valid() {
printf '%s\n' "$1" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]*$'
}
ledger_block() {
# Every `- key: value` line of one lane's block, in file order. A block ends at
# the next heading of any kind, so prose a manager adds between lanes is never
# absorbed into the row above it.
awk -v target="$2" '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
/^## / {
current = ($0 ~ /^## Lane: /) ? trim(substr($0, 10)) : ""
next
}
current == target && /^- [a-z][a-z0-9_]*:/ {
entry = substr($0, 3)
split_at = index(entry, ":")
print substr(entry, 1, split_at - 1) "\t" trim(substr(entry, split_at + 1))
}
' "$1"
}
ledger_value() {
# Empty output means absent: a value is never allowed to be empty, so the two
# cases cannot be confused by a caller.
awk -F '\t' -v key="$1" '$1 == key { value = $2 } END { print value }' "$2"
}
lane_record() {
lane_file=''
lane_id=''
lane_repo=''
lane_sets_seen=0
lane_tmp=$(mktemp -d "${TMPDIR:-/tmp}/linchpin-lane.XXXXXX")
trap 'rm -rf -- "$lane_tmp"' EXIT HUP INT TERM
: > "$lane_tmp/sets"
while [ "$#" -gt 0 ]; do
case "$1" in
--set)
[ "$#" -ge 2 ] || die 'lane --set needs key=value'
case "$2" in
*=*) ;;
*) die "lane --set needs key=value: $2" ;;
esac
lane_key=${2%%=*}
lane_value=${2#*=}
printf '%s\n' "$lane_key" | grep -Eq '^[a-z][a-z0-9_]*$' ||
die "lane field name is not a lowercase identifier: $lane_key"
[ -n "$lane_value" ] || die "lane field has an empty value: $lane_key (write an explicit value such as none)"
case "$lane_value" in
*"$(printf '\t')"*) die "lane field value contains a tab: $lane_key" ;;
esac
[ "$(printf '%s' "$lane_value" | wc -l | tr -d ' ')" -eq 0 ] ||
die "lane field value spans more than one line: $lane_key"
printf '%s\t%s\n' "$lane_key" "$lane_value" >> "$lane_tmp/sets"
lane_sets_seen=$((lane_sets_seen + 1))
shift 2 ;;
--repo) [ "$#" -ge 2 ] || die 'lane --repo needs a directory'; lane_repo="$2"; shift 2 ;;
--*) die "unknown lane option: $1" ;;
*)
if [ -z "$lane_file" ]; then lane_file="$1"
elif [ -z "$lane_id" ]; then lane_id="$1"
else die "unexpected lane argument: $1"
fi
shift ;;
esac
done
[ -n "$lane_file" ] || die 'usage: linchpin.sh lane LEDGER LANE_ID --set key=value...'
[ -n "$lane_id" ] || die 'usage: linchpin.sh lane LEDGER LANE_ID --set key=value...'
ledger_lane_valid "$lane_id" || die "lane id is malformed: $lane_id"
[ "$lane_sets_seen" -gt 0 ] || die 'lane needs at least one --set key=value'
# The ledger lives at <repo>/.linchpin/run-<timestamp>.md, so the repository
# that holds the lane's commit is two levels up unless the caller says otherwise.
[ -n "$lane_repo" ] || lane_repo=$(CDPATH= cd -- "$(dirname -- "$(dirname -- "$lane_file")")" && pwd)
if [ -e "$lane_file" ]; then
[ -f "$lane_file" ] || die "run ledger is not a file: $lane_file"
ledger_block "$lane_file" "$lane_id" > "$lane_tmp/existing"
else
lane_dir=$(dirname -- "$lane_file")
[ -d "$lane_dir" ] || die "run ledger directory does not exist: $lane_dir (run linchpin.sh workspace first)"
printf '%s\n' '# Linchpin run ledger' '' > "$lane_file"
: > "$lane_tmp/existing"
fi
# Merge: an existing field keeps its position and takes the new value, a new
# field is appended. Rewriting the row from the --set list alone would silently
# drop every field an earlier call recorded.
: > "$lane_tmp/merged"
while IFS="$(printf '\t')" read -r lane_key lane_value; do
[ -n "$lane_key" ] || continue
lane_override=$(ledger_value "$lane_key" "$lane_tmp/sets")
[ -z "$lane_override" ] || lane_value="$lane_override"
printf '%s\t%s\n' "$lane_key" "$lane_value" >> "$lane_tmp/merged"
done < "$lane_tmp/existing"
while IFS="$(printf '\t')" read -r lane_key lane_value; do
[ -n "$lane_key" ] || continue
[ -z "$(ledger_value "$lane_key" "$lane_tmp/existing")" ] || continue
[ -z "$(ledger_value "$lane_key" "$lane_tmp/merged")" ] || continue
# Read the value back rather than trusting this line: a field set twice in
# one call must land on the same last-wins value an existing field would.
printf '%s\t%s\n' "$lane_key" "$(ledger_value "$lane_key" "$lane_tmp/sets")" >> "$lane_tmp/merged"
done < "$lane_tmp/sets"
lane_state=$(ledger_value state "$lane_tmp/merged")
[ -n "$lane_state" ] || die "lane row has no state: $lane_id (--set state=PENDING|RUNNING|PARTIAL|BLOCKED|'DELIVERED(pr)'|'DELIVERED(branch)')"
case "$lane_state" in
MERGED|merged)
# The coordinator forbids this word as a product state on purpose: it bakes
# pr delivery into the ledger's vocabulary and makes branch delivery a
# redesign instead of a config value.
die "MERGED is not a lane state; use DELIVERED(pr) or DELIVERED(branch)" ;;
PENDING|RUNNING|PARTIAL|BLOCKED|'DELIVERED(pr)'|'DELIVERED(branch)') ;;
*) die "unknown lane state: $lane_state (PENDING, RUNNING, PARTIAL, BLOCKED, DELIVERED(pr), DELIVERED(branch))" ;;
esac
lane_commit=$(ledger_value commit "$lane_tmp/merged")
if [ -n "$lane_commit" ]; then
# A recorded sha the worker never created is the false ledger row the
# coordinator names and could not catch. Resolving it costs one git call.
printf '%s\n' "$lane_commit" | grep -Eq '^[0-9a-f]{7,40}$' ||
die "lane commit is not a git object id: $lane_commit"
command -v git >/dev/null 2>&1 || die 'git is required to verify a recorded lane commit'
git -C "$lane_repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
die "lane commit cannot be verified: $lane_repo is not a Git repository (pass --repo)"
git -C "$lane_repo" cat-file -e "$lane_commit^{commit}" 2>/dev/null ||
die "recorded lane commit does not exist in $lane_repo: $lane_commit"
fi
case "$lane_state" in
'DELIVERED(pr)'|'DELIVERED(branch)')
for lane_required in prd branch commit gates review; do
[ -n "$(ledger_value "$lane_required" "$lane_tmp/merged")" ] ||
die "a delivered lane needs $lane_required: $lane_id"
done
lane_gates=$(ledger_value gates "$lane_tmp/merged")
# Either the evidence file exists or the PRD declared no controls, which is
# what `gate` reports. An asserted evidence path that is not on disk is the
# same claim-without-evidence the gate rule exists to reject.
if [ "$lane_gates" != 'NOT-DECLARED' ] && [ ! -f "$lane_gates" ]; then
case "$lane_gates" in
/*) die "gate evidence file does not exist: $lane_gates" ;;
*) [ -f "$lane_repo/$lane_gates" ] || die "gate evidence file does not exist: $lane_gates (relative to $lane_repo)" ;;
esac
fi
;;
BLOCKED)
for lane_required in reason resume; do
[ -n "$(ledger_value "$lane_required" "$lane_tmp/merged")" ] ||
die "a blocked lane needs $lane_required: $lane_id"
done
;;
esac
{
printf '## Lane: %s\n' "$lane_id"
while IFS="$(printf '\t')" read -r lane_key lane_value; do
[ -n "$lane_key" ] || continue
printf '%s\n' "- $lane_key: $lane_value"
done < "$lane_tmp/merged"
} > "$lane_tmp/block"
awk -v target="$lane_id" -v blockfile="$lane_tmp/block" '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
# The trailing blank line is emitted here, not by the caller: an update
# consumes the blank line that followed the old block, and without this every
# re-record would jam the next lane heading against the row above it.
function emit( i) { for (i = 1; i <= block_lines; i++) print block[i]; print "" }
BEGIN { while ((getline block_line < blockfile) > 0) block[++block_lines] = block_line }
/^## / {
if ($0 ~ /^## Lane: / && trim(substr($0, 10)) == target) {
emit(); replaced = 1; inside = 1; next
}
inside = 0; print; next
}
inside { next }
{ print }
END { if (!replaced) emit() }
' "$lane_file" > "$lane_tmp/ledger"
cat "$lane_tmp/ledger" > "$lane_file"
printf 'LANE-RECORDED %s state=%s fields=%s\n' \
"$lane_id" "$lane_state" "$(awk 'NF' "$lane_tmp/merged" | wc -l | tr -d ' ')"
}
run_status() {
status_file="${1:-}"
[ -n "$status_file" ] || die 'usage: linchpin.sh status LEDGER'
require_file "$status_file"
status_lines=$(awk '
function trim(value) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
return value
}
function flush( line, i, key) {
if (lane == "") return
line = (value["state"] == "" ? "UNRECORDED" : value["state"]) " lane=" lane
for (i = 1; i <= reported; i++) {
key = report[i]
if (value[key] != "") line = line " " key "=" value[key]
}
print line
lane = ""
split("", value)
}
BEGIN {
reported = split("prd branch commit gates review reason resume", report, " ")
}
/^## / {
flush()
if ($0 ~ /^## Lane: /) lane = trim(substr($0, 10))
next
}
lane != "" && /^- [a-z][a-z0-9_]*:/ {
entry = substr($0, 3)
split_at = index(entry, ":")
value[substr(entry, 1, split_at - 1)] = trim(substr(entry, split_at + 1))
}
END { flush() }
' "$status_file")
[ -n "$status_lines" ] || die "run ledger has no lane rows: $status_file"
printf '%s\n' "$status_lines"
status_count() {
printf '%s\n' "$status_lines" | grep -c "$1" || true
}
status_delivered=$(status_count '^DELIVERED(')
status_partial=$(status_count '^PARTIAL ')
status_blocked=$(status_count '^BLOCKED ')
status_pending=$(status_count '^PENDING ')
status_running=$(status_count '^RUNNING ')
status_unrecorded=$(status_count '^UNRECORDED ')
status_open=$((status_partial + status_pending + status_running + status_unrecorded))
printf 'RUN-STATUS delivered=%s partial=%s blocked=%s pending=%s running=%s unrecorded=%s\n' \
"$status_delivered" "$status_partial" "$status_blocked" "$status_pending" \
"$status_running" "$status_unrecorded"
# Three outcomes, not two. A goal loop needs "keep going" and "stop, a human is
# required" to be different answers, and a summary that says done while a lane
# is still PARTIAL is the prose claim this command replaces.
[ "$status_open" -eq 0 ] || exit 1
[ "$status_blocked" -eq 0 ] || exit 2
}
usage() {
cat <<'USAGE'
linchpin.sh COMMAND [ARGS]
route INTENT [SCORE] [PRD ...] [--config-dir DIR] classify a request
contract PRD report every contract problem
migrate PRD [--out PATH] [--force] write a v1 copy; never edits PRD
brief PRD [LANE_ID LANE_MODE DELIVERY_MODE] emit the worker brief
[--out PATH] [--config-dir DIR]
brief-check PRD BRIEF [--config-dir DIR] verify a brief against its PRD
review-brief PRD LANE_ID --gates PATH --commit SHA emit the read-only review brief
[--out PATH]
files PRD print the parsed Files (N) list
mode EXECUTION PRD... [--config-dir DIR] group lanes by file collision
schedule EXECUTION STATUS LANE... [--config-dir DIR]
STATUS: ok | worktree-fail | dirty-tree | unparsed-files | config
gate PRD REPORT check observed-red evidence
config [REPO] print resolved .linchpin.toml
workspace [REPO] make .linchpin/ and ignore run output
lane LEDGER LANE_ID --set key=value... [--repo DIR] record one run-ledger row
state: PENDING | RUNNING | PARTIAL | BLOCKED | DELIVERED(pr) | DELIVERED(branch)
a recorded commit must resolve in the repository; DELIVERED needs
prd, branch, commit, gates, review; BLOCKED needs reason, resume
status LEDGER read the ledger back
exit 0 every lane delivered; 1 a lane is still open; 2 only blocked lanes remain
worktree REPO LANE_SLUG BASE_REF [--path DIR] create one isolated lane worktree
await PIDFILE... [--interval S] [--timeout S] block until a group's lanes exit
preflight [MODELS_CACHE.json] check the worker model
help this text
EXECUTION is auto, parallel, or sequential.
USAGE
}
command_name="${1:-}"
shift || true
case "$command_name" in
contract) [ "$#" -eq 1 ] || die 'usage: linchpin.sh contract PRD'; contract_check "$1" ;;
migrate) [ "$#" -ge 1 ] || die 'usage: linchpin.sh migrate PRD [--out PATH] [--force]'; migrate "$@" ;;
brief) [ "$#" -ge 1 ] || die 'usage: linchpin.sh brief PRD [LANE_ID LANE_MODE DELIVERY_MODE] [--config-dir DIR]'; brief "$@" ;;
brief-check) [ "$#" -ge 2 ] || die 'usage: linchpin.sh brief-check PRD BRIEF [--config-dir DIR]'; brief_check "$@" ;;
review-brief) [ "$#" -ge 1 ] || die 'usage: linchpin.sh review-brief PRD LANE_ID --gates PATH --commit SHA [--out PATH]'; review_brief "$@" ;;
files)
[ "$#" -eq 1 ] || die 'usage: linchpin.sh files PRD'
if ! files_list "$1"; then
# Silence plus exit 1 reads as a broken helper. Say which of the two it is.
printf 'NO-FILES-LIST %s has no machine-readable `Files (N)` list; run `mode` for its derived set.\n' "$1" >&2
exit 1
fi
;;
help|--help|-h) usage; exit 0 ;;
config) [ "$#" -le 1 ] || die 'usage: linchpin.sh config [repo]'; config_values "${1:-${LINCHPIN_CONFIG_DIR:-$PWD}}" ;;
workspace) [ "$#" -le 1 ] || die 'usage: linchpin.sh workspace [repo]'; workspace "${1:-}" ;;
lane) [ "$#" -ge 3 ] || die 'usage: linchpin.sh lane LEDGER LANE_ID --set key=value... [--repo DIR]'; lane_record "$@" ;;
status) [ "$#" -eq 1 ] || die 'usage: linchpin.sh status LEDGER'; run_status "$1" ;;
worktree) [ "$#" -ge 3 ] || die 'usage: linchpin.sh worktree REPO LANE_SLUG BASE_REF [--path DIR]'; lane_worktree "$@" ;;
await) [ "$#" -ge 1 ] || die 'usage: linchpin.sh await PIDFILE... [--interval SECONDS] [--timeout SECONDS]'; lane_await "$@" ;;
route) [ "$#" -ge 1 ] || die 'usage: linchpin.sh route INTENT [SCORE] [PRD ...] [--config-dir DIR]'; route "$@" ;;
mode) [ "$#" -ge 2 ] || die 'usage: linchpin.sh mode EXECUTION PRD...'; mode_selection "$@" ;;
schedule) [ "$#" -ge 3 ] || die 'usage: linchpin.sh schedule EXECUTION WORKTREE_STATUS LANE...'; schedule "$@" ;;
gate) [ "$#" -eq 2 ] || die 'usage: linchpin.sh gate PRD REPORT'; gate_evidence "$1" "$2" ;;
preflight) [ "$#" -le 1 ] || die 'usage: linchpin.sh preflight [models_cache.json]'; preflight_model "${1:-}" ;;
*) usage >&2; exit 1 ;;
esac
SHA-256: d74e00b1b423687a65198ee55291c1d8deb9a9fd479179e58ead5739867879de