Files
ctask/notes.md
T

38 KiB

ctask — Session Handoff Notes

Last touched: 2026-05-09. v0.5.2 is shipped on main and installed locally as v0.5.2. v0.5.3 implementation is complete on branch feat/v0.5.3-persistent-session-mode (20 commits) but is NOT yet merged or installed — it is awaiting the user's manual WSL smoke verification.

Where we are

  • main: v0.5.2 (workspace retrieval + cross-workspace context). Installed binary at %LOCALAPPDATA%\ctask\bin\ctask.exe is v0.5.2.
  • feat/v0.5.3-persistent-session-mode: v0.5.3 (persistent session mode via tmux). 20 commits. All automated tests + go vet + cross-compile (just build-linux produces a static ELF) green on the Windows host. The Linux binary at dist/ctask-linux-amd64 runs and reports ctask v0.5.3 under WSL debian-dev.
  • Pending action: the user runs the manual smoke checklist at docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md (v2 — explicit terminals, corrected expectations) and reports PASS/FAIL per section. On all-PASS we merge the branch to main, just install to refresh the installed binary, and optionally git tag v0.5.3.
  • Remote: none (local-only, intentional — see CLAUDE.md).
  • ctask doctor reports 5 pass/fail + 2 seed-directory + 1 CTASK_PROJECT_ROOT check (all three-state). Once v0.5.3 lands, doctor adds an INFO line for Session mode: direct|persistent plus an INFO/FAIL line for tmux when persistent.

What v0.4 delivered (still true, unchanged)

Workspace concurrency protection — session lease with heartbeat (Layer 1), metadata write lock (Layer 2), stale-workspace detection (Layer 3), session handoff summary (Layer 4). --force flag on resume/last/open. WriteMetaLocked, LaunchOpts.Force, Preflight. All unchanged in v0.5.

What v0.4.1 delivered (still true, unchanged)

Correctness and polish: config.SearchRoots() multi-root discovery; two-depth scanWorkspaces (flat + category, first-match-wins); nested-git documentation; doctor seed-dir three-state checks; archive active-session warning with non-TTY refuse; provisional-cleanup exit-code gate. Still load-bearing — see "From v0.4.1" section below.

What v0.5 delivered

Theme: separate workspace management from project code. --project now scaffolds a project subdirectory and session commands cd into it. Twelve commits on main (175fbb0..8130a68).

  1. launch_dir field in TaskMeta (175fbb0)
    • New LaunchDir string with yaml:"launch_dir,omitempty" — empty for tasks and pre-v0.5 projects, defaults to the project slug for new projects.
    • Omitempty keeps on-disk task.yaml clean for backward compatibility.
  2. workspace.ResolveLaunch helper (dcb1610)
    • Converts a relative launch_dir into an absolute child-cwd with three outcomes:
      • Valid dir → return absolute path, no warning, no error.
      • Missing path (os.IsNotExist) or path-is-a-file → return (wsDir, warning, nil) so the caller warns and falls back.
      • Absolute path, ..-escape, or any non-IsNotExist stat error (permission, ENOTDIR, invalid name) → return ("", "", error) — caller must surface.
    • This asymmetry is deliberate — masking a permission error as a warning fallback would silently strip launch-dir semantics.
  3. Project subdirectory scaffolding (7cfafdc)
    • ctask new --project creates an empty subdir named after the final suffixed slug (so dup-2 collisions produce matching dup-2/ subdir).
    • ctask does not seed anything inside the subdir — the user places their own CLAUDE.md, source code, project configuration.
    • Task workspaces are completely unchanged — no subdir, no launch_dir.
  4. CTASK_LAUNCH_DIR env var (509a6d6)
    • Added as a 7th arg to config.EnvVars. All three cmd callers (new, resume, open) pass ws.Meta.LaunchDir.
  5. Session launch routed through LaunchDir (103f2cd)
    • LaunchOpts.LaunchDir added. session.Run calls ResolveLaunch before banner/exec, prints any warning to stderr, aborts on security error, and passes the absolute path to shell.ExecAgent / shell.ExecShell as the child's working directory.
    • Banner gains a [ctask] project dir: <name>/ line when launch_dir is set.
    • Lease, manifest, heartbeat, and summary scope stays the workspace root — only the child's cwd changes.
  6. ctask info shows launch fields (cdff7f3)
    • For workspaces with launch_dir set, info prints Launch dir:, Launch path:, and Dir exists: yes|no (via direct os.Stat, not via ResolveLaunch — info is a display command, not a launch command).
  7. SearchRoots default fallback (47430a1)
    • When CTASK_PROJECT_ROOT is unset, SearchRoots() now appends $CTASK_ROOT/projects/ so default-location projects are findable from any shell (resolves the v0.4.1 env-var scoping footgun from the previous follow-up list).
    • Dedupe in scanAllRoots prevents duplicate results when the same workspace is reachable via both the depth-2 scan under CTASK_ROOT and the explicit $CTASK_ROOT/projects/ search root.
  8. Doctor CTASK_PROJECT_ROOT check (70bd167)
    • Three-state, matching the checkSeedDir pattern: [INFO] when unset (points at $CTASK_ROOT/projects/), [INFO] with user-scope advisory when set+exists, [FAIL] when set but missing. Only FAIL increments the failure counter.
    • Wording is advisory ("recommended: set at user scope…"), not prescriptive.
  9. Project CLAUDE.md template rewrite (cdf1c55)
    • Adds a "Workspace Structure" section explicitly describing the root-vs-subdir split.
    • Keeps the "Git" section from v0.4.1, now with an explicit mention that the project subdir is tracked by the root repo.
  10. Status line helpers show effective launch path (0976dce)
    • Both .sh and .ps1 build DISPLAY_PATH = $CTASK_WORKSPACE + (CTASK_LAUNCH_DIR if set). When CTASK_LAUNCH_DIR != CTASK_TASK, the tag becomes |project:<launch_dir> (user-overridden launch directory).
  11. Docs updated (82c9445)
    • docs/commands.md: workspace-layout diagram, launch_dir semantics (default, override, fallback vs error), CTASK_LAUNCH_DIR env var, doctor example with new INFO line, Query Resolution default-discovery paragraph.
  12. Version bump 0.4.10.5.0 (8130a68).

What v0.5.1 delivered

Two rounds shipped under the v0.5.1 tag: wall-clock date for user-facing surfaces (a162aec, a11d48b) and Linux portability baseline (7a7b249, 1033072).

Round 1 — date fix (a162aec, a11d48b)

  • Bug: workspace directory names used UTC date, so at 20:22 EDT on April 22 a new workspace was named 2026-04-23_foo. Confusing in file explorer, ctask list, and ctask info.
  • Fix: internal/workspace/create.go now uses time.Now() (local) for the directory-prefix date and the YYYYMMDD-HHMMSS ID. cmd/info.go formats Created / Updated / Archived with .Local().
  • Stored timestamps still UTC. task.yaml CreatedAt / UpdatedAt / ArchivedAt, session logs, lease, manifest, and summary all continue to store UTC. Only user-facing surfaces (directory prefix + info display) switched.
  • Regression guards: TestCreateDirectoryPrefixUsesLocalDate and TestInfoFormatsTimestampsInLocalZone enforce both invariants.

Round 2 — Linux portability baseline (7a7b249, 1033072)

  • Build targets (justfile): build-linux, build-windows, build-all output to dist/. Both cross-targets force CGO_ENABLED=0 so the artifact is pure-Go statically linked regardless of build host (a native Linux build otherwise defaults to CGO_ENABLED=1 and links against host glibc).
  • POSIX install scripts (scripts/install.sh, scripts/uninstall.sh): mirror the .ps1 UX. Default install dir ~/.local/bin, optional override arg, PATH check warns with the right shell snippet (zsh / bash / fallback) — the script does NOT modify shell config. Statusline helper ctask-statusline.sh ships alongside the binary; the .ps1 helper is intentionally not installed on Linux. Workspace data is preserved on uninstall.
  • .gitignore now covers ctask, ctask-*, dist/ (in addition to *.exe).
  • WorkspacePath removed from TaskMeta. The audit (audit-report.md) confirmed the field was write-only with no production readers — it persisted an absolute Windows path into task.yaml that would have been misleading on cross-OS shares. Existing task.yaml files with workspace_path continue to load (Go's YAML unmarshal silently ignores unknown fields). TestMetaTypeMissingDefaultsToTask keeps the legacy YAML literal in its fixture as a backward-compat regression guard.
  • Validation: Windows go test ./... green across all 7 packages; cross-compile produces ELF x86-64. WSL-native validation passed: go test ./... -count=1 green; just build-linux produces a statically linked ELF (ldd reports not a dynamic executable); ./scripts/install.sh installs to ~/.local/bin/ctask and registers the statusline helper; ctask doctor recognizes the Linux statusline helper.

What v0.5.2 delivered

Theme: workspace retrieval and cross-workspace context. Five feature commits (a5e508b..3b6be0d) plus a version bump (5910100).

  • Three new direct-lookup commands (176e788):

    • ctask restore <ws> — un-archive (metadata-only flip; mirrors archive's lease guard; refuses already-active workspaces)
    • ctask notes <ws> — stream notes.md to stdout raw, no framing; [ctask]-prefixed stderr on missing file (SilenceErrors: true so the spec format is the only diagnostic shown)
    • ctask path <ws> — print absolute filesystem path on one line, OS-native separators
  • Direct-lookup commands are archived-inclusive by default (b923ae8). The --all/-a flag was dropped from info entirely; the Status: line in info output surfaces archived state. delete and open keep their --all flags (they are potentially destructive). resume and archive resolve archived-inclusive then filter, so they can give actionable error messages.

  • ctask resume <archived-ws> prints a restore hint (b923ae8):

    [ctask] error: workspace "X" is archived
    
    To restore it:
      ctask restore X
    

    Genuine not-found and ambiguity behavior preserved.

  • ctask list --names (56d2e07) — machine-readable enumeration. One workspace directory basename per line, no header. Empty stdout on no match (no "No workspaces found." placeholder). Honors all existing list filters. Spec invariant TestListNamesCandidatesResolveUniquely enforces that every emitted line resolves to exactly one workspace.

  • Shell completion via Cobra (176e788, b923ae8). ctask completion {bash,zsh,fish,powershell} works through Cobra's auto-injected subcommand. ValidArgsFunction hooks per spec policy:

    • resume, archive → active only
    • restore → archived only
    • info, notes, path → both
    • delete, open → active only (flag-aware completion deferred)

    Candidates are workspace directory basenames (unique under the resolver's exact-match step), not bare slugs (which can collide across categories or dates).

  • Cross-workspace context section in seed CLAUDE.md (3b6be0d). Both task and project templates teach agents to inspect related workspaces with list --all / info / notes / path before starting work, and to treat other workspaces as read-only. Applies to newly-created workspaces only — no retroactive overwrite of existing CLAUDE.md files.

  • Version bump to 0.5.2 (5910100).

Validation:

  • Windows: go test ./... -count=1 green across all 7 packages; go vet clean; go build clean. End-to-end smoke against the binary covered the full archive ↔ restore cycle plus completion-script generation.
  • WSL/Linux: refreshed validation copy; reinstalled via install.sh; full smoke checklist passed (16 behaviors across new + active + archived + restore + completion). Cross-built Linux binary is statically linked (file reports statically linked; ldd reports not a dynamic executable).

Known limitation (v0.5.2)

  • ctask resume <archived-ws> prints Error: workspace archived below the actionable hint. Cosmetic only — the user-visible hint is shown first and is correct. Suppressing the trailing line would require SilenceErrors: true on resume, which would also swallow unrelated session-launch errors. Acceptable trade; revisit only if the redundancy actively confuses users.

Next: v0.6 (planning)

Not yet specced. Likely scope:

  • Config / agent profile work. The long-flagged direction towards ~/.config/ctask/ configuration; may supersede the env-var resolution dance for CTASK_ROOT/CTASK_PROJECT_ROOT/CTASK_SEED_DIR/CTASK_AGENT.
  • Optional polish for resume error output. Silence the trailing Cobra "Error: workspace archived" line — likely via per-error-path SilenceErrors or a sentinel-error wrapper.
  • Flag-aware completion for open/delete --all. Cobra supports RegisterFlagCompletionFunc per flag; would let completion offer archived candidates only when --all is passed.

Post-v0.4 bugfixes (still live, carried forward)

Provisional-workspace cleanup (2026-04-22, commits 02dcdcc, ba8b3a1)

Covered in v0.4.1 notes. The exit-code gate (childExitCode != 0 && startManifest != nil && emptyDiff && NewlyCreated) is unchanged in v0.5.

Tree state at pause

  • main clean with respect to v0.4, v0.4.1, v0.5, v0.5.1, v0.5.2. Latest tip is e448eff docs(v0.5.2): record v0.5.2 completion in notes.md.
  • HEAD is on feat/v0.5.3-persistent-session-mode (20 commits ahead of main, 0 behind). Branch was created cleanly from main at the start of v0.5.3 work.
  • Installed ctask.exe is v0.5.2 — DO NOT reinstall yet. Wait until v0.5.3 has passed manual smoke + been merged.
  • Untracked files (do NOT touch without asking):
    • .claude/settings.local.json (modified — Claude Code local settings)
    • bugfix-provisional-workspace.md (spec for the 2026-04-22 initial provisional fix; may be deleted or archived)
    • docs/superpowers/plans/2026-04-06-install-workflow.md (install-workflow plan from an earlier session)
    • docs/superpowers/plans/2026-04-21-v0.4-implementation.md (v0.4 plan — executed)
    • docs/superpowers/plans/2026-04-22-v0.4.1-patch.md (v0.4.1 plan — executed)
    • docs/superpowers/plans/2026-04-22-v0.5-implementation.md (v0.5 plan — executed)
    • v0.4-spec.md, v0.4.1-patch-spec.md, v0.5-spec.md, v0.5.3-spec.md (specs the implementations followed)
  • Files committed ON the v0.5.3 branch (already tracked, not in the untracked list above):
    • docs/superpowers/plans/2026-05-08-v0.5.3-implementation.md — the executed plan
    • docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-log.md — automated portions of Task 17 (cross-compile, version, native-Windows refusal, doctor on both platforms — all PASS)
    • docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md — manual checklist v2 for the user to run

How to resume

Completing the v0.5.3 ship (current pending action)

The v0.5.3 branch is ready — only manual WSL smoke verification stands between it and main.

cd C:\Users\Warren\claude_tasks\ctask
git checkout feat/v0.5.3-persistent-session-mode
just test                      # all green on Windows
just build-linux               # produces dist/ctask-linux-amd64 (static ELF)

Then the user runs through docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md in WSL (three terminals: WSL-A, WSL-B, PS-C) and reports PASS/FAIL per section.

After all-PASS:

git checkout main
git merge --no-ff feat/v0.5.3-persistent-session-mode
just install                   # refresh installed binary to v0.5.3
ctask --version                # expect: ctask v0.5.3
git branch -d feat/v0.5.3-persistent-session-mode
git tag v0.5.3                 # optional

If anything fails: capture the exact output (especially around the adopted-reattach summary fields and the tmux session-name hash) and feed it back to the next session.

General resume (when on main after a ship)

cd C:\Users\Warren\claude_tasks\ctask
just test                      # go test ./... -count=1
just build                     # or: go build -o ctask.exe .
just install                   # only reinstall if source changed

Quick sanity checks that v0.5.1 is live:

ctask --version                # expect: ctask v0.5.1
ctask doctor                   # expect: 5 pass/fail + 2 seed-dir INFO + 1 CTASK_PROJECT_ROOT INFO

# Local-time directory prefix (v0.5.1):
ctask new --project "tz-check" --no-launch
# Expected output: [ctask] created projects/YYYY-MM-DD_tz-check — YYYY-MM-DD matches
# the user's local wall-clock date, not UTC.
ctask info tz-check            # Created/Updated in local zone
ctask delete --force tz-check

# Project subdir + banner (v0.5):
ctask new --project "smoke" --no-launch
# A subdir named smoke/ exists inside the workspace.
ctask resume smoke             # banner shows "[ctask] project dir: smoke/" and Claude
                               # launches with cwd inside smoke/. /exit to leave.
ctask delete --force smoke

# Default discovery (v0.5):
# Projects created without $CTASK_PROJECT_ROOT should now be findable from any shell.
ctask list --projects

Load-bearing design points (don't forget)

From v0.4 (unchanged)

  • Coexisting-session limitation: "Continue anyway?" or --force → second session runs without its own lease. Documented in docs/commands.md. Don't redesign without a real user complaint.
  • end_manifest in the summary is ctask-internal (not in spec example JSON) and powers Layer-3 stale-workspace diff. Removing it breaks Layer 3.
  • Write-lock skip classification (best-effort vs important-but-non-fatal): unchanged.
  • cmd/delete.go two-check invariant: unchanged. CTASK_WORKSPACE match → refuse; .ctask/manifest-start.json exists → refuse. Both before any mutation.
  • session.Run() lifecycle in internal/session/run.go:
    1. PreflightFull (Layers 3 + 1)
    2. Write lock → write lease (unless coexisting)
    3. Write lock → capture + write start manifest
    4. Print launch-context banner (Layer 4)
    5. Start heartbeat (only if we own the lease)
    6. v0.5: workspace.ResolveLaunch(opts.WsDir, opts.LaunchDir) — print warning or abort on security error
    7. Banner lines (including project dir: when LaunchDir set) + exec child with cwd = resolved launch path
    8. Stop heartbeat
    9. handleProvisional (gated on NewlyCreated && childExitCode != 0 && emptyDiff) — if it removes the workspace, skip finalize entirely
    10. finalize: single write-lock acquire → append log + write summary + remove lease (if owned) + remove manifest-start
  • new is intentionally not given --force. Brand-new workspaces have nothing for Layer 1/3 to warn about.
  • internal/lockfile is a neutral package — no other ctask imports.

From v0.4.1 (unchanged, still load-bearing)

  • config.SearchRoots() is the canonical way to enumerate workspace roots. Always includes CTASK_ROOT. In v0.5 the semantics of the second entry expanded — see "From v0.5" below.
  • QueryResult.Root is how callers render relative paths and compute CTASK_ROOT env var. Populated by scanAllRoots.
  • scanWorkspaces is a two-depth scan, first-match-wins. Depth-1 (flat) check wins if present; never descends into a detected workspace.
  • Archive's non-TTY stdin refuses. Fresh lease + non-TTY = exit non-zero without mutating. Don't regress to "silent proceed".
  • Doctor seed-dir checks are three-state. Only "configured but missing" counts as a failure.
  • Provisional-cleanup exit-code gate. childExitCode == 0 means preserve. Don't special-case specific codes.

From v0.5 (new — don't unlearn)

  • launch_dir is stored as workspace-relative in task.yaml, empty for tasks and pre-v0.5 projects. Never store absolute paths. Never interpret launch_dir outside workspace.ResolveLaunch.
  • workspace.ResolveLaunch has three outcomes: valid, soft fallback (with warning), security/permission error. Soft fallback is (wsDir, warning, nil) — caller warns and keeps launching. Error is ("", "", err) — caller must abort. The asymmetry between os.IsNotExist (fallback) and other stat errors (propagate) is intentional; masking a permission error as a warning would silently strip launch-dir semantics.
  • Project subdir name = final suffixed slug. dup-2 collision means subdir is dup-2/, not dup/. The actualSlug variable in workspace.Create is the single source of truth.
  • Subdir is created after seeds, empty, never seeded. Layer 1 (built-in defaults), Layer 2 (general seed), Layer 3 (project seed) all target the workspace root. The if opts.IsProject { os.MkdirAll(projDir, 0755) } block runs AFTER seed overlays and BEFORE task.yaml is written.
  • Session scope is workspace-wide even when launched in the subdir. Lease, manifest capture, write lock, summary, stale-workspace diff all operate on opts.WsDir (the workspace root), not on the resolved launch path. Only the child process's cwd changes.
  • CTASK_LAUNCH_DIR is always exported (empty string for tasks). Status line helpers and any future child-side integration can check -n "$CTASK_LAUNCH_DIR" safely.
  • SearchRoots() now appends $CTASK_ROOT/projects/ by default when CTASK_PROJECT_ROOT is unset. This is redundant with the v0.4.1 two-depth scan under $CTASK_ROOT but explicit per spec. Dedupe in scanAllRoots (searchRootKey — cleaned + lower-cased on Windows) prevents double-counting. Don't remove the default fallback without also reverting the spec-driven expectation that default projects are findable from any shell.
  • Doctor check for CTASK_PROJECT_ROOT is advisory, not prescriptive. "Recommended: set at user scope so all terminals can discover these workspaces." Don't harden into a FAIL when the variable is set but the path exists.

From v0.5.1 (new — don't unlearn)

  • Workspace directory prefix uses LOCAL time. time.Now() in workspace.Create feeds the YYYY-MM-DD date and the YYYYMMDD-HHMMSS ID. Don't switch back to .UTC() for these. The TestCreateDirectoryPrefixUsesLocalDate regression test enforces this.
  • ctask info displays timestamps in local zone via .Local().Format(...). Stored timestamps in task.yaml are still UTC (meta.CreatedAt = nowLocal.UTC().Truncate(time.Second)) — only the display converts.
  • Stored timestamps everywhere else remain UTC. Session logs, lease StartedAt/LastHeartbeatAt, manifest capture times, summary timestamps are all unambiguous UTC. Don't "localize" those — they're machine-readable.
  • Cross-builds force CGO_ENABLED=0. just build-linux and just build-windows set this explicitly so the artifact is pure-Go static. Don't drop the flag — a native Linux build defaults to cgo and produces a glibc-linked binary that won't run in Alpine / distroless / scratch containers.
  • WorkspacePath is gone from TaskMeta. Don't add it back. If a persistent workspace identifier is ever needed, add a properly relative-to-root workspace_id (forward-slash normalized). Old task.yaml files with workspace_path parse silently — the field is ignored on read.
  • POSIX install script does not modify shell config. It warns the user about PATH but never edits ~/.bashrc / ~/.zshrc. Don't change this without an explicit user request.

From v0.5.2 (new — don't unlearn)

  • info no longer has --all/-a. Direct lookup is archived-inclusive by default. Don't add the flag back. The Status: line in info output makes archived state obvious. TestInfoFindsArchivedWorkspaceWithoutFlag enforces this.
  • resume / archive resolve archived-inclusive then filter. Don't switch back to includeArchived=false at the resolver layer. The two-step pattern (resolve broadly, reject explicitly) is what lets resume give the actionable restore hint instead of a generic "not found".
  • Direct-lookup commands (info, notes, path, restore) use includeArchived=true. Active-only commands (resume, archive) reject archived after resolve. Potentially-destructive commands (delete, open) keep their explicit --all opt-in. Don't drift back to a uniform default — the policy is intentional. See v0.5.2-spec.md "Workspace Lookup Policy".
  • list --names emits directory basenames, not bare slugs. Bare slugs can collide (e.g., promptvolley archived in v1, active in v2). Basenames are unique under the resolver's exact-match step. TestListNamesCandidatesResolveUniquely enforces the invariant.
  • ValidArgsFunction hooks call workspace.ListWorkspaces directly. No subprocess shell-out to ctask list --names. Don't add one — direct calls are faster and don't depend on PATH state.
  • Cobra adds the completion subcommand lazily on first Execute(). A test that calls rootCmd.Find("completion") before any Execute() returns "unknown command". For unit tests, prefer the rootCmd.GenXxxCompletion(...) generators directly. For end-to-end, one SetArgs(...) + Execute() per test — running multiple Execute() calls in succession with different shell args has state issues.
  • notes uses SilenceErrors: true so the [ctask] no notes.md found in workspace "X" stderr line is the only diagnostic the user/agent sees. Don't set SilenceErrors: false and add a [ctask] prefix to the returned error message — Cobra would then print both, doubling the message.

From v0.5.3 (new — don't unlearn) [pending merge to main]

  • CTASK_SESSION_MODE is the only persistent-mode trigger. No flag promotes a single command to persistent. ctask attach is the inverse — it always uses tmux regardless of env. Don't add a --persistent flag; the existing directpersistentattach triangle covers every use case.
  • tmux command construction lives in exactly one place per operationinternal/shell/tmux.go. AttachExisting and AdoptExistingPersistentSession use shell primitives via test seams (adoptAttacher, adoptPoll, attacher); they do NOT hand-roll their own exec.Command("tmux", ...) calls. If you find a fresh exec.Command("tmux", ...) outside internal/shell/tmux.go, that's drift — fix it.
  • session.Run never calls exec.LookPath("tmux"). The cmd-layer preflight (cmd/persistent.go::preflightPersistentEntry) is the single source of truth for tmux discovery; the validated path flows through LaunchOpts.TmuxPath. Run errors if TmuxPath == "" in persistent mode.
  • The persistent-mode dispatcher is cmd/entry.go::dispatchPersistent(hasTmuxSession, leaseState) — a pure function. Three outcomes: dispatchOwnerCreate, dispatchPassive, dispatchAdopted. The cmd-layer runWorkspaceEntry is a package-level variable (test seam); per-command tests stub it to assert each entry command produces the right WorkspaceEntryOptions. Don't move the decision into the session package — it depends on cmd-layer prompts (fresh_remote confirmation, --direct bypass).
  • Session names are deterministic via session.SessionName(category, slug, absWsPath). Format: ctask-<sanitized-category>-<sanitized-slug-truncated-30>-<sha256_6>. On Windows the path is lowercased before hashing to match searchRootKey. Don't change the algorithm — name stability across runs is what makes passive reattach work without state. tmux's status bar truncates the name aggressively (e.g., [ctask-pro0:bash*]) — that's a tmux display thing, not a ctask bug.
  • AdoptExistingPersistentSession bumps task.yaml.UpdatedAt ONLY on successful adoption, not on the race-guard fall-through. The TestAdoptionBumpsUpdatedAtOnSuccess and TestAdoptionRaceGuardFallsThroughAndDoesNotBumpUpdatedAt tests enforce both branches.
  • A fresh remote lease (LeaseStateFreshRemote) is NEVER silently overwritten. cmd/persistent.go::confirmFreshRemoteAdoption prompts on TTY, refuses on non-TTY (with the remote hostname in the error). Don't drop the prompt or relax the non-TTY refusal.
  • shouldRunProvisional(opts) gates handleProvisional and is false in persistent mode. The "user hit Esc → empty diff → reclaim workspace" UX assumption does not translate to tmux, where the polling loop typically reports clean exit even on abrupt session kills. The four-row table test TestShouldRunProvisional enforces this.
  • finalize stamps EndReason / DetectedVia / SessionOwnership based on opts.SessionMode — direct: child_exited / child_exit; persistent owner-create: tmux_session_ended / polling / created; adopted: same plus adopted and AdoptedFromOrphanAt. These fields are omitempty so pre-v0.5.3 summaries continue to round-trip.
  • The tmux polling cadence is 3s (shell.PollInterval). Below the 30s heartbeat interval so finalize lag is bounded; above 1s so exec overhead is negligible. Don't lower below 1s without measuring impact.
  • Native Windows refuses persistent mode with a WSL recommendation. The check is runtime.GOOS == "windows" && os.Getenv("WSL_DISTRO_NAME") == "". WSL sets WSL_DISTRO_NAME automatically, so WSL paths pass. Don't replace this with a runtime.GOOS == "linux" allowlist — that breaks macOS.
  • ctask new runs the persistent preflight BEFORE workspace.Create. A missing tmux must not leave a half-initialized workspace on disk. The cmd/new.go ordering is load-bearing — don't move the preflight after creation.
  • The [ctask] adopting orphaned persistent session... line is the discriminator between passive reattach and adoption in user-visible output. Don't suppress it; the manual smoke test relies on it.

Open follow-ups (NOT in v0.4/v0.4.1/v0.5, deferred)

Potentially worth doing

  • Pre-v0.5 project workspaces have no launch_dir. They launch from the workspace root, not from any project subdir. No migration path today. Acceptable — users can manually add a launch_dir to their task.yaml if they want the new behavior. Revisit only if someone complains.
  • Drop unused slug / category / workspacePath parameters from seed.ClaudeMD (v0.3 follow-up, still open).
  • Status-line: color the |project / |project:<dir> marker if Claude Code's statusLine ever supports ANSI.
  • Coexisting-session detection (more than one live lease per workspace).
  • Cross-machine PID liveness check.
  • --force symmetry on other commands — no use case yet.
  • A ctask sessions subcommand to inspect lease/summary.

Repo hygiene

  • Several untracked working docs (v0.4-spec.md, v0.4.1-patch-spec.md, v0.5-spec.md, bugfix-provisional-workspace.md, the plan files) could be committed alongside v0.2-spec.md/v0.3-spec.md for durability. Currently session-local.
  • .claude/settings.local.json has uncommitted changes of unknown scope. Leave alone unless asked.

Resolved (don't re-add to this list)

  • CTASK_PROJECT_ROOT env-var scoping UX footgun → resolved in v0.5 via the default $CTASK_ROOT/projects/ fallback in SearchRoots().
  • UTC date in directory names / info display → resolved in v0.5.1.

Files to read first when resuming

For the v0.5 surface:

  1. v0.5-spec.md — the spec v0.5 followed
  2. docs/superpowers/plans/2026-04-22-v0.5-implementation.md — the executed plan (includes scanner-safety regression tests, dedupe tests, amendment history)
  3. internal/workspace/launchdir.goResolveLaunch with the three-outcome contract
  4. internal/workspace/create.go — subdir scaffold + local-time date + launch_dir default
  5. internal/session/run.goLaunchOpts.LaunchDir, ResolveLaunch call site, banner extension
  6. cmd/info.go — local-time display + launch fields
  7. cmd/doctor.gocheckProjectRoot helper
  8. scripts/ctask-statusline.{sh,ps1} — effective-path display logic
  9. internal/seed/templates.goClaudeMDProject with Workspace Structure section

For the v0.4.1 surface (still load-bearing):

  1. internal/config/config.goSearchRoots(), samePath, searchRootKey dedup; default $CTASK_ROOT/projects/ fallback
  2. internal/workspace/query.go — two-depth scanWorkspaces, scanAllRoots, QueryResult.Root
  3. cmd/archive.go — active-session check + non-TTY refusal + isStdinTerminal

For the v0.4 surface:

  1. internal/session/run_preflight.go — Layer 3 + Layer 1 preflight
  2. internal/session/lease.go — Lease + freshness + cleanup
  3. internal/session/summary.go — SessionSummary + launch-context banner
  4. internal/lockfile/writelock.go — write-lock primitive
  5. internal/session/run_provisional.gohandleProvisional with childExitCode gate
  6. docs/commands.md — user-facing surface

Don't re-do

  • Do not invent remote install / go install commands (per CLAUDE.md, this project is local-only)
  • Do not touch cmd/delete.go's two-check protection
  • Do not weaken the v0.3 .gitignore seed-wins rule
  • Do not redesign the lease model around multiple concurrent leases without a real user-reported problem
  • Do not re-run the v0.4 smoke tests in a fresh session unless source has changed
  • Do not expand the provisional-cleanup gate beyond "strictly empty manifest diff AND non-zero child exit". No size thresholds, no heuristics, no exit-code-specific mappings.
  • Do not extend provisional cleanup to resume / open / last. NewlyCreated is set only by cmd/new.go.
  • Do not drop QueryResult.Root or collapse SearchRoots() back to a single string.
  • Do not change archive's non-TTY behavior back to "proceed silently".
  • Do not add a workspace registry file. The default $CTASK_ROOT/projects/ fallback solved the discovery footgun.
  • v0.5: Do not change launch_dir storage to absolute paths. Workspace-relative is load-bearing.
  • v0.5: Do not mask non-os.IsNotExist stat errors in ResolveLaunch as warning fallbacks. The asymmetry between soft-fallback (missing / not-a-dir) and hard-error (permission / ENOTDIR / invalid name) is deliberate.
  • v0.5: Do not seed anything inside the project subdirectory. Seeds target the workspace root only.
  • v0.5: Do not let the session launch change scope (lease, manifest, summary, write-lock) away from the workspace root. Only the child's cwd is the launch dir.
  • v0.5: Do not remove the default $CTASK_ROOT/projects/ fallback in SearchRoots() — it resolves the v0.4.1 env-var scoping footgun.
  • v0.5.1: Do not switch the directory prefix / ID back to UTC. The TestCreateDirectoryPrefixUsesLocalDate test enforces local time.
  • v0.5.1: Do not remove .Local() from the ctask info Created/Updated/Archived formatting. TestInfoFormatsTimestampsInLocalZone enforces local display.
  • v0.5.1: Do not change stored timestamps (task.yaml, session logs, lease, manifest, summary) to local time. UTC storage is deliberate — only display converts.
  • v0.5.3: Do not call exec.Command("tmux", ...) outside internal/shell/tmux.go. The single-construction-site rule is what makes passive reattach and adoption stay in sync. Test seams (adoptAttacher, adoptPoll, attacher) wrap the primitives — they don't replace them.
  • v0.5.3: Do not move tmux discovery into session.Run. cmd/persistent.go::preflightPersistentEntry is the single source of truth; the validated path flows through LaunchOpts.TmuxPath.
  • v0.5.3: Do not enable provisional cleanup in persistent mode. shouldRunProvisional returns false on SessionMode == "persistent" — the gate's UX assumption doesn't translate to tmux.
  • v0.5.3: Do not silently overwrite a fresh remote lease. confirmFreshRemoteAdoption prompts on TTY and refuses on non-TTY. The non-TTY refusal carries the remote hostname so the user can disambiguate.
  • v0.5.3: Do not run the persistent preflight after workspace.Create in cmd/new.go. Pre-create ordering prevents half-initialized workspaces when tmux is missing.
  • v0.5.3: Do not change the SessionName algorithm. Name stability across processes is what makes passive reattach work without state. Windows path lowercasing matches searchRootKey.

What v0.5.3 delivered

Persistent session mode is in. Key user-facing surfaces:

  • New env var CTASK_SESSION_MODE (direct | persistent); direct is the default and requires no setup.
  • ctask attach <workspace> — always-tmux entry command. Defaults to launching the agent.
  • --direct flag on new / resume / last / open to bypass persistent mode for one invocation, with confirmation when a tmux session already exists.
  • ctask doctor now reports tmux presence and version when persistent mode is configured.

Architecture notes:

  • tmux is invoked via a three-call pattern (has-session, new-session -d, attach-session) with a 3-second polling loop to detect session end. The polling cadence is below the 30-second heartbeat interval, so finalize lag is bounded.
  • Session names are deterministic: ctask-<category>-<slug>-<hash6>, where the hash is the first 6 hex chars of sha256(canonical absolute workspace path). On Windows the path is lowercased before hashing.
  • Three entry paths (owner-create, passive reattach, adopted reattach) are picked based on tmux session existence and InspectLease four-state classification.
  • Adoption transfers ownership under the metadata write lock with a re-check race guard. The previous lease is replaced, task.yaml.UpdatedAt is bumped, a fresh start manifest is captured, and finalize stamps session_ownership: "adopted" plus adopted_from_orphan_at.
  • The v0.4 four-layer concurrency model is preserved verbatim. Layer 3 is selectively skipped on reattach paths because no reliable end_manifest baseline exists from a previous orphaned owner.
  • Provisional cleanup is bypassed in persistent mode — the gate's UX assumption ("Esc on prompt -> empty diff") does not translate to tmux.
  • last-session-summary.json gains four optional fields (end_reason, detected_via, session_ownership, adopted_from_orphan_at); pre-v0.5.3 summaries continue to load.

Out of scope (deferred to future releases):

  • Native Windows persistent mode (PSmux is a candidate; not committed).
  • Config file (~/.config/ctask/config.yaml) — env var remains the only config surface until v0.6.
  • switch-client for nested-tmux entry, tmux wait-for / set-hook-based detection, banner injection inside tmux, ctask sessions listing command.