# ctask — Session Handoff Notes Last touched: 2026-05-15. **v0.6 Phases 1–3 are all implemented and verified on branch `feat/v0.6-multi-agent-config` (18 commits ahead of `main`, NOT yet merged). Version is bumped to `0.6.0`. The branch is feature-complete for v0.6 and awaiting review before the merge into `main`. The installed binary at `%LOCALAPPDATA%\ctask\bin\ctask.exe` is still `v0.5.4` — the branch builds locally but has not been installed.** ## Where we are - **`main`:** v0.5.4 (session-visibility polish). Tip at merge commit `10b7d5a`; version-bump commit `7704cd9`. Installed binary at `%LOCALAPPDATA%\ctask\bin\ctask.exe` is `v0.5.4`. Not refreshed — the v0.6 branch builds locally but has not been installed. - **Active branches:** `feat/v0.6-multi-agent-config` — 18 commits ahead of `main`, all under the v0.6 theme (Phase 1 + Phase 2 + Phase 3). Not yet merged. - **Pending action:** review of v0.6 Phase 3, then merge `feat/v0.6-multi-agent-config` into `main`. Do not merge before explicit approval. - Remote: none (local-only, intentional — see `CLAUDE.md`). - `ctask doctor` reports 5 pass/fail + 2 seed-directory + 1 `CTASK_PROJECT_ROOT` check + 1 `Session mode` INFO line + 1 tmux INFO/FAIL line (when persistent mode is configured) + the new v0.6 `── Settings ──` block with per-key source attribution. ### 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: /` 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:` (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.1` → `0.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 ` — un-archive (metadata-only flip; mirrors archive's lease guard; refuses already-active workspaces) - `ctask notes ` — 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 ` — 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 ` 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 ` prints `Error: workspace archived` below the actionable hint.**~~ Resolved in v0.5.4 (`ae9bfaf`) via the `errArchivedWorkspace` sentinel + conditional `SilenceErrors`. Other resume errors still flow through Cobra's default rendering. ### What v0.5.3 delivered Theme: **persistent session mode via tmux** — workspaces can survive terminal disconnect, get reattached from a different terminal, and recover orphaned ownership when a previous foreground ctask died. Merge commit `1e93332`; final polish commit `c204d87`. User-facing surface: - New env var `CTASK_SESSION_MODE` (`direct` | `persistent`); `direct` is the default and requires no setup. Unknown values fall back to `direct` with a stderr warning. - `ctask attach ` — always-tmux entry command (ignores `CTASK_SESSION_MODE`). Defaults to launching the agent; the `--shell` flag swaps in an interactive shell. - `--direct` flag on `new` / `resume` / `last` / `open` to bypass persistent mode for one invocation. When a tmux session already exists for the workspace, the user gets a TTY confirmation prompt; non-TTY proceeds with a single stderr warning. - `ctask doctor` reports `Session mode: direct|persistent` plus an INFO/FAIL line for tmux presence and version when persistent. - v0.4 lease-prompt tightening (`c204d87`): when entering direct mode on a workspace that already has a live tmux session, the "Continue anyway?" prompt now suggests ` attach ` 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). - Invocation name in user-facing command suggestions (`c204d87`): the binary name printed in bypass / restore / "create one with" suggestions reflects `filepath.Base(os.Args[0])` instead of a hard-coded "ctask". Local-build PowerShell users running `.\ctask.exe` see `ctask.exe new --direct`; installed contexts continue to see `ctask`. Test seam (`cmd/invocation.go::invocationNameOverride`) pins the name to `"ctask"` in unit tests so substring assertions stay stable. Descriptive prose ("ctask persistent mode requires...") and the ssh-remote hint (`ssh -t ctask `) intentionally keep the literal "ctask" — they refer to program identity / remote invocation, not the local command form. 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 (`shell.PollInterval`). Polling cadence is below the 30-second heartbeat interval, so finalize lag is bounded. - Session names are deterministic: `ctask---`, where the hash is the first 6 hex chars of `sha256(canonical absolute workspace path)`. On Windows the path is lowercased before hashing (matches `searchRootKey`). tmux's status bar truncates the name aggressively (e.g., `[ctask-pro0:bash*]`) — that's a tmux display thing, not a bug. - Three entry paths (owner-create, passive reattach, adopted reattach) picked by the pure function `dispatchPersistent(hasTmuxSession, leaseState)`. Passive reattach (`AttachExisting`) runs no Preflight / lease / manifest / heartbeat / banner / finalize — it's a viewer-only attach via `shell.AttachSession`. The owner handles finalize. - Adoption (`AdoptExistingPersistentSession`) transfers ownership under the metadata write lock with a re-check race guard. The previous lease is replaced, `task.yaml.UpdatedAt` is bumped only on successful adoption, 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 (`shouldRunProvisional` returns false on `SessionMode == "persistent"`) — the gate's UX assumption ("Esc on prompt → empty diff → reclaim") does not translate to tmux. - `last-session-summary.json` gains four optional fields (`end_reason`, `detected_via`, `session_ownership`, `adopted_from_orphan_at`); all `omitempty` so pre-v0.5.3 summaries continue to round-trip. - 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. macOS / Linux pass naturally. - `ctask new` runs the persistent preflight BEFORE `workspace.Create` — a missing tmux must not leave a half-initialized workspace on disk. Validation: - Windows host: `go test ./... -count=1` green across all 7 packages; `go vet` clean; `go build` clean; `just build-linux` produces a statically linked ELF. - Manual WSL smoke test (`docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md`): completed end-to-end on 2026-05-14 across three terminals (WSL-A, WSL-B, PS-C). All sections PASS: owner-create (O1-O3), passive reattach (P1-P2), adopted reattach (A1-A6), non-TTY refusal (T1), nested-tmux refusal, --direct confirmation (D1-D4), tmux-missing workspace-not-created refusal (M1-M4), native Windows refusal (10a-10f), doctor output (11), cleanup (12). T2 (ssh-without-t) skipped — sshd not running on the WSL distro (explicitly allowed by checklist). - The checklist itself was hardened during the smoke run (10 corrections); see commit `c204d87` for the diff. Out of scope (deferred): - 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. ### Known limitation (v0.5.3) - **Refusal/bypass hints reflect `basename(os.Args[0])` for the command-form line, but descriptive prose ("ctask persistent mode requires...") and the ssh-remote hint stay hardcoded as "ctask".** Intentional — descriptive prose refers to program identity, and the ssh-remote `ctask` runs on the remote, not the local binary. **v0.5.4 audit confirmed this split is the right line** (spec §2 codified it: command-form via `invocationName()`, product-identity literal). Closed. - ~~**Adoption requires waiting 60s for the previous owner's lease to go stale (`StaleLeaseAfter`).**~~ **RESOLVED in v0.6 Phase 3.** A lease whose owner PID is confirmed dead on the local host is now treated as stale immediately via the PID-aware `IsStale` predicate — the 60s wall-clock wait no longer applies to a Ctrl-C'd / terminal-closed local session. See "What v0.6 Phase 3 delivered". ### What v0.5.4 delivered Theme: **session visibility + polish**. Pure polish — no new subsystems, no new metadata fields, no behavioral changes to session lifecycle. Merge commit `10b7d5a`. Commit list: - `7f2c43d` `feat(v0.5.4): SessionStatus display-only helper` — new `internal/session.SessionStatus(wsDir)` returning `{State, Mode, PID, Hostname, Diagnostic}`. Pure read of `.ctask/session.json` with no tmux invocation, no PID liveness, no lock acquisition, no mutation. States: `none` | `active` | `stale`. Pre-v0.5.3 leases without a `mode` field default to `direct` for display only. Display-only contract — lifecycle code keeps using `ReadLease`/`IsFresh`/`InspectLease`. - `e0e9cd7` `feat(v0.5.4): info Session block` — `ctask info` adds a Session block between `Path:` and the launch-dir fields. Mode/Owner/Attach/Note rows aligned at column 14. Owner line omits hostname when it matches the local machine. Attach hint surfaces only for active+persistent and uses `invocationName()`. Malformed leases render as stale with a diagnostic and no Mode/Owner/Attach rows. Exposes `session.CurrentHostname()` so the cmd layer has a single source of truth for the local-vs-remote check. - `0c8076a` `feat(v0.5.4): list SESSION column` — `ctask list` gets a SESSION column inserted right of STATUS. Values: `direct`, `persistent`, `stale`, or em dash. Archived workspaces always show em dash (display simplification — `info` still surfaces the raw lease state for diagnostic purposes). One short lease-file read per workspace; `ctask list --names` is **unchanged** (basename-only, no header, no session column — protected by `TestListNamesUnchangedHasNoSessionColumn`). - `0fb8de6` `polish(v0.5.4): invocation-name audit + regression tests` — audit walked every `cmd/` and `internal/` file producing user-facing text. **No production code changes were needed** — v0.5.3's `c204d87` had already routed every command-form hint through `invocationName()`. Two new regression tests pin a non-canonical name (`my-bin`) for the resume restore hint and the Layer-1 active-session attach hint, so a future change that hard-codes "ctask" in the format string fails loudly. Extracted `formatResumeRestoreHint` and `formatDirectModeTmuxHint` as testable string-only helpers; production paths are behaviorally identical. - `4fd0bef` `docs(v0.5.4): rewrite commands.md as a structured reference` — `docs/commands.md` was last substantively updated in v0.5; v0.5.2 (restore/notes/path/list --names/completion + archive-inclusive lookup), v0.5.3 (attach + --direct + persistent session mode), and v0.5.4 (info Session block + list SESSION column) were all undocumented. Rewrite follows spec §3 (Purpose / Usage / Scenarios / Examples / Flags / Notes / Related per command) with new sections for workspace layout, env-vars table, query resolution, session modes, and shell completion. Examples use the canonical `ctask` name per spec — docs describe the product, not the user's local binary path. - `ae9bfaf` `polish(v0.5.4): suppress Cobra duplicate Error on archived resume` — closes the v0.5.2 cosmetic-only known limitation. New `errArchivedWorkspace` sentinel; `runResume` flips `SilenceErrors=true` only when the inner error is that sentinel. All other resume errors (lookup failure, metadata write failure, etc.) flow through Cobra's default rendering unchanged. Verified end-to-end through Cobra `Execute()` (not just `runResume` direct invocation) so the SilenceErrors-flip-from-RunE timing is exercised by `TestResumeArchivedHintNoDuplicateError`. - `7704cd9` `release(v0.5.4): bump version to 0.5.4`. - `10b7d5a` `Merge branch 'feat/v0.5.4-session-visibility-polish' into main`. Validation: - Windows host: `go test ./... -count=1` green across all 7 packages; `go vet ./...` clean; `just build` produces `ctask.exe`; `just build-linux` produces a statically linked ELF (`file` reports `statically linked, not stripped`); `just install` succeeded; installed `ctask --version` reports `0.5.4`; `ctask doctor` 5/5 pass. - Smoke (synthetic fixtures): `ctask list` shows SESSION column with `persistent` / `direct` / `stale` / em dash; `ctask list --all` shows em dash for archived even with stranded lease; `ctask list --names` remains basename-only with no whitespace and no session tokens; `ctask info ` shows the Session block with hostname omitted (local) and Attach hint via `invocationName()`; `ctask resume ` prints only the `[ctask]` block + restore hint (no trailing Cobra `Error:` line). - No manual WSL smoke needed — v0.5.4 changes are display-only and exercised by unit tests against real lease fixture files. Spec deviations (intentional, not bugs): - **The v0.5.4 spec referred to `.ctask/lease.json` and a `session_mode` field; the actual existing implementation uses `.ctask/session.json` and `mode`.** The implementation correctly followed the existing metadata names per the spec's "no new metadata fields, no behavioral changes" constraint — renaming would have been a v0.5.3 schema change masquerading as polish. Future specs touching this surface should use `.ctask/session.json` and `mode` unless intentionally renaming the metadata. - **The invocation-name audit found no production hardcoded command-form hints to change.** v0.5.3's `c204d87` had already done the work. v0.5.4 added two regression tests (`TestInvocationNameInActiveSessionPrompt`, `TestInvocationNameInRestoreHintNonCanonical`) that pin a non-canonical invocation name to prevent future drift. The split between command-form (uses `invocationName()`) and product-identity (literal `"ctask"`) is the codified rule per spec §2. ### What v0.6 Phase 1 delivered (branch `feat/v0.6-multi-agent-config`, NOT merged) Theme: **infrastructure foundation for v0.6** — global config file, schema versioning in task.yaml, and source attribution in doctor/info. Five commits on the feature branch; no version bump (lands at end of Phase 3). All Phase 1 spec items (`v0.6-spec.md` sections 1–4) covered, plus a follow-up commit added on reviewer request for the native-Windows session_mode launch-time warning. #### Commit list (oldest → newest) - `6f80c8b` `feat(v0.6): config file parser + resolver + source attribution` - `internal/config/configfile.go` — strict-key YAML parser; unknown top-level keys invalidate the whole file with the offending key named; future schema versions rejected with an upgrade message. Platform-appropriate path (`$XDG_CONFIG_HOME/ctask/config.yaml` → `~/.config/...` fallback on Unix; `%APPDATA%\ctask\config.yaml` on native Windows). - `internal/config/resolver.go` — `Resolver` / `ResolvedSetting` / `SettingSource` with five values (`Builtin`, `ConfigFileSrc`, `EnvVar`, `CLIFlag` reserved for v0.6 Phase 2, `PlatformOverride`). Each setting carries an `Override` chain so doctor can render `env var (overrides config file: X)`. Per-setting methods: `CtaskRoot`, `ProjectRoot`, `SeedDir`, `DefaultAgent`, `DefaultCategory`, `Editor`, `SessionMode`. `SessionMode` applies the native-Windows platform override at the resolver layer with the configured value chained as `Override`. Exports `SetConfigPathForTest` and `SetIsNativeWindowsForTest` so cross-package tests can isolate. - `internal/config/config.go` — legacy `ResolveRoot` / `ResolveAgent` / `ResolveSeedDir` / `ResolveProjectRoot` / `ResolveSessionMode` wrappers migrated to `LoadResolver()` so config-file values take effect for entry commands. `ResolveProjectRoot` preserves the `""` sentinel for `Builtin` source so `SearchRoots` and doctor's `checkProjectRoot` keep their v0.5 fallback semantics. `ResolveSessionMode` keeps the unknown-value stderr warning, distinguishing env-var vs config-file sources in the message. - Tests: 6 LoadConfigFile + 3 ConfigFilePath + 11 resolver cases including layering, override chains, path expansion, platform override, and `SettingSource.String()` rendering. - `0b21b8d` `feat(v0.6): schema_version and workspace.mode in task.yaml` - `CurrentMetaSchemaVersion = 1` constant + `WorkspaceSection struct{ Mode string }` nested block. - `TaskMeta` gains `SchemaVersion int` and `Workspace WorkspaceSection` fields at the top of the struct. Both `omitempty` — this is what enforces the no-opportunistic-writes invariant: legacy task.yaml files (no `schema_version`, no `workspace:`) round-trip through `WriteMeta` / `WriteMetaLocked` without acquiring those keys. - `EffectiveSchemaVersion(meta)` returns 1 for stored-value-0 legacy workspaces; non-zero stored values pass through verbatim. - `ValidateSchemaVersion(slug, meta)` rejects values above `CurrentMetaSchemaVersion` with the spec-mandated upgrade message; `ValidateWorkspaceMode(slug, meta)` rejects values other than `""` and `"native"` (so a Phase 1 binary refuses an "adopted" mode set by hand or by a future v0.7). - `ReadMeta` now runs both validators after YAML unmarshal. The error includes the workspace slug (derived from the file's `slug:` field or the directory basename when the file itself is corrupt). - `workspace.Create` stamps every new meta with `SchemaVersion: 1` and `Workspace.Mode: "native"`. This is the ONLY write site for these fields in v0.6. - Tests: 10 cases including the dedicated `TestLegacyTaskYamlNotBackfilledByWrite` and `TestLegacyTaskYamlNotBackfilledByLockedWrite` regressions that pin the no-opportunistic-writes invariant. - `c918e5c` `feat(v0.6): doctor Settings section with source attribution` - New `── Settings ──` block appended between the existing `checkTmux` line and the `N checks passed, M failed` summary. Loaded via `config.LoadResolver()` exactly once per `runDoctor` invocation and reused across every settings line (per user correction: "load the resolver once and reuse it"). - First emits the config-file lifecycle line: `not found` (INFO), `` (valid, INFO), or `` + `[FAIL] unknown key: "..."` + advisory (invalid, increments `failed`). - Then iterates `r.CtaskRoot() / ProjectRoot() / SeedDir() / DefaultAgent() / DefaultCategory() / SessionMode() / Editor()` and renders each via `printSettingLine` (key/value/source trio + chained-override info). `PlatformOverride` adds a `configured: ` row so doctor surfaces both the effective and the user-asked-for value. - Helper `formatSettingSource` centralises the `EnvVar → CTASK_X env var` / `PlatformOverride → ... (persistent mode requires tmux; not available on native Windows)` / override-chain wording. - Tests: 6 cases including the platform-override `configured: persistent` rendering. - `937a1c8` `feat(v0.6): info source attribution on Agent and Launch session mode` - `cmd/info.go::runInfo` calls `config.LoadResolver()` once and reuses it. - `Agent:` line gains `(workspace)` when `m.Agent` is non-empty (the common case), or `(default)` / `(default — )` when the legacy field is empty and the value falls through to the resolver. - New `Launch session mode:` row inserted directly between `Agent:` and `Created:`, **outside** the v0.5.4 Session block (per user decision: the Session block represents the current lease's recorded mode; the new line represents the configured launch default — two different things). - Helper `infoSourceLabel` is the info-side counterpart to `formatSettingSource`. No override-chain suffix in info (single-row layout has no room for the extra parenthetical). - Tests: 5 cases including a placement check that asserts `Agent < Launch session mode < Created` in the rendered output. - `6182d89` `feat(v0.6): platform-override stderr warning on launch paths` - Reviewer follow-up: the v0.6 spec section 1.8 also calls for a stderr warning at launch time when persistent mode is downgraded; commits 1–4 covered the doctor/info display path but the launch paths were still downgrading silently. This commit fixes that. - New helper `cmd/persistent.go::emitPlatformOverrideWarningIfNeeded(alwaysPersistent bool)` — loads the resolver, emits the warning on stderr when `SessionMode()` reports `PlatformOverride` source AND the caller is not `AlwaysPersistent`. - Single call site at the top of `cmd/entry.go::defaultRunWorkspaceEntry`, before any launch work. That function is the funnel for `new`, `resume`, `last`, `open` (which can downgrade) AND `attach` (which can't). attach sets `AlwaysPersistent: true`, so the helper short-circuits. - Warning text (exact): `[ctask] warning: persistent session mode is not supported on native Windows; using direct mode. Use WSL for persistent sessions.` - "Once per invocation" is provided implicitly by the call site running at most once per ctask command — no warn-once subsystem. - Tests: 5 cases including the AlwaysPersistent skip gate. attach's actual native-Windows refusal contract (via `preflightPersistentEntry`) continues to be enforced by the pre-existing `TestPreflightRefusesNativeWindows`. #### Verification (run on `feat/v0.6-multi-agent-config` tip `6182d89`) - `go test ./... -count=1` — all 7 packages `ok`, 0 failures. - `go vet ./...` — exit 0. - `go build -o ctask.exe .` — exit 0. - `just build-linux` — produces `dist/ctask-linux-amd64`, statically linked ELF (`file` reports `statically linked`). - Version remains `v0.5.4`; bump deferred to the end-of-Phase-3 commit per `v0.6-spec.md` "Commit ordering". #### Phase 1 constraints held - **No config auto-creation.** `LoadConfigFile` returns `(nil, nil)` on `IsNotExist`; nothing in the codebase writes a config file. Missing file renders as INFO in doctor, never FAIL. - **No opportunistic schema writes.** The two new task.yaml fields use YAML `omitempty`; legacy files round-trip through `WriteMetaLocked` (the path used by `resume` / `archive` / `restore`) without acquiring `schema_version` or `workspace:`. Pinned by `TestLegacyTaskYamlNotBackfilledByWrite` + `TestLegacyTaskYamlNotBackfilledByLockedWrite`. - **Env vars preserved as overrides.** The resolver layers them above config; no deprecation. Override chain captured in `ResolvedSetting.Override` so doctor renders `CTASK_X env var (overrides config file: Y)`. - **task.yaml remains workspace state.** Phase 1 does NOT introduce per-workspace fields like `agent.type` or fold task.yaml into the resolver chain. The Agent and Launch session mode lines in info correctly distinguish `(workspace)` from user-level-default sources. - **No Phase 2 work started.** Verified by diff: no agent-profile fields in `TaskMeta`, no `--agent` flag on `ctask new`, no AGENTS.md / CLAUDE.md shim generation, no `ctask agents check`, no `context/notes-archive/` scaffolding. `internal/seed/templates.go` unchanged. - **No Phase 3 work started.** Verified by diff: no PID-liveness logic, no changes to `internal/session/lease.go` or `internal/session/adopt.go`, no change to the 60s `StaleLeaseAfter` threshold or its callers. #### Native-Windows platform-override behavior (codified in Phase 1) - **Doctor + info show source attribution.** Doctor's Settings block renders `session_mode: direct` + `source: platform override (...)` + `configured: persistent` when config says persistent on native Windows. Info's `Launch session mode:` line surfaces the same effective value with its source label. - **Launch paths warn once per invocation and downgrade to direct.** `new`, `resume`, `last`, `open` all funnel through `defaultRunWorkspaceEntry`, which calls `emitPlatformOverrideWarningIfNeeded(false)` at the top. Exactly one stderr line per process invocation. No warn-once subsystem — the call site frequency is what enforces "once". - **`ctask attach` does NOT downgrade.** attach sets `AlwaysPersistent: true`, which (a) bypasses the warning helper and (b) routes through `preflightPersistentEntry` which refuses on native Windows with the v0.5.3 "ctask persistent mode requires tmux ... Recommended: Run ctask from WSL" message. attach has no direct-mode equivalent — refusal is the right contract. #### Architecture notes (worth preserving) - **One resolver per command.** `config.LoadResolver()` is cheap (one config file read + env snapshot) but doctor/info call it exactly once per invocation and reuse the result across many `Resolver.X()` accessors. Legacy `Resolve*` wrappers (`ResolveRoot` etc.) each construct a fresh resolver — acceptable for entry commands that call them once or twice; new code in `cmd/` should follow the doctor/info pattern. - **Test seams are exported.** `config.SetConfigPathForTest(t, path)` and `config.SetIsNativeWindowsForTest(t, f)` are exported helpers so `cmd/`-package tests can isolate from developer-host config files and simulate platform-override scenarios without `runtime.GOOS` skips. - **Validation lives in `ReadMeta`, not in callers.** The schema-version and workspace-mode checks happen at read time — so any path that gets a `*TaskMeta` has already passed validation. Callers do not need to re-check. - **`omitempty` is the load-bearing primitive for "no opportunistic writes."** If a future change needs to track another optional field, follow the same pattern: zero value must round-trip without serialization, and only `workspace.Create` (or an explicit migration) writes a non-zero value. ### What v0.6 Phase 2 delivered (branch `feat/v0.6-multi-agent-config`, NOT merged) Theme: **the multi-agent layer** — ctask is now agent-agnostic. task.yaml carries an agent profile (`type` / `command` / `args` / `env`), `ctask new --agent` selects the agent type, the launch path carries a resolved command + args + env end-to-end, new workspaces get an `AGENTS.md` canonical instruction file (CLAUDE.md becomes a thin shim), and `ctask agents check` validates agent configuration. Six commits on the feature branch; no version bump (lands at end of Phase 3). All Phase 2 spec items (`v0.6-spec.md` §5–§7, plus the §8 seed scaffolding that rides along with the template change) covered. #### Commit list (oldest → newest) - `8120c39` `feat(v0.6): AgentSpec field on TaskMeta with backward-compat unmarshal` - `TaskMeta.Agent` changes from `string` to `workspace.AgentSpec{Type, Command, Args, Env}`. Custom `UnmarshalYAML` accepts both the v0.6 mapping form and the legacy scalar form. **Legacy scalar handling (corrected at plan review):** a scalar matching a built-in name (`claude`, `opencode`) maps to `AgentSpec{Type: }`; any other scalar (a path, `aider`, etc.) maps to `AgentSpec{Type: "custom", Command: }`; a missing `agent:` key leaves `Type` empty so the resolver fills in `default_agent`. The scalar value is NEVER dropped — a legacy workspace keeps launching the agent it was created with. - `IsBuiltinAgentType(name)` helper in `internal/workspace` (true for `claude`, `opencode`) — the built-in/custom discriminator used by the unmarshaler. `internal/agent.BuiltinProfiles` mirrors the same set. - `ValidateAgentSpec` (run inside `ReadMeta`): known type only; `type=custom` requires `command`; `command` rejects whitespace and shell metacharacters (`|&;<>()$\``) with a hint pointing at `args` — ctask does not shell-split. - `CreateOpts.Agent string` → `CreateOpts.AgentSpec workspace.AgentSpec`. cmd-layer call sites patched to the minimum needed to compile (full wiring lands in commit 3). - `24f2134` `feat(v0.6): internal/agent package — Resolve + BuiltinProfiles` - New `internal/agent` package: `Profile`, `BuiltinProfiles` (`claude`, `opencode`), `IsKnownType` (those two + `custom`), `Resolved{Type, Command, Args, Env}`, and `Resolve(spec, defaultAgent)`. Resolution: empty `Type` falls through to `defaultAgent`; `custom` requires `command`; otherwise command is `spec.Command` or the built-in default. **No I/O** — PATH lookup stays in `shell.ExecAgent` and `ctask agents check`, so `Resolve` is trivially testable. - `b75b82e` `feat(v0.6): launch path carries ResolvedAgent (command + args + env)` - `LaunchOpts.Agent string` and `WorkspaceEntryOptions.Agent string` → `*agent.Resolved`. All five entry commands (`new`, `resume`, `last`, `open`, `attach`) construct an `AgentSpec`, apply `--agent` as a one-shot `agent.command` override (Open Q 1 — resume/last/attach `--agent` stays a command override, NOT a type selector), call `agent.Resolve`, and pass the result through. `cmd/entry.go::resolveEntryAgent` centralises the resume/last/open/attach path. - `shell.ExecAgent` and `shell.ExecTmuxAgent` gain an `args []string` parameter. `agent.env` is merged into the child environment at the `session.Run` launch switch, AFTER ctask's own `CTASK_*` exports — `agent.env` wins on collision per spec §5. `session.mergeAgentEnv` is the centralised merge; `var execAgent = shell.ExecAgent` is a new test seam. - Lease, manifest, write lock, heartbeat, summary, and provisional cleanup are untouched. The `Agent string` fields on `Lease` / `SessionSummary` / `SessionInfo` still record the resolved command string for diagnostics. - `a61f900` `feat(v0.6): --agent flag on ctask new selects agent type` - `Resolver` gains `cliFlagAgent` + `SetCLIFlagAgent`; `DefaultAgent()` layers `CLIFlag` above `EnvVar` so doctor/info render the full precedence chain. `SettingSource.CLIFlag` (reserved in Phase 1) is now reachable. - `ctask new --agent ` writes `agent.type` into the new workspace's task.yaml. Resolution + validation run BEFORE `workspace.Create`, so `--agent custom` with no companion command refuses (`type "custom" requires command`) without leaving a half-created workspace on disk. The deferred Phase 1 test `TestCLIFlagOverridesEnvVar` landed here. - `0c6ed0c` `feat(v0.6): AGENTS.md seed + CLAUDE.md shim + handoff + context-archive scaffold` - New workspaces get `AGENTS.md` (canonical, always — handoff workflow, notes-archive convention with a ~300-500 line trigger, cross-workspace discovery, do-not-touch warnings; project variant adds workspace-structure + git-conventions sections), a `CLAUDE.md` shim (claude type only — opencode shim deferred), `handoff.md` (minimal current-state template), and `context/notes-archive/.gitkeep`. - `seed.ClaudeMD` and `seed.ClaudeMDProject` deleted — no callers remain. `seed.NotesMD` retained. Existing workspaces are NOT retroactively modified (pinned by `TestCreateDoesNotModifyExistingWorkspace`). - `0f96d20` `feat(v0.6): ctask agents check + doctor integration` - `ctask agents check [workspace]` — pure validation, no launch: agent type known, command resolvable on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present (WARN, claude only). `agent.env` keys displayed informationally; a WARN line names any key shadowing a `CTASK_*` export. Non-zero exit on any FAIL. - `ctask doctor` runs the same sweep against the most-recently-active workspace (`workspace.MostRecentActive`); skips with `Agent check: skipped (no workspace context)` when none exists. `runAgentsCheckOnWorkspace` is shared between the standalone command and doctor. - `TestCompletionSubcommandViaExecute` was made order-independent: cobra's default `completion` command captures the root output writer once, on the first `Execute()` in the process — the new agents-check tests run an `Execute()` earlier in the suite, so the test now drops any pre-created completion command before its own `Execute()`. #### Verification (run on `feat/v0.6-multi-agent-config` tip `0f96d20`) - `go test ./... -count=1` — all 8 packages `ok` (the new `internal/agent` package included), 0 failures. - `go vet ./...` — exit 0. - `just build` — `ctask.exe` builds locally. - `just build-linux` — produces `dist/ctask-linux-amd64`, statically linked ELF. - Version remains `v0.5.4`; bump deferred to the end-of-Phase-3 commit. #### Phase 2 constraints held - **No PID liveness / lazy-cleanup / adoption changes.** `internal/session/lease.go` untouched; `internal/session/adopt.go` changed only for the mechanical `ResolvedAgent` passthrough. The 60s `StaleLeaseAfter` threshold is unchanged. - **No per-workspace `session_mode`.** No new field on `TaskMeta` beyond the `AgentSpec`. - **No user-defined named agent profiles in config.** `internal/config/configfile.go` is unchanged in Phase 2 — agent profiles live in task.yaml, not config.yaml. - **No opencode-specific shim.** `writeBuiltinDefaults` writes `CLAUDE.md` only when `agent.Type == "claude"`. - **No auto-modification of existing workspaces.** All new seed files are written only by `workspace.Create`. - **No version bump, no merge.** `version.go` untouched; branch stays `feat/v0.6-multi-agent-config`. #### Architecture notes (worth preserving) - **`internal/agent.Resolve` is I/O-free.** PATH validation is deliberately NOT in `Resolve` — it lives in `shell.ExecAgent` (fail-fast at launch) and `ctask agents check`. This keeps resolution a pure function and lets `agents check` validate without launching. - **`agent.env` precedence is intentional.** It merges AFTER ctask's `CTASK_*` exports and wins on collision (spec §5). This is a feature, not a bug — `agents check` surfaces the shadowing with a WARN so the user is not surprised. - **Two parallel built-in-type lists, kept in sync deliberately.** `workspace.IsBuiltinAgentType` / `workspace.knownAgentTypes` and `internal/agent.BuiltinProfiles` both enumerate the built-ins. `internal/workspace` cannot import `internal/agent` (the dependency runs the other way), so the lists are mirrored, not shared. When a new built-in lands, update all three. - **AGENTS.md is canonical; CLAUDE.md is a shim.** The shim exists only to point Claude Code at AGENTS.md. The seed-overlay rule still applies — a user seed dir's `AGENTS.md` / `CLAUDE.md` overrides the built-in. - **cobra's default `completion` command captures the output writer once.** `InitDefaultCompletionCmd` snapshots `c.OutOrStdout()` into the `bash`/`zsh`/etc. subcommand closures on the first `Execute()` anywhere in the process. Tests that drive `rootCmd.Execute()` with a redirected output buffer and then assert on completion output must drop the pre-created completion command first. `cmd/agents_check_test.go::captureRootCmd` restores `rootCmd`'s out/err/args on cleanup to limit this class of cross-test contamination. ### What v0.6 Phase 3 delivered (branch `feat/v0.6-multi-agent-config`, NOT merged) Theme: **lazy-cleanup via PID liveness** — a lease whose owner process is confirmed dead on the local machine is treated as stale immediately, without the 60-second wall-clock wait. Closes the v0.5.3 known limitation (Ctrl-C / terminal-close then immediate `ctask resume`). The v0.6.0 version bump rides along. Four commits; v0.6-spec.md §8 (context-file scaffolding) was already delivered in Phase 2 commit `0c6ed0c`, so §9 (PID liveness) was the only remaining Phase 3 feature. #### Commit list (oldest → newest) - `9070c42` `feat(v0.6): tri-state PID liveness probe (ProcessAlive/Dead/Unknown)` - New `internal/session/pidcheck.go` (`ProcessState` tri-state + `checkProcess` test seam) and build-tagged platform files: `pidcheck_unix.go` (`syscall.Kill(pid, 0)` — `nil`/`EPERM`→Alive, `ESRCH`→Dead, else→Unknown) and `pidcheck_windows.go` (`syscall.OpenProcess` — opens→Alive, `ERROR_INVALID_PARAMETER`(87)→Dead, else→Unknown). Stdlib `syscall` only; no `golang.org/x/sys` dependency, `go.mod` unchanged. - `f379a6d` `feat(v0.6): IsStale supplements wall-clock freshness with PID liveness` - `IsStale(l, now, threshold)` in `lease.go`. Parameterized free function mirroring `IsFresh` (not the spec's zero-arg method form — deviation approved at plan review to preserve the package's injected-clock testability). PID liveness applies only to local leases (`l.Hostname == currentHostname()`) with `pid > 0`; remote leases, `pid <= 0`, and `ProcessUnknown` fall back to wall-clock. Wall-clock staleness is checked first and wins unconditionally — PID liveness only flips fresh → stale, never stale → fresh. - `d575ddd` `feat(v0.6): route lease-freshness callsites through IsStale` - Four freshness consumers now route through `IsStale`: `InspectLease`, `CleanupStaleLease`, `runActiveLeaseCheck`, and `statusAt`. `SessionStatus` / `ctask list` / `ctask info` reflect PID liveness automatically — only the one-line `statusAt` predicate swap was needed; the `Status` struct and all cmd-layer rendering are untouched. Also corrected three cmd-package session-display test fixtures (`list_session_test.go`, `info_session_test.go`) that built "active" leases with the local hostname but synthetic PIDs — now that freshness is PID-aware, an honest "active" fixture must use `os.Getpid()`. - `beb5174` `chore(v0.6): bump version to 0.6.0` - `cmd/root.go` `version` `0.5.4` → `0.6.0`. `ctask --version` reports `ctask v0.6.0`. #### Verification (run on tip `beb5174`) - `go test ./... -count=1` — all 8 packages `ok`, 0 failures. - `go vet ./...` — exit 0. - `just build` — `ctask.exe` (PE32+ x86-64). - `just build-linux` — `dist/ctask-linux-amd64`, statically linked ELF (the only check that compiles `pidcheck_unix.go`). - Manual interactive smoke (Ctrl-C a live session then immediate `ctask resume`; stale display in `info`/`list`) — **pending**: requires a real TTY + agent session and was deferred to hands-on review. The behavior is covered deterministically by `TestInspectLeaseDeadLocalPIDIsStale`, the `TestIsStale*` matrix, and the `TestCheckProcess*` real-syscall tests. #### Phase 3 constraints held - Four-layer concurrency model unchanged — PID liveness only makes Layer 1's "is this lease stale?" question smarter. - `StaleLeaseAfter` (60s) unchanged; PID liveness supplements it and remains the fallback for remote leases and inconclusive checks. - Lease creation, heartbeat, write lock, manifest, and summary shapes unchanged. `adopt.go` untouched. - Remote leases remain wall-clock-only (PID checks skipped when the lease hostname differs from the current host). - `IsFresh` retained as the pure wall-clock primitive `IsStale` builds on. - No new agent/profile/config/template work. #### Architecture notes (worth preserving) - **`IsStale` is the single freshness predicate** for stale-detection decisions. All four callsites route through it; `IsFresh` is now an internal building block (still exported, still directly tested). - **PID liveness is conservative by construction.** Only a definitive `ProcessDead` on a local lease shortcuts the wait. `ProcessUnknown` (permission errors, unexpected OS errors) and remote leases preserve the pre-v0.6 wall-clock behavior exactly. - **No `golang.org/x/sys` dependency.** Windows process probing uses stdlib `syscall.OpenProcess`; `PROCESS_QUERY_LIMITED_INFORMATION` (`0x1000`) and `ERROR_INVALID_PARAMETER` (`87`) are local constants. - **Known conservative edge cases** (acceptable — they never falsely declare a live owner dead): OS PID reuse reads the recycled PID as alive → wall-clock fallback; a Windows zombie handle reads as alive. - **Plan:** `docs/superpowers/plans/2026-05-15-v0.6-phase3-implementation.md`. ### Historical: original Phase 1 plan (now shipped — kept for traceability) **Phase 1 scope (only thing to start next):** - **Config file parsing + resolver + tests.** Read `~/.config/ctask/config.yaml` (XDG) and Windows equivalent. Layered resolution: env var > config file > built-in default. Surface every existing `CTASK_*` setting through the new resolver without changing default behavior. Tests cover layering, malformed YAML, missing file, env-var override. - **`schema_version` field in `task.yaml`.** Bump to `1`. Old workspaces (no `schema_version`) parse silently and are treated as version 0 (current behavior). New `ctask new` writes version 1. - **`workspace.mode` field in `task.yaml`.** Replaces the implicit "always `local`" assumption. New workspaces default to `local`; reading is forward-compatible. No behavioral consequence yet — purely metadata. - **Doctor source attribution.** When reporting a configuration value, doctor says where it came from: `[INFO] CTASK_ROOT: /home/warren/ai-workspaces (env)` vs `(config: ~/.config/ctask/config.yaml)` vs `(default)`. Three-state visibility for every resolved setting. - **Info source attribution.** `ctask info ` includes the source for relevant resolved values (e.g., agent override path). **Phase 1 do-not-touch (deferred to Phase 2):** - Agent profiles (`profiles:` block in config). - AGENTS.md / handoff.md / context-file templates / `context/notes-archive/` scaffolding (the design recorded in `v0.5.4-spec.md` § "v0.6 Context-File Architecture" is for Phase 2 — read it then, not now). - PID liveness check on leases. v0.5.4 spec §1 explicitly defers this so it's only built once with behavioral consequences. - Lazy-cleanup-friendly adoption (the 60s `StaleLeaseAfter` revisit). - `orphaned` session state detection in display. - Flag-aware completion for `open --all` / `delete --all`. The scope split exists because Phase 1 is foundation (resolver, schema versioning, source visibility) that Phase 2 features will build on. Implementing them together is what makes v0.6 stall. ## 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` tip is unchanged: `10b7d5a Merge branch 'feat/v0.5.4-session-visibility-polish' into main` (v0.5.4 shipped). - `feat/v0.6-multi-agent-config` is the active branch, 18 commits ahead of `main`. Tip `beb5174` (pre-notes-closeout). NOT merged — feature-complete for v0.6, awaiting review then merge. - No tag pushed for v0.5.4 (no remote — the project is intentionally local-only per `CLAUDE.md`). v0.5.3 had `git tag v0.5.3` locally; v0.5.4 has none. No v0.6 tag yet — a post-merge task. - Installed `ctask.exe` at `%LOCALAPPDATA%\ctask\bin\ctask.exe` is still **v0.5.4** — the v0.6 branch has NOT been installed. Local `ctask.exe` in the repo root is a `beb5174` build reporting `v0.6.0`. `dist/ctask-linux-amd64` is the Phase-3 Linux cross-build (statically linked ELF). - Memory follow-ups (see `memory/MEMORY.md`): - `feedback_design_for_lazy_cleanup` — the 60s-freshness-wait concern it raised is **addressed by v0.6 Phase 3** (PID-aware `IsStale`). The underlying principle (lifecycle UX must recover from Ctrl-C / terminal close) remains a live design value. - `feedback_invocation_name_in_hints` — partially closed by the v0.5.4 audit (split between command-form and product-identity is now codified). Memory entry retained for the descriptive-prose question. - Untracked files (do NOT touch without asking — pre-existing session-local working docs, unchanged from this session): - `.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) - `docs/superpowers/plans/2026-05-08-v0.5.3-implementation.md` (v0.5.3 plan — executed; was claimed-committed in earlier notes but actually stayed untracked) - `v0.4-spec.md`, `v0.4.1-patch-spec.md`, `v0.5-spec.md`, `v0.5.3-spec.md`, `v0.5.4-spec.md` (specs the implementations followed; `v0.5.4-spec.md` is the spec the just-shipped work was driven from) - Files committed on the v0.5.4 branch that are now on `main`: - `internal/session/status.go` + `status_test.go` (SessionStatus helper) - `cmd/info_session_test.go`, `cmd/list_session_test.go` (session-block / session-column behavioral tests) - `cmd/invocation_audit_test.go` (regression tests pinning a non-canonical invocation name) - `cmd/resume_archived_polish_test.go` (Cobra-end-to-end test that the duplicate `Error:` line is suppressed) - `docs/commands.md` (rewrite — see commit `4fd0bef`) - Files committed on `feat/v0.6-multi-agent-config` (NOT yet merged): - `internal/config/configfile.go` + `configfile_test.go` (strict-key YAML parser + ConfigFilePath) - `internal/config/resolver.go` + `resolver_test.go` (Resolver + ResolvedSetting + SettingSource + 2 exported test seams) - `internal/config/config.go` modified (5 legacy wrappers migrated to LoadResolver) - `internal/config/config_test.go` + `config_roots_test.go` modified (config-path isolation in tests that assert Builtin defaults) - `internal/workspace/metadata.go` modified (CurrentMetaSchemaVersion, WorkspaceSection, SchemaVersion + Workspace fields on TaskMeta, EffectiveSchemaVersion + ValidateSchemaVersion + ValidateWorkspaceMode helpers, ReadMeta validation) - `internal/workspace/create.go` modified (writes schema_version: 1 + workspace.mode: native) - `internal/workspace/schema_test.go` (10 tests including the two no-opportunistic-writes regressions) - `cmd/doctor.go` modified (── Settings ── block + checkSettings + printSettingLine + formatSettingSource) - `cmd/doctor_test.go` modified (config import + 2 tests gated by SetIsNativeWindowsForTest) - `cmd/doctor_settings_test.go` (6 tests covering the new Settings block) - `cmd/info.go` modified (Agent source label + new Launch session mode line + agentLineWithSource + infoSourceLabel) - `cmd/info_attribution_test.go` (5 tests including placement assertion) - `cmd/new_persistent_test.go` modified (config import + 1 test gated by SetIsNativeWindowsForTest) - `cmd/entry.go` modified (emitPlatformOverrideWarningIfNeeded call at top of defaultRunWorkspaceEntry) - `cmd/persistent.go` modified (config import + platformOverrideWarning const + emitPlatformOverrideWarningIfNeeded helper) - `cmd/platform_warning_test.go` (5 tests covering the warning + AlwaysPersistent skip) ## How to resume v0.6 Phase 1 is shipped on `feat/v0.6-multi-agent-config` and reviewed. **The branch is NOT merged into `main` yet** — Phase 2 (and Phase 3) will continue on the same branch per the v0.6 spec's "one branch, three sequential milestones" policy. The eventual merge happens at the end of Phase 3 alongside the version bump. **Do not begin Phase 2 work** (agent profiles, `--agent` flag, AGENTS.md + CLAUDE.md shim, `ctask agents check`) until the Phase 2 plan is written and reviewed. Phase 3 (PID liveness, lazy-cleanup adoption, context-file scaffolding) does not start until Phase 2 is implemented, tested, and reviewed. ### General resume (on `main` after a ship) ```powershell 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.4 is live: ```powershell ctask --version # expect: ctask v0.5.4 ctask doctor # expect: 5 pass/fail + 2 seed-dir INFO + 1 CTASK_PROJECT_ROOT INFO + 1 Session mode INFO # v0.5.4 surfaces: ctask list # SESSION column inserted right of STATUS ctask list --names # bare basenames, no header, no session column ctask info # Session block between Path: and (if any) launch fields # Direct mode (the default — no env var needed): ctask new --project "tz-check" --no-launch # Expected output: [ctask] created projects/YYYY-MM-DD_tz-check ctask info tz-check # info should show Session: none for the just-created ws ctask delete --force tz-check # Persistent mode sanity (v0.5.3, requires tmux — WSL only): # (WSL terminal) export CTASK_SESSION_MODE=persistent ctask doctor 2>&1 | grep -E "Session mode|tmux" # Expect: # [INFO] Session mode: persistent # [INFO] tmux found: tmux (/path/to/tmux) unset CTASK_SESSION_MODE ``` ### Starting v0.6 Phase 2 Branch is already `feat/v0.6-multi-agent-config`; check it out and continue. **Read the Phase 1 ship report above first** — Phase 2 builds on the resolver and the task.yaml schema fields it landed. Suggested opening session prompt (paste into a fresh ctask agent session): > Begin v0.6 Phase 2 implementation planning only. Phase 1 is complete on `feat/v0.6-multi-agent-config` (see `notes.md`). Phase 2 scope per `v0.6-spec.md` sections 5–7: agent profile system in task.yaml (`agent.type` / `agent.command` / `agent.args` / `agent.env`; built-in types `claude` and `opencode`, escape hatch `custom`), `--agent` flag on `ctask new` with the deferred `TestCLIFlagOverridesEnvVar`, AGENTS.md canonical file + CLAUDE.md shim (claude type only — opencode shim deferred until conventions are verified), and `ctask agents check` validation command. Per spec implementation notes, the AGENTS.md template + `handoff.md` + `context/notes-archive/` scaffold land together in one seed-template change (commit 7 in the spec's ordering) — Phase 3's context-file scaffolding rides along with Phase 2's seed work to avoid rewriting templates twice. Stop after Phase 2 planning; do not start Phase 3 (PID liveness). Resume sanity checks: ```powershell cd C:\Users\Warren\claude_tasks\ctask git checkout feat/v0.6-multi-agent-config git log --oneline main..HEAD # expect 5 commits, tip 6182d89 just test # all 7 packages green just build # ctask.exe builds locally just build-linux # dist/ctask-linux-amd64 statically linked ``` ## 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) - **`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 `direct` ↔ `persistent` ↔ `attach` triangle covers every use case. - **tmux command construction lives in exactly one place per operation** — `internal/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---`. 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. ### From v0.5.4 (new — don't unlearn) - **`internal/session.SessionStatus(wsDir)` is display-only.** Pure read of `.ctask/session.json`. No tmux invocation, no PID liveness, no lock acquisition, no mutation of lease state. It must NOT be called from lifecycle or adoption code: the "missing `mode` field defaults to `direct`" rule and the malformed-lease-as-stale classification are display choices, not behavioral truths. Lifecycle code keeps using `ReadLease` / `IsFresh` / `InspectLease`. The status.go file calls this out at the function declaration — keep that comment. - **Lease file is `.ctask/session.json` and the mode field is `mode`.** The v0.5.4 spec called these `lease.json` and `session_mode` respectively; the implementation correctly preserved the existing v0.4/v0.5.3 names per the spec's "no new metadata fields" constraint. Future specs touching this surface should use the actual names unless intentionally renaming the metadata. - **`session.CurrentHostname()` is the exported wrapper around `currentHostname()`.** Use it in cmd-layer hostname comparisons (e.g., info's "omit hostname when local" rule) so the unknown-fallback semantics stay consistent with the rest of the package. Don't call `os.Hostname()` directly in cmd code. - **`ctask info` Session block alignment is at column 14 across all rows.** Top-level `Session:` (8 chars + 6 spaces) and indented sub-rows (` Mode:` / ` Owner:` / ` Last owner:` / ` Attach:` / ` Note:`) all align values at column 14. Malformed leases render only `Session: stale` + ` Note: ` — no Mode/Owner/Attach rows because we can't trust values from a broken file. - **`ctask info` Session block uses `invocationName()` for the Attach hint, NOT `session.SessionStatus`.** SessionStatus stays neutral — the hint string is built in `cmd/info.go::printSessionBlock`. Keep that boundary: the cmd layer owns invocation-name rendering; the session package owns lease parsing. - **`ctask list` SESSION column is between STATUS and TYPE.** Order: status, session, type, mode, category, date, slug. No header (consistent with the existing list format). Archived workspaces ALWAYS show em dash regardless of any lease file present — display simplification, not a lifecycle invariant. `info` still surfaces the raw state for archived workspaces because info is the diagnostic command. - **`ctask list --names` is unchanged in v0.5.4.** Bare basenames, no header, no session column, no whitespace per line, empty stdout on no match. `TestListNamesUnchangedHasNoSessionColumn` enforces this — if you touch the list rendering path, this test must keep passing. - **`errArchivedWorkspace` is a sentinel, not a generic error.** `runResume` flips `cmd.SilenceErrors=true` only when the inner error is this exact sentinel. All other resume errors continue to flow through Cobra's default rendering. Don't generalize the sentinel into "any error we already printed" — the reason this works is the precise scoping. Test: `TestResumeArchivedHintNoDuplicateError` exercises the full Cobra `Execute()` path. - **`formatResumeRestoreHint` and `formatDirectModeTmuxHint` are testable string-only helpers extracted purely so the audit can pin format strings without simulating tmux or stderr capture.** They have no production callers other than the original sites. Don't inline them back into their callers — the regression tests rely on calling them directly. - **Invocation-name rule is codified per spec §2: command-form hints use `invocationName()`, product-identity references stay literal `"ctask"`.** Examples of literal-`"ctask"` (intentional): `[ctask]` log/error prefix, `"ctask persistent mode requires tmux"` (descriptive prose), the SSH-remote `ssh -t ctask ` hint (the remote runs `ctask`, not the user's local binary), and Cobra `Use:` / `Long:` strings. The two regression tests in `cmd/invocation_audit_test.go` pin `my-bin` (not `ctask`) for the resume restore hint and Layer-1 attach hint specifically to detect format-string regressions that would silently work under `withInvocationName(t, "ctask")`. ## Open follow-ups (deferred; not in any shipped v0.4–v0.5.4 work) ### 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:` marker** if Claude Code's `statusLine` ever supports ANSI. ### Concurrency-related (from v0.4) - 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`, `v0.5.3-spec.md`, `v0.5.4-spec.md`, `bugfix-provisional-workspace.md`, the implementation plans for v0.4 / v0.4.1 / v0.5 / v0.5.3, and the install-workflow plan) could be committed alongside `v0.2-spec.md`/`v0.3-spec.md` for durability. Currently session-local; per `notes.md`'s long-standing rule, leave alone unless explicitly asked. - `.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. - ~~Duplicate Cobra `Error: workspace archived` line on `ctask resume `~~ → resolved in v0.5.4 (`ae9bfaf`) via `errArchivedWorkspace` sentinel + conditional `SilenceErrors`. - ~~`info` / `list` show no session state — users have to check lease files manually~~ → resolved in v0.5.4 (`e0e9cd7`, `0c8076a`) via Session block + SESSION column derived from new `internal/session.SessionStatus` helper. - ~~`docs/commands.md` missing v0.5.2/v0.5.3/v0.5.4 commands and surfaces~~ → resolved in v0.5.4 (`4fd0bef`) via structured rewrite. ## Files to read first when resuming For the v0.5.4 surface (just-shipped): 1. `v0.5.4-spec.md` — the spec v0.5.4 followed (note: spec mentions `lease.json` and `session_mode`; actual code uses `session.json` and `mode` — the implementation correctly preserved the existing names) 2. `internal/session/status.go` + `status_test.go` — `SessionStatus` display-only helper with the explicit no-tmux/no-PID/no-lock contract 3. `cmd/info.go::printSessionBlock` — Session block rendering with column-14 alignment + invocation-name Attach hint 4. `cmd/list.go::sessionColumn` — SESSION column with archived-as-em-dash rule 5. `cmd/resume.go` — `errArchivedWorkspace` sentinel + conditional `SilenceErrors` 6. `cmd/invocation_audit_test.go` — regression tests pinning a non-canonical name 7. `docs/commands.md` — rewritten v0.5.4 reference (Purpose / Usage / Scenarios / Examples / Flags / Notes / Related per command) For the v0.5 surface: 8. `v0.5-spec.md` — the spec v0.5 followed 9. `docs/superpowers/plans/2026-04-22-v0.5-implementation.md` — the executed plan (includes scanner-safety regression tests, dedupe tests, amendment history) 10. `internal/workspace/launchdir.go` — `ResolveLaunch` with the three-outcome contract 11. `internal/workspace/create.go` — subdir scaffold + local-time date + `launch_dir` default 12. `internal/session/run.go` — `LaunchOpts.LaunchDir`, `ResolveLaunch` call site, banner extension 13. `cmd/info.go` — local-time display + launch fields (Session block sits between Path and these) 14. `cmd/doctor.go` — `checkProjectRoot` helper 15. `scripts/ctask-statusline.{sh,ps1}` — effective-path display logic 16. `internal/seed/templates.go` — `ClaudeMDProject` with Workspace Structure section For the v0.4.1 surface (still load-bearing): 17. `internal/config/config.go` — `SearchRoots()`, `samePath`, `searchRootKey` dedup; default `$CTASK_ROOT/projects/` fallback 18. `internal/workspace/query.go` — two-depth `scanWorkspaces`, `scanAllRoots`, `QueryResult.Root` 19. `cmd/archive.go` — active-session check + non-TTY refusal + `isStdinTerminal` For the v0.4 surface: 20. `internal/session/run_preflight.go` — Layer 3 + Layer 1 preflight 21. `internal/session/lease.go` — Lease + freshness + cleanup 22. `internal/session/summary.go` — SessionSummary + launch-context banner 23. `internal/lockfile/writelock.go` — write-lock primitive 24. `internal/session/run_provisional.go` — `handleProvisional` with `childExitCode` gate 25. `docs/commands.md` — user-facing surface (rewritten in v0.5.4 — see entry #7 above) ## 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`. - **v0.5.4:** Do not call `session.SessionStatus` from lifecycle or adoption code. It is display-only — the `mode`-defaults-to-`direct` rule and the malformed-as-stale classification are wrong for behavioral logic. Use `ReadLease`/`IsFresh`/`InspectLease` for lifecycle decisions. - **v0.5.4:** Do not invoke tmux, check PID liveness, or acquire locks inside `SessionStatus` or its callers (`info` Session block, `list` SESSION column). PID liveness in particular is deferred to v0.6 Phase 2 where it has behavioral consequences (changing adoption thresholds) — building it display-only now would mean building it twice. - **v0.5.4:** Do not add a session column or header to `ctask list --names` output. Machine-readable bare-basename-per-line is the contract. `TestListNamesUnchangedHasNoSessionColumn` enforces the invariant. - **v0.5.4:** Do not collapse archived workspaces' SESSION column to anything other than the em dash in `list`. The display simplification is intentional. `info` is the diagnostic command — it still shows the raw lease state for archived workspaces if a lease file exists. - **v0.5.4:** Do not generalize the `errArchivedWorkspace` sentinel into "any pre-printed error". The Cobra `SilenceErrors` flip is intentionally scoped to a single error path so unrelated resume errors keep getting Cobra's default rendering. - **v0.5.4:** Do not move the Attach-hint string construction into `session.SessionStatus`. The cmd layer owns invocation-name rendering; the session package owns lease parsing. The boundary is what keeps SessionStatus testable without a `withInvocationName` seam. - **v0.5.4:** Do not re-introduce a hardcoded `"ctask"` in command-form hints. Use `invocationName()`. The regression tests pin a non-canonical name specifically to catch this.