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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user