- Add scripts/build-release.sh: cross-compile linux+windows amd64 with ldflags injecting main.version and main.commit, write checksums-sha256.txt and release-manifest.json (full commit SHA). - Add scripts/release-check.sh: local mirror of CI (test, vet, build, --version substring check); falls back to Windows artifact when run on a Windows host where the Linux binary can't exec. - Wire main.version / main.commit -> cmd.SetVersionInfo. Default to "dev" / "" so local builds without ldflags still produce a sensible string. Output format: single line 'ctask <version> (<short-sha>)' or 'ctask <version>' / 'ctask dev'. - Add .gitea/workflows/release.yml: triggered on v* tags, runs-on ctask-release (golang:1.26-bookworm). Tag parsed from gitea.ref (not gitea.ref_name). Pure shell + Gitea API; no actions/checkout, no setup-go, no third-party release action. Installs jq at job start. RC tags are deletable+recreatable; final tags are immutable. Verify step downloads published assets, sha256sum -c's, and runs --version. - notes.md: log Phase 0/2/3 + version-injection completion.
92 KiB
ctask — Session Handoff Notes
Last touched: 2026-05-15. v0.6.0 is SHIPPED. All three phases (config infrastructure, agent-agnostic layer, PID-liveness lazy-cleanup) merged into main at merge commit b368973, tagged v0.6.0. The installed binary at %LOCALAPPDATA%\ctask\bin\ctask.exe is now v0.6.0. The feat/v0.6-multi-agent-config branch has been deleted (fully merged). main is the only branch.
Where we are
main: v0.6.0 (multi-agent core + config + context architecture + PID-liveness lazy-cleanup). Tip at merge commitb368973 Merge branch 'feat/v0.6-multi-agent-config' into main; taggedv0.6.0. Installed binary at%LOCALAPPDATA%\ctask\bin\ctask.exerefreshed tov0.6.0viajust install.- Active branches: none —
feat/v0.6-multi-agent-configwas merged (--no-ff) and deleted.mainis the only branch. - Pending action: none for v0.6. v0.7 (
ctask adopt/workspace.mode: adopted) is the next theme perv0.6-spec.mdNon-Goals. - Remote: none (local-only, intentional — see
CLAUDE.md). ctask doctorreports 5 pass/fail + 2 seed-directory + 1CTASK_PROJECT_ROOTcheck + 1Session modeINFO 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).
launch_dirfield inTaskMeta(175fbb0)- New
LaunchDir stringwithyaml:"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.
- New
workspace.ResolveLaunchhelper (dcb1610)- Converts a relative
launch_dirinto 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-IsNotExiststat 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.
- Converts a relative
- Project subdirectory scaffolding (
7cfafdc)ctask new --projectcreates an empty subdir named after the final suffixed slug (sodup-2collisions produce matchingdup-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.
CTASK_LAUNCH_DIRenv var (509a6d6)- Added as a 7th arg to
config.EnvVars. All three cmd callers (new,resume,open) passws.Meta.LaunchDir.
- Added as a 7th arg to
- Session launch routed through
LaunchDir(103f2cd)LaunchOpts.LaunchDiradded.session.RuncallsResolveLaunchbefore banner/exec, prints any warning to stderr, aborts on security error, and passes the absolute path toshell.ExecAgent/shell.ExecShellas the child's working directory.- Banner gains a
[ctask] project dir: <name>/line whenlaunch_diris set. - Lease, manifest, heartbeat, and summary scope stays the workspace root — only the child's cwd changes.
ctask infoshows launch fields (cdff7f3)- For workspaces with
launch_dirset, info printsLaunch dir:,Launch path:, andDir exists: yes|no(via directos.Stat, not viaResolveLaunch— info is a display command, not a launch command).
- For workspaces with
SearchRootsdefault fallback (47430a1)- When
CTASK_PROJECT_ROOTis 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
scanAllRootsprevents duplicate results when the same workspace is reachable via both the depth-2 scan underCTASK_ROOTand the explicit$CTASK_ROOT/projects/search root.
- When
- Doctor
CTASK_PROJECT_ROOTcheck (70bd167)- Three-state, matching the
checkSeedDirpattern:[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.
- Three-state, matching the
- 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.
- Status line helpers show effective launch path (
0976dce)- Both
.shand.ps1buildDISPLAY_PATH = $CTASK_WORKSPACE + (CTASK_LAUNCH_DIR if set). WhenCTASK_LAUNCH_DIR != CTASK_TASK, the tag becomes|project:<launch_dir>(user-overridden launch directory).
- Both
- Docs updated (
82c9445)docs/commands.md: workspace-layout diagram,launch_dirsemantics (default, override, fallback vs error),CTASK_LAUNCH_DIRenv var, doctor example with new INFO line, Query Resolution default-discovery paragraph.
- 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, andctask info. - Fix:
internal/workspace/create.gonow usestime.Now()(local) for the directory-prefix date and theYYYYMMDD-HHMMSSID.cmd/info.goformatsCreated/Updated/Archivedwith.Local(). - Stored timestamps still UTC.
task.yamlCreatedAt/UpdatedAt/ArchivedAt, session logs, lease, manifest, and summary all continue to store UTC. Only user-facing surfaces (directory prefix + info display) switched. - Regression guards:
TestCreateDirectoryPrefixUsesLocalDateandTestInfoFormatsTimestampsInLocalZoneenforce both invariants.
Round 2 — Linux portability baseline (7a7b249, 1033072)
- Build targets (
justfile):build-linux,build-windows,build-alloutput todist/. Both cross-targets forceCGO_ENABLED=0so the artifact is pure-Go statically linked regardless of build host (a native Linux build otherwise defaults toCGO_ENABLED=1and links against host glibc). - POSIX install scripts (
scripts/install.sh,scripts/uninstall.sh): mirror the.ps1UX. 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 helperctask-statusline.shships alongside the binary; the.ps1helper is intentionally not installed on Linux. Workspace data is preserved on uninstall. .gitignorenow coversctask,ctask-*,dist/(in addition to*.exe).WorkspacePathremoved fromTaskMeta. The audit (audit-report.md) confirmed the field was write-only with no production readers — it persisted an absolute Windows path intotask.yamlthat would have been misleading on cross-OS shares. Existingtask.yamlfiles withworkspace_pathcontinue to load (Go's YAML unmarshal silently ignores unknown fields).TestMetaTypeMissingDefaultsToTaskkeeps 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=1green;just build-linuxproduces astatically linkedELF (lddreportsnot a dynamic executable);./scripts/install.shinstalls to~/.local/bin/ctaskand registers the statusline helper;ctask doctorrecognizes 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>— streamnotes.mdto stdout raw, no framing;[ctask]-prefixed stderr on missing file (SilenceErrors: trueso 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/-aflag was dropped frominfoentirely; theStatus:line in info output surfaces archived state.deleteandopenkeep their--allflags (they are potentially destructive).resumeandarchiveresolve 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 XGenuine 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 invariantTestListNamesCandidatesResolveUniquelyenforces 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.ValidArgsFunctionhooks per spec policy:resume,archive→ active onlyrestore→ archived onlyinfo,notes,path→ bothdelete,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 withlist --all/info/notes/pathbefore 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=1green across all 7 packages;go vetclean;go buildclean. 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 (filereportsstatically linked;lddreportsnot a dynamic executable).
Known limitation (v0.5.2)
Resolved in v0.5.4 (ctask resume <archived-ws>printsError: workspace archivedbelow the actionable hint.ae9bfaf) via theerrArchivedWorkspacesentinel + conditionalSilenceErrors. 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);directis the default and requires no setup. Unknown values fall back todirectwith a stderr warning. ctask attach <workspace>— always-tmux entry command (ignoresCTASK_SESSION_MODE). Defaults to launching the agent; the--shellflag swaps in an interactive shell.--directflag onnew/resume/last/opento 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 doctorreportsSession mode: direct|persistentplus 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 viaPreflightOpts.ActiveLeaseHint/LaunchOpts.ActiveLeaseHint; computed incmd/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 reflectsfilepath.Base(os.Args[0])instead of a hard-coded "ctask". Local-build PowerShell users running.\ctask.exeseectask.exe new <ws> --direct; installed contexts continue to seectask. 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 ofsha256(canonical absolute workspace path). On Windows the path is lowercased before hashing (matchessearchRootKey). 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 viashell.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.UpdatedAtis bumped only on successful adoption, a fresh start manifest is captured, and finalize stampssession_ownership: "adopted"plusadopted_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 (
shouldRunProvisionalreturns false onSessionMode == "persistent") — the gate's UX assumption ("Esc on prompt → empty diff → reclaim") does not translate to tmux. last-session-summary.jsongains four optional fields (end_reason,detected_via,session_ownership,adopted_from_orphan_at); allomitemptyso 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 setsWSL_DISTRO_NAMEautomatically, so WSL paths pass. macOS / Linux pass naturally. ctask newruns the persistent preflight BEFOREworkspace.Create— a missing tmux must not leave a half-initialized workspace on disk.
Validation:
- Windows host:
go test ./... -count=1green across all 7 packages;go vetclean;go buildclean;just build-linuxproduces 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
c204d87for 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-clientfor nested-tmux entry,tmux wait-for/set-hook-based detection, banner injection inside tmux,ctask sessionslisting 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-remotectaskruns on the remote, not the local binary. v0.5.4 audit confirmed this split is the right line (spec §2 codified it: command-form viainvocationName(), product-identity literal). Closed. Adoption requires waiting 60s for the previous owner's lease to go stale (RESOLVED in v0.6 Phase 3. A lease whose owner PID is confirmed dead on the local host is now treated as stale immediately via the PID-awareStaleLeaseAfter).IsStalepredicate — the 60s wall-clock wait no longer applies to a Ctrl-C'd / terminal-closed local session. See "What v0.6 Phase 3 delivered".
What v0.5.4 delivered
Theme: session visibility + polish. Pure polish — no new subsystems, no new metadata fields, no behavioral changes to session lifecycle. Merge commit 10b7d5a.
Commit list:
7f2c43dfeat(v0.5.4): SessionStatus display-only helper— newinternal/session.SessionStatus(wsDir)returning{State, Mode, PID, Hostname, Diagnostic}. Pure read of.ctask/session.jsonwith no tmux invocation, no PID liveness, no lock acquisition, no mutation. States:none|active|stale. Pre-v0.5.3 leases without amodefield default todirectfor display only. Display-only contract — lifecycle code keeps usingReadLease/IsFresh/InspectLease.e0e9cd7feat(v0.5.4): info Session block—ctask infoadds a Session block betweenPath: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 usesinvocationName(). Malformed leases render as stale with a diagnostic and no Mode/Owner/Attach rows. Exposessession.CurrentHostname()so the cmd layer has a single source of truth for the local-vs-remote check.0c8076afeat(v0.5.4): list SESSION column—ctask listgets a SESSION column inserted right of STATUS. Values:direct,persistent,stale, or em dash. Archived workspaces always show em dash (display simplification —infostill surfaces the raw lease state for diagnostic purposes). One short lease-file read per workspace;ctask list --namesis unchanged (basename-only, no header, no session column — protected byTestListNamesUnchangedHasNoSessionColumn).0fb8de6polish(v0.5.4): invocation-name audit + regression tests— audit walked everycmd/andinternal/file producing user-facing text. No production code changes were needed — v0.5.3'sc204d87had already routed every command-form hint throughinvocationName(). 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. ExtractedformatResumeRestoreHintandformatDirectModeTmuxHintas testable string-only helpers; production paths are behaviorally identical.4fd0befdocs(v0.5.4): rewrite commands.md as a structured reference—docs/commands.mdwas 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 canonicalctaskname per spec — docs describe the product, not the user's local binary path.ae9bfafpolish(v0.5.4): suppress Cobra duplicate Error on archived resume— closes the v0.5.2 cosmetic-only known limitation. NewerrArchivedWorkspacesentinel;runResumeflipsSilenceErrors=trueonly 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 CobraExecute()(not justrunResumedirect invocation) so the SilenceErrors-flip-from-RunE timing is exercised byTestResumeArchivedHintNoDuplicateError.7704cd9release(v0.5.4): bump version to 0.5.4.10b7d5aMerge branch 'feat/v0.5.4-session-visibility-polish' into main.
Validation:
- Windows host:
go test ./... -count=1green across all 7 packages;go vet ./...clean;just buildproducesctask.exe;just build-linuxproduces a statically linked ELF (filereportsstatically linked, not stripped);just installsucceeded; installedctask --versionreports0.5.4;ctask doctor5/5 pass. - Smoke (synthetic fixtures):
ctask listshows SESSION column withpersistent/direct/stale/ em dash;ctask list --allshows em dash for archived even with stranded lease;ctask list --namesremains basename-only with no whitespace and no session tokens;ctask info <active-persistent>shows the Session block with hostname omitted (local) and Attach hint viainvocationName();ctask resume <archived>prints only the[ctask]block + restore hint (no trailing CobraError: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.jsonand asession_modefield; the actual existing implementation uses.ctask/session.jsonandmode. 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.jsonandmodeunless intentionally renaming the metadata. - The invocation-name audit found no production hardcoded command-form hints to change. v0.5.3's
c204d87had 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 (usesinvocationName()) and product-identity (literal"ctask") is the codified rule per spec §2.
What v0.6 Phase 1 delivered (branch feat/v0.6-multi-agent-config, NOT merged)
Theme: infrastructure foundation for v0.6 — global config file, schema versioning in task.yaml, and source attribution in doctor/info. Five commits on the feature branch; no version bump (lands at end of Phase 3). All Phase 1 spec items (v0.6-spec.md sections 1–4) covered, plus a follow-up commit added on reviewer request for the native-Windows session_mode launch-time warning.
Commit list (oldest → newest)
6f80c8bfeat(v0.6): config file parser + resolver + source attributioninternal/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.yamlon native Windows).internal/config/resolver.go—Resolver/ResolvedSetting/SettingSourcewith five values (Builtin,ConfigFileSrc,EnvVar,CLIFlagreserved for v0.6 Phase 2,PlatformOverride). Each setting carries anOverridechain so doctor can renderenv var (overrides config file: X). Per-setting methods:CtaskRoot,ProjectRoot,SeedDir,DefaultAgent,DefaultCategory,Editor,SessionMode.SessionModeapplies the native-Windows platform override at the resolver layer with the configured value chained asOverride. ExportsSetConfigPathForTestandSetIsNativeWindowsForTestso cross-package tests can isolate.internal/config/config.go— legacyResolveRoot/ResolveAgent/ResolveSeedDir/ResolveProjectRoot/ResolveSessionModewrappers migrated toLoadResolver()so config-file values take effect for entry commands.ResolveProjectRootpreserves the""sentinel forBuiltinsource soSearchRootsand doctor'scheckProjectRootkeep their v0.5 fallback semantics.ResolveSessionModekeeps 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.
0b21b8dfeat(v0.6): schema_version and workspace.mode in task.yamlCurrentMetaSchemaVersion = 1constant +WorkspaceSection struct{ Mode string }nested block.TaskMetagainsSchemaVersion intandWorkspace WorkspaceSectionfields at the top of the struct. Bothomitempty— this is what enforces the no-opportunistic-writes invariant: legacy task.yaml files (noschema_version, noworkspace:) round-trip throughWriteMeta/WriteMetaLockedwithout 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 aboveCurrentMetaSchemaVersionwith 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).ReadMetanow runs both validators after YAML unmarshal. The error includes the workspace slug (derived from the file'sslug:field or the directory basename when the file itself is corrupt).workspace.Createstamps every new meta withSchemaVersion: 1andWorkspace.Mode: "native". This is the ONLY write site for these fields in v0.6.- Tests: 10 cases including the dedicated
TestLegacyTaskYamlNotBackfilledByWriteandTestLegacyTaskYamlNotBackfilledByLockedWriteregressions that pin the no-opportunistic-writes invariant.
c918e5cfeat(v0.6): doctor Settings section with source attribution- New
── Settings ──block appended between the existingcheckTmuxline and theN checks passed, M failedsummary. Loaded viaconfig.LoadResolver()exactly once perrunDoctorinvocation 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, incrementsfailed). - Then iterates
r.CtaskRoot() / ProjectRoot() / SeedDir() / DefaultAgent() / DefaultCategory() / SessionMode() / Editor()and renders each viaprintSettingLine(key/value/source trio + chained-override info).PlatformOverrideadds aconfigured: <value>row so doctor surfaces both the effective and the user-asked-for value. - Helper
formatSettingSourcecentralises theEnvVar → 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: persistentrendering.
- New
937a1c8feat(v0.6): info source attribution on Agent and Launch session modecmd/info.go::runInfocallsconfig.LoadResolver()once and reuses it.Agent:line gains(workspace)whenm.Agentis 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 betweenAgent:andCreated:, 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
infoSourceLabelis the info-side counterpart toformatSettingSource. 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 < Createdin the rendered output.
6182d89feat(v0.6): platform-override stderr warning on launch paths- Reviewer follow-up: the v0.6 spec section 1.8 also calls for a stderr warning at launch time when persistent mode is downgraded; commits 1–4 covered the doctor/info display path but the launch paths were still downgrading silently. This commit fixes that.
- New helper
cmd/persistent.go::emitPlatformOverrideWarningIfNeeded(alwaysPersistent bool)— loads the resolver, emits the warning on stderr whenSessionMode()reportsPlatformOverridesource AND the caller is notAlwaysPersistent. - Single call site at the top of
cmd/entry.go::defaultRunWorkspaceEntry, before any launch work. That function is the funnel fornew,resume,last,open(which can downgrade) ANDattach(which can't). attach setsAlwaysPersistent: 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-existingTestPreflightRefusesNativeWindows.
Verification (run on feat/v0.6-multi-agent-config tip 6182d89)
go test ./... -count=1— all 7 packagesok, 0 failures.go vet ./...— exit 0.go build -o ctask.exe .— exit 0.just build-linux— producesdist/ctask-linux-amd64, statically linked ELF (filereportsstatically linked).- Version remains
v0.5.4; bump deferred to the end-of-Phase-3 commit perv0.6-spec.md"Commit ordering".
Phase 1 constraints held
- No config auto-creation.
LoadConfigFilereturns(nil, nil)onIsNotExist; 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 throughWriteMetaLocked(the path used byresume/archive/restore) without acquiringschema_versionorworkspace:. Pinned byTestLegacyTaskYamlNotBackfilledByWrite+TestLegacyTaskYamlNotBackfilledByLockedWrite. - Env vars preserved as overrides. The resolver layers them above config; no deprecation. Override chain captured in
ResolvedSetting.Overrideso doctor rendersCTASK_X env var (overrides config file: Y). - task.yaml remains workspace state. Phase 1 does NOT introduce per-workspace fields like
agent.typeor 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--agentflag onctask new, no AGENTS.md / CLAUDE.md shim generation, noctask agents check, nocontext/notes-archive/scaffolding.internal/seed/templates.gounchanged. - No Phase 3 work started. Verified by diff: no PID-liveness logic, no changes to
internal/session/lease.goorinternal/session/adopt.go, no change to the 60sStaleLeaseAfterthreshold 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: persistentwhen config says persistent on native Windows. Info'sLaunch 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,openall funnel throughdefaultRunWorkspaceEntry, which callsemitPlatformOverrideWarningIfNeeded(false)at the top. Exactly one stderr line per process invocation. No warn-once subsystem — the call site frequency is what enforces "once". ctask attachdoes NOT downgrade. attach setsAlwaysPersistent: true, which (a) bypasses the warning helper and (b) routes throughpreflightPersistentEntrywhich 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 manyResolver.X()accessors. LegacyResolve*wrappers (ResolveRootetc.) each construct a fresh resolver — acceptable for entry commands that call them once or twice; new code incmd/should follow the doctor/info pattern. - Test seams are exported.
config.SetConfigPathForTest(t, path)andconfig.SetIsNativeWindowsForTest(t, f)are exported helpers socmd/-package tests can isolate from developer-host config files and simulate platform-override scenarios withoutruntime.GOOSskips. - Validation lives in
ReadMeta, not in callers. The schema-version and workspace-mode checks happen at read time — so any path that gets a*TaskMetahas already passed validation. Callers do not need to re-check. omitemptyis 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 onlyworkspace.Create(or an explicit migration) writes a non-zero value.
What v0.6 Phase 2 delivered (branch feat/v0.6-multi-agent-config, NOT merged)
Theme: the multi-agent layer — ctask is now agent-agnostic. task.yaml carries an agent profile (type / command / args / env), ctask new --agent selects the agent type, the launch path carries a resolved command + args + env end-to-end, new workspaces get an AGENTS.md canonical instruction file (CLAUDE.md becomes a thin shim), and ctask agents check validates agent configuration. Six commits on the feature branch; no version bump (lands at end of Phase 3). All Phase 2 spec items (v0.6-spec.md §5–§7, plus the §8 seed scaffolding that rides along with the template change) covered.
Commit list (oldest → newest)
8120c39feat(v0.6): AgentSpec field on TaskMeta with backward-compat unmarshalTaskMeta.Agentchanges fromstringtoworkspace.AgentSpec{Type, Command, Args, Env}. CustomUnmarshalYAMLaccepts both the v0.6 mapping form and the legacy scalar form. Legacy scalar handling (corrected at plan review): a scalar matching a built-in name (claude,opencode) maps toAgentSpec{Type: <scalar>}; any other scalar (a path,aider, etc.) maps toAgentSpec{Type: "custom", Command: <scalar>}; a missingagent:key leavesTypeempty so the resolver fills indefault_agent. The scalar value is NEVER dropped — a legacy workspace keeps launching the agent it was created with.IsBuiltinAgentType(name)helper ininternal/workspace(true forclaude,opencode) — the built-in/custom discriminator used by the unmarshaler.internal/agent.BuiltinProfilesmirrors the same set.ValidateAgentSpec(run insideReadMeta): known type only;type=customrequirescommand;commandrejects whitespace and shell metacharacters (|&;<>()$\``) with a hint pointing atargs` — ctask does not shell-split.CreateOpts.Agent string→CreateOpts.AgentSpec workspace.AgentSpec. cmd-layer call sites patched to the minimum needed to compile (full wiring lands in commit 3).
24f2134feat(v0.6): internal/agent package — Resolve + BuiltinProfiles- New
internal/agentpackage:Profile,BuiltinProfiles(claude,opencode),IsKnownType(those two +custom),Resolved{Type, Command, Args, Env}, andResolve(spec, defaultAgent). Resolution: emptyTypefalls through todefaultAgent;customrequirescommand; otherwise command isspec.Commandor the built-in default. No I/O — PATH lookup stays inshell.ExecAgentandctask agents check, soResolveis trivially testable.
- New
b75b82efeat(v0.6): launch path carries ResolvedAgent (command + args + env)LaunchOpts.Agent stringandWorkspaceEntryOptions.Agent string→*agent.Resolved. All five entry commands (new,resume,last,open,attach) construct anAgentSpec, apply--agentas a one-shotagent.commandoverride (Open Q 1 — resume/last/attach--agentstays a command override, NOT a type selector), callagent.Resolve, and pass the result through.cmd/entry.go::resolveEntryAgentcentralises the resume/last/open/attach path.shell.ExecAgentandshell.ExecTmuxAgentgain anargs []stringparameter.agent.envis merged into the child environment at thesession.Runlaunch switch, AFTER ctask's ownCTASK_*exports —agent.envwins on collision per spec §5.session.mergeAgentEnvis the centralised merge;var execAgent = shell.ExecAgentis a new test seam.- Lease, manifest, write lock, heartbeat, summary, and provisional cleanup are untouched. The
Agent stringfields onLease/SessionSummary/SessionInfostill record the resolved command string for diagnostics.
a61f900feat(v0.6): --agent flag on ctask new selects agent typeResolvergainscliFlagAgent+SetCLIFlagAgent;DefaultAgent()layersCLIFlagaboveEnvVarso doctor/info render the full precedence chain.SettingSource.CLIFlag(reserved in Phase 1) is now reachable.ctask new --agent <type>writesagent.typeinto the new workspace's task.yaml. Resolution + validation run BEFOREworkspace.Create, so--agent customwith no companion command refuses (type "custom" requires command) without leaving a half-created workspace on disk. The deferred Phase 1 testTestCLIFlagOverridesEnvVarlanded here.
0c6ed0cfeat(v0.6): AGENTS.md seed + CLAUDE.md shim + handoff + context-archive scaffold- New workspaces get
AGENTS.md(canonical, always — handoff workflow, notes-archive convention with a ~300-500 line trigger, cross-workspace discovery, do-not-touch warnings; project variant adds workspace-structure + git-conventions sections), aCLAUDE.mdshim (claude type only — opencode shim deferred),handoff.md(minimal current-state template), andcontext/notes-archive/.gitkeep. seed.ClaudeMDandseed.ClaudeMDProjectdeleted — no callers remain.seed.NotesMDretained. Existing workspaces are NOT retroactively modified (pinned byTestCreateDoesNotModifyExistingWorkspace).
- New workspaces get
0f96d20feat(v0.6): ctask agents check + doctor integrationctask agents check [workspace]— pure validation, no launch: agent type known, command resolvable on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present (WARN, claude only).agent.envkeys displayed informationally; a WARN line names any key shadowing aCTASK_*export. Non-zero exit on any FAIL.ctask doctorruns the same sweep against the most-recently-active workspace (workspace.MostRecentActive); skips withAgent check: skipped (no workspace context)when none exists.runAgentsCheckOnWorkspaceis shared between the standalone command and doctor.TestCompletionSubcommandViaExecutewas made order-independent: cobra's defaultcompletioncommand captures the root output writer once, on the firstExecute()in the process — the new agents-check tests run anExecute()earlier in the suite, so the test now drops any pre-created completion command before its ownExecute().
Verification (run on feat/v0.6-multi-agent-config tip 0f96d20)
go test ./... -count=1— all 8 packagesok(the newinternal/agentpackage included), 0 failures.go vet ./...— exit 0.just build—ctask.exebuilds locally.just build-linux— producesdist/ctask-linux-amd64, statically linked ELF.- Version remains
v0.5.4; bump deferred to the end-of-Phase-3 commit.
Phase 2 constraints held
- No PID liveness / lazy-cleanup / adoption changes.
internal/session/lease.gountouched;internal/session/adopt.gochanged only for the mechanicalResolvedAgentpassthrough. The 60sStaleLeaseAfterthreshold is unchanged. - No per-workspace
session_mode. No new field onTaskMetabeyond theAgentSpec. - No user-defined named agent profiles in config.
internal/config/configfile.gois unchanged in Phase 2 — agent profiles live in task.yaml, not config.yaml. - No opencode-specific shim.
writeBuiltinDefaultswritesCLAUDE.mdonly whenagent.Type == "claude". - No auto-modification of existing workspaces. All new seed files are written only by
workspace.Create. - No version bump, no merge.
version.gountouched; branch staysfeat/v0.6-multi-agent-config.
Architecture notes (worth preserving)
internal/agent.Resolveis I/O-free. PATH validation is deliberately NOT inResolve— it lives inshell.ExecAgent(fail-fast at launch) andctask agents check. This keeps resolution a pure function and letsagents checkvalidate without launching.agent.envprecedence is intentional. It merges AFTER ctask'sCTASK_*exports and wins on collision (spec §5). This is a feature, not a bug —agents checksurfaces the shadowing with a WARN so the user is not surprised.- Two parallel built-in-type lists, kept in sync deliberately.
workspace.IsBuiltinAgentType/workspace.knownAgentTypesandinternal/agent.BuiltinProfilesboth enumerate the built-ins.internal/workspacecannot importinternal/agent(the dependency runs the other way), so the lists are mirrored, not shared. When a new built-in lands, update all three. - AGENTS.md is canonical; CLAUDE.md is a shim. The shim exists only to point Claude Code at AGENTS.md. The seed-overlay rule still applies — a user seed dir's
AGENTS.md/CLAUDE.mdoverrides the built-in. - cobra's default
completioncommand captures the output writer once.InitDefaultCompletionCmdsnapshotsc.OutOrStdout()into thebash/zsh/etc. subcommand closures on the firstExecute()anywhere in the process. Tests that driverootCmd.Execute()with a redirected output buffer and then assert on completion output must drop the pre-created completion command first.cmd/agents_check_test.go::captureRootCmdrestoresrootCmd's out/err/args on cleanup to limit this class of cross-test contamination.
What v0.6 Phase 3 delivered (branch feat/v0.6-multi-agent-config, NOT merged)
Theme: lazy-cleanup via PID liveness — a lease whose owner process is
confirmed dead on the local machine is treated as stale immediately,
without the 60-second wall-clock wait. Closes the v0.5.3 known limitation
(Ctrl-C / terminal-close then immediate ctask resume). The v0.6.0
version bump rides along. Four commits; v0.6-spec.md §8 (context-file
scaffolding) was already delivered in Phase 2 commit 0c6ed0c, so §9
(PID liveness) was the only remaining Phase 3 feature.
Commit list (oldest → newest)
9070c42feat(v0.6): tri-state PID liveness probe (ProcessAlive/Dead/Unknown)- New
internal/session/pidcheck.go(ProcessStatetri-state +checkProcesstest seam) and build-tagged platform files:pidcheck_unix.go(syscall.Kill(pid, 0)—nil/EPERM→Alive,ESRCH→Dead, else→Unknown) andpidcheck_windows.go(syscall.OpenProcess— opens→Alive,ERROR_INVALID_PARAMETER(87)→Dead, else→Unknown). Stdlibsyscallonly; nogolang.org/x/sysdependency,go.modunchanged.
- New
f379a6dfeat(v0.6): IsStale supplements wall-clock freshness with PID livenessIsStale(l, now, threshold)inlease.go. Parameterized free function mirroringIsFresh(not the spec's zero-arg method form — deviation approved at plan review to preserve the package's injected-clock testability). PID liveness applies only to local leases (l.Hostname == currentHostname()) withpid > 0; remote leases,pid <= 0, andProcessUnknownfall back to wall-clock. Wall-clock staleness is checked first and wins unconditionally — PID liveness only flips fresh → stale, never stale → fresh.
d575dddfeat(v0.6): route lease-freshness callsites through IsStale- Four freshness consumers now route through
IsStale:InspectLease,CleanupStaleLease,runActiveLeaseCheck, andstatusAt.SessionStatus/ctask list/ctask inforeflect PID liveness automatically — only the one-linestatusAtpredicate swap was needed; theStatusstruct and all cmd-layer rendering are untouched. Also corrected three cmd-package session-display test fixtures (list_session_test.go,info_session_test.go) that built "active" leases with the local hostname but synthetic PIDs — now that freshness is PID-aware, an honest "active" fixture must useos.Getpid().
- Four freshness consumers now route through
beb5174chore(v0.6): bump version to 0.6.0cmd/root.goversion0.5.4→0.6.0.ctask --versionreportsctask v0.6.0.
Verification (run on tip beb5174)
go test ./... -count=1— all 8 packagesok, 0 failures.go vet ./...— exit 0.just build—ctask.exe(PE32+ x86-64).just build-linux—dist/ctask-linux-amd64, statically linked ELF (the only check that compilespidcheck_unix.go).- Binary smoke (v0.6.0
ctask.exe, real workspaces under a tempCTASK_ROOT) — passed. A lease carrying a reaped, genuinely-dead owner PID, the real host's hostname, and a 9-second-old heartbeat (unambiguously wall-clock-fresh) was auto-cleaned byctask resumewith[ctask] Cleaned up stale session … last seen 9s ago— no Layer-1 "Continue anyway?" prompt, no 60s wait, resume proceeded (exit 0).ctask info/ctask liston the same kind of lease reportstale(they are read-only — they do not remove it). The dead PID alone (not wall-clock age) drove the staleness, confirming PID liveness end-to-end in the shipped binary. - Smoke finding worth keeping: on Windows, if another process holds an
open handle to the dead owner (e.g. a parent that has not yet reaped
it),
OpenProcessstill succeeds anddefaultCheckProcessreturnsProcessAlive— the documented conservative "zombie handle" case. The lease then falls back to wall-clock. The real Ctrl-C / terminal-close path has no such lingering handle, so PID liveness fires as intended.
Phase 3 constraints held
- Four-layer concurrency model unchanged — PID liveness only makes Layer 1's "is this lease stale?" question smarter.
StaleLeaseAfter(60s) unchanged; PID liveness supplements it and remains the fallback for remote leases and inconclusive checks.- Lease creation, heartbeat, write lock, manifest, and summary shapes
unchanged.
adopt.gountouched. - Remote leases remain wall-clock-only (PID checks skipped when the lease hostname differs from the current host).
IsFreshretained as the pure wall-clock primitiveIsStalebuilds on.- No new agent/profile/config/template work.
Architecture notes (worth preserving)
IsStaleis the single freshness predicate for stale-detection decisions. All four callsites route through it;IsFreshis now an internal building block (still exported, still directly tested).- PID liveness is conservative by construction. Only a definitive
ProcessDeadon a local lease shortcuts the wait.ProcessUnknown(permission errors, unexpected OS errors) and remote leases preserve the pre-v0.6 wall-clock behavior exactly. - No
golang.org/x/sysdependency. Windows process probing uses stdlibsyscall.OpenProcess;PROCESS_QUERY_LIMITED_INFORMATION(0x1000) andERROR_INVALID_PARAMETER(87) are local constants. - Known conservative edge cases (acceptable — they never falsely declare a live owner dead): OS PID reuse reads the recycled PID as alive → wall-clock fallback; a Windows zombie handle reads as alive.
- Plan:
docs/superpowers/plans/2026-05-15-v0.6-phase3-implementation.md.
Historical: original Phase 1 plan (now shipped — kept for traceability)
Phase 1 scope (only thing to start next):
- Config file parsing + resolver + tests. Read
~/.config/ctask/config.yaml(XDG) and Windows equivalent. Layered resolution: env var > config file > built-in default. Surface every existingCTASK_*setting through the new resolver without changing default behavior. Tests cover layering, malformed YAML, missing file, env-var override. schema_versionfield intask.yaml. Bump to1. Old workspaces (noschema_version) parse silently and are treated as version 0 (current behavior). Newctask newwrites version 1.workspace.modefield intask.yaml. Replaces the implicit "alwayslocal" assumption. New workspaces default tolocal; 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 inv0.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
StaleLeaseAfterrevisit). orphanedsession 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
maintip isb368973 Merge branch 'feat/v0.6-multi-agent-config' into main— the v0.6.0--no-ffmerge commit. The merged feature work spanned 18 commits (Phase 1 ×6, Phase 2 ×7 incl. closeout, Phase 3 ×5 incl. closeouts).- Tag
v0.6.0points atb368973. Local tags:v0.5.3,v0.6.0(no remote — local-only perCLAUDE.md; v0.5.4 was never tagged). feat/v0.6-multi-agent-confighas been deleted (git branch -d, fully merged; last tip wasc538e23).mainis the only branch.- Post-merge validation on
main:go test ./... -count=1all 8 packagesok;go vetexit 0;just build+just build-linuxclean;just installrefreshed the installed binary;ctask --version→ctask v0.6.0.ctask doctorexits 1 with a single[FAIL] AGENTS.md missing— this is the legacy workspacemicrocenter-gpu-watcher(pre-v0.6, hasCLAUDE.mdbut noAGENTS.md); v0.6 deliberately does not retrofit existing workspaces, soagents checkcorrectly flags it. Not a regression. - No tag pushed for v0.5.4 (no remote — the project is intentionally local-only per
CLAUDE.md). v0.5.3 hadgit tag v0.5.3locally; v0.5.4 has none. No v0.6 tag yet — a post-merge task. - Installed
ctask.exeat%LOCALAPPDATA%\ctask\bin\ctask.exeis still v0.5.4 — the v0.6 branch has NOT been installed. Localctask.exein the repo root is abeb5174build reportingv0.6.0.dist/ctask-linux-amd64is the Phase-3 Linux cross-build (statically linked ELF). - Memory follow-ups (see
memory/MEMORY.md):feedback_design_for_lazy_cleanup— the 60s-freshness-wait concern it raised is addressed by v0.6 Phase 3 (PID-awareIsStale). The underlying principle (lifecycle UX must recover from Ctrl-C / terminal close) remains a live design value.feedback_invocation_name_in_hints— partially closed by the v0.5.4 audit (split between command-form and product-identity is now codified). Memory entry retained for the descriptive-prose question.
- Untracked files (do NOT touch without asking — pre-existing session-local working docs, unchanged from this session):
.claude/settings.local.json(modified — Claude Code local settings)bugfix-provisional-workspace.md(spec for the 2026-04-22 initial provisional fix; may be deleted or archived)docs/superpowers/plans/2026-04-06-install-workflow.md(install-workflow plan from an earlier session)docs/superpowers/plans/2026-04-21-v0.4-implementation.md(v0.4 plan — executed)docs/superpowers/plans/2026-04-22-v0.4.1-patch.md(v0.4.1 plan — executed)docs/superpowers/plans/2026-04-22-v0.5-implementation.md(v0.5 plan — executed)docs/superpowers/plans/2026-05-08-v0.5.3-implementation.md(v0.5.3 plan — executed; was claimed-committed in earlier notes but actually stayed untracked)v0.4-spec.md,v0.4.1-patch-spec.md,v0.5-spec.md,v0.5.3-spec.md,v0.5.4-spec.md(specs the implementations followed;v0.5.4-spec.mdis 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 duplicateError:line is suppressed)docs/commands.md(rewrite — see commit4fd0bef)
- 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.gomodified (5 legacy wrappers migrated to LoadResolver)internal/config/config_test.go+config_roots_test.gomodified (config-path isolation in tests that assert Builtin defaults)internal/workspace/metadata.gomodified (CurrentMetaSchemaVersion, WorkspaceSection, SchemaVersion + Workspace fields on TaskMeta, EffectiveSchemaVersion + ValidateSchemaVersion + ValidateWorkspaceMode helpers, ReadMeta validation)internal/workspace/create.gomodified (writes schema_version: 1 + workspace.mode: native)internal/workspace/schema_test.go(10 tests including the two no-opportunistic-writes regressions)cmd/doctor.gomodified (── Settings ── block + checkSettings + printSettingLine + formatSettingSource)cmd/doctor_test.gomodified (config import + 2 tests gated by SetIsNativeWindowsForTest)cmd/doctor_settings_test.go(6 tests covering the new Settings block)cmd/info.gomodified (Agent source label + new Launch session mode line + agentLineWithSource + infoSourceLabel)cmd/info_attribution_test.go(5 tests including placement assertion)cmd/new_persistent_test.gomodified (config import + 1 test gated by SetIsNativeWindowsForTest)cmd/entry.gomodified (emitPlatformOverrideWarningIfNeeded call at top of defaultRunWorkspaceEntry)cmd/persistent.gomodified (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(seenotes.md). Phase 2 scope perv0.6-spec.mdsections 5–7: agent profile system in task.yaml (agent.type/agent.command/agent.args/agent.env; built-in typesclaudeandopencode, escape hatchcustom),--agentflag onctask newwith the deferredTestCLIFlagOverridesEnvVar, AGENTS.md canonical file + CLAUDE.md shim (claude type only — opencode shim deferred until conventions are verified), andctask agents checkvalidation 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 indocs/commands.md. Don't redesign without a real user complaint. end_manifestin 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.gotwo-check invariant: unchanged.CTASK_WORKSPACEmatch → refuse;.ctask/manifest-start.jsonexists → refuse. Both before any mutation.session.Run()lifecycle ininternal/session/run.go:PreflightFull(Layers 3 + 1)- Write lock → write lease (unless coexisting)
- Write lock → capture + write start manifest
- Print launch-context banner (Layer 4)
- Start heartbeat (only if we own the lease)
- v0.5:
workspace.ResolveLaunch(opts.WsDir, opts.LaunchDir)— print warning or abort on security error - Banner lines (including
project dir:when LaunchDir set) + exec child with cwd = resolved launch path - Stop heartbeat
handleProvisional(gated onNewlyCreated && childExitCode != 0 && emptyDiff) — if it removes the workspace, skip finalize entirelyfinalize: single write-lock acquire → append log + write summary + remove lease (if owned) + remove manifest-start
newis intentionally not given--force. Brand-new workspaces have nothing for Layer 1/3 to warn about.internal/lockfileis 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.Rootis how callers render relative paths and computeCTASK_ROOTenv var. Populated byscanAllRoots.scanWorkspacesis 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 == 0means preserve. Don't special-case specific codes.
From v0.5 (new — don't unlearn)
launch_diris stored as workspace-relative intask.yaml, empty for tasks and pre-v0.5 projects. Never store absolute paths. Never interpretlaunch_diroutsideworkspace.ResolveLaunch.workspace.ResolveLaunchhas 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 betweenos.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-2collision means subdir isdup-2/, notdup/. TheactualSlugvariable inworkspace.Createis 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 BEFOREtask.yamlis 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_DIRis 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 whenCTASK_PROJECT_ROOTis unset. This is redundant with the v0.4.1 two-depth scan under$CTASK_ROOTbut explicit per spec. Dedupe inscanAllRoots(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_ROOTis 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()inworkspace.Createfeeds theYYYY-MM-DDdate and theYYYYMMDD-HHMMSSID. Don't switch back to.UTC()for these. TheTestCreateDirectoryPrefixUsesLocalDateregression test enforces this. ctask infodisplays timestamps in local zone via.Local().Format(...). Stored timestamps intask.yamlare 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-linuxandjust build-windowsset 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. WorkspacePathis gone fromTaskMeta. Don't add it back. If a persistent workspace identifier is ever needed, add a properly relative-to-rootworkspace_id(forward-slash normalized). Oldtask.yamlfiles withworkspace_pathparse 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)
infono longer has--all/-a. Direct lookup is archived-inclusive by default. Don't add the flag back. TheStatus:line in info output makes archived state obvious.TestInfoFindsArchivedWorkspaceWithoutFlagenforces this.resume/archiveresolve archived-inclusive then filter. Don't switch back toincludeArchived=falseat the resolver layer. The two-step pattern (resolve broadly, reject explicitly) is what letsresumegive the actionable restore hint instead of a generic "not found".- Direct-lookup commands (
info,notes,path,restore) useincludeArchived=true. Active-only commands (resume,archive) reject archived after resolve. Potentially-destructive commands (delete,open) keep their explicit--allopt-in. Don't drift back to a uniform default — the policy is intentional. Seev0.5.2-spec.md"Workspace Lookup Policy". list --namesemits directory basenames, not bare slugs. Bare slugs can collide (e.g.,promptvolleyarchived in v1, active in v2). Basenames are unique under the resolver's exact-match step.TestListNamesCandidatesResolveUniquelyenforces the invariant.ValidArgsFunctionhooks callworkspace.ListWorkspacesdirectly. No subprocess shell-out toctask list --names. Don't add one — direct calls are faster and don't depend on PATH state.- Cobra adds the
completionsubcommand lazily on firstExecute(). A test that callsrootCmd.Find("completion")before anyExecute()returns "unknown command". For unit tests, prefer therootCmd.GenXxxCompletion(...)generators directly. For end-to-end, oneSetArgs(...)+Execute()per test — running multipleExecute()calls in succession with different shell args has state issues. notesusesSilenceErrors: trueso the[ctask] no notes.md found in workspace "X"stderr line is the only diagnostic the user/agent sees. Don't setSilenceErrors: falseand 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_MODEis the only persistent-mode trigger. No flag promotes a single command to persistent.ctask attachis the inverse — it always uses tmux regardless of env. Don't add a--persistentflag; the existingdirect↔persistent↔attachtriangle covers every use case.- tmux command construction lives in exactly one place per operation —
internal/shell/tmux.go.AttachExistingandAdoptExistingPersistentSessionuse shell primitives via test seams (adoptAttacher,adoptPoll,attacher); they do NOT hand-roll their ownexec.Command("tmux", ...)calls. If you find a freshexec.Command("tmux", ...)outsideinternal/shell/tmux.go, that's drift — fix it. session.Runnever callsexec.LookPath("tmux"). The cmd-layer preflight (cmd/persistent.go::preflightPersistentEntry) is the single source of truth for tmux discovery; the validated path flows throughLaunchOpts.TmuxPath.Runerrors ifTmuxPath == ""in persistent mode.- The persistent-mode dispatcher is
cmd/entry.go::dispatchPersistent(hasTmuxSession, leaseState)— a pure function. Three outcomes:dispatchOwnerCreate,dispatchPassive,dispatchAdopted. The cmd-layerrunWorkspaceEntryis a package-level variable (test seam); per-command tests stub it to assert each entry command produces the rightWorkspaceEntryOptions. 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 matchsearchRootKey. 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. AdoptExistingPersistentSessionbumpstask.yaml.UpdatedAtONLY on successful adoption, not on the race-guard fall-through. TheTestAdoptionBumpsUpdatedAtOnSuccessandTestAdoptionRaceGuardFallsThroughAndDoesNotBumpUpdatedAttests enforce both branches.- A fresh remote lease (
LeaseStateFreshRemote) is NEVER silently overwritten.cmd/persistent.go::confirmFreshRemoteAdoptionprompts 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)gateshandleProvisionaland isfalsein 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 testTestShouldRunProvisionalenforces this.finalizestampsEndReason/DetectedVia/SessionOwnershipbased onopts.SessionMode— direct:child_exited/child_exit; persistent owner-create:tmux_session_ended/polling/created; adopted: same plusadoptedandAdoptedFromOrphanAt. These fields areomitemptyso 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 setsWSL_DISTRO_NAMEautomatically, so WSL paths pass. Don't replace this with aruntime.GOOS == "linux"allowlist — that breaks macOS. ctask newruns the persistent preflight BEFOREworkspace.Create. A missing tmux must not leave a half-initialized workspace on disk. Thecmd/new.goordering 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 "missingmodefield defaults todirect" rule and the malformed-lease-as-stale classification are display choices, not behavioral truths. Lifecycle code keeps usingReadLease/IsFresh/InspectLease. The status.go file calls this out at the function declaration — keep that comment.- Lease file is
.ctask/session.jsonand the mode field ismode. The v0.5.4 spec called theselease.jsonandsession_moderespectively; 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 aroundcurrentHostname(). 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 callos.Hostname()directly in cmd code.ctask infoSession block alignment is at column 14 across all rows. Top-levelSession:(8 chars + 6 spaces) and indented sub-rows (Mode:/Owner:/Last owner:/Attach:/Note:) all align values at column 14. Malformed leases render onlySession: stale+Note: <diagnostic>— no Mode/Owner/Attach rows because we can't trust values from a broken file.ctask infoSession block usesinvocationName()for the Attach hint, NOTsession.SessionStatus. SessionStatus stays neutral — the hint string is built incmd/info.go::printSessionBlock. Keep that boundary: the cmd layer owns invocation-name rendering; the session package owns lease parsing.ctask listSESSION 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.infostill surfaces the raw state for archived workspaces because info is the diagnostic command.ctask list --namesis unchanged in v0.5.4. Bare basenames, no header, no session column, no whitespace per line, empty stdout on no match.TestListNamesUnchangedHasNoSessionColumnenforces this — if you touch the list rendering path, this test must keep passing.errArchivedWorkspaceis a sentinel, not a generic error.runResumeflipscmd.SilenceErrors=trueonly 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:TestResumeArchivedHintNoDuplicateErrorexercises the full CobraExecute()path.formatResumeRestoreHintandformatDirectModeTmuxHintare 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-remotessh -t <host> ctask <subcmd>hint (the remote runsctask, not the user's local binary), and CobraUse:/Long:strings. The two regression tests incmd/invocation_audit_test.gopinmy-bin(notctask) for the resume restore hint and Layer-1 attach hint specifically to detect format-string regressions that would silently work underwithInvocationName(t, "ctask").
Open follow-ups (deferred; not in any shipped v0.4–v0.5.4 work)
Potentially worth doing
- Pre-v0.5 project workspaces have no
launch_dir. They launch from the workspace root, not from any project subdir. No migration path today. Acceptable — users can manually add alaunch_dirto theirtask.yamlif they want the new behavior. Revisit only if someone complains. - Drop unused
slug/category/workspacePathparameters fromseed.ClaudeMD(v0.3 follow-up, still open). - Status-line: color the
|project/|project:<dir>marker if Claude Code'sstatusLineever supports ANSI.
Concurrency-related (from v0.4)
- Coexisting-session detection (more than one live lease per workspace).
- Cross-machine PID liveness check.
--forcesymmetry on other commands — no use case yet.- A
ctask sessionssubcommand 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 alongsidev0.2-spec.md/v0.3-spec.mdfor durability. Currently session-local; pernotes.md's long-standing rule, leave alone unless explicitly asked. .claude/settings.local.jsonhas 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 inSearchRoots().UTC date in directory names / info display→ resolved in v0.5.1.Duplicate Cobra→ resolved in v0.5.4 (Error: workspace archivedline onctask resume <archived-ws>ae9bfaf) viaerrArchivedWorkspacesentinel + conditionalSilenceErrors.→ resolved in v0.5.4 (info/listshow no session state — users have to check lease files manuallye0e9cd7,0c8076a) via Session block + SESSION column derived from newinternal/session.SessionStatushelper.→ resolved in v0.5.4 (docs/commands.mdmissing v0.5.2/v0.5.3/v0.5.4 commands and surfaces4fd0bef) via structured rewrite.
Files to read first when resuming
For the v0.5.4 surface (just-shipped):
v0.5.4-spec.md— the spec v0.5.4 followed (note: spec mentionslease.jsonandsession_mode; actual code usessession.jsonandmode— the implementation correctly preserved the existing names)internal/session/status.go+status_test.go—SessionStatusdisplay-only helper with the explicit no-tmux/no-PID/no-lock contractcmd/info.go::printSessionBlock— Session block rendering with column-14 alignment + invocation-name Attach hintcmd/list.go::sessionColumn— SESSION column with archived-as-em-dash rulecmd/resume.go—errArchivedWorkspacesentinel + conditionalSilenceErrorscmd/invocation_audit_test.go— regression tests pinning a non-canonical namedocs/commands.md— rewritten v0.5.4 reference (Purpose / Usage / Scenarios / Examples / Flags / Notes / Related per command)
For the v0.5 surface:
v0.5-spec.md— the spec v0.5 followeddocs/superpowers/plans/2026-04-22-v0.5-implementation.md— the executed plan (includes scanner-safety regression tests, dedupe tests, amendment history)internal/workspace/launchdir.go—ResolveLaunchwith the three-outcome contractinternal/workspace/create.go— subdir scaffold + local-time date +launch_dirdefaultinternal/session/run.go—LaunchOpts.LaunchDir,ResolveLaunchcall site, banner extensioncmd/info.go— local-time display + launch fields (Session block sits between Path and these)cmd/doctor.go—checkProjectRoothelperscripts/ctask-statusline.{sh,ps1}— effective-path display logicinternal/seed/templates.go—ClaudeMDProjectwith Workspace Structure section
For the v0.4.1 surface (still load-bearing):
internal/config/config.go—SearchRoots(),samePath,searchRootKeydedup; default$CTASK_ROOT/projects/fallbackinternal/workspace/query.go— two-depthscanWorkspaces,scanAllRoots,QueryResult.Rootcmd/archive.go— active-session check + non-TTY refusal +isStdinTerminal
For the v0.4 surface:
internal/session/run_preflight.go— Layer 3 + Layer 1 preflightinternal/session/lease.go— Lease + freshness + cleanupinternal/session/summary.go— SessionSummary + launch-context bannerinternal/lockfile/writelock.go— write-lock primitiveinternal/session/run_provisional.go—handleProvisionalwithchildExitCodegatedocs/commands.md— user-facing surface (rewritten in v0.5.4 — see entry #7 above)
Don't re-do
- Do not invent remote install /
go installcommands (perCLAUDE.md, this project is local-only) - Do not touch
cmd/delete.go's two-check protection - Do not weaken the v0.3
.gitignoreseed-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.NewlyCreatedis set only bycmd/new.go. - Do not drop
QueryResult.Rootor collapseSearchRoots()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_dirstorage to absolute paths. Workspace-relative is load-bearing. - v0.5: Do not mask non-
os.IsNotExiststat errors inResolveLaunchas 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 inSearchRoots()— it resolves the v0.4.1 env-var scoping footgun. - v0.5.1: Do not switch the directory prefix / ID back to UTC. The
TestCreateDirectoryPrefixUsesLocalDatetest enforces local time. - v0.5.1: Do not remove
.Local()from thectask infoCreated/Updated/Archived formatting.TestInfoFormatsTimestampsInLocalZoneenforces 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", ...)outsideinternal/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::preflightPersistentEntryis the single source of truth; the validated path flows throughLaunchOpts.TmuxPath. - v0.5.3: Do not enable provisional cleanup in persistent mode.
shouldRunProvisionalreturns false onSessionMode == "persistent"— the gate's UX assumption doesn't translate to tmux. - v0.5.3: Do not silently overwrite a fresh remote lease.
confirmFreshRemoteAdoptionprompts 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.Createincmd/new.go. Pre-create ordering prevents half-initialized workspaces when tmux is missing. - v0.5.3: Do not change the
SessionNamealgorithm. Name stability across processes is what makes passive reattach work without state. Windows path lowercasing matchessearchRootKey. - v0.5.4: Do not call
session.SessionStatusfrom lifecycle or adoption code. It is display-only — themode-defaults-to-directrule and the malformed-as-stale classification are wrong for behavioral logic. UseReadLease/IsFresh/InspectLeasefor lifecycle decisions. - v0.5.4: Do not invoke tmux, check PID liveness, or acquire locks inside
SessionStatusor its callers (infoSession block,listSESSION 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 --namesoutput. Machine-readable bare-basename-per-line is the contract.TestListNamesUnchangedHasNoSessionColumnenforces 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.infois 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
errArchivedWorkspacesentinel into "any pre-printed error". The CobraSilenceErrorsflip 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 awithInvocationNameseam. - v0.5.4: Do not re-introduce a hardcoded
"ctask"in command-form hints. UseinvocationName(). The regression tests pin a non-canonical name specifically to catch this.
Release-Publishing Pipeline (plan: ctask-release-pipeline-plan-final.md)
Phase 0 — Runner & Release API Preflight — BLOCKED (started 2026-05-19)
Tooling versions:
- Workstation: go 1.26.3, git 2.53.0, curl 8.18.0, sha256sum (coreutils 8.32), jq 1.8.1, bash 5.2.37
- VPS host (
netcup): git 2.47.3, curl 8.14.1, sha256sum (coreutils 9.7), jq 1.7; no Go on host
Gitea instance:
- Gitea 1.25.5 in Docker container
gitea(gitea/gitea:1.25.5), fronted by Traefik. - API
https://git.typebased.dev/api/v1reachable (200) from workstation and VPS. - No explicit
[actions]section in app.ini -> Actions enabled by default (1.21+). - Org
typebasedio-> 404 unauthenticated; repotypebasedio/ctask-> 404 (not created).
BLOCKERS (Phase 0 gate NOT satisfied):
- No Gitea Actions runner exists anywhere on the VPS — no
act_runnercontainer/binary/process/service. Phases 3-4 cannot run without a registered, labelled runner. Plan assumed a runner already existed. - Token value not located.
workstation-dev-environmenttoken value needed for release API preflight +RELEASE_TOKENsecret. Awaiting user retrieval instructions (must not create a second token).
Open issues: runner install/registration decision; token retrieval source.
Update 2026-05-20 — runner provisioned
Shared Typebased runner vps-act-runner-01 came online (provisioned by the
windows-dev agent; canonical docs in typebasedio/windows-dev-setup @ ae9eef5,
typebasedio/project-registry @ 2ddc8c6). Contract for ctask:
runs-on: ctask-release- Backed by
golang:1.26-bookworm(jq NOT in base image — workflow installs it) - Pure Go workflow only; no Docker-in-job, no Docker image build steps
RELEASE_TOKENrepo/user secret used for release create/upload- Prior runner-registration token leak rotated; current credential is fresh
Phase 0 runner blocker resolved. Token still pending (Bitwarden locked at last check; user will unlock and we'll fetch by item name).
Phase 2 — Local release build target — DONE (2026-05-20)
Added:
scripts/build-release.sh— single recipe shared with CI; wipesdist/, cross-compilesctask-linux-amd64andctask-windows-amd64.exewithCGO_ENABLED=0and ldflags injectingmain.versionandmain.commit, writeschecksums-sha256.txtandrelease-manifest.json.scripts/release-check.sh— runsgo test ./...,go vet ./..., invokesbuild-release.sh, then asserts the binary's--versioncontains the requested tag. Falls back to the Windows binary on Windows hosts where the Linux binary can't exec.
Phase 5 prerequisite — version injection — DONE (2026-05-20)
main.godeclaresvar version = "dev"andvar commit = ""and forwards them tocmd.SetVersionInfobeforecmd.Execute().cmd/root.goexposesSetVersionInfo(v, c)and rebuilds the Cobra version template viaapplyVersionTemplate().- Output format (single line, decided pre-Phase 5):
ctask v0.6.1-rc.1 (0e8e4a5)— tagged build, commit knownctask v0.6.1-rc.1— tagged build, no commit injectedctask dev— local build (no ldflags)
- Phase 5 Dockerfile exact-equality assertion (optional in the plan) will need
to be a substring/prefix check because the commit is appended. Sample:
./ctask --version | grep -qF "${CTASK_VERSION}". just install/ local Windows workflow unchanged — local builds simply now printctask devinstead ofctask v0.6.0. No behavioral regression.
Local validation result (commit 0e8e4a5)
bash scripts/release-check.sh v0.6.1-rc.1:
- tests: all packages pass (
cmd,internal/agent,internal/config,internal/lockfile,internal/seed,internal/session,internal/shell,internal/workspace) - vet: clean
- artifacts:
dist/ctask-linux-amd64sha256808c71f982a3ed50f63bd5c4e1d25c4cf0643c887b8c2e011c5181a9020d1004(5,288,098 bytes)dist/ctask-windows-amd64.exesha256c8dee43d5ade90899020fb8b31a41230672057b74478aa78f91d6f509dd689e8(5,511,168 bytes)dist/checksums-sha256.txt(sha256sum -cpasses)dist/release-manifest.json(valid JSON; commit0e8e4a5d7bc4320cd933008d5b6e505f2b3c5ec4)
--versionoutput on Windows host:ctask v0.6.1-rc.1 (0e8e4a5)- Linux binary cross-exec'd on VPS (
netcup):ctask v0.6.1-rc.1 (0e8e4a5)✓
Phase 3 — Gitea Actions workflow — DRAFTED (2026-05-20)
Added .gitea/workflows/release.yml. Not yet committed/pushed.
- Trigger:
pushof tags matchingv* runs-on: ctask-release- Tag parsed from
gitea.ref(stripsrefs/tags/), NOTgitea.ref_name - Installs jq at job start (
golang:1.26-bookwormbase lacks it) - Steps: derive tag →
git clone --depth 1 --branch(noactions/checkout) →go test ./...+go vet ./...→bash scripts/build-release.sh→ create release via Gitea API (delete+recreate if RC and exists; refuse if non-RC and exists) → upload 4 assets → download-verify withsha256sum -cand--versionsubstring match. - All JSON bodies built with
jq -ncrather than string-interpolated to prevent quoting bugs.
Still pending (token-dependent)
- Phase 0 token check: create+delete a draft release against the live repo.
- Phase 1: create
typebasedio/ctask(public), wireoriginremote, pushmain. - Phase 3 secret: set
RELEASE_TOKENfrom the existingworkstation-dev-environmenttoken (no new PAT). - Phase 4: commit changes, push, tag
v0.6.1-rc.1, validate the CI run.