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
7 changes: 5 additions & 2 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-104u | ✅ closed 2026-05-19 | **[Windows dogfood r4 finding]** `install.sh` `link()` semantics bug on existing-directory dst: when `dst` is already a directory (e.g. `~/.claude/.pm` is a real dir from a prior install or manual setup), `ln -s "$src" "$dst"` is interpreted as "create link inside the dir named $(basename "$src")" → produces `dst/basename(src)` (e.g. `.pm/pm`) instead of failing cleanly. CC-104c's link_or_copy inherits this from `ln`. Observed: `ln: failed to create symbolic link '/c/Users/Lien Chen/.claude/.pm/pm': File exists`. Copy fallback masked the symptom but `manifest` records a wrong dst. Fix: `link_or_copy` should `[[ -d "$dst" && ! -L "$dst" ]]` precheck → return CONFLICT (rc=2) with clear message, OR use `ln -sn` (no-dereference) consistently. Also audit `pm-schema` install block path-handling | ops/install | 2026-05-18 | pr:#100 | P2 | oss |
| CC-104v | ✅ closed 2026-05-21 | **[Windows dogfood r4 — docs]** Document copy-mode install snapshot semantics: when `link_or_copy` falls back to copy (Git Bash without dev-mode), changes to source repo do NOT propagate to install dst — user must re-run `install.sh` after any source edit. Currently surfaced only via per-file `portable: fallback copy path ... symlink post-check failed` stderr. Add a single summary banner at end of install when copy-mode entries > 0 (`N files installed via copy fallback; source edits will require re-install`). Also add section to `docs/platform-support.md` Windows page. UX, not correctness | docs/install/ux | 2026-05-18 | pr:#116 | — | oss |
| CC-200 | ⏸ deferred | **[Reuse debt]** `scripts/lib/executor-router.sh` — 抽出共用 codex/claude routing logic(目前 `/pm`、`/pr-gate` 各寫一套,未來 N=3 consumer 痛點) | arch/reuse | 2026-05-17 | — | — | reuse-debt |
| CC-201 | ⏸ deferred | **[Reuse debt]** `detect_executor_profile()` shim 進 `scripts/lib/portable.sh` — `install-hooks.sh` + `pr-gate.sh` 各自重複 `command -v codex` 判斷 | arch/reuse | 2026-05-17 | | — | reuse-debt |
| CC-201 | ✅ closed 2026-05-23 | **[Reuse debt]** `detect_executor_profile()` shim 進 `scripts/lib/portable.sh` — `install-hooks.sh` + `pr-gate.sh` 各自重複 `command -v codex` 判斷 | arch/reuse | 2026-05-17 | pr:#123 | — | reuse-debt |
| CC-202 | ⏸ deferred | **[Reuse debt]** handover validator framework — `dispatch_handover_v1` 與 `pr-gate-handover_v1` 共用 fence/metadata/body validator 抽象;future handover schemas 不再手刻 | arch/reuse | 2026-05-17 | — | — | reuse-debt |
| CC-203 | ⏸ deferred | **[Reuse debt]** `scripts/lib/test-harness.sh` — 8+ 個 `test-*.sh` 都各寫 `--filter/--list`/`should_run()`/PASS-FAIL counter/scratch dir setup;source-able 共用 lib 統一 | ops/test/reuse | 2026-05-17 | — | — | reuse-debt |
| CC-204 | ⏸ deferred | **[Reuse debt]** hook framework — pm-write-guard/codex-bash-guard/codex-write-guard/routing-log 共通 stdin-json-parse → decision-matrix → audit-log 結構;目前 copy-paste-modify | arch/hook/reuse | 2026-05-17 | — | — | reuse-debt |
Expand Down Expand Up @@ -1037,11 +1037,14 @@ reusing the same agent/fan-out primitives for a different cognitive mode.
**Why**: A third consumer would turn the duplicated route logic into a maintenance cost and make executor behavior easier to drift.
**Requirement**: Extract shared codex/claude routing into `scripts/lib/executor-router.sh`, preserving existing CLI behavior for current callers.

## CC-201 — Reuse debt: `detect_executor_profile()` shim(deferred)
## CC-201 — Reuse debt: `detect_executor_profile()` shim ✅ 2026-05-23

**See**: PR #123

**Problem**: `install-hooks.sh` and `pr-gate.sh` both repeat `command -v codex` style executor-profile detection.
**Why**: Profile detection should be consistent across install and dispatch paths.
**Requirement**: Move executor-profile detection into a shared shim, likely `scripts/lib/portable.sh` or a focused executor helper, and update both consumers.
**Closed**: `codex_available()` + `detect_executor_profile()` added to `scripts/lib/portable.sh`; `install-hooks.sh` / `pr-gate.sh` / `doctor.sh` (×2) consumers updated; `pr-gate.sh` + `doctor.sh` source the shim behind a graceful copy-mode fallback. +4 `test-portable.sh` cases, +1 `test-doctor.sh` regression case. PR #123 (v0.3.0 M0).

## CC-202 — Reuse debt: handover validator framework(deferred)

Expand Down
8 changes: 6 additions & 2 deletions scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ else
esac ;;
esac
}
# Copy-mode parity: portable.sh would supply codex_available(); define a
# matching fallback so check_codex() / the hook-profile case do not hit an
# undefined function when lib/ is absent (CC-201).
codex_available() { command -v codex >/dev/null 2>&1; }
fi

if [[ "$_PORTABLE_AVAILABLE" -eq 1 && -f "$SCRIPT_DIR/lib/memory-dir.sh" ]]; then
Expand Down Expand Up @@ -158,7 +162,7 @@ check_claude() {
}

check_codex() {
if command -v codex >/dev/null 2>&1; then
if codex_available; then
emit_check codex ok "codex available"
else
emit_check codex warn "codex not found — full-profile hooks (hook-codex-bash-guard.sh etc.) will be skipped; minimal profile active" \
Expand Down Expand Up @@ -280,7 +284,7 @@ check_hooks() {
case "$PROFILE" in
full) _want_full=1 ;;
minimal) _want_full=0 ;;
*) command -v codex >/dev/null 2>&1 && _want_full=1 || _want_full=0 ;;
*) if codex_available; then _want_full=1; else _want_full=0; fi ;;
esac
if [[ "$_want_full" -eq 1 ]]; then
profile="full"
Expand Down
6 changes: 1 addition & 5 deletions scripts/install-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ case "$PLATFORM" in
esac

if [[ -z "$PROFILE" ]]; then
if command -v codex >/dev/null 2>&1; then
PROFILE=full
else
PROFILE=minimal
fi
PROFILE="$(detect_executor_profile)"
fi

if [[ "$PLATFORM" == "auto" ]]; then
Expand Down
15 changes: 15 additions & 0 deletions scripts/lib/portable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ detect_platform() {
printf 'unknown\n'
}

# Return 0 if the `codex` CLI is on PATH, 1 otherwise.
codex_available() {
command -v codex >/dev/null 2>&1
}

# Echo the auto-detected executor profile: "full" when codex is available,
# "minimal" otherwise.
detect_executor_profile() {
if codex_available; then
printf 'full\n'
else
printf 'minimal\n'
fi
}

# Resolve a path with GNU-like normalization for use as a cross-platform shim.
realpath_m() {
local path="${1:-}"
Expand Down
29 changes: 19 additions & 10 deletions scripts/pr-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ case "$EXECUTOR_OPTION" in
;;
esac

if [[ "$EXECUTOR_OPTION" == "auto" ]]; then
if command -v codex >/dev/null 2>&1; then
EXECUTOR="codex"
else
EXECUTOR="claude"
fi
else
EXECUTOR="$EXECUTOR_OPTION"
fi

cd "$WORK_DIR"

# ── Detect base branch ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -219,6 +209,25 @@ while [[ -L "$_self" ]]; do
[[ "$_self" == /* ]] || _self="$_self_dir/$_self"
done
SCRIPT_DIR="$(cd "$(dirname "$_self")" && pwd)"
# Source portable.sh for codex_available(); graceful fallback for standalone
# copies (test harness, copy-mode install) where the sibling lib/ is absent.
if [[ -f "$SCRIPT_DIR/lib/portable.sh" ]]; then
# shellcheck source=scripts/lib/portable.sh
. "$SCRIPT_DIR/lib/portable.sh"
else
codex_available() { command -v codex >/dev/null 2>&1; }
fi

if [[ "$EXECUTOR_OPTION" == "auto" ]]; then
if codex_available; then
EXECUTOR="codex"
else
EXECUTOR="claude"
fi
else
EXECUTOR="$EXECUTOR_OPTION"
fi

unset _self _self_dir

# Track all brief files for EXIT cleanup
Expand Down
32 changes: 32 additions & 0 deletions scripts/test-doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,37 @@ case_doctor_copy_mode_no_lib() {
fi
}

case_doctor_copy_mode_no_lib_no_codex() {
# CC-201 regression: in copy-mode (no lib/portable.sh) codex_available()
# comes from doctor.sh's own fallback block, not portable.sh. Verify
# doctor.sh still runs gracefully when codex is ALSO absent from PATH —
# the fallback codex_available must be defined so check_codex() and the
# hook-profile case degrade to a warning rather than hitting an undefined
# function.
#
# Steps:
# 1. Copy doctor.sh to a temp dir with no lib/ subdirectory.
# 2. Run it with a PATH containing claude but NOT codex.
# 3. Assert exit 0 or 1 (no crash) and output contains "Summary:".
local name="doctor-copy-mode-no-lib-no-codex"
should_run "$name" || return 0
local copydir="$tmp_root/copy-scripts-nocodex"
mkdir -p "$copydir"
cp "$DOCTOR" "$copydir/doctor.sh"
local home="$tmp_root/home-copy-nocodex" out status=0
mkdir -p "$home/.claude"
write_minimal_settings "$home"
write_manifest "$home"
local path
path="$(make_stub_bin "$tmp_root/bin-copy-nocodex" claude)"
out="$(HOME="$home" CLAUDE_CONFIG_DIR="$home/.claude" PATH="$path" bash "$copydir/doctor.sh" --no-color --repo "$REPO_ROOT" 2>&1)" || status=$?
if [[ ( "$status" -eq 0 || "$status" -eq 1 ) && "$out" == *"Summary:"* ]]; then
pass "$name"
else
fail "$name" "expected exit 0 or 1 with Summary: (no crash when codex absent in copy-mode); status=$status out=$out"
fi
}

case_doctor_installed_copy_no_repo() {
# Verifies that doctor.sh emits [FAIL] with a helpful message when invoked
# in copy-mode (no lib/) without --repo, so the user is not misled by
Expand Down Expand Up @@ -1155,6 +1186,7 @@ case_doctor_windows_path_hooks_stale
case_doctor_stale_hook_path_warns
case_doctor_symlink_invocation
case_doctor_copy_mode_no_lib
case_doctor_copy_mode_no_lib_no_codex
case_doctor_installed_copy_no_repo
case_doctor_installed_copy_no_repo_json
case_doctor_claude_config_dir
Expand Down
76 changes: 76 additions & 0 deletions scripts/test-portable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,78 @@ case_detect_platform_ostype_msys() {
fi
}

case_codex_available_true_when_stub_present() {
local name="portable-codex-available-true-when-stub-present"
should_run "$name" || return 0
local old_path="$PATH"
local stub_dir="$tmp_root/codex-stub-available"
mkdir -p "$stub_dir"
printf '#!/usr/bin/env sh\nprintf "codex-stub\\n"\n' > "$stub_dir/codex"
chmod +x "$stub_dir/codex"

PATH="$stub_dir:$old_path"
if codex_available; then
pass "$name"
else
fail "$name" "expected codex_available success when codex stub is on PATH"
fi
PATH="$old_path"
}

case_codex_available_false_without_stub() {
local name="portable-codex-available-false-without-stub"
should_run "$name" || return 0
local old_path="$PATH"
local no_codex_path="$tmp_root/empty-path"
mkdir -p "$no_codex_path"

PATH="$no_codex_path"
if codex_available; then
fail "$name" "expected codex_available failure when codex is absent from PATH"
else
pass "$name"
fi
PATH="$old_path"
}

case_detect_executor_profile_full_when_stub_present() {
local name="portable-detect-executor-profile-full-when-stub-present"
should_run "$name" || return 0
local old_path="$PATH"
local stub_dir="$tmp_root/detect-profile-stub"
local got
mkdir -p "$stub_dir"
printf '#!/usr/bin/env sh\nprintf "codex-stub\\n"\n' > "$stub_dir/codex"
chmod +x "$stub_dir/codex"

PATH="$stub_dir:$old_path"
got="$(detect_executor_profile)"
PATH="$old_path"
if [[ "$got" == "full" ]]; then
pass "$name"
else
fail "$name" "expected profile 'full' got '$got'"
fi
}

case_detect_executor_profile_minimal_without_stub() {
local name="portable-detect-executor-profile-minimal-without-stub"
should_run "$name" || return 0
local old_path="$PATH"
local no_codex_path="$tmp_root/empty-path-profile"
local got
mkdir -p "$no_codex_path"

PATH="$no_codex_path"
got="$(detect_executor_profile)"
PATH="$old_path"
if [[ "$got" == "minimal" ]]; then
pass "$name"
else
fail "$name" "expected profile 'minimal' got '$got'"
fi
}

case_realpath_m_symlink_resolves() {
local name="portable-realpath-m-symlink-resolves"
should_run "$name" || return 0
Expand Down Expand Up @@ -909,6 +981,10 @@ case_serialize_with_lock_missing_parent
case_detect_platform_override_windows
case_detect_platform_host_native
case_detect_platform_ostype_msys
case_codex_available_true_when_stub_present
case_codex_available_false_without_stub
case_detect_executor_profile_full_when_stub_present
case_detect_executor_profile_minimal_without_stub
case_file_size_bytes_returns_size
case_file_size_bytes_missing_file
case_link_or_copy_copy_refresh_stale() {
Expand Down