Files
ctask/notes.md
T
typebasedio 6c4c3e8df2 docs(v0.6): Phase 1 closeout in notes.md
Records:

- Branch feat/v0.6-multi-agent-config is the active surface for v0.6;
  Phase 1 (5 commits) is implemented, tested, and reviewed but the
  branch is NOT merged into main. 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.
- The five Phase 1 commits with one-line summaries each:
    6f80c8b config file parser + resolver + source attribution
    0b21b8d schema_version and workspace.mode in task.yaml
    c918e5c doctor Settings section with source attribution
    937a1c8 info source attribution on Agent and Launch session mode
    6182d89 platform-override stderr warning on launch paths
- Per-commit detail: what each commit landed, where the load-bearing
  code lives, and what the test coverage is.
- Verification gate (all clean on tip 6182d89):
    go test ./... -count=1, go vet ./..., go build, just build-linux
    (statically linked ELF). Version stays v0.5.4 — bump is deferred
    to the end-of-Phase-3 commit.
- Phase 1 constraints held: no config auto-creation, no opportunistic
  schema writes, env vars remain overrides, task.yaml remains
  workspace state, no Phase 2 or Phase 3 work started.
- Native-Windows platform-override behavior across the three surfaces:
    * doctor + info — show source attribution
    * launch paths (new/resume/last/open) — emit one stderr warning
      per invocation and downgrade to direct
    * attach — does NOT downgrade; continues to refuse via
      preflightPersistentEntry because it has no direct-mode equivalent
- Architecture notes worth preserving for future Phase work:
  "one resolver per command", exported test seams, validation in
  ReadMeta rather than callers, omitempty as the load-bearing
  primitive for no-opportunistic-writes.
- Files-committed list and "How to resume" section both updated to
  point at Phase 2 as the next horizon and to enumerate the resume
  sanity checks against the unmerged branch.
2026-05-14 22:20:10 -04:00

72 KiB
Raw Blame History

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.10.5.0 (8130a68).

What v0.5.1 delivered

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

Round 1 — date fix (a162aec, a11d48b)

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

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

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

What v0.5.2 delivered

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

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

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

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

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

    Genuine not-found and ambiguity behavior preserved.

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

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

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

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

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

  • Version bump to 0.5.2 (5910100).

Validation:

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

Known limitation (v0.5.2)

  • ctask resume <archived-ws> prints Error: workspace archived below the actionable hint. 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 blockctask 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 columnctask 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 referencedocs/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 14) 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.goResolver / 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 14 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.

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)

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:

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 57: 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:

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

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.4v0.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.
  • 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.goSessionStatus 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.goerrArchivedWorkspace 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:

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

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

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

For the v0.4 surface:

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