Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 83 additions & 25 deletions bin/cb-agent-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
# READING GUIDE
# -------------
# 1. Universal input validation <- contain paths and identify the adapter.
# 2. Config and Git preflight <- fail closed before tool execution.
# 2. Custody/config/Git preflight <- consume the Launcher handoff exactly once.
# 3. Isolated tool invocation <- direct prompt or bounded GNHF loop.
# 4. Candidate normalization <- publish exact local SHA or stable error.
#
# MAIN FLOW
# ---------
# step input -> config/Git preflight -> isolated tool -> Git facts -> output
# step input -> Launcher custody -> config/Git preflight -> tool -> output
#
# PUBLIC API
# ----------
Expand All @@ -22,10 +22,10 @@
# INTERNALS
# ---------
# usage, fail_contract, publish_outcome, reject, valid_config,
# build_environment, install_git_guard
# build_environment, install_git_guard, canonical_git_common
#
# @exports none
# @deps bash, env, git, jq, realpath
# @deps bash, env, git, jq, realpath, stat
set -euo pipefail

usage() {
Expand Down Expand Up @@ -166,12 +166,8 @@ valid_config() {
(explode|all(.[]; .!=0)));
.config |
type=="object" and
keys==["argv","base_sha","branch","environment","output_schema",
"prompt","schema","worktree"] and
keys==["argv","environment","output_schema","prompt","schema"] and
.schema=="combo.coder/direct-agent/v1" and
(.worktree|type=="string" and startswith("/")) and
(.base_sha|type=="string" and test("^[0-9a-f]{40}([0-9a-f]{24})?$")) and
(.branch|type=="string" and length>0) and
(.argv|strings and
all(.[]; ((.=="--push" or startswith("--push="))|not))) and
(.prompt|type=="string" and length>0) and
Expand All @@ -196,13 +192,10 @@ valid_config() {
(explode|all(.[]; .!=0)));
.config |
type=="object" and
keys==["agent","argv","base_sha","branch","current_branch",
"environment","max_iterations","meteor_frequency","output_schema",
"prevent_sleep","prompt","schema","stop_when","worktree"] and
keys==["agent","argv","current_branch","environment","max_iterations",
"meteor_frequency","output_schema","prevent_sleep","prompt","schema",
"stop_when"] and
.schema=="combo.coder/gnhf/v1" and
(.worktree|type=="string" and startswith("/")) and
(.base_sha|type=="string" and test("^[0-9a-f]{40}([0-9a-f]{24})?$")) and
(.branch|type=="string" and length>0) and
(.argv|strings and
all(.[]; ((.=="--push" or startswith("--push="))|not))) and
(.prompt|type=="string" and length>0) and
Expand Down Expand Up @@ -234,22 +227,87 @@ if ! valid_config; then
fi
fi

worktree=$(jq -r '.config.worktree' "$input")
base_sha=$(jq -r '.config.base_sha' "$input")
branch=$(jq -r '.config.branch' "$input")
run_dir=$(jq -r '.paths.run_dir' "$input")
[ -d "$run_dir" ] && [ ! -L "$run_dir" ] \
|| reject "custody:run_dir_invalid"
run_root=$(realpath "$run_dir" 2>/dev/null) \
|| reject "custody:run_dir_invalid"
[ "$run_root" = "$run_dir" ] || reject "custody:run_dir_invalid"
case "$invocation_root" in "$run_root"/steps/*/attempt-"$attempt") ;;
*) reject "custody:run_dir_invalid" ;;
esac

ownership=$run_root/agents/launcher.ownership.json
if [ ! -e "$ownership" ] && [ ! -L "$ownership" ]; then
reject "custody:missing"
fi
[ -f "$ownership" ] && [ ! -L "$ownership" ] \
|| reject "custody:invalid"
[ "$(realpath "$ownership" 2>/dev/null || true)" = "$ownership" ] \
|| reject "custody:invalid"
ownership_mode=$(stat -c '%a' "$ownership" 2>/dev/null \
|| stat -f '%Lp' "$ownership" 2>/dev/null || true)
[ "$ownership_mode" = 444 ] || reject "custody:not_read_only"
if ! jq -e --arg run "$run_id" '
def sha:
type=="string" and test("^[0-9a-f]{40}([0-9a-f]{24})?$");
def text:
type=="string" and length>0 and
(explode | all(.[]; .>=32 and .!=127));
type=="object" and
.run==$run and
(.runway_kind=="treehouse" or .runway_kind=="git-worktree-explicit") and
(.repo_dir|text and startswith("/")) and
(.worktree|text and startswith("/")) and
(.branch|text) and (.base_sha|sha) and
if .runway_kind=="treehouse" then
keys==[
"base_sha","branch","lease_id","repo_dir","run","runway_kind","worktree"
] and .lease_id==$run
else
keys==[
"base_sha","branch","ownership_id","repo_dir","run","runway_kind",
"worktree"
] and .ownership_id==("git-worktree:" + $run)
end
' "$ownership" >/dev/null 2>&1; then
reject "custody:invalid"
fi

repo_dir=$(jq -r '.repo_dir' "$ownership")
worktree=$(jq -r '.worktree' "$ownership")
base_sha=$(jq -r '.base_sha' "$ownership")
branch=$(jq -r '.branch' "$ownership")
[ -d "$repo_dir" ] && [ ! -L "$repo_dir" ] \
|| reject "custody:repo_mismatch"
[ "$(realpath "$repo_dir" 2>/dev/null || true)" = "$repo_dir" ] \
|| reject "custody:repo_mismatch"
[ -d "$worktree" ] && [ ! -L "$worktree" ] \
|| reject "config:unsafe_worktree"
|| reject "custody:worktree_mismatch"
worktree_root=$(realpath "$worktree" 2>/dev/null) \
|| reject "config:unsafe_worktree"
[ "$worktree_root" = "$worktree" ] || reject "config:unsafe_worktree"
|| reject "custody:worktree_mismatch"
[ "$worktree_root" = "$worktree" ] || reject "custody:worktree_mismatch"
[ "$(git -C "$worktree" rev-parse --show-toplevel 2>/dev/null || true)" = "$worktree" ] \
|| reject "config:not_worktree_root"
|| reject "custody:worktree_mismatch"
canonical_git_common() {
local directory=$1 common
common=$(git -C "$directory" rev-parse --git-common-dir 2>/dev/null) \
|| return 1
case "$common" in
/*) realpath "$common" 2>/dev/null ;;
*) realpath "$directory/$common" 2>/dev/null ;;
esac
}
repo_common=$(canonical_git_common "$repo_dir" || true)
worktree_common=$(canonical_git_common "$worktree" || true)
[ -n "$repo_common" ] && [ "$worktree_common" = "$repo_common" ] \
|| reject "custody:repo_mismatch"
git check-ref-format --branch "$branch" >/dev/null 2>&1 \
|| reject "config:invalid_branch"
|| reject "custody:invalid"
[ "$(git -C "$worktree" symbolic-ref --quiet --short HEAD 2>/dev/null || true)" = "$branch" ] \
|| reject "candidate:branch_mismatch"
|| reject "custody:branch_mismatch"
git -C "$worktree" cat-file -e "$base_sha^{commit}" 2>/dev/null \
|| reject "config:base_commit_missing"
|| reject "custody:base_mismatch"

pre_head=$(git -C "$worktree" rev-parse HEAD 2>/dev/null) \
|| reject "candidate:head_unreadable"
Expand Down
72 changes: 63 additions & 9 deletions bin/cb-chain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,27 @@
# READING GUIDE
# -------------
# 1. Plan validation and publication guard <- freeze the traversal boundary.
# 2. invoke_step and fold helpers <- universal adapter interaction.
# 2. invoke_step and fold helpers <- endpoint-backed adapter interaction.
# 3. Coder/Reviewer loop <- same-input full-round fold.
# 4. Gate, Cleaner, and result publication <- preserve terminal + cleanup.
#
# MAIN FLOW
# ---------
# plan -> Launcher -> Coder <-> Reviewer* -> Gate -> Cleaner -> chain result
# plan -> role endpoints -> Launcher -> Coder <-> Reviewer* -> Gate -> Cleaner
#
# PUBLIC API
# ----------
# cb-chain.sh <runId> Execute the plan and print immutable result path.
#
# INTERNALS
# ---------
# usage, fail_contract, invoke_step, merge_result_artifacts,
# usage, fail_contract, resolve_effective_attempt, invoke_step,
# merge_result_artifacts,
# add_findings_artifact, record_reviewer_failure, set_terminal_from_result,
# set_invocation_failure, cleanup
#
# @exports none
# @deps bash, jq, realpath, bin/cb-step.sh
# @deps bash, jq, realpath, bin/cb-step.sh, optional bin/cb-run.sh dispatcher
set -euo pipefail

usage() {
Expand Down Expand Up @@ -106,6 +107,16 @@ script_dir=$(CDPATH='' cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)
step_runner=$script_dir/cb-step.sh
[ -f "$step_runner" ] && [ ! -L "$step_runner" ] \
|| fail_contract "step runner is missing or unsafe" 73
dispatcher=${CB_CHAIN_DISPATCHER:-}
if [ -n "$dispatcher" ]; then
case "$dispatcher" in /*) ;; *)
fail_contract "endpoint dispatcher path must be absolute" 73 ;;
esac
[ -f "$dispatcher" ] && [ ! -L "$dispatcher" ] && [ -x "$dispatcher" ] \
|| fail_contract "endpoint dispatcher is missing or unsafe" 73
[ "$(realpath "$dispatcher" 2>/dev/null)" = "$dispatcher" ] \
|| fail_contract "endpoint dispatcher path must be canonical" 73
fi

max_rounds=${CB_CHAIN_MAX_REVIEW_ROUNDS:-20}
case "$max_rounds" in ''|0|0*|*[!0-9]*) fail_contract "invalid max review rounds" ;; esac
Expand All @@ -117,18 +128,56 @@ reviewer_degraded=$(jq -r '.reviewer.degraded' "$plan")
reviewer_member_failures='[]'
# -/ 1/4

# -- 2/4 HELPER · Invoke universal steps and carry artifact references --
# -- 2/4 HELPER · Invoke endpoint-backed steps and carry artifact references --
resolve_effective_attempt() {
local step=$1 requested=$2 step_index ordinal safe_step step_dir effective
if [ -z "$dispatcher" ]; then
printf '%s\n' "$requested"
return 0
fi
step_index=$(jq -r --arg id "$step" '
[.steps | to_entries[] | select(.value.id==$id) | .key] |
if length==1 then .[0] else empty end
' "$plan")
[ -n "$step_index" ] || return 1
printf -v ordinal '%02d' "$((step_index + 1))"
safe_step=${step//\//-}
step_dir=$run_root/steps/$ordinal-$safe_step
effective=$requested
while [ -e "$step_dir/attempt-$effective" ] \
|| [ -L "$step_dir/attempt-$effective" ] \
|| [ -e "$run_root/dispatch/jobs/$safe_step-attempt-$effective.job.json" ] \
|| [ -L "$run_root/dispatch/jobs/$safe_step-attempt-$effective.job.json" ] \
|| [ -e "$run_root/dispatch/$safe_step-attempt-$effective.receipt.json" ] \
|| [ -L "$run_root/dispatch/$safe_step-attempt-$effective.receipt.json" ]; do
effective=$((effective + 1))
done
printf '%s\n' "$effective"
}

invoke_step() {
local step=$1 attempt=$2 candidate=$3
local output status expected_root
local -a args=("$run" "$step" "$attempt")
local output status expected_root effective_attempt endpoint_role
effective_attempt=$(resolve_effective_attempt "$step" "$attempt") \
|| { last_step_status=65; return 1; }
local -a args=("$run" "$step" "$effective_attempt")
if [ "$candidate" != null ]; then
args+=(--candidate-sha "$candidate")
fi
args+=(--prior-artifacts "$prior_artifacts")

set +e
output=$(bash "$step_runner" "${args[@]}" </dev/null)
if [ -n "$dispatcher" ]; then
endpoint_role=$(jq -r --arg id "$step" '
.steps[] | select(.id==$id) | .role
' "$plan")
output=$(
"$dispatcher" --dispatch "$run" "$endpoint_role" "$step" \
"$effective_attempt" "$candidate" "$prior_artifacts" </dev/null
)
else
output=$(bash "$step_runner" "${args[@]}" </dev/null)
fi
status=$?
set -e
last_step_status=$status
Expand All @@ -145,7 +194,8 @@ invoke_step() {
*) last_step_status=65; return 1 ;;
esac
if ! jq -e \
--arg run "$run" --arg step "$step" --argjson attempt "$attempt" '
--arg run "$run" --arg step "$step" \
--argjson attempt "$effective_attempt" '
.schema=="combo.step-output/v1" and .run_id==$run and
.step_id==$step and .attempt==$attempt
' "$output" >/dev/null 2>&1; then
Expand Down Expand Up @@ -260,6 +310,10 @@ else
set_invocation_failure launcher
terminal_set=1
fi
if [ "$terminal_set" -eq 0 ] \
&& [ "${CB_CHAIN_STOP_AFTER_ROLE:-}" = launcher ]; then
exit 130
fi

coder_attempt=0
review_round=0
Expand Down
Loading
Loading