polish(v0.5.3): v0.4 lease-prompt hint, invocation-name in user-facing commands, smoke-checklist fixes

Three independent v0.5.3 polish items surfaced during manual WSL smoke testing,
bundled into one commit since they're all string/UX touches with no behavior change.

1. v0.4 lease-prompt tightening (`internal/session/{lease,run,run_preflight}.go`,
   `cmd/entry.go`): when ctask is about to enter direct mode on a workspace that
   already has a live tmux session, the "Continue anyway?" prompt now suggests
   `ctask attach <slug>` as the reattach path. Threaded via
   `PreflightOpts.ActiveLeaseHint` / `LaunchOpts.ActiveLeaseHint`; computed in
   `cmd/entry.go::directModeTmuxHint` (best-effort: silent when no tmux on PATH
   or no session for the workspace). Closes the footgun where a user forgot to
   `export CTASK_SESSION_MODE=persistent` in a second terminal and hit the v0.4
   coexistence prompt without realizing tmux passive-reattach was the intended
   path.

2. Invocation-name in user-facing command hints (`cmd/invocation.go` new,
   `cmd/{persistent,entry,resume,doctor}.go`): the binary name printed in
   bypass / restore / "create one with" suggestions now reflects
   `filepath.Base(os.Args[0])` instead of a hard-coded "ctask". Local-build
   PowerShell users running `.\ctask.exe` see `ctask.exe new <ws> --direct`,
   matching what they need to type. Installed contexts continue to see `ctask`.
   Test seam (`invocationNameOverride`) pins the name to "ctask" in unit tests
   so substring assertions stay stable across Go test binary names. Descriptive
   prose ("ctask persistent mode requires...") and the ssh-remote hint
   (`ssh -t <host> ctask <subcmd>`) intentionally keep the literal "ctask" —
   they refer to the program identity / remote invocation, not the local
   command form. Affected tests: `cmd/{persistent,resume}_test.go` tightened to
   check the full `"<binary> <subcmd> <workspace> --direct"` form.

3. Smoke-checklist fixes (`docs/.../2026-05-08-v0.5.3-smoke-test-checklist.md`):
   six issues caught during the run -- S2 now exports CTASK_SESSION_MODE in
   both terminals (previously only WSL-A had it, which routed WSL-B's
   secondary resume through direct mode instead of passive reattach); O3/A2
   tmux-ls expectations corrected (tmux doesn't emit a literal "(detached)"
   token); P2 expected behavior rewritten (passive reattach detach returns to
   prompt immediately -- AttachExisting calls only shell.AttachSession with
   no PollSessionEnd, the owner is responsible for finalize); A1 no longer
   asks for Ctrl-C in WSL-B; A6 path now uses a glob (was hardcoded to the
   checklist's authoring date, broke on v0.5.1 local-time directory naming);
   M1 PATH-hide rewritten to `$HOME/.local/bin` only (was `:/bin` which is a
   symlink to /usr/bin on usrmerge systems, did not hide tmux) and uses
   `command -v` instead of `which` (which is itself in /usr/bin, unreachable
   under the minimal PATH); M3/M4 reordered so the no-workspace verification
   runs after PATH is restored (was silently failing under minimal PATH);
   T1 wording made "substring match" explicit; T2 wording made N/A
   unambiguous; section 10 split into 10a-10f, each a separate input, to
   work around PSReadLine multi-line paste parsing.
This commit is contained in:
2026-05-14 18:22:27 -04:00
parent 548e292310
commit c204d87b47
12 changed files with 344 additions and 72 deletions
@@ -52,13 +52,17 @@ ctask --version # Expect: ctask v0.5.3
which tmux && tmux -V # Expect: /usr/bin/tmux + tmux 3.5a (or similar)
```
### S2. (WSL-A and WSL-B both) Set the smoke-test root
### S2. (WSL-A and WSL-B both) Set the smoke-test root and session mode
Run in **BOTH WSL-A AND WSL-B** (a fresh shell doesn't inherit env vars):
Run in **BOTH WSL-A AND WSL-B** (a fresh shell doesn't inherit env vars, and
`CTASK_SESSION_MODE` is the **only** trigger for persistent mode — without it
in WSL-B, the secondary `ctask resume` in section P1 falls back to direct mode
and hits the v0.4 "Continue anyway?" lease prompt instead of passive reattach):
```bash
export PATH="$HOME/.local/bin:$PATH"
export CTASK_ROOT=/tmp/ctask-053-smoke
export CTASK_SESSION_MODE=persistent
mkdir -p "$CTASK_ROOT"
```
@@ -68,25 +72,32 @@ This keeps your real workspaces untouched — cleanup is one `rm -rf` later.
## Owner-create path (Step 3)
### O1. (WSL-A) Enable persistent mode and create the project
### O1. (WSL-A) Create the project under persistent mode
`CTASK_SESSION_MODE=persistent` is already exported in both terminals from S2.
Run in **WSL-A**:
```bash
export CTASK_SESSION_MODE=persistent
ctask new --project --agent bash ctask-053-smoke
```
**What you'll see (and the expected reality, not the misleading v1 text):**
The `YYYY-MM-DD` shown below is **today's local-time date** (v0.5.1 — the
workspace directory uses `time.Now()`, not UTC). If you're running this
checklist on 2026-05-14, the path will read `2026-05-14_ctask-053-smoke`,
not `2026-05-08_ctask-053-smoke` (which was the day the checklist was
written).
- For ~50ms, the outer terminal will print three lines (you may not have
time to read them — tmux clears the screen on attach):
- `[ctask] created projects/2026-05-08_ctask-053-smoke`
- `[ctask] created projects/YYYY-MM-DD_ctask-053-smoke`
- `[ctask] local :: ctask-053-smoke`
- `[ctask] /tmp/ctask-053-smoke/projects/2026-05-08_ctask-053-smoke`
- `[ctask] /tmp/ctask-053-smoke/projects/YYYY-MM-DD_ctask-053-smoke`
- `[ctask] project dir: ctask-053-smoke/`
- Then tmux's alternate screen takes over. You see:
- A bash prompt like `warren@DESKTOP-VGJVN77:/tmp/ctask-053-smoke/projects/2026-05-08_ctask-053-smoke/ctask-053-smoke$`
- A bash prompt like `warren@DESKTOP-VGJVN77:/tmp/ctask-053-smoke/projects/YYYY-MM-DD_ctask-053-smoke/ctask-053-smoke$`
(the path includes the project subdir — that's the v0.5 `launch_dir`).
- A green status bar at the bottom showing something like
`[ctask-projects-ctask-053-smoke-XXXXXX:bash*]` on the left, with
@@ -131,7 +142,11 @@ tmux ls
```
**Expected:** A line containing `ctask-projects-ctask-053-smoke-XXXXXX`
followed by `(1 windows) ... (attached)` or `(detached)`.
followed by `(1 windows) ...`. The line may include `(attached)` if a
client is still connected (WSL-A's `ctask new` is still attaching to the
session at this point), or no marker at all if no client is connected.
**tmux does not emit a literal `(detached)` token** — the absence of
`(attached)` is how you know nothing is attached.
PASS / FAIL: ___
@@ -164,10 +179,18 @@ PASS / FAIL: ___
Press `Ctrl-b d` in WSL-B.
WSL-B's shell prompt also won't return (same polling-loop behavior). That's
fine for now — both WSL-A and WSL-B are sitting in polling loops on the
same tmux session. Both will exit cleanly when the tmux session ends in
section A3 below.
**What you'll see (passive-reattach detach behavior — different from O2):**
- The tmux full-screen alternate display closes; WSL-B's outer terminal is
restored.
- **WSL-B's shell prompt returns immediately.** Unlike WSL-A's owner-create
path, passive reattach does NOT enter a polling loop on detach.
`AttachExisting` (`internal/session/attach.go`) only calls
`shell.AttachSession` — no `PollSessionEnd`, no finalize. Passive viewers
detach freely because the **owner** is the one responsible for finalize
when the session ends.
- The owner in WSL-A is still locked in its polling loop on the same tmux
session; that's correct.
PASS / FAIL: ___
@@ -192,8 +215,8 @@ non-responsive cursor) and press `Ctrl-C`.
- WSL-A's shell prompt finally returns.
In **WSL-B** (which is now also in polling-loop mode after the P2 detach),
also press `Ctrl-C`. Its prompt should return too.
(No Ctrl-C needed in WSL-B — passive reattach already exited cleanly after
the P2 detach; WSL-B has been at its shell prompt since then.)
### A2. (WSL-A) Verify the tmux session survived
@@ -201,7 +224,10 @@ also press `Ctrl-C`. Its prompt should return too.
tmux ls
```
**Expected:** still shows `ctask-projects-ctask-053-smoke-XXXXXX (... detached)`.
**Expected:** still shows `ctask-projects-ctask-053-smoke-XXXXXX: 1 windows ...`.
No `(attached)` marker now (both clients have detached). As noted in O3,
tmux does not print a literal `(detached)` token; absence of `(attached)`
is the signal.
### A3. (WSL-A) Wait for the lease to go stale
@@ -245,8 +271,12 @@ detects session end and runs adoption finalize. WSL-A's prompt returns.
### A6. (WSL-A) Inspect the summary
The workspace directory uses **today's local-time date** (v0.5.1). Use a
glob so this works on any day:
```bash
WS=$CTASK_ROOT/projects/2026-05-08_ctask-053-smoke
WS=$(ls -d "$CTASK_ROOT"/projects/*_ctask-053-smoke 2>/dev/null | head -1)
echo "$WS" # expect: /tmp/ctask-053-smoke/projects/YYYY-MM-DD_ctask-053-smoke
cat "$WS/.ctask/last-session-summary.json" | python3 -m json.tool
```
@@ -256,7 +286,7 @@ cat "$WS/.ctask/last-session-summary.json" | python3 -m json.tool
"end_reason": "tmux_session_ended",
"detected_via": "polling",
"session_ownership": "adopted",
"adopted_from_orphan_at": "2026-05-08T..."
"adopted_from_orphan_at": "<today>T..."
```
(Other fields like session_id, hostname, files_added etc. will also be
@@ -276,10 +306,23 @@ Run in **WSL-A** (now back at a normal prompt):
echo "" | ctask resume ctask-053-smoke
```
**Expected:** A multi-line refusal message containing all of:
**Expected:** A multi-line refusal message containing **(substring match,
not exact)** all three of:
- `ctask persistent mode requires an interactive terminal`
- `ssh -t <host> ctask resume <workspace>`
- `ctask resume <workspace> --direct`
- `resume <workspace> --direct`
Wording around these phrases (headings like "Over SSH, use:", a trailing
period, etc.) is incidental — PASS as long as all three substrings are
present.
> **Note on the bypass hint:** the binary name in the printed bypass
> line reflects `basename(os.Args[0])`. On this WSL setup that resolves
> to `ctask` (so the line reads `ctask resume <workspace> --direct`,
> matching the third substring). On Windows when running a local build
> as `.\ctask.exe` it resolves to `ctask.exe` (line reads
> `ctask.exe resume <workspace> --direct`). Both forms satisfy the
> "resume <workspace> --direct" substring above.
Exit code: 1 (`echo $?` to verify).
@@ -287,7 +330,11 @@ PASS / FAIL: ___
### T2. (Optional, WSL-A) SSH-without-t equivalent
Skip this if `sshd` is not running on localhost.
**Skip with N/A if `ssh localhost` fails with "Connection refused"**
that just means sshd isn't running on this WSL distro (the common case).
The test only exercises the same non-TTY refusal path that T1 already
covers, via a different no-TTY transport; not running it does not gate
v0.5.3 sign-off.
```bash
ssh localhost -- bash -lc "PATH=\$HOME/.local/bin:\$PATH CTASK_SESSION_MODE=persistent CTASK_ROOT=$CTASK_ROOT ctask resume ctask-053-smoke"
@@ -295,7 +342,7 @@ ssh localhost -- bash -lc "PATH=\$HOME/.local/bin:\$PATH CTASK_SESSION_MODE=pers
**Expected:** Same refusal message. (No `-t` means no TTY allocation.)
PASS / FAIL (or N/A): ___
PASS / FAIL / N/A: ___
---
@@ -404,18 +451,33 @@ half-initialized workspace on disk.
### M1. (WSL-A) Hide tmux from PATH (carefully)
We need to remove `/usr/bin` from PATH but keep `~/.local/bin/ctask`
reachable. Run in **WSL-A**:
We need `~/.local/bin/ctask` reachable but `tmux` NOT findable on PATH.
On modern Debian / Ubuntu / WSL distros (the "usrmerge" change), `/bin` is
a symlink to `/usr/bin`, so listing `/bin` on PATH still surfaces tmux.
The cleanest fix is to set PATH to only `~/.local/bin`.
A separate gotcha: once PATH is reduced this far, the `which` utility
itself is no longer reachable (it lives in `/usr/bin`). Use the POSIX
shell built-in `command -v` instead — it's a bash built-in, so it works
regardless of PATH.
```bash
ORIG_PATH=$PATH
export PATH="$HOME/.local/bin:/bin" # excludes /usr/bin where tmux lives
which tmux 2>/dev/null && echo "ERROR: tmux still on PATH"
which ctask # should print $HOME/.local/bin/ctask
export PATH="$HOME/.local/bin" # only ctask reachable; nothing else
command -v tmux && echo "ERROR: tmux still on PATH" # expect: NO output (no path, no ERROR)
command -v ctask # expect: /home/<you>/.local/bin/ctask
```
If `which tmux` finds tmux at e.g. `/bin/tmux` or `~/.local/bin/tmux`,
adjust accordingly — the goal is `which tmux` returns nothing.
ctask is a pure-Go binary that does not shell out to anything except tmux,
so a PATH containing only its own directory is sufficient for the M2 / M3
refusal-path check. The earlier `PATH=$HOME/.local/bin:/bin` form and the
`which` invocation were both checklist bugs — neither correctly verifies
that tmux is hidden on a usrmerge system with a minimal PATH.
If `command -v tmux` still prints a path (e.g., `~/.local/bin/tmux`), some
copy of tmux remains reachable — remove or rename that file for the
duration of this section.
### M2. (WSL-A) Try `ctask new` with persistent mode
@@ -440,35 +502,82 @@ To disable persistent mode:
unset CTASK_SESSION_MODE
```
### M3. (WSL-A) Verify NO workspace was created
(The binary name in the bypass line is `basename(os.Args[0])` — in WSL
with an installed `~/.local/bin/ctask` on PATH, the shell-resolved
absolute path basenames to `ctask`, so the printed line matches the form
shown above.)
```bash
ls "$CTASK_ROOT/projects/" 2>/dev/null
```
### M3. (WSL-A) Restore PATH
**Expected:** only `2026-05-08_ctask-053-smoke` (from earlier sections),
NO `ctask-053-no-tmux`.
PASS / FAIL: ___
### M4. (WSL-A) Restore PATH
Restore PATH **before** the directory-listing verification — under the
minimal PATH from M1, `/usr/bin/ls` isn't reachable and the verification
silently fails (bash prints "ls: command not found" to stderr, which a
naive `2>/dev/null` would swallow).
```bash
export PATH="$ORIG_PATH"
which tmux # Expected: /usr/bin/tmux
command -v tmux # Expected: an absolute path to tmux,
# e.g. /usr/bin/tmux OR /bin/tmux — both are
# the same file on usrmerge systems where
# /bin is a symlink to /usr/bin.
```
### M4. (WSL-A) Verify NO workspace was created
```bash
ls "$CTASK_ROOT/projects/"
```
**Expected:** only the `YYYY-MM-DD_ctask-053-smoke` directory created
earlier in this run, NO `ctask-053-no-tmux` and NO
`YYYY-MM-DD_ctask-053-no-tmux`. (If you ran the older version of this
step under the minimal PATH and got "no output", that was the
`ls`-not-on-PATH bug — it does not mean the workspace was created
silently. Redo after the PATH restore.)
PASS / FAIL: ___
---
## Native Windows refusal (Step 10)
Run in **PS-C** (Windows PowerShell, NOT WSL):
Run in **PS-C** (Windows PowerShell 7, NOT WSL).
> **Important:** run **each numbered block below as its own input** —
> press Enter after each one and let PowerShell complete it before
> typing the next. Do NOT paste all five at once. With PSReadLine /
> Oh-My-Posh, multi-line paste is parsed as a single script block, and
> the execution order can confuse the user (PowerShell will happily run
> `.\ctask.exe` before `cd` has taken effect, producing "term not
> recognized" plus "go.mod not found" errors).
### Step 10a. cd into the repo
```powershell
cd C:\Users\Warren\claude_tasks\ctask
```
Verify with `Get-Location` — expect the path to end in `\ctask`.
### Step 10b. Build the Windows binary
```powershell
go build -o ctask.exe .
```
Expect no output (success). The repo's `go.mod` lives at the root, so
this only works after step 10a.
### Step 10c. Set the persistent-mode env vars
```powershell
$env:CTASK_SESSION_MODE = "persistent"
$env:WSL_DISTRO_NAME = $null
```
### Step 10d. Trigger the native-Windows refusal
```powershell
.\ctask.exe new --no-launch ctask-053-windows
```
@@ -482,14 +591,19 @@ Recommended:
sudo apt install tmux
Or bypass persistent mode:
ctask new <workspace> --direct
ctask.exe new <workspace> --direct
```
NO workspace created (verify under `%USERPROFILE%\ai-workspaces\`).
The bypass line reflects `basename(os.Args[0])` — running this section as
`.\ctask.exe` means the printed binary name is `ctask.exe`. (Running an
installed `ctask` would print `ctask.exe` too on Windows, since the OS
resolves it to the same `.exe`.)
### --direct under persistent on Windows
NO workspace created (verify under `%USERPROFILE%\ai-workspaces\`
typical location for default Windows installs; the `ctask-053-windows`
directory should not exist).
Still in **PS-C**:
### Step 10e. --direct under persistent on Windows
```powershell
.\ctask.exe new --no-launch --direct ctask-053-win-direct
@@ -498,7 +612,7 @@ Still in **PS-C**:
**Expected:** workspace created with one warning line:
`[ctask] warning: --direct bypassing persistent mode (no tmux session exists for this workspace)`
### Cleanup PS-C state
### Step 10f. Cleanup PS-C state
```powershell
$leftover = Get-ChildItem -Path "$env:USERPROFILE\ai-workspaces\general" -Filter "*ctask-053-win-direct*" -ErrorAction SilentlyContinue