Skip to content
Open
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
3 changes: 3 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env bash
# Install or uninstall macos-scripts entrypoint symlinks and its managed shell
# configuration block. Existing targets are replaced only after confirmation;
# --dry-run performs no filesystem changes.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down
7 changes: 7 additions & 0 deletions mqlaunch/b2_tui/adapters/obsidian_writer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Persist B2 prompt-run artifacts in the mqobsidian runs directory.

This adapter writes presentation artifacts only; it does not promote durable
memory, rank recommendations, or invoke mqobsidian orchestration.
"""

from __future__ import annotations

import re
Expand All @@ -14,6 +20,7 @@ def _slug(text: str) -> str:


def write_run(prompt: Prompt, task: str, composed: str) -> Path:
"""Write one timestamped B2 run artifact and return its path."""
RUNS_DIR.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d-%H%M%S")
filename = f"{ts}-b2-{_slug(task)}.md"
Expand Down
6 changes: 6 additions & 0 deletions mqlaunch/b2_tui/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Define B2 TUI paths into the default mqobsidian checkout and local history.

Paths currently assume ``~/mqobsidian`` and are not discovered from
``MQ_OBSIDIAN_DIR``; callers should treat missing paths as unavailable input.
"""

from __future__ import annotations

from pathlib import Path
Expand Down
15 changes: 11 additions & 4 deletions mqlaunch/lib/git-menus.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
# bash, so it carries a bash shebang and is covered by shellcheck.

# Opens git menu.
git_menu_exit_is_restartable() {
case "${1:-1}" in
0|130|143) return 0 ;;
*) return 1 ;;
esac
}

open_git_menu() {
local repo_arg="${1:-}"
local git_script="$BASE_DIR/terminal/launchers/gitlaunch.sh"
Expand Down Expand Up @@ -49,10 +56,10 @@ open_git_menu() {

[[ -f "$back_marker" ]] && break

# A non-zero exit is a failure to start, not the mid-session crash the
# restart counter is for. Restarting five times would print the same
# "Repo path not found" five times before giving up.
if (( menu_status != 0 )); then
# Ordinary non-zero exits are startup or validation failures. Signal-safe
# exits from an active Git operation (130/143) are restartable so an
# interrupted post-push cleanup returns to Gitlaunch, not the main menu.
if ! git_menu_exit_is_restartable "$menu_status"; then
break
fi

Expand Down
3 changes: 3 additions & 0 deletions system/tweaks/macos-tweaks.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env bash
# Apply or revert curated macOS defaults changes. Mutating presets create a
# timestamped backup first; --dry-run prints commands without executing them.
# Command strings are internal constants and are executed through eval.
set -euo pipefail

SCRIPT_NAME="$(basename "$0")"
Expand Down
62 changes: 50 additions & 12 deletions terminal/launchers/gitlaunch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ REQUESTED_REPO="${MQ_GIT_REPO:-${1:-}}"
WORK_DIR=""
_BANNER_SHOWN=0
BACK_MARKER="${MQ_GITLAUNCH_BACK_MARKER:-}"
PR_RESTORE_BASE_BRANCH=""
PR_RESTORE_REPO=""

if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null)" -ge 8 ]]; then
C_RESET=$'\e[0m'
Expand Down Expand Up @@ -816,11 +818,41 @@ function branch_slug() {
echo "${slug:0:48}"
}

# Restores the checkout after a protected-branch push. Keeping this state in
# the gitlaunch process lets the EXIT trap recover even if a later command
# terminates the menu before normal cleanup runs.
function restore_pr_push_checkout() {
local restore_script

if [[ -z "$PR_RESTORE_BASE_BRANCH" || -z "$PR_RESTORE_REPO" ]]; then
return 0
fi

restore_script="${GITLAUNCH_DIR}/../../tools/scripts/git-restore-to-base.sh"
if "$restore_script" "$PR_RESTORE_BASE_BRANCH" "$PR_RESTORE_REPO"; then
PR_RESTORE_BASE_BRANCH=""
PR_RESTORE_REPO=""
return 0
fi

echo "Gitlaunch could not restore the checkout after the PR push." >&2
return 1
}

# Arms recovery before switching away from the protected base branch.
function arm_pr_push_restore() {
PR_RESTORE_BASE_BRANCH="$1"
PR_RESTORE_REPO="${2:-$PWD}"
trap 'restore_pr_push_checkout || true' EXIT
trap 'restore_pr_push_checkout || true; exit 130' INT
trap 'restore_pr_push_checkout || true; exit 143' TERM
}

# Creates pr branch for push through the configured workflow.
function create_pr_branch_for_push() {
local base_branch="$1"
local commit_message="$2"
local slug pr_branch confirm output status
local slug pr_branch confirm output push_status

slug="$(branch_slug "$commit_message")"
pr_branch="mq/${slug}-$(date +%Y%m%d-%H%M%S)"
Expand All @@ -837,16 +869,15 @@ function create_pr_branch_for_push() {
return 1
fi

trap '"${GITLAUNCH_DIR}/../../tools/scripts/git-restore-to-base.sh" "$base_branch" "." || true; exit 130' INT
trap '"${GITLAUNCH_DIR}/../../tools/scripts/git-restore-to-base.sh" "$base_branch" "." || true; exit 143' TERM
arm_pr_push_restore "$base_branch" "$PWD"

git switch -c "$pr_branch" 2>/dev/null || git checkout -b "$pr_branch"
output=$(git push -u origin "$pr_branch" 2>&1)
status=$?
push_status=$?
echo "$output"

local rc="$status"
if [[ "$status" -eq 0 ]]; then
local rc="$push_status"
if [[ "$push_status" -eq 0 ]]; then
if command -v gh >/dev/null 2>&1; then
echo ""
printf "%bCreate the pull request now? [Y/n]: %b" "$C_LABEL" "$C_RESET"
Expand All @@ -867,15 +898,18 @@ function create_pr_branch_for_push() {
fi
fi

"${GITLAUNCH_DIR}/../../tools/scripts/git-restore-to-base.sh" "$base_branch" "." || true
trap - INT TERM
if restore_pr_push_checkout; then
trap - EXIT INT TERM
else
rc=1
fi
return "$rc"
}

# Coordinates pr aware push behavior.
function pr_aware_push() {
local commit_message="${1:-update project files}"
local branch output status
local branch output push_status
local -a push_args

push_args=("${@:2}")
Expand All @@ -896,15 +930,15 @@ function pr_aware_push() {
else
output=$(git push 2>&1)
fi
status=$?
push_status=$?
echo "$output"

if [[ "$status" -ne 0 ]] && echo "$output" | grep -E "GH013|Changes must be made through a pull request" >/dev/null; then
if [[ "$push_status" -ne 0 ]] && echo "$output" | grep -E "GH013|Changes must be made through a pull request" >/dev/null; then
create_pr_branch_for_push "$branch" "$commit_message"
return $?
fi

return "$status"
return "$push_status"
}

# Runs push through guardrails before acting.
Expand Down Expand Up @@ -1043,6 +1077,10 @@ function run_ai_commit() {
# ------------------------
# WORKSPACE RESUME
# ------------------------
if [[ -n "${GITLAUNCH_SOURCE_ONLY:-}" ]]; then
return 0 2>/dev/null || exit 0
fi

if [[ -z "$REQUESTED_REPO" ]] && load_state; then
echo "🔁 Resume last workspace?"
echo "Repo: $REPO"
Expand Down
89 changes: 89 additions & 0 deletions tests/gitlaunch-pr-push-recovery-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail

# Exercises the recovery functions from the gitlaunch implementation that
# mqlaunch option 3 actually starts. The older restore smoke covers the
# separate mq-git-menu implementation and did not catch this path.

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GITLAUNCH="$ROOT/terminal/launchers/gitlaunch.sh"
GIT_MENUS="$ROOT/mqlaunch/lib/git-menus.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

git init -q --bare "$TMP/origin.git"
git clone -q "$TMP/origin.git" "$TMP/repo"
git -C "$TMP/repo" config user.email test@example.invalid
git -C "$TMP/repo" config user.name Test
git -C "$TMP/repo" checkout -q -B main
printf 'initial\n' > "$TMP/repo/file.txt"
git -C "$TMP/repo" add file.txt
git -C "$TMP/repo" commit -q -m initial
git -C "$TMP/repo" push -q -u origin main

prepare_pr_branch() {
git -C "$TMP/repo" switch -q main
printf 'change-%s\n' "$1" > "$TMP/repo/file.txt"
git -C "$TMP/repo" commit -q -am "change $1"
git -C "$TMP/repo" switch -q -C "mq/test-$1"
}

assert_restored() {
[[ "$(git -C "$TMP/repo" branch --show-current)" == main ]]
[[ "$(git -C "$TMP/repo" rev-parse main)" == "$(git -C "$TMP/repo" rev-parse origin/main)" ]]
[[ -z "$(git -C "$TMP/repo" status --porcelain)" ]]
}

echo "[1/4] gitlaunch can be sourced without entering its menu loop"
GITLAUNCH_SOURCE_ONLY=1 GITLAUNCH="$GITLAUNCH" zsh -c '
source "$GITLAUNCH"
whence -w restore_pr_push_checkout >/dev/null
whence -w arm_pr_push_restore >/dev/null
'

echo "[2/4] normal recovery restores a clean base branch"
prepare_pr_branch normal
GITLAUNCH_SOURCE_ONLY=1 GITLAUNCH="$GITLAUNCH" TEST_REPO="$TMP/repo" zsh -c '
source "$GITLAUNCH"
cd "$TEST_REPO"
arm_pr_push_restore main "$TEST_REPO"
restore_pr_push_checkout
'
assert_restored

echo "[3/4] EXIT recovery restores after an unexpected process exit"
prepare_pr_branch exit
set +e
GITLAUNCH_SOURCE_ONLY=1 GITLAUNCH="$GITLAUNCH" TEST_REPO="$TMP/repo" zsh -c '
source "$GITLAUNCH"
cd "$TEST_REPO"
arm_pr_push_restore main "$TEST_REPO"
exit 23
'
exit_status=$?
set -e
[[ "$exit_status" -eq 23 ]]
assert_restored

echo "[4/4] recovery failures are visible and signal-safe exits are restartable"
set +e
failure_output="$(GITLAUNCH_SOURCE_ONLY=1 GITLAUNCH="$GITLAUNCH" TEST_REPO="$TMP/repo" zsh -c '
source "$GITLAUNCH"
cd "$TEST_REPO"
arm_pr_push_restore missing-base "$TEST_REPO"
restore_pr_push_checkout
' 2>&1)"
failure_status=$?
set -e
[[ "$failure_status" -ne 0 ]]
grep -q "Could not restore checkout" <<< "$failure_output"

GIT_MENUS="$GIT_MENUS" bash -c '
source "$GIT_MENUS"
git_menu_exit_is_restartable 0
git_menu_exit_is_restartable 130
git_menu_exit_is_restartable 143
! git_menu_exit_is_restartable 1
'

echo "gitlaunch PR-push recovery smoke OK"
1 change: 1 addition & 0 deletions tests/manifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ git-menu-surface-smoke.sh active -
git-restore-to-base-smoke.sh active -
git-status-contract-smoke.sh active -
gitlaunch-menu-surface-smoke.sh active -
gitlaunch-pr-push-recovery-smoke.sh active -
gitmerge-safe-smoke.sh active -
hal-args-no-eval-smoke.sh active -
hal-command-surface-smoke.sh active -
Expand Down
19 changes: 19 additions & 0 deletions tests/mq-git-protected-push-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,23 @@ grep -Fq "Staged:" "$LEGACY_MENU"
grep -Fq "if [[ -t 0 && -t 1 ]]" "$LEGACY_MENU"
grep -Fq "PR branch" "$DOCS"

# gitlaunch runs under zsh, where `status` is a read-only special parameter.
# Exercise the successful, non-protected push path: assigning a local named
# `status` terminates the submenu after a commit and returns to the main menu.
{
sed -n '/^function pr_aware_push()/,/^}/p' "$LEGACY_MENU"
cat <<'ZSH_TEST'
function is_protected_branch() { return 1; }
function git() {
case "$1" in
branch) print -r -- "test/menu-loop" ;;
push) print -r -- "local push ok" ;;
*) return 1 ;;
esac
}

pr_aware_push "menu loop regression"
ZSH_TEST
} | zsh >/dev/null

echo "mq git protected-push smoke OK"
3 changes: 3 additions & 0 deletions tools/scripts/cleanup.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env bash
# Interactively remove Trash contents, old Downloads files, Xcode DerivedData,
# and package-manager caches. Each destructive category requires confirmation;
# deletion failures are intentionally suppressed and may leave partial cleanup.
set -euo pipefail

CYAN='\033[0;36m'
Expand Down
5 changes: 5 additions & 0 deletions tools/scripts/mq-mcp-review.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ def is_allowed_file(path: Path) -> bool:


def iter_target_files(targets: list[str], max_files: int) -> list[Path]:
"""Return at most ``max_files`` eligible targets in deterministic order.

Directories listed in ``SKIP_DIRS`` and secret-like filenames are excluded,
but this is filename-based filtering rather than a complete secret scanner.
"""
found: list[Path] = []

for raw in targets:
Expand Down
1 change: 1 addition & 0 deletions tools/scripts/test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ echo "== Running mqlaunch headless checks =="
"$PROJECT_ROOT/tests/ui-spinner-smoke.sh"
"$PROJECT_ROOT/tests/mq-stack-contract-smoke.sh"
"$PROJECT_ROOT/tests/gitlaunch-menu-surface-smoke.sh"
"$PROJECT_ROOT/tests/gitlaunch-pr-push-recovery-smoke.sh"
"$PROJECT_ROOT/tests/gitmerge-safe-smoke.sh"
"$PROJECT_ROOT/tests/gitpr-merge-safe-smoke.sh"
"$PROJECT_ROOT/tests/git-menu-surface-smoke.sh"
Expand Down
Loading