diff --git a/bin/lacp-bootstrap-system b/bin/lacp-bootstrap-system index c23f1227..e4cb19e4 100755 --- a/bin/lacp-bootstrap-system +++ b/bin/lacp-bootstrap-system @@ -154,7 +154,7 @@ if [[ "${FRESH_CHECK}" -eq 1 ]]; then cmd_orchestrate=$? "${ROOT}/bin/lacp" worktree list --json >/dev/null 2>&1 cmd_worktree=$? - "${ROOT}/bin/lacp" swarm doctor --json >/dev/null 2>&1 + "${ROOT}/bin/lacp" swarm --help >/dev/null 2>&1 cmd_swarm=$? set -e @@ -172,7 +172,7 @@ if [[ "${FRESH_CHECK}" -eq 1 ]]; then commands:{ orchestrate_doctor:{ok:($cmd_orchestrate == 0), rc:$cmd_orchestrate}, worktree_list:{ok:($cmd_worktree == 0), rc:$cmd_worktree}, - swarm_doctor:{ok:($cmd_swarm == 0), rc:$cmd_swarm} + swarm_help:{ok:($cmd_swarm == 0), rc:$cmd_swarm} } }')" fi diff --git a/bin/lacp-claude-hooks b/bin/lacp-claude-hooks index 2e6fce76..2c424355 100755 --- a/bin/lacp-claude-hooks +++ b/bin/lacp-claude-hooks @@ -415,9 +415,9 @@ cmd_apply_profile() { # Composite profile: hardened-session = quality-gate-v2 + session-start + pretool-guard + write-validate if [[ "${profile}" == "hardened-session" ]]; then local composite_args=() - [[ -n "${claude_dir_arg:-}" ]] && composite_args+=(--claude-dir "${claude_dir_arg}") - [[ "${dry_run}" -eq 1 ]] && composite_args+=(--dry-run) - [[ "${json}" -eq 1 ]] && composite_args+=(--json) + [[ -n "${claude_dir:-}" ]] && composite_args+=(--claude-dir "${claude_dir}") + [[ "${dry_run}" == "true" ]] && composite_args+=(--dry-run) + [[ "${as_json}" == "true" ]] && composite_args+=(--json) for sub_profile in quality-gate-v2 session-start pretool-guard write-validate; do log "Applying sub-profile: ${sub_profile}" cmd_apply_profile --profile "${sub_profile}" "${composite_args[@]}" || true diff --git a/bin/lacp-console b/bin/lacp-console index f3768e7e..a0604da0 100755 --- a/bin/lacp-console +++ b/bin/lacp-console @@ -68,8 +68,9 @@ default_project() { fi } -declare -A CUSTOM_COMMAND_PATHS=() -declare -A CUSTOM_COMMAND_SOURCE=() +declare -a CUSTOM_COMMAND_NAMES=() +declare -a CUSTOM_COMMAND_PATHS=() +declare -a CUSTOM_COMMAND_SOURCES=() declare -a EVAL_LINES=() PROJECT_COMMANDS_ENABLED=1 @@ -82,6 +83,36 @@ TIME_CLIENT="" TIME_SESSION_ID="" TIME_TRACKING_ACTIVE=0 +custom_command_index() { + local needle="$1" + local idx + for idx in "${!CUSTOM_COMMAND_NAMES[@]}"; do + if [[ "${CUSTOM_COMMAND_NAMES[idx]}" == "${needle}" ]]; then + printf '%s' "${idx}" + return 0 + fi + done + return 1 +} + +custom_command_exists() { + custom_command_index "$1" >/dev/null +} + +custom_command_add() { + local name="$1" + local path="$2" + local source_label="$3" + + if custom_command_exists "${name}"; then + return 0 + fi + + CUSTOM_COMMAND_NAMES+=("${name}") + CUSTOM_COMMAND_PATHS+=("${path}") + CUSTOM_COMMAND_SOURCES+=("${source_label}") +} + while [[ $# -gt 0 ]]; do case "$1" in --eval) @@ -159,10 +190,7 @@ load_custom_commands_from_dir() { base="$(basename "${file}")" name="${base%.*}" [[ -n "${name}" ]] || continue - if [[ -z "${CUSTOM_COMMAND_PATHS["${name}"]+x}" ]]; then - CUSTOM_COMMAND_PATHS["${name}"]="${file}" - CUSTOM_COMMAND_SOURCE["${name}"]="${source_label}" - fi + custom_command_add "${name}" "${file}" "${source_label}" done < <(find "${dir}" -maxdepth 1 -type f -print0 | sort -z) } @@ -186,8 +214,11 @@ run_lacp_subcommand() { run_custom_command() { local name="$1" local args="${2:-}" - local path="${CUSTOM_COMMAND_PATHS["${name}"]:-}" - [[ -n "${path}" ]] || return 1 + local idx + idx="$(custom_command_index "${name}" || true)" + [[ -n "${idx}" ]] || return 1 + + local path="${CUSTOM_COMMAND_PATHS[idx]}" if [[ -x "${path}" ]]; then local cmd="\"${path}\"" @@ -227,7 +258,10 @@ run_custom_command() { run_loop_shortcut() { local raw="$1" local -a words=() - mapfile -d '' -t words < <(python3 - <<'PY' "${raw}" + local word + while IFS= read -r -d '' word; do + words+=("${word}") + done < <(python3 - "${raw}" <> "${checks_tmp}" } add_hint() { local hint="$1" + hint="${hint//$'\r'/ }" + hint="${hint//$'\n'/ }" + hint="${hint//$'\t'/ }" [[ -n "${hint}" ]] || return 0 if ! grep -Fqx "${hint}" "${hints_tmp}" 2>/dev/null; then printf '%s\n' "${hint}" >> "${hints_tmp}" diff --git a/bin/lacp-harness-run b/bin/lacp-harness-run index 2e26b3cc..30e9b501 100755 --- a/bin/lacp-harness-run +++ b/bin/lacp-harness-run @@ -16,13 +16,6 @@ import subprocess import sys from pathlib import Path -try: - import yaml # type: ignore -except Exception as exc: - print(f"[lacp] ERROR: missing yaml support: {exc}", file=sys.stderr) - raise SystemExit(2) - - def utc_stamp() -> str: return dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") @@ -419,6 +412,12 @@ def main() -> int: parser.add_argument("--json", action="store_true", help="Emit JSON summary") args = parser.parse_args() + try: + import yaml # type: ignore + except Exception as exc: + print(f"[lacp] ERROR: missing yaml support: {exc}", file=sys.stderr) + return 2 + root_env = os.environ.get("LACP_SCRIPT_ROOT", "") if not root_env: print("[lacp] ERROR: missing LACP_SCRIPT_ROOT", file=sys.stderr) diff --git a/bin/lacp-install b/bin/lacp-install index 122c36a6..b9b9e72a 100755 --- a/bin/lacp-install +++ b/bin/lacp-install @@ -599,6 +599,15 @@ ONBOARD_ARGS=() if [[ "${WITH_VERIFY}" -eq 1 ]]; then ONBOARD_ARGS+=(--with-verify --hours "${HOURS}") fi +if [[ "${AUTO_DEPS}" -eq 0 ]]; then + ONBOARD_ARGS+=(--no-auto-deps) +fi +if [[ "${AUTO_DEPS_DRY_RUN}" -eq 1 ]]; then + ONBOARD_ARGS+=(--auto-deps-dry-run) +fi +if [[ "${AUTO_DEPS_FORCE}" -eq 1 ]]; then + ONBOARD_ARGS+=(--auto-deps-force) +fi if [[ "${AUTO_ADOPT}" -eq 0 ]]; then ONBOARD_ARGS+=(--no-auto-adopt) fi diff --git a/bin/lacp-knowledge-doctor b/bin/lacp-knowledge-doctor index 610d4460..88ff9c3b 100755 --- a/bin/lacp-knowledge-doctor +++ b/bin/lacp-knowledge-doctor @@ -45,7 +45,9 @@ done require_cmd python3 [[ -d "${SCAN_ROOT}" ]] || die "Knowledge root does not exist: ${SCAN_ROOT}" -result_json="$(python3 - <<'PY' "${SCAN_ROOT}" +result_json="$(python3 - "${SCAN_ROOT}" </dev/null + "${ROOT}/bin/lacp-install" \ + --profile starter \ + --no-auto-deps \ + --no-auto-adopt \ + --no-auto-hook-optimize \ + --no-obsidian-setup >/dev/null "${ROOT}/bin/lacp-posture" --strict --json >/dev/null "${ROOT}/bin/lacp-doctor" --json | jq -e '.ok == true' >/dev/null "${ROOT}/bin/lacp-knowledge-doctor" --json >/dev/null || true "${ROOT}/scripts/ci/test-route-policy.sh" + check_env_guard log "Quick suite passed" exit 0 fi diff --git a/docs/local-dev-loop.md b/docs/local-dev-loop.md index f50b9336..08f48850 100644 --- a/docs/local-dev-loop.md +++ b/docs/local-dev-loop.md @@ -20,7 +20,7 @@ bin/lacp orchestrate doctor --json | jq ## 3) Iterate in isolated cycles ```bash -bin/lacp test --quick +HOMEBREW_NO_AUTO_UPDATE=1 bin/lacp test --quick --isolated bin/lacp loop --task "targeted change" --repo-trust trusted --dry-run --json -- /bin/echo hello ``` @@ -36,7 +36,7 @@ bin/lacp run --task "guarded mutation" --repo-trust trusted --context-contract " ```bash bin/lacp worktree list --json | jq -bin/lacp swarm doctor --json | jq +bin/lacp orchestrate doctor --json | jq '.backends' bin/lacp swarm status --latest --json | jq '.collaboration_summary' ``` diff --git a/scripts/ci/test-cli-and-isolated-env-guard.sh b/scripts/ci/test-cli-and-isolated-env-guard.sh index 12674823..94196a6a 100755 --- a/scripts/ci/test-cli-and-isolated-env-guard.sh +++ b/scripts/ci/test-cli-and-isolated-env-guard.sh @@ -82,7 +82,7 @@ LACP_BENCH_LOOKBACK="30" EOF before_hash="$(shasum "${ENV_FILE}" | awk '{print $1}')" -"${ROOT}/bin/lacp" test --isolated >/dev/null +"${ROOT}/bin/lacp" test --quick --isolated >/dev/null after_hash="$(shasum "${ENV_FILE}" | awk '{print $1}')" if [[ "${before_hash}" != "${after_hash}" ]]; then diff --git a/scripts/lacp-lib.sh b/scripts/lacp-lib.sh index fa4e49de..c5d97629 100755 --- a/scripts/lacp-lib.sh +++ b/scripts/lacp-lib.sh @@ -3,6 +3,22 @@ set -euo pipefail LACP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +lacp_prepend_path_if_missing() { + local dir="$1" + [[ -d "${dir}" ]] || return 0 + case ":${PATH}:" in + *":${dir}:"*) return 0 ;; + esac + PATH="${dir}:${PATH}" +} + +lacp_prepend_path_if_missing "${HOME}/.local/bin" +lacp_prepend_path_if_missing "/opt/homebrew/bin" +lacp_prepend_path_if_missing "/opt/homebrew/sbin" +lacp_prepend_path_if_missing "/usr/local/bin" +lacp_prepend_path_if_missing "/usr/local/sbin" +export PATH + if [[ "${LACP_SKIP_DOTENV:-0}" != "1" && -f "${LACP_ROOT}/.env" ]]; then # Strict KEY=VALUE parser — does not execute .env as bash (H2: CWE-94) while IFS='=' read -r key value; do @@ -17,9 +33,12 @@ if [[ "${LACP_SKIP_DOTENV:-0}" != "1" && -f "${LACP_ROOT}/.env" ]]; then value="${value#\'}" ; value="${value%\'}" # Validate key is a valid identifier [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue - # Safe expansion: $HOME, ${HOME}, and ~ (no general variable/command expansion) + # Safe expansion: a small allowlist of path anchors only. + # This keeps the parser non-executable while still supporting the shipped env example. value="${value//\$\{HOME\}/${HOME}}" value="${value//\$HOME/${HOME}}" + value="${value//\$\{LACP_ROOT\}/${LACP_ROOT}}" + value="${value//\$LACP_ROOT/${LACP_ROOT}}" value="${value/#\~\//${HOME}/}" export "${key}=${value}" done < "${LACP_ROOT}/.env"