diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2ea7c54..161c292 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,6 +24,29 @@ on: workflow_dispatch: jobs: + # Keep the process launch contract independently visible while the full + # lane-helper suite runs. Each GitHub job has its own workstation runner; + # the canonical wrapper still serializes any pytest processes on that runner. + guard-launch-mode: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + fetch-depth: 1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.x' + - name: install the test runner + run: python3 -m pip install --disable-pip-version-check pytest==8.3.4 + - name: guard launch mode and repository hygiene + env: + GIT_AUTHOR_NAME: openRepoTools CI + GIT_AUTHOR_EMAIL: ci@openrepotools.invalid + GIT_COMMITTER_NAME: openRepoTools CI + GIT_COMMITTER_EMAIL: ci@openrepotools.invalid + run: bash tests/run.sh -k 'guard_launch_mode or repo_hygiene' + tests: runs-on: ubuntu-latest steps: diff --git a/AGENTS.md b/AGENTS.md index 68cca5c..5f1554e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,14 +23,14 @@ amendments — and `docs/README-lanes.md` is its manual. **The code is here; the data is not.** Every one of the four finds the register, the logs and the handoffs through `$AGENT_PROTOCOL_ROOT/workspace.yaml`'s `repository:` and `path:`, never from its own location on disk, and refuses with exit 1 naming -`openRepoTools wip init` where that file does not answer — with ONE exception, -and it is a mechanism and not a taste: `lanes-edit.sh guard`, the -`UserPromptSubmit` name guard Amendment 12 adds, answers that same unreadable -workspace with **2**, because only a 2 blocks a prompt and a 1 would print the -refusal and let the work through it. Their exit codes are -the protocol's and not this toolset's — 1 is *registry not found* there and -*findings were printed* here — so never read a number without knowing which -command produced it. +`openRepoTools wip init` where that file does not answer. For lane sessions, +`lanes-edit.sh guard`, Amendment 12's `UserPromptSubmit` name guard, answers an +unreadable workspace with **2**: only 2 blocks a prompt; 1 would let it through. +Profile-only guard calls carrying exact `CLAUDE_NO_LANE=1` return 0 before input +or identity probes, even with unreadable configuration; other values enforce. +Brett Heap directed this exception in `openspec/changes/skip-no-lane-guard/`; +Shared hooks stay installed. Exit 1 means *registry not found* in lane tooling, +and *findings were printed* in estate status; identify the command before interpreting it. ## Driving `park `, `resume ` and `status ` diff --git a/docs/README-lanes.md b/docs/README-lanes.md index 38fe6a6..2af4a1a 100644 --- a/docs/README-lanes.md +++ b/docs/README-lanes.md @@ -1862,7 +1862,7 @@ because a hook that fails is a hook that breaks the session it was meant to orient (R-A8-1). Where another live process carries this session id it **SAYS so** and names the retire act; the refusal is the guard's. -### Fail closed, and the one bypass +### Fail closed for lane sessions, and safe-mode recovery A mismatch refuses, and so does an **indeterminate read** — session records unreadable, tmux not answering, the register unreadable, a payload naming no @@ -1872,8 +1872,9 @@ checkout last had it (R19) and **the guard never fetches**: this hook runs at every prompt, and putting the network there would be R-A8-1's objection several times over. -**There is no environment flag that turns it off.** `claude --safe-mode` runs -with every hook disabled and is the one bypass — deliberate, visible in the +Profile-only launches with exact `CLAUDE_NO_LANE=1` are exempt, as described +under Scope below. For a session subject to the guard, `claude --safe-mode` runs +with every hook disabled — deliberate, visible in the prompt box, and **a session started that way is not a lane session**: it may not write the register or claim an object. The hook's `timeout 5` is the other way it can fail open, and it is the amendment's own number: a hook that exceeds its @@ -1881,6 +1882,13 @@ timeout is killed, and a killed hook does not exit 2. ### Scope +Sessions launched with `CLAUDE_NO_LANE=1` skip the prompt guard, silently. The +workBenches profile launcher supplies this marker for a launch without a lane +and clears it for an explicit lane launch. Explicit `--no-lane` wins over lane +options. Other marker values keep the normal checks. The hook stays installed +in profile settings so a concurrent lane session using the same profile still +receives enforcement. Existing sessions need a relaunch to change launch mode. + The guard applies to every session whose `cwd` is under `$PROJECTS_ROOT` (default `~/projects`), which is every estate session, and is **silent elsewhere** (D1): a name there is a title and nothing more. A **subagent's** diff --git a/lanes-edit.sh b/lanes-edit.sh index 4b9b9d7..653fd93 100755 --- a/lanes-edit.sh +++ b/lanes-edit.sh @@ -420,6 +420,26 @@ set -u +# Parse the global flags before probing paths, git or workstation identity. +# A profile-only prompt must not depend on any of that lane infrastructure. +SWEEP_MODE="no-sweep" +while [ $# -gt 0 ]; do + case "${1-}" in + --no-sweep) SWEEP_MODE="no-sweep"; shift ;; + --sweep) SWEEP_MODE="sweep"; shift ;; + --no-github) LANES_NO_GITHUB=1; shift ;; + *) break ;; + esac +done + +if [ "${1-}" = guard ] && [ "${CLAUDE_NO_LANE:-}" = 1 ]; then + if [ "$#" -ne 1 ]; then + printf '%s\n' "lanes-edit: usage: guard (the UserPromptSubmit hook; the hook's JSON on stdin)" >&2 + exit 2 + fi + exit 0 +fi + SELF="$0" if command -v readlink >/dev/null 2>&1; then RESOLVED="$(readlink -f "$SELF" 2>/dev/null || printf '%s' "$SELF")" @@ -711,19 +731,6 @@ LANES_PATH="${LANES_PATH:-$(git -C "${LANES_DIR:-.}" rev-parse --show-prefix 2>/ CP_PATHS=("$LANES_PATH") CP_AFTER_REBASE="" -# --no-sweep (default) / --sweep — see USAGE above. Consumed here, ahead of -# subcommand dispatch, so either flag may appear anywhere before the -# subcommand name. -SWEEP_MODE="no-sweep" -while [ $# -gt 0 ]; do - case "${1-}" in - --no-sweep) SWEEP_MODE="no-sweep"; shift ;; - --sweep) SWEEP_MODE="sweep"; shift ;; - --no-github) LANES_NO_GITHUB=1; shift ;; - *) break ;; - esac -done - # die "" [] — the MESSAGE is $1 alone. It used to be "$*", # which joined the exit code on to the end of every refusal that passed one: # `… is not a stale claim. 2`. The code is the caller's business and the @@ -6307,10 +6314,10 @@ EOF # FAIL CLOSED (clause (d)). A mismatch refuses; so does an INDETERMINATE read — # session records unreadable, tmux not answering, the register unreadable — # naming the read, because a triple that cannot be verified is not a triple that -# agrees. There is no environment flag that turns this off: `claude --safe-mode` -# disables every hook and is the one bypass, deliberate and visible in the -# prompt box, and a session started that way is not a lane session and may not -# write the register or claim an object. +# agrees. Profile-only launches carrying exactly `CLAUDE_NO_LANE=1` are exempt +# from this guard. `claude --safe-mode` disables every hook, deliberate and +# visible in the prompt box; a session started that way is not a lane session +# and may not write the register or claim an object. # # WHAT IT COSTS, AND THE ONE WAY IT FAILS OPEN. Measured on Eagle 2026-09-14 # against 329 session records and a 1.3 MB register: the register read is ~2 s @@ -6734,9 +6741,9 @@ guard_triple() { # [] return 0 } -# THE ONE BYPASS, NAMED WHERE A PERSON COULD OTHERWISE BE STUCK (clause (d)). -# There is no environment flag that turns this guard off, deliberately; Claude's -# own `--safe-mode` disables every hook and is visible in the prompt box, and a +# SAFE-MODE RECOVERY, NAMED WHERE A PERSON COULD BE STUCK (clause (d)). +# Profile-only launches are exempt through `CLAUDE_NO_LANE=1` at dispatch. +# Claude's `--safe-mode` disables every hook and is visible in the prompt box; a # session started that way is not a lane session and may not write the register # or claim an object. It is printed by the INDETERMINATE refusals — the reads # that could not be made — and not by the mismatches, which have a cure of their diff --git a/openspec/changes/skip-no-lane-guard/.openspec.yaml b/openspec/changes/skip-no-lane-guard/.openspec.yaml new file mode 100644 index 0000000..1aca8b9 --- /dev/null +++ b/openspec/changes/skip-no-lane-guard/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-30 diff --git a/openspec/changes/skip-no-lane-guard/design.md b/openspec/changes/skip-no-lane-guard/design.md new file mode 100644 index 0000000..c595bf1 --- /dev/null +++ b/openspec/changes/skip-no-lane-guard/design.md @@ -0,0 +1,19 @@ +## Context + +See proposal.md. workBenches selects profile and lane modes per launch; profile settings can be used concurrently by both modes. + +## Goals / Non-Goals + +Honor the process's no-lane marker without editing shared profile settings. Other lane checks and commands retain their existing behavior. + +## Decisions + +The `guard` handler exits silently with status 0 for the exact environment value `CLAUDE_NO_LANE=1`, before reading hook input or validating lane identity. All other values use the existing enforcement path. Changing profile settings would affect concurrent lane sessions, so the exemption belongs to the child process environment. + +## Risks / Trade-offs + +An inherited marker could exempt a later lane launch. workBenches clears it for explicit lane requests and lane handoff; only explicit `--no-lane` wins over those requests. Existing Claude processes keep their launch environment and must be relaunched to change mode. + +## Migration Plan + +Install the marker-aware guard with the normal openRepoTools installation path and install the matching workBenches launcher. Revert both changes to roll back. Do not edit the pinned workBenches vendor copy directly. diff --git a/openspec/changes/skip-no-lane-guard/proposal.md b/openspec/changes/skip-no-lane-guard/proposal.md new file mode 100644 index 0000000..baa4ed5 --- /dev/null +++ b/openspec/changes/skip-no-lane-guard/proposal.md @@ -0,0 +1,22 @@ +## Why + +Profile-only Claude launches currently encounter the lane name guard even though the operator chose to work without a lane. The guard must honor that launch mode for the lifetime of the process. + +## What Changes + +- Skip the prompt guard when the launcher supplies `CLAUDE_NO_LANE=1`. +- Keep the hook installed and enforce existing checks for every other value. +- Document the process environment contract with workBenches. +- Run the expanded guard and repository hygiene checks in a focused CI job using the canonical serialized wrapper. + +## Capabilities + +### New Capabilities + +- `profile-only-lane-guard`: per-session exemption for a launch without a lane. + +### Modified Capabilities + +## Impact + +`lanes-edit.sh guard`, lane documentation and regression coverage. workBenches supplies the marker. Implementation handoff: `specs/002-skip-no-lane-guard/`. diff --git a/openspec/changes/skip-no-lane-guard/specs/profile-only-lane-guard/spec.md b/openspec/changes/skip-no-lane-guard/specs/profile-only-lane-guard/spec.md new file mode 100644 index 0000000..b29184f --- /dev/null +++ b/openspec/changes/skip-no-lane-guard/specs/profile-only-lane-guard/spec.md @@ -0,0 +1,43 @@ +## Purpose + +Allow Claude sessions launched without a lane to submit prompts while lane sessions retain their normal identity checks. + +## ADDED Requirements + +### Requirement: Profile-only prompts avoid lane initialization + +The guard SHALL apply the exact launch exemption before probing workspace +configuration, git, workstation identity or its own command path. + +#### Scenario: Infrastructure probes are unavailable + +- **WHEN** a marked guard invocation would encounter unavailable identity commands +- **THEN** it exits silently with status 0 without invoking those commands + +#### Scenario: Invalid guard arguments accompany the marker + +- **WHEN** a marked guard invocation includes unexpected arguments +- **THEN** it prints usage and returns status 2 without identity probes + +### Requirement: Respect the session launch mode + +The prompt guard SHALL silently return status 0 when `CLAUDE_NO_LANE=1`, including when lane identity cannot be read. It SHALL keep existing enforcement for all other values and SHALL NOT alter profile hook settings or lane records for the exempt prompt. + +#### Scenario: Profile session inside the projects root + +- **WHEN** a prompt arrives from a process launched with `CLAUDE_NO_LANE=1` inside the projects root +- **THEN** it is allowed without consulting tmux or requiring a lane row + +#### Scenario: Unsupported marker value + +- **WHEN** the marker is absent, empty, `0`, or `true` and no lane workspace can be read +- **THEN** the guard returns status 2 through its existing refusal path + +### Requirement: Limit the exemption to the prompt guard + +Other lane commands SHALL retain their existing validation even when `CLAUDE_NO_LANE=1` is present. + +#### Scenario: Registry read with no workspace + +- **WHEN** a registry read runs with `CLAUDE_NO_LANE=1` and no workspace configuration +- **THEN** it still refuses the missing workspace diff --git a/openspec/changes/skip-no-lane-guard/tasks.md b/openspec/changes/skip-no-lane-guard/tasks.md new file mode 100644 index 0000000..be0972a --- /dev/null +++ b/openspec/changes/skip-no-lane-guard/tasks.md @@ -0,0 +1,5 @@ +## 1. Speckit handoff + +- [x] 1.1 Record the authorized behavior and hand off implementation to `specs/002-skip-no-lane-guard/tasks.md`; verify the spec, plan and task files exist. + +Implementation tracking is owned by that Speckit task file. diff --git a/specs/002-skip-no-lane-guard/plan.md b/specs/002-skip-no-lane-guard/plan.md new file mode 100644 index 0000000..f41d2fe --- /dev/null +++ b/specs/002-skip-no-lane-guard/plan.md @@ -0,0 +1,7 @@ +# Implementation Plan + +Governance: `openspec/changes/skip-no-lane-guard/`. + +Add an exact-value early exit before script initialization using Bash 3.2 syntax, preserving global option parsing and status-2 argument validation. Update the scope documentation and repository contract. Add subprocess regression coverage against isolated home/config directories and sentinels for tmux, git, hostname, readlink and workspace parsing. Verify with `tests/run.sh -k guard_launch_mode`, Bash syntax checks and `openspec validate skip-no-lane-guard --strict`. + +Dependency: workBenches feature `017-export-no-lane-mode` supplies and clears the process marker. Installed estate commands and vendored pins use their existing release paths. diff --git a/specs/002-skip-no-lane-guard/spec.md b/specs/002-skip-no-lane-guard/spec.md new file mode 100644 index 0000000..83f49ae --- /dev/null +++ b/specs/002-skip-no-lane-guard/spec.md @@ -0,0 +1,30 @@ +# Feature Specification: Skip the guard for no-lane sessions + +**Feature Branch**: `002-skip-no-lane-guard` +**Created**: 2026-09-30 +**Status**: Approved for implementation by the user's request +**Input**: Implement the agreed per-session no-lane hook exemption. + +## User Scenarios & Testing + +### User Story 1 - Work without a lane (Priority: P1) + +A profile session submits prompts even inside a projects directory with no tmux or readable lane workspace. Test the guard with the launch marker and missing lane infrastructure. + +### User Story 2 - Keep lane enforcement (Priority: P1) + +A lane session must still satisfy the existing identity checks. Test absent and unsupported marker values against the same missing-workspace fixture and expect status 2. + +## Requirements + +- **FR-001**: Exact `CLAUDE_NO_LANE=1` bypasses only the prompt guard, silently and before reading hook input. +- **FR-002**: Absent or unsupported values retain the existing checks. +- **FR-003**: Exempt prompts cause no lane record or tmux mutation. +- **FR-004**: Keep shared profile hooks installed. +- **FR-005**: Marked guard invocations avoid workspace, git, workstation and command-path probes, including when global flags are present; invalid guard arguments still return status 2. + +## Success Criteria + +- Exempt prompt fixtures return status 0 with empty stdout and stderr. +- Non-exempt fixtures refuse unreadable lane identity with status 2. +- Other lane commands still refuse missing workspace configuration. diff --git a/specs/002-skip-no-lane-guard/tasks.md b/specs/002-skip-no-lane-guard/tasks.md new file mode 100644 index 0000000..9fbf057 --- /dev/null +++ b/specs/002-skip-no-lane-guard/tasks.md @@ -0,0 +1,12 @@ +# Tasks + +- [x] T001 Add the guard exemption and document its scope. +- [x] T002 Add regression cases for exact marker handling, unreadable identity and unaffected non-guard commands. +- [x] T003 Run focused tests, syntax checks and governance validation; record results. + +## Verification + +- The canonical focused CI command `bash tests/run.sh -k 'guard_launch_mode or repo_hygiene'` passed 165 checks, with 528 deselected, on implementation commit `64f4e93`. This includes all 13 expanded launch-mode cases and proves marked prompts do not invoke identity probes. +- The launcher/guard end-to-end smoke check passed: a profile-only launch exports the marker and permits its prompt, while an unmarked guard still refuses unreadable identity with status 2. +- Changed shell files pass `bash -n`; diff whitespace checks and strict OpenSpec validation pass. +- Windows checks and macOS Bash 3.2 syntax parsing passed. The focused CI runner honors the same wrapper and serialization rules as local runs. diff --git a/tests/test_guard_launch_mode.py b/tests/test_guard_launch_mode.py new file mode 100644 index 0000000..e323ff4 --- /dev/null +++ b/tests/test_guard_launch_mode.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: Apache-2.0 +"""The prompt guard respects process launch mode without lane infrastructure.""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess + +import pytest + +from conftest import REPO, WINDOWS_SKIP + +pytestmark = [WINDOWS_SKIP, pytest.mark.skipif( + shutil.which("bash") is None, reason="the guard is a bash command")] + + +@pytest.fixture +def guard_launch_mode(tmp_path): + home = tmp_path / "home" + projects = home / "projects" + projects.mkdir(parents=True) + fakebin = tmp_path / "bin" + fakebin.mkdir() + tmux_log = tmp_path / "tmux.log" + tmux = fakebin / "tmux" + tmux.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$GUARD_TMUX_LOG"\nexit 1\n') + tmux.chmod(0o755) + probe_log = tmp_path / "identity-probes.log" + for name in ("git", "hostname", "readlink", "sed"): + probe = fakebin / name + probe.write_text('#!/bin/sh\nprintf "%s\\n" "$0 $*" >> "$GUARD_PROBE_LOG"\nexit 1\n') + probe.chmod(0o755) + env = {name: value for name, value in os.environ.items() + if not name.startswith(("LANES_", "CLAUDE_", "WORKBENCHES_", "TMUX"))} + env.update(HOME=str(home), AGENT_PROTOCOL_ROOT=str(home / ".agents"), + PROJECTS_ROOT=str(projects), GUARD_TMUX_LOG=str(tmux_log), + GUARD_PROBE_LOG=str(probe_log), + PATH=str(fakebin) + os.pathsep + env.get("PATH", "")) + payload = json.dumps({"session_id": "profile-session", "cwd": str(projects), + "prompt": "do the work", "hook_event_name": "UserPromptSubmit"}) + + def run(marker, *, command="guard", hook_input=payload, prefix_args=(), extra_args=()): + child_env = dict(env) + if marker is not None: + child_env["CLAUDE_NO_LANE"] = marker + return subprocess.run(["bash", str(REPO / "lanes-edit.sh"), *prefix_args, command, *extra_args], + input=hook_input, text=True, capture_output=True, + env=child_env, timeout=10, check=False) + + return run, home, tmux_log, probe_log + + +@pytest.mark.parametrize("hook_input", [None, "not JSON", ""]) +def test_guard_launch_mode_profile_session_skips_identity_reads(guard_launch_mode, hook_input): + run, home, tmux_log, probe_log = guard_launch_mode + result = run("1") if hook_input is None else run("1", hook_input=hook_input) + assert result.returncode == 0, result.stderr + assert result.stdout == result.stderr == "" + assert not tmux_log.exists() + assert not probe_log.exists() + assert not (home / ".agents").exists() + assert not (home / ".claude").exists() + + +@pytest.mark.parametrize("marker", [None, "", "0", "true", "2"]) +def test_guard_launch_mode_other_values_keep_enforcement(guard_launch_mode, marker): + run, _, _, _ = guard_launch_mode + result = run(marker) + assert result.returncode == 2, result.stderr + assert "THE NAME GUARD REFUSES" in result.stderr + + +def test_guard_launch_mode_does_not_exempt_other_commands(guard_launch_mode): + run, _, _, _ = guard_launch_mode + result = run("1", command="verify-row") + assert result.returncode == 1, result.stderr + assert "openRepoTools wip init" in result.stderr + + +@pytest.mark.parametrize("flag", ["--no-sweep", "--sweep", "--no-github"]) +def test_guard_launch_mode_global_flags_keep_early_exemption(guard_launch_mode, flag): + run, _, tmux_log, probe_log = guard_launch_mode + result = run("1", prefix_args=(flag,)) + assert result.returncode == 0, result.stderr + assert result.stdout == result.stderr == "" + assert not tmux_log.exists() + assert not probe_log.exists() + + +def test_guard_launch_mode_invalid_arguments_still_refuse(guard_launch_mode): + run, _, tmux_log, probe_log = guard_launch_mode + result = run("1", extra_args=("unexpected",)) + assert result.returncode == 2 + assert "usage: guard" in result.stderr + assert not tmux_log.exists() + assert not probe_log.exists() diff --git a/tests/test_lane_helpers.sh b/tests/test_lane_helpers.sh index 256f9ed..bdfd938 100755 --- a/tests/test_lane_helpers.sh +++ b/tests/test_lane_helpers.sh @@ -100,7 +100,7 @@ export CLAUDE_CONFIG_DIR="$HOME/.claude" # them. `AGENT_PROTOCOL_ROOT` is then re-exported into the sandbox, two lines # down, so the default finds the sandbox's file and never the operator's. unset LANES_FILE LANES_EDIT LANES_REPO LANES_PATH LANES_LANE LANES_WORKSPACE_ROOT \ - LANES_REPOS_TSV LANES_REPOS_TSV_SHIPPED PROJECTS_ROOT CLAUDE_PROJECTS_DIR CLAUDE_BIN 2>/dev/null + LANES_REPOS_TSV LANES_REPOS_TSV_SHIPPED PROJECTS_ROOT CLAUDE_PROJECTS_DIR CLAUDE_BIN CLAUDE_NO_LANE 2>/dev/null export AGENT_PROTOCOL_ROOT="$HOME/.agents" mkdir -p "$AGENT_PROTOCOL_ROOT" # THE ONE `LANES_*` SEAM THIS SUITE SETS RATHER THAN UNSETS, for the reason the @@ -6121,7 +6121,7 @@ gd_run "$GD_LANE_ID" chmod 755 "$profiles_root" is "session records that cannot be READ block the prompt — fail CLOSED (clause (d))" "$rc" 2 has "…naming the read that failed" "$err" "session records could not be read" -has "…and the one bypass, which is not an environment flag" "$err" "claude --safe-mode" +has "…and safe-mode recovery for a session subject to the guard" "$err" "claude --safe-mode" # AND THE `SessionStart` HOOK MEETS THE SAME UNREADABLE RECORDS AND STILL EXITS # 0. The two hooks read the same directories through the same function and are # held to OPPOSITE contracts — one refuses on a read it could not make (clause @@ -6152,7 +6152,7 @@ out="$(printf "$gd_hook" "$GD_LANE_ID" "$HOME/projects/repoGD" "do the work" | L err="$(cat "$SANDBOX/stderr")" is "a register this workstation cannot read blocks the prompt with 2, never the dispatcher's 1" "$rc" 2 has "…saying it is the ROW half of the triple that could not be read" "$err" "the ROW cannot be read" -has "…and the one bypass" "$err" "claude --safe-mode" +has "…and safe-mode recovery" "$err" "claude --safe-mode" out="$(printf '' | LANES_FILE="$SANDBOX/no-such-register.md" "$E" session-start 2>/dev/null)"; rc=$? is "…while session-start on the same missing register still exits 0, as it always has" "$rc" 0 gd_run "$GD_LANE_ID" "do the work" "" @@ -6188,7 +6188,7 @@ out="$(printf "$gd_hook" "$GD_LANE_ID" "$HOME/projects/repoGD" "do the work" | P err="$(cat "$SANDBOX/stderr")" is "…while a tmux that answers nothing is an indeterminate read and not 'no window'" "$rc" 2 has "…naming the read that did not happen" "$err" "tmux did not answer" -has "…and the one bypass" "$err" "claude --safe-mode" +has "…and safe-mode recovery" "$err" "claude --safe-mode" # ---- CLAUSE (h) RULE 2: THE OFFER, AND ITS THREE ANSWERS. gd_rec "$GD_LANE_ID" repoGD-2 user "$GD_NEW_MS" diff --git a/tests/test_lane_helpers_suite.py b/tests/test_lane_helpers_suite.py index e4de855..d983f7f 100644 --- a/tests/test_lane_helpers_suite.py +++ b/tests/test_lane_helpers_suite.py @@ -99,7 +99,7 @@ def test_the_lane_helper_suite_passes(): for name in ("LANES_FILE", "LANES_EDIT", "LANES_REPO", "LANES_PATH", "LANES_LANE", "LANES_WORKSPACE_ROOT", "LANES_REPOS_TSV", "LANES_REPOS_TSV_SHIPPED", "AGENT_PROTOCOL_ROOT", - "OPENREPOTOOLS_BIN_DIR", "PROJECTS_ROOT"): + "OPENREPOTOOLS_BIN_DIR", "PROJECTS_ROOT", "CLAUDE_NO_LANE"): env.pop(name, None) # `errors="replace"`, because the thing this wrapper exists to print is the