398 lines
38 KiB
Markdown
398 lines
38 KiB
Markdown
# ctask — Session Handoff Notes
|
|
|
|
Last touched: 2026-05-09. **v0.5.2 is shipped on `main` and installed locally as `v0.5.2`. v0.5.3 implementation is complete on branch `feat/v0.5.3-persistent-session-mode` (20 commits) but is NOT yet merged or installed — it is awaiting the user's manual WSL smoke verification.**
|
|
|
|
## Where we are
|
|
|
|
- **`main`:** v0.5.2 (workspace retrieval + cross-workspace context). Installed binary at `%LOCALAPPDATA%\ctask\bin\ctask.exe` is `v0.5.2`.
|
|
- **`feat/v0.5.3-persistent-session-mode`:** v0.5.3 (persistent session mode via tmux). 20 commits. All automated tests + `go vet` + cross-compile (`just build-linux` produces a static ELF) green on the Windows host. The Linux binary at `dist/ctask-linux-amd64` runs and reports `ctask v0.5.3` under WSL `debian-dev`.
|
|
- **Pending action:** the user runs the manual smoke checklist at `docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md` (v2 — explicit terminals, corrected expectations) and reports PASS/FAIL per section. On all-PASS we merge the branch to `main`, `just install` to refresh the installed binary, and optionally `git tag v0.5.3`.
|
|
- Remote: none (local-only, intentional — see `CLAUDE.md`).
|
|
- `ctask doctor` reports 5 pass/fail + 2 seed-directory + 1 `CTASK_PROJECT_ROOT` check (all three-state). Once v0.5.3 lands, doctor adds an INFO line for `Session mode: direct|persistent` plus an INFO/FAIL line for tmux when persistent.
|
|
|
|
### What v0.4 delivered (still true, unchanged)
|
|
|
|
Workspace concurrency protection — session lease with heartbeat (Layer 1), metadata write lock (Layer 2), stale-workspace detection (Layer 3), session handoff summary (Layer 4). `--force` flag on `resume`/`last`/`open`. `WriteMetaLocked`, `LaunchOpts.Force`, `Preflight`. All unchanged in v0.5.
|
|
|
|
### What v0.4.1 delivered (still true, unchanged)
|
|
|
|
Correctness and polish: `config.SearchRoots()` multi-root discovery; two-depth `scanWorkspaces` (flat + category, first-match-wins); nested-git documentation; doctor seed-dir three-state checks; archive active-session warning with non-TTY refuse; provisional-cleanup exit-code gate. Still load-bearing — see "From v0.4.1" section below.
|
|
|
|
### What v0.5 delivered
|
|
|
|
Theme: **separate workspace management from project code**. `--project` now scaffolds a project subdirectory and session commands `cd` into it. Twelve commits on `main` (`175fbb0..8130a68`).
|
|
|
|
1. **`launch_dir` field in `TaskMeta`** (`175fbb0`)
|
|
- New `LaunchDir string` with `yaml:"launch_dir,omitempty"` — empty for tasks and pre-v0.5 projects, defaults to the project slug for new projects.
|
|
- Omitempty keeps on-disk task.yaml clean for backward compatibility.
|
|
2. **`workspace.ResolveLaunch` helper** (`dcb1610`)
|
|
- Converts a relative `launch_dir` into an absolute child-cwd with three outcomes:
|
|
- Valid dir → return absolute path, no warning, no error.
|
|
- Missing path (`os.IsNotExist`) or path-is-a-file → return `(wsDir, warning, nil)` so the caller warns and falls back.
|
|
- Absolute path, `..`-escape, or any non-`IsNotExist` stat error (permission, ENOTDIR, invalid name) → return `("", "", error)` — caller must surface.
|
|
- This asymmetry is deliberate — masking a permission error as a warning fallback would silently strip launch-dir semantics.
|
|
3. **Project subdirectory scaffolding** (`7cfafdc`)
|
|
- `ctask new --project` creates an empty subdir named after the **final suffixed slug** (so `dup-2` collisions produce matching `dup-2/` subdir).
|
|
- ctask does not seed anything inside the subdir — the user places their own CLAUDE.md, source code, project configuration.
|
|
- Task workspaces are completely unchanged — no subdir, no `launch_dir`.
|
|
4. **`CTASK_LAUNCH_DIR` env var** (`509a6d6`)
|
|
- Added as a 7th arg to `config.EnvVars`. All three cmd callers (`new`, `resume`, `open`) pass `ws.Meta.LaunchDir`.
|
|
5. **Session launch routed through `LaunchDir`** (`103f2cd`)
|
|
- `LaunchOpts.LaunchDir` added. `session.Run` calls `ResolveLaunch` before banner/exec, prints any warning to stderr, aborts on security error, and passes the absolute path to `shell.ExecAgent` / `shell.ExecShell` as the child's working directory.
|
|
- Banner gains a `[ctask] project dir: <name>/` line when `launch_dir` is set.
|
|
- Lease, manifest, heartbeat, and summary scope stays the **workspace root** — only the child's cwd changes.
|
|
6. **`ctask info` shows launch fields** (`cdff7f3`)
|
|
- For workspaces with `launch_dir` set, info prints `Launch dir:`, `Launch path:`, and `Dir exists: yes|no` (via direct `os.Stat`, not via `ResolveLaunch` — info is a display command, not a launch command).
|
|
7. **`SearchRoots` default fallback** (`47430a1`)
|
|
- When `CTASK_PROJECT_ROOT` is unset, `SearchRoots()` now appends `$CTASK_ROOT/projects/` so default-location projects are findable from any shell (resolves the v0.4.1 env-var scoping footgun from the previous follow-up list).
|
|
- Dedupe in `scanAllRoots` prevents duplicate results when the same workspace is reachable via both the depth-2 scan under `CTASK_ROOT` and the explicit `$CTASK_ROOT/projects/` search root.
|
|
8. **Doctor `CTASK_PROJECT_ROOT` check** (`70bd167`)
|
|
- Three-state, matching the `checkSeedDir` pattern: `[INFO]` when unset (points at `$CTASK_ROOT/projects/`), `[INFO]` with user-scope advisory when set+exists, `[FAIL]` when set but missing. Only FAIL increments the failure counter.
|
|
- Wording is advisory ("recommended: set at user scope…"), not prescriptive.
|
|
9. **Project CLAUDE.md template rewrite** (`cdf1c55`)
|
|
- Adds a "Workspace Structure" section explicitly describing the root-vs-subdir split.
|
|
- Keeps the "Git" section from v0.4.1, now with an explicit mention that the project subdir is tracked by the root repo.
|
|
10. **Status line helpers show effective launch path** (`0976dce`)
|
|
- Both `.sh` and `.ps1` build `DISPLAY_PATH = $CTASK_WORKSPACE + (CTASK_LAUNCH_DIR if set)`. When `CTASK_LAUNCH_DIR != CTASK_TASK`, the tag becomes `|project:<launch_dir>` (user-overridden launch directory).
|
|
11. **Docs updated** (`82c9445`)
|
|
- `docs/commands.md`: workspace-layout diagram, `launch_dir` semantics (default, override, fallback vs error), `CTASK_LAUNCH_DIR` env var, doctor example with new INFO line, Query Resolution default-discovery paragraph.
|
|
12. **Version bump `0.4.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.** Cosmetic only — the user-visible hint is shown first and is correct. Suppressing the trailing line would require `SilenceErrors: true` on `resume`, which would also swallow unrelated session-launch errors. Acceptable trade; revisit only if the redundancy actively confuses users.
|
|
|
|
### Next: v0.6 (planning)
|
|
|
|
Not yet specced. Likely scope:
|
|
|
|
- **Config / agent profile work.** The long-flagged direction towards `~/.config/ctask/` configuration; may supersede the env-var resolution dance for `CTASK_ROOT`/`CTASK_PROJECT_ROOT`/`CTASK_SEED_DIR`/`CTASK_AGENT`.
|
|
- **Optional polish for resume error output.** Silence the trailing Cobra "Error: workspace archived" line — likely via per-error-path `SilenceErrors` or a sentinel-error wrapper.
|
|
- **Flag-aware completion for `open`/`delete --all`.** Cobra supports `RegisterFlagCompletionFunc` per flag; would let completion offer archived candidates only when `--all` is passed.
|
|
|
|
## Post-v0.4 bugfixes (still live, carried forward)
|
|
|
|
### Provisional-workspace cleanup (2026-04-22, commits `02dcdcc`, `ba8b3a1`)
|
|
|
|
Covered in v0.4.1 notes. The exit-code gate (`childExitCode != 0 && startManifest != nil && emptyDiff && NewlyCreated`) is unchanged in v0.5.
|
|
|
|
## Tree state at pause
|
|
|
|
- `main` clean with respect to v0.4, v0.4.1, v0.5, v0.5.1, v0.5.2. Latest tip is `e448eff docs(v0.5.2): record v0.5.2 completion in notes.md`.
|
|
- HEAD is on `feat/v0.5.3-persistent-session-mode` (20 commits ahead of main, 0 behind). Branch was created cleanly from `main` at the start of v0.5.3 work.
|
|
- Installed `ctask.exe` is **v0.5.2** — DO NOT reinstall yet. Wait until v0.5.3 has passed manual smoke + been merged.
|
|
- Untracked files (do NOT touch without asking):
|
|
- `.claude/settings.local.json` (modified — Claude Code local settings)
|
|
- `bugfix-provisional-workspace.md` (spec for the 2026-04-22 initial provisional fix; may be deleted or archived)
|
|
- `docs/superpowers/plans/2026-04-06-install-workflow.md` (install-workflow plan from an earlier session)
|
|
- `docs/superpowers/plans/2026-04-21-v0.4-implementation.md` (v0.4 plan — executed)
|
|
- `docs/superpowers/plans/2026-04-22-v0.4.1-patch.md` (v0.4.1 plan — executed)
|
|
- `docs/superpowers/plans/2026-04-22-v0.5-implementation.md` (v0.5 plan — executed)
|
|
- `v0.4-spec.md`, `v0.4.1-patch-spec.md`, `v0.5-spec.md`, `v0.5.3-spec.md` (specs the implementations followed)
|
|
- Files committed ON the v0.5.3 branch (already tracked, not in the untracked list above):
|
|
- `docs/superpowers/plans/2026-05-08-v0.5.3-implementation.md` — the executed plan
|
|
- `docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-log.md` — automated portions of Task 17 (cross-compile, version, native-Windows refusal, doctor on both platforms — all PASS)
|
|
- `docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md` — manual checklist v2 for the user to run
|
|
|
|
## How to resume
|
|
|
|
### Completing the v0.5.3 ship (current pending action)
|
|
|
|
The v0.5.3 branch is ready — only manual WSL smoke verification stands between
|
|
it and `main`.
|
|
|
|
```powershell
|
|
cd C:\Users\Warren\claude_tasks\ctask
|
|
git checkout feat/v0.5.3-persistent-session-mode
|
|
just test # all green on Windows
|
|
just build-linux # produces dist/ctask-linux-amd64 (static ELF)
|
|
```
|
|
|
|
Then the user runs through `docs/superpowers/plans/2026-05-08-v0.5.3-smoke-test-checklist.md`
|
|
in WSL (three terminals: WSL-A, WSL-B, PS-C) and reports PASS/FAIL per section.
|
|
|
|
After all-PASS:
|
|
|
|
```powershell
|
|
git checkout main
|
|
git merge --no-ff feat/v0.5.3-persistent-session-mode
|
|
just install # refresh installed binary to v0.5.3
|
|
ctask --version # expect: ctask v0.5.3
|
|
git branch -d feat/v0.5.3-persistent-session-mode
|
|
git tag v0.5.3 # optional
|
|
```
|
|
|
|
If anything fails: capture the exact output (especially around the
|
|
adopted-reattach summary fields and the tmux session-name hash) and feed
|
|
it back to the next session.
|
|
|
|
### General resume (when on main after a ship)
|
|
|
|
```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.1 is live:
|
|
|
|
```powershell
|
|
ctask --version # expect: ctask v0.5.1
|
|
ctask doctor # expect: 5 pass/fail + 2 seed-dir INFO + 1 CTASK_PROJECT_ROOT INFO
|
|
|
|
# Local-time directory prefix (v0.5.1):
|
|
ctask new --project "tz-check" --no-launch
|
|
# Expected output: [ctask] created projects/YYYY-MM-DD_tz-check — YYYY-MM-DD matches
|
|
# the user's local wall-clock date, not UTC.
|
|
ctask info tz-check # Created/Updated in local zone
|
|
ctask delete --force tz-check
|
|
|
|
# Project subdir + banner (v0.5):
|
|
ctask new --project "smoke" --no-launch
|
|
# A subdir named smoke/ exists inside the workspace.
|
|
ctask resume smoke # banner shows "[ctask] project dir: smoke/" and Claude
|
|
# launches with cwd inside smoke/. /exit to leave.
|
|
ctask delete --force smoke
|
|
|
|
# Default discovery (v0.5):
|
|
# Projects created without $CTASK_PROJECT_ROOT should now be findable from any shell.
|
|
ctask list --projects
|
|
```
|
|
|
|
## Load-bearing design points (don't forget)
|
|
|
|
### From v0.4 (unchanged)
|
|
|
|
- **Coexisting-session limitation:** "Continue anyway?" or `--force` → second session runs without its own lease. Documented in `docs/commands.md`. Don't redesign without a real user complaint.
|
|
- **`end_manifest` in the summary** is ctask-internal (not in spec example JSON) and powers Layer-3 stale-workspace diff. Removing it breaks Layer 3.
|
|
- **Write-lock skip classification** (best-effort vs important-but-non-fatal): unchanged.
|
|
- **`cmd/delete.go` two-check invariant:** unchanged. `CTASK_WORKSPACE` match → refuse; `.ctask/manifest-start.json` exists → refuse. Both before any mutation.
|
|
- **`session.Run()` lifecycle** in `internal/session/run.go`:
|
|
1. `PreflightFull` (Layers 3 + 1)
|
|
2. Write lock → write lease (unless coexisting)
|
|
3. Write lock → capture + write start manifest
|
|
4. Print launch-context banner (Layer 4)
|
|
5. Start heartbeat (only if we own the lease)
|
|
6. **v0.5: `workspace.ResolveLaunch(opts.WsDir, opts.LaunchDir)` — print warning or abort on security error**
|
|
7. Banner lines (including `project dir:` when LaunchDir set) + exec child with cwd = resolved launch path
|
|
8. Stop heartbeat
|
|
9. `handleProvisional` (gated on `NewlyCreated && childExitCode != 0 && emptyDiff`) — if it removes the workspace, skip finalize entirely
|
|
10. `finalize`: single write-lock acquire → append log + write summary + remove lease (if owned) + remove manifest-start
|
|
- **`new` is intentionally not given `--force`.** Brand-new workspaces have nothing for Layer 1/3 to warn about.
|
|
- **`internal/lockfile` is a neutral package** — no other ctask imports.
|
|
|
|
### From v0.4.1 (unchanged, still load-bearing)
|
|
|
|
- **`config.SearchRoots()` is the canonical way to enumerate workspace roots.** Always includes CTASK_ROOT. In v0.5 the semantics of the second entry expanded — see "From v0.5" below.
|
|
- **`QueryResult.Root`** is how callers render relative paths and compute `CTASK_ROOT` env var. Populated by `scanAllRoots`.
|
|
- **`scanWorkspaces` is a two-depth scan, first-match-wins.** Depth-1 (flat) check wins if present; never descends into a detected workspace.
|
|
- **Archive's non-TTY stdin refuses.** Fresh lease + non-TTY = exit non-zero without mutating. Don't regress to "silent proceed".
|
|
- **Doctor seed-dir checks are three-state.** Only "configured but missing" counts as a failure.
|
|
- **Provisional-cleanup exit-code gate.** `childExitCode == 0` means preserve. Don't special-case specific codes.
|
|
|
|
### From v0.5 (new — don't unlearn)
|
|
|
|
- **`launch_dir` is stored as workspace-relative in `task.yaml`**, empty for tasks and pre-v0.5 projects. Never store absolute paths. Never interpret `launch_dir` outside `workspace.ResolveLaunch`.
|
|
- **`workspace.ResolveLaunch` has three outcomes: valid, soft fallback (with warning), security/permission error.** Soft fallback is `(wsDir, warning, nil)` — caller warns and keeps launching. Error is `("", "", err)` — caller must abort. The asymmetry between `os.IsNotExist` (fallback) and other stat errors (propagate) is intentional; masking a permission error as a warning would silently strip launch-dir semantics.
|
|
- **Project subdir name = final suffixed slug.** `dup-2` collision means subdir is `dup-2/`, not `dup/`. The `actualSlug` variable in `workspace.Create` is the single source of truth.
|
|
- **Subdir is created after seeds, empty, never seeded.** Layer 1 (built-in defaults), Layer 2 (general seed), Layer 3 (project seed) all target the workspace root. The `if opts.IsProject { os.MkdirAll(projDir, 0755) }` block runs AFTER seed overlays and BEFORE `task.yaml` is written.
|
|
- **Session scope is workspace-wide even when launched in the subdir.** Lease, manifest capture, write lock, summary, stale-workspace diff all operate on `opts.WsDir` (the workspace root), not on the resolved launch path. Only the child process's cwd changes.
|
|
- **`CTASK_LAUNCH_DIR` is always exported** (empty string for tasks). Status line helpers and any future child-side integration can check `-n "$CTASK_LAUNCH_DIR"` safely.
|
|
- **`SearchRoots()` now appends `$CTASK_ROOT/projects/` by default** when `CTASK_PROJECT_ROOT` is unset. This is redundant with the v0.4.1 two-depth scan under `$CTASK_ROOT` but explicit per spec. Dedupe in `scanAllRoots` (`searchRootKey` — cleaned + lower-cased on Windows) prevents double-counting. Don't remove the default fallback without also reverting the spec-driven expectation that default projects are findable from any shell.
|
|
- **Doctor check for `CTASK_PROJECT_ROOT` is advisory, not prescriptive.** "Recommended: set at user scope so all terminals can discover these workspaces." Don't harden into a FAIL when the variable is set but the path exists.
|
|
|
|
### From v0.5.1 (new — don't unlearn)
|
|
|
|
- **Workspace directory prefix uses LOCAL time.** `time.Now()` in `workspace.Create` feeds the `YYYY-MM-DD` date and the `YYYYMMDD-HHMMSS` ID. Don't switch back to `.UTC()` for these. The `TestCreateDirectoryPrefixUsesLocalDate` regression test enforces this.
|
|
- **`ctask info` displays timestamps in local zone via `.Local().Format(...)`.** Stored timestamps in `task.yaml` are still UTC (`meta.CreatedAt = nowLocal.UTC().Truncate(time.Second)`) — only the display converts.
|
|
- **Stored timestamps everywhere else remain UTC.** Session logs, lease `StartedAt`/`LastHeartbeatAt`, manifest capture times, summary timestamps are all unambiguous UTC. Don't "localize" those — they're machine-readable.
|
|
- **Cross-builds force `CGO_ENABLED=0`.** `just build-linux` and `just build-windows` set this explicitly so the artifact is pure-Go static. Don't drop the flag — a native Linux build defaults to cgo and produces a glibc-linked binary that won't run in Alpine / distroless / scratch containers.
|
|
- **`WorkspacePath` is gone from `TaskMeta`.** Don't add it back. If a persistent workspace identifier is ever needed, add a properly relative-to-root `workspace_id` (forward-slash normalized). Old `task.yaml` files with `workspace_path` parse silently — the field is ignored on read.
|
|
- **POSIX install script does not modify shell config.** It warns the user about PATH but never edits `~/.bashrc` / `~/.zshrc`. Don't change this without an explicit user request.
|
|
|
|
### From v0.5.2 (new — don't unlearn)
|
|
|
|
- **`info` no longer has `--all`/`-a`.** Direct lookup is archived-inclusive by default. Don't add the flag back. The `Status:` line in info output makes archived state obvious. `TestInfoFindsArchivedWorkspaceWithoutFlag` enforces this.
|
|
- **`resume` / `archive` resolve archived-inclusive then filter.** Don't switch back to `includeArchived=false` at the resolver layer. The two-step pattern (resolve broadly, reject explicitly) is what lets `resume` give the actionable restore hint instead of a generic "not found".
|
|
- **Direct-lookup commands (`info`, `notes`, `path`, `restore`) use `includeArchived=true`.** Active-only commands (`resume`, `archive`) reject archived after resolve. Potentially-destructive commands (`delete`, `open`) keep their explicit `--all` opt-in. Don't drift back to a uniform default — the policy is intentional. See `v0.5.2-spec.md` "Workspace Lookup Policy".
|
|
- **`list --names` emits directory basenames, not bare slugs.** Bare slugs can collide (e.g., `promptvolley` archived in v1, active in v2). Basenames are unique under the resolver's exact-match step. `TestListNamesCandidatesResolveUniquely` enforces the invariant.
|
|
- **`ValidArgsFunction` hooks call `workspace.ListWorkspaces` directly.** No subprocess shell-out to `ctask list --names`. Don't add one — direct calls are faster and don't depend on PATH state.
|
|
- **Cobra adds the `completion` subcommand lazily on first `Execute()`.** A test that calls `rootCmd.Find("completion")` before any `Execute()` returns "unknown command". For unit tests, prefer the `rootCmd.GenXxxCompletion(...)` generators directly. For end-to-end, one `SetArgs(...)` + `Execute()` per test — running multiple `Execute()` calls in succession with different shell args has state issues.
|
|
- **`notes` uses `SilenceErrors: true`** so the `[ctask] no notes.md found in workspace "X"` stderr line is the only diagnostic the user/agent sees. Don't set `SilenceErrors: false` and add a `[ctask]` prefix to the returned error message — Cobra would then print both, doubling the message.
|
|
|
|
### From v0.5.3 (new — don't unlearn) [pending merge to main]
|
|
|
|
- **`CTASK_SESSION_MODE` is the only persistent-mode trigger.** No flag promotes a single command to persistent. `ctask attach` is the inverse — it always uses tmux regardless of env. Don't add a `--persistent` flag; the existing `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.
|
|
|
|
## Open follow-ups (NOT in v0.4/v0.4.1/v0.5, deferred)
|
|
|
|
### Potentially worth doing
|
|
|
|
- **Pre-v0.5 project workspaces have no `launch_dir`.** They launch from the workspace root, not from any project subdir. No migration path today. Acceptable — users can manually add a `launch_dir` to their `task.yaml` if they want the new behavior. Revisit only if someone complains.
|
|
- **Drop unused `slug` / `category` / `workspacePath` parameters from `seed.ClaudeMD`** (v0.3 follow-up, still open).
|
|
- **Status-line: color the `|project` / `|project:<dir>` marker** if Claude Code's `statusLine` ever supports ANSI.
|
|
|
|
### 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`, `bugfix-provisional-workspace.md`, the plan files) could be committed alongside `v0.2-spec.md`/`v0.3-spec.md` for durability. Currently session-local.
|
|
- `.claude/settings.local.json` has uncommitted changes of unknown scope. Leave alone unless asked.
|
|
|
|
### Resolved (don't re-add to this list)
|
|
|
|
- ~~CTASK_PROJECT_ROOT env-var scoping UX footgun~~ → resolved in v0.5 via the default `$CTASK_ROOT/projects/` fallback in `SearchRoots()`.
|
|
- ~~UTC date in directory names / info display~~ → resolved in v0.5.1.
|
|
|
|
## Files to read first when resuming
|
|
|
|
For the v0.5 surface:
|
|
|
|
1. `v0.5-spec.md` — the spec v0.5 followed
|
|
2. `docs/superpowers/plans/2026-04-22-v0.5-implementation.md` — the executed plan (includes scanner-safety regression tests, dedupe tests, amendment history)
|
|
3. `internal/workspace/launchdir.go` — `ResolveLaunch` with the three-outcome contract
|
|
4. `internal/workspace/create.go` — subdir scaffold + local-time date + `launch_dir` default
|
|
5. `internal/session/run.go` — `LaunchOpts.LaunchDir`, `ResolveLaunch` call site, banner extension
|
|
6. `cmd/info.go` — local-time display + launch fields
|
|
7. `cmd/doctor.go` — `checkProjectRoot` helper
|
|
8. `scripts/ctask-statusline.{sh,ps1}` — effective-path display logic
|
|
9. `internal/seed/templates.go` — `ClaudeMDProject` with Workspace Structure section
|
|
|
|
For the v0.4.1 surface (still load-bearing):
|
|
|
|
10. `internal/config/config.go` — `SearchRoots()`, `samePath`, `searchRootKey` dedup; default `$CTASK_ROOT/projects/` fallback
|
|
11. `internal/workspace/query.go` — two-depth `scanWorkspaces`, `scanAllRoots`, `QueryResult.Root`
|
|
12. `cmd/archive.go` — active-session check + non-TTY refusal + `isStdinTerminal`
|
|
|
|
For the v0.4 surface:
|
|
|
|
13. `internal/session/run_preflight.go` — Layer 3 + Layer 1 preflight
|
|
14. `internal/session/lease.go` — Lease + freshness + cleanup
|
|
15. `internal/session/summary.go` — SessionSummary + launch-context banner
|
|
16. `internal/lockfile/writelock.go` — write-lock primitive
|
|
17. `internal/session/run_provisional.go` — `handleProvisional` with `childExitCode` gate
|
|
18. `docs/commands.md` — user-facing surface
|
|
|
|
## Don't re-do
|
|
|
|
- Do not invent remote install / `go install` commands (per `CLAUDE.md`, this project is local-only)
|
|
- Do not touch `cmd/delete.go`'s two-check protection
|
|
- Do not weaken the v0.3 `.gitignore` seed-wins rule
|
|
- Do not redesign the lease model around multiple concurrent leases without a real user-reported problem
|
|
- Do not re-run the v0.4 smoke tests in a fresh session unless source has changed
|
|
- Do not expand the provisional-cleanup gate beyond "strictly empty manifest diff AND non-zero child exit". No size thresholds, no heuristics, no exit-code-specific mappings.
|
|
- Do not extend provisional cleanup to `resume` / `open` / `last`. `NewlyCreated` is set only by `cmd/new.go`.
|
|
- Do not drop `QueryResult.Root` or collapse `SearchRoots()` back to a single string.
|
|
- Do not change archive's non-TTY behavior back to "proceed silently".
|
|
- Do not add a workspace registry file. The default `$CTASK_ROOT/projects/` fallback solved the discovery footgun.
|
|
- **v0.5:** Do not change `launch_dir` storage to absolute paths. Workspace-relative is load-bearing.
|
|
- **v0.5:** Do not mask non-`os.IsNotExist` stat errors in `ResolveLaunch` as warning fallbacks. The asymmetry between soft-fallback (missing / not-a-dir) and hard-error (permission / ENOTDIR / invalid name) is deliberate.
|
|
- **v0.5:** Do not seed anything inside the project subdirectory. Seeds target the workspace root only.
|
|
- **v0.5:** Do not let the session launch change scope (lease, manifest, summary, write-lock) away from the workspace root. Only the child's cwd is the launch dir.
|
|
- **v0.5:** Do not remove the default `$CTASK_ROOT/projects/` fallback in `SearchRoots()` — it resolves the v0.4.1 env-var scoping footgun.
|
|
- **v0.5.1:** Do not switch the directory prefix / ID back to UTC. The `TestCreateDirectoryPrefixUsesLocalDate` test enforces local time.
|
|
- **v0.5.1:** Do not remove `.Local()` from the `ctask info` Created/Updated/Archived formatting. `TestInfoFormatsTimestampsInLocalZone` enforces local display.
|
|
- **v0.5.1:** Do not change *stored* timestamps (task.yaml, session logs, lease, manifest, summary) to local time. UTC storage is deliberate — only display converts.
|
|
- **v0.5.3:** Do not call `exec.Command("tmux", ...)` outside `internal/shell/tmux.go`. The single-construction-site rule is what makes passive reattach and adoption stay in sync. Test seams (`adoptAttacher`, `adoptPoll`, `attacher`) wrap the primitives — they don't replace them.
|
|
- **v0.5.3:** Do not move tmux discovery into `session.Run`. `cmd/persistent.go::preflightPersistentEntry` is the single source of truth; the validated path flows through `LaunchOpts.TmuxPath`.
|
|
- **v0.5.3:** Do not enable provisional cleanup in persistent mode. `shouldRunProvisional` returns false on `SessionMode == "persistent"` — the gate's UX assumption doesn't translate to tmux.
|
|
- **v0.5.3:** Do not silently overwrite a fresh remote lease. `confirmFreshRemoteAdoption` prompts on TTY and refuses on non-TTY. The non-TTY refusal carries the remote hostname so the user can disambiguate.
|
|
- **v0.5.3:** Do not run the persistent preflight after `workspace.Create` in `cmd/new.go`. Pre-create ordering prevents half-initialized workspaces when tmux is missing.
|
|
- **v0.5.3:** Do not change the `SessionName` algorithm. Name stability across processes is what makes passive reattach work without state. Windows path lowercasing matches `searchRootKey`.
|
|
|
|
## What v0.5.3 delivered
|
|
|
|
Persistent session mode is in. Key user-facing surfaces:
|
|
|
|
- New env var `CTASK_SESSION_MODE` (`direct` | `persistent`); `direct` is the default and requires no setup.
|
|
- `ctask attach <workspace>` — always-tmux entry command. Defaults to launching the agent.
|
|
- `--direct` flag on `new` / `resume` / `last` / `open` to bypass persistent mode for one invocation, with confirmation when a tmux session already exists.
|
|
- `ctask doctor` now reports tmux presence and version when persistent mode is configured.
|
|
|
|
Architecture notes:
|
|
|
|
- tmux is invoked via a three-call pattern (`has-session`, `new-session -d`, `attach-session`) with a 3-second polling loop to detect session end. The polling cadence is below the 30-second heartbeat interval, so finalize lag is bounded.
|
|
- Session names are deterministic: `ctask-<category>-<slug>-<hash6>`, where the hash is the first 6 hex chars of `sha256(canonical absolute workspace path)`. On Windows the path is lowercased before hashing.
|
|
- Three entry paths (owner-create, passive reattach, adopted reattach) are picked based on tmux session existence and `InspectLease` four-state classification.
|
|
- Adoption transfers ownership under the metadata write lock with a re-check race guard. The previous lease is replaced, `task.yaml.UpdatedAt` is bumped, a fresh start manifest is captured, and finalize stamps `session_ownership: "adopted"` plus `adopted_from_orphan_at`.
|
|
- The v0.4 four-layer concurrency model is preserved verbatim. Layer 3 is selectively skipped on reattach paths because no reliable end_manifest baseline exists from a previous orphaned owner.
|
|
- Provisional cleanup is bypassed in persistent mode — the gate's UX assumption ("Esc on prompt -> empty diff") does not translate to tmux.
|
|
- `last-session-summary.json` gains four optional fields (`end_reason`, `detected_via`, `session_ownership`, `adopted_from_orphan_at`); pre-v0.5.3 summaries continue to load.
|
|
|
|
Out of scope (deferred to future releases):
|
|
- Native Windows persistent mode (PSmux is a candidate; not committed).
|
|
- Config file (`~/.config/ctask/config.yaml`) — env var remains the only config surface until v0.6.
|
|
- `switch-client` for nested-tmux entry, `tmux wait-for` / `set-hook`-based detection, banner injection inside tmux, `ctask sessions` listing command.
|