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
9 changes: 5 additions & 4 deletions scripts/architecture-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"clone 2c627b34911b src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts",
"clone 2c83ed0d1788 src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts",
"clone 2c9f8493e38e src/llm/muse.ts,src/llm/openrouter.ts",
"clone 2cb1493c835d src/orchestrator/__tests__/main-tree-tripwire.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone 2e4f5c004508 src/init/__tests__/cli.test.ts,src/orchestrator/__tests__/cost-history-cli.test.ts",
"clone 2eefefd492bd src/checkpoints/__tests__/cli.test.ts,src/codemap/__tests__/cli.test.ts,src/codemap/__tests__/units.test.ts,src/dashboard/__tests__/cli.test.ts,src/dashboard/__tests__/page-route.test.ts,src/dashboard/__tests__/self-improvement-metrics.test.ts,src/dashboard/__tests__/sigint-shutdown.test.ts,src/github/__tests__/installations.test.ts,src/orchestrator/__tests__/analyze-cli.test.ts,src/orchestrator/__tests__/cost-estimate.test.ts,src/orchestrator/__tests__/log-analysis.test.ts,src/orchestrator/__tests__/spawn-state-reset.test.ts,src/orchestrator/__tests__/status.test.ts,src/run-tests-script.test.ts,src/tools/__tests__/set-indentation.test.ts",
"clone 2ef48d0cbf0f src/engine/__tests__/new-task.test.ts,src/engine/__tests__/switch-mode.test.ts",
Expand Down Expand Up @@ -254,7 +255,7 @@
"clone 44f4c5f6148f src/cloud/__tests__/openshell-provider.test.ts",
"clone 45181f328e82 src/orchestrator/task-files.ts",
"clone 45edfde1d33f src/orchestrator/task-files.ts",
"clone 464303e3e594 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone 464303e3e594 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/main-tree-tripwire.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone 465b90f368f7 src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts",
"clone 4666d04d2662 src/llm/muse.ts,src/llm/openrouter.ts",
"clone 46ffdeff854a src/orchestrator/resume.ts",
Expand Down Expand Up @@ -384,6 +385,7 @@
"clone 68612079ddc5 src/llm/preflight.ts",
"clone 68654f999273 src/engine/__tests__/stack-rules-content.test.ts,src/stack-rules-content.test.ts",
"clone 687e46486dc5 src/engine/__tests__/stack-rules-content.test.ts,src/engine/__tests__/stacks.test.ts",
"clone 6893410ec1b1 src/orchestrator/__tests__/main-tree-tripwire.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone 68b1356447b2 src/orchestrator/cli.ts",
"clone 68ceda9cfcd3 src/codesearch/files.ts,src/engine/stacks.ts",
"clone 694e054dcbfb src/codesearch/files.ts,src/engine/stacks.ts",
Expand Down Expand Up @@ -764,7 +766,7 @@
"clone c7d2cfba43b6 src/engine/__tests__/new-task.test.ts,src/engine/__tests__/switch-mode.test.ts",
"clone c9648e69e45e src/orchestrator/__tests__/cli.test.ts",
"clone c9726dcfaf3b src/engine/local-explore.ts,src/llm/ollama.ts",
"clone c9fe2d9273f4 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone c9fe2d9273f4 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/main-tree-tripwire.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone ca31b6bb0f50 src/budget/__tests__/concurrency.test.ts,src/watcher/__tests__/watch.test.ts",
"clone cb208607f464 src/dashboard/server.ts",
"clone cb54ce2ca134 src/orchestrator/__tests__/cli.test.ts,src/orchestrator/__tests__/resume.test.ts",
Expand Down Expand Up @@ -826,7 +828,7 @@
"clone d8dfa6d1a846 src/orchestrator/cli.ts",
"clone d8e97665e9ec src/checkpoints/__tests__/cli.test.ts,src/codemap/__tests__/cli.test.ts,src/dashboard/__tests__/cli.test.ts",
"clone d9003b766f88 src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts",
"clone d95677f0fc38 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone d95677f0fc38 src/codeintel/__tests__/implementations.test.ts,src/orchestrator/__tests__/main-tree-tripwire.test.ts,src/orchestrator/__tests__/review-gates.test.ts",
"clone d96d4c124839 src/dashboard/trend-cli.ts,src/orchestrator/cleanup.ts",
"clone d9ab3b61fdbc src/llm/preflight.ts",
"clone d9cbef8f79d1 src/engine/local-explore.ts,src/llm/ollama.ts",
Expand Down Expand Up @@ -1252,7 +1254,6 @@
"src/permissions/commands.ts complexity 88bfec36eee2",
"src/permissions/commands.ts complexity a59b0df8679a",
"src/permissions/commands.ts complexity bcaa684da117",
"src/permissions/commands.ts complexity c3e30364b4e2",
"src/permissions/commands.ts nesting bcaa684da117",
"src/permissions/commands.ts params 300fe7554f69",
"src/permissions/config.ts complexity addac3b5a99c",
Expand Down
123 changes: 123 additions & 0 deletions scripts/check-main-tree-clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# check-main-tree-clean.sh — issue #136 Part B tripwire.
#
# A worker MUST write only inside its own worktree. Part A (the
# execute_command confinement gate) blocks relative-climb writes for the
# builtin executor, but the harness cannot prove a negative for every
# provider and every bypass (sudo/ssh/xargs -exec, guessed absolute
# paths, ...). This script is the backstop: run-worker.sh / run-qa.sh
# snapshot the MAIN checkout's git status before the worker starts and
# diff it right after the worker exits. Anything NEW in main — outside
# the orchestrator's own dirs — means the worker wrote outside its
# worktree. The finding is recorded in a marker file the orchestrator
# surfaces (watch.ts / reconcileGroups persist it as `main_tree_dirty`)
# and echoed loudly to stderr: the wrapper redirects stderr into
# harness.log/qa.log, while stdout stays clean for PID capture.
#
# Usage: check-main-tree-clean.sh <worker|qa> <snapshot|check> <wt-path>
# Always exits 0: tripwire infra must never fail (or pass) a worker run.
#
# Exclusions (orchestrator-owned; churn mid-run in multi-group rounds):
# .worktrees/ other groups' worktrees + .orchestrator-state.json
# plans/parallel-tasks/ task/rework/continuation files for other groups
# Documented blind spot: a worker escaping INTO those two dirs evades
# this tripwire. plans/issues/* and everything else is covered.
# Concurrent operator edits to main during the run window also trip the
# wire — the marker is evidence for a human/reviewer, NOT an auto-fail.
set -u -o pipefail

# All diagnostics go to stderr, never stdout: run-worker.sh prints the
# wrapper PID on stdout (a contract stdout-capturing callers and tests
# rely on), and the wrapper redirects stderr into harness.log/qa.log
# anyway, so stderr stays loud in exactly the right place.
exec >&2

ROLE="${1:?usage: check-main-tree-clean.sh <worker|qa> <snapshot|check> <wt-path>}"
MODE="${2:?usage: check-main-tree-clean.sh <worker|qa> <snapshot|check> <wt-path>}"
WT_PATH="${3:?usage: check-main-tree-clean.sh <worker|qa> <snapshot|check> <wt-path>}"

case "$ROLE" in
worker) STEM=".harness" ;;
qa) STEM=".qa" ;;
*) echo "TRIPWIRE-SKIPPED: bad role '$ROLE'" ; exit 0 ;;
esac
case "$MODE" in
snapshot | check) ;;
*) echo "TRIPWIRE-SKIPPED: bad mode '$MODE'" ; exit 0 ;;
esac

BEFORE="$WT_PATH/$STEM.main-tree-before"
MARKER="$WT_PATH/$STEM.main-tree-dirty"

# The MAIN checkout is the common git dir's parent. Fail open with a
# loud warning when git metadata is unusable — never fail the worker.
COMMON="$(git -C "$WT_PATH" rev-parse --git-common-dir 2>/dev/null)" || {
echo "TRIPWIRE-SKIPPED: cannot resolve main checkout from '$WT_PATH'"
exit 0
}
case "$COMMON" in
/*) MAIN_DIR="$COMMON" ;;
*) MAIN_DIR="$WT_PATH/$COMMON" ;;
esac
WT_REAL="$(cd "$WT_PATH" 2>/dev/null && pwd)" || {
echo "TRIPWIRE-SKIPPED: worktree '$WT_PATH' unreadable"
exit 0
}
MAIN_REAL="$(cd "$MAIN_DIR/.." 2>/dev/null && pwd)" || {
echo "TRIPWIRE-SKIPPED: main checkout under '$MAIN_DIR' unreadable"
exit 0
}
if [ "$WT_REAL" = "$MAIN_REAL" ]; then
echo "TRIPWIRE-SKIPPED: worker running directly on main checkout '$MAIN_REAL' — confinement by review only"
exit 0
fi

# One porcelain snapshot of main, minus orchestrator-owned dirs. Rename
# entries (`R old -> new`) match on the NEW path too.
snapshot_status() {
git -C "$MAIN_REAL" status --porcelain -uall 2>/dev/null | awk '
{
line = $0; sub(/^.{3}/, "", line); p = line
i = index(line, " -> ")
if (i > 0) p = substr(line, i + 4)
if (index(p, ".worktrees/") == 1) next
if (index(p, "plans/parallel-tasks/") == 1) next
print
}
'
}

if [ "$MODE" = "snapshot" ]; then
TMP="$BEFORE.tmp.$$"
if snapshot_status >"$TMP"; then
mv "$TMP" "$BEFORE"
else
rm -f "$TMP"
echo "TRIPWIRE-SKIPPED: cannot snapshot main checkout '$MAIN_REAL'"
fi
exit 0
fi

AFTER="$(snapshot_status)" || {
echo "TRIPWIRE-SKIPPED: cannot re-read main checkout '$MAIN_REAL'"
exit 0
}
if [ -f "$BEFORE" ]; then
BEFORE_LINES="$(cat "$BEFORE")"
else
echo "TRIPWIRE: no baseline snapshot for '$WT_PATH' — treating all main-checkout dirt as new"
BEFORE_LINES=""
fi
NEW="$(comm -13 <(printf '%s\n' "$BEFORE_LINES" | sort) <(printf '%s\n' "$AFTER" | sort) | grep . || true)"
if [ -z "$NEW" ]; then
rm -f "$MARKER"
exit 0
fi
printf '%s\n' "$NEW" >"$MARKER"
COUNT="$(printf '%s\n' "$NEW" | wc -l | tr -d ' ')"
WORD="entries"
[ "$COUNT" = "1" ] && WORD="entry"
echo "MAIN-TREE-DIRTY: worker '$WT_PATH' left $COUNT new $WORD in main checkout '$MAIN_REAL':"
printf '%s\n' "$NEW" | sed 's/^/MAIN-TREE-DIRTY: /'
echo "MAIN-TREE-DIRTY: marker: $MARKER (window between snapshot and worker exit; concurrent operator edits also match)"
exit 0
7 changes: 7 additions & 0 deletions scripts/run-qa.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,25 @@ if [ -n "${HEADLESSCODE_MAX_DURATION_MS:-}" ]; then
BUDGET_ARG="$BUDGET_ARG --max-duration-ms '$HEADLESSCODE_MAX_DURATION_MS'"
fi

# Issue #136 Part B: after QA exits, diff the MAIN checkout against the
# pre-launch snapshot — same tripwire as run-worker.sh (qa marker stem).
TRIPWIRE_CHECK="bash '$HARNESS_ROOT/scripts/check-main-tree-clean.sh' qa check '$WT_PATH' || echo 'TRIPWIRE-FAILED: main-tree check did not run — confinement unverified for this run'"

WRAPPER="
cd '$HARNESS_ROOT'
$CLI --task-file '$TASK_FILE' --workspace '$WT_PATH' --mode '$MODE' $MODEL_ARG $LOG_FILE_ARG $BUDGET_ARG
CODE=\$?
printf '%s' \"\$CODE\" > '$WT_PATH/.qa.exit'
$TRIPWIRE_CHECK
mkdir '$WT_PATH/.qa.done' 2>/dev/null
exit \$CODE
"

# ── Launch detached + record pid + pgid ─────────────────────────────────────
rm -f "$WT_PATH/.qa.exit"
rm -rf "$WT_PATH/.qa.done"
# Issue #136 Part B: snapshot the MAIN checkout BEFORE QA starts.
bash "$HARNESS_ROOT/scripts/check-main-tree-clean.sh" qa snapshot "$WT_PATH" || echo "run-qa: warning: main-tree tripwire snapshot failed" >&2

# Issue #20 (same fix as run-worker.sh): run the QA wrapper in its OWN process
# group via setsid so a stop command can kill the whole tree, and record the
Expand Down
13 changes: 13 additions & 0 deletions scripts/run-worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,18 @@ if [ -n "${HEADLESSCODE_WINDOW_SIZE:-}" ]; then
BUDGET_ARG="$BUDGET_ARG --window-size '$HEADLESSCODE_WINDOW_SIZE'"
fi

# Issue #136 Part B: after the worker exits, diff the MAIN checkout against
# the pre-launch snapshot (taken below) — a worker that wrote outside its
# worktree trips the wire (marker file + loud harness.log lines). Runs
# before mkdir .done, so a present .done marker implies the check ran.
TRIPWIRE_CHECK="bash '$HARNESS_ROOT/scripts/check-main-tree-clean.sh' worker check '$WT_PATH' || echo 'TRIPWIRE-FAILED: main-tree check did not run — confinement unverified for this run'"

WRAPPER="
cd '$HARNESS_ROOT'
$CLI $TASK_FLAG '$TASK_PATH' --workspace '$WT_PATH' --mode '$MODE' $MODEL_ARG $LOG_FILE_ARG $MEMORY_DIR_ARG $BUDGET_ARG
CODE=\$?
printf '%s' \"\$CODE\" > '$WT_PATH/.harness.exit'
$TRIPWIRE_CHECK
mkdir '$WT_PATH/.harness.done' 2>/dev/null
exit \$CODE
"
Expand All @@ -332,13 +339,15 @@ if [ "$EXECUTOR" = "muse" ]; then
PROMPT_CLEANUP="$MUSE_PROMPT_FILE"
fi
MAX_MODEL_STEPS_VALUE="${MAX_ITERATIONS:-${HEADLESSCODE_MAX_ITERATIONS:-}}"
TRIPWIRE_CHECK_Q="bash $(printf '%q' "$HARNESS_ROOT/scripts/check-main-tree-clean.sh") worker check $(printf '%q' "$WT_PATH") || echo $(printf '%q' "TRIPWIRE-FAILED: main-tree check did not run — confinement unverified for this run")"
WRAPPER="cd $(printf '%q' "$HARNESS_ROOT")
bash $(printf '%q' "$HARNESS_ROOT/scripts/run-muse-worker.sh") \
$(printf '%q' "$WT_PATH") $(printf '%q' "$MUSE_PROMPT_FILE") \
$(printf '%q' "$MUSE_MODEL") $(printf '%q' "$MAX_MODEL_STEPS_VALUE")
CODE=\$?
if [ -n $(printf '%q' "$PROMPT_CLEANUP") ]; then rm -f -- $(printf '%q' "$PROMPT_CLEANUP"); fi
printf '%s' \"\$CODE\" > $(printf '%q' "$WT_PATH/.harness.exit")
$TRIPWIRE_CHECK_Q
mkdir $(printf '%q' "$WT_PATH/.harness.done") 2>/dev/null
exit \$CODE
"
Expand Down Expand Up @@ -369,6 +378,7 @@ cd '$HARNESS_ROOT'
$CLI${SANDBOX_ARGS_QUOTED}
CODE=\$?
printf '%s' \"\$CODE\" > '$WT_PATH/.harness.exit'
$TRIPWIRE_CHECK
mkdir '$WT_PATH/.harness.done' 2>/dev/null
exit \$CODE
"
Expand All @@ -380,6 +390,9 @@ fi
# ── Launch detached + record pid + pgid ─────────────────────────────────────
rm -f "$WT_PATH/.harness.exit"
rm -rf "$WT_PATH/.harness.done"
# Issue #136 Part B: snapshot the MAIN checkout BEFORE the worker starts;
# the wrapper diffs against this right after the worker exits.
bash "$HARNESS_ROOT/scripts/check-main-tree-clean.sh" worker snapshot "$WT_PATH" || echo "run-worker: warning: main-tree tripwire snapshot failed" >&2
# A worktree is reused across continuation/rework respawns, so harness.log
# must be APPENDED to, never truncated — a worker that hits the iteration cap
# or gets a review rework cycle keeps its full log history (and the orchestrator
Expand Down
Loading
Loading