630 lines
81 KiB
Markdown
630 lines
81 KiB
Markdown
# ctask — Session Handoff Notes
|
||
|
||
Last touched: 2026-05-14. **v0.6 Phase 1 is implemented and verified on branch `feat/v0.6-multi-agent-config` (5 commits, NOT yet merged into `main`). v0.5.4 remains the shipped tip on `main` and the installed binary is still `v0.5.4` — no version bump in Phase 1 (per spec, the bump lands at the end of Phase 3). Phase 2 is the next horizon and is explicitly NOT started.**
|
||
|
||
## 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 during Phase 1 — the branch builds locally but has not been installed.
|
||
- **Active branches:** `feat/v0.6-multi-agent-config` — 5 commits ahead of `main`, all under the v0.6 theme. Not yet merged. Phase 2 + Phase 3 work will continue on this branch.
|
||
- **Pending action:** Phase 2 planning (multi-agent layer: agent profile system in task.yaml, `--agent` flag on `ctask new`, AGENTS.md + CLAUDE.md shim generation, `ctask agents check`). Phase 2 must NOT start until Phase 1 has been reviewed.
|
||
- 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: <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.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 <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.**~~ 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 <workspace>` — 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 `<binary> attach <slug>` as the reattach path. Threaded via `PreflightOpts.ActiveLeaseHint` / `LaunchOpts.ActiveLeaseHint`; computed in `cmd/entry.go::directModeTmuxHint` (best-effort: silent when no tmux on PATH or no session for the workspace).
|
||
- 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 <ws> --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 <host> ctask <subcmd>`) 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-<sanitized-category>-<sanitized-slug-truncated-30>-<sha256_6>`, 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`).** A user who Ctrl-C's the foreground `ctask` and immediately re-runs `ctask resume` hits the v0.4 Layer-1 prompt instead of the adoption path. Acceptable but lazy-cleanup-unfriendly; deferred to v0.6 Phase 2 (see follow-ups). Not in v0.6 Phase 1 scope.
|
||
|
||
### 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 <active-persistent>` shows the Session block with hostname omitted (local) and Attach hint via `invocationName()`; `ctask resume <archived>` 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), `<path>` (valid, INFO), or `<path>` + `[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: <value>` 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 — <source>)` 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: <scalar>}`; any other scalar (a path, `aider`, etc.) maps to `AgentSpec{Type: "custom", Command: <scalar>}`; 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 <type>` 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.
|
||
|
||
### 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 <ws>` 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, 5 commits ahead of `main`. Tip `6182d89`. NOT merged — Phase 2 and Phase 3 will continue on this same branch per spec.
|
||
- 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 — that's a post-Phase-3 task.
|
||
- Installed `ctask.exe` at `%LOCALAPPDATA%\ctask\bin\ctask.exe` is still **v0.5.4** — Phase 1 did NOT refresh the installed binary. Local `ctask.exe` in the repo root is a `6182d89` build. `dist/ctask-linux-amd64` is the Phase-1 Linux cross-build (statically linked ELF).
|
||
- Memory follow-ups (still live from v0.5.3, both relevant to v0.6 Phase 2 — see `memory/MEMORY.md`):
|
||
- `feedback_design_for_lazy_cleanup` — drives v0.6 Phase 2 work on the 60s freshness wait + PID liveness.
|
||
- `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, which Phase 2 may revisit.
|
||
- 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 <some-active-ws> # 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 <version> (/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-<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.
|
||
|
||
### 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: <diagnostic>` — 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 <host> ctask <subcmd>` 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:<dir>` 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 <archived-ws>`~~ → 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.
|
||
|