feat(v0.5.1): Linux portability baseline

- justfile: add build-linux, build-windows, build-all (output to dist/)
- .gitignore: cover ctask, ctask-*, dist/
- scripts/install.sh + scripts/uninstall.sh: POSIX equivalents of .ps1
- remove WorkspacePath metadata field (no production readers; legacy
  task.yaml files continue to parse silently)

Linux smoke-test on WSL/container pending.
See audit-report.md and v0.5.1-spec.md.
This commit is contained in:
2026-05-07 18:22:41 -04:00
parent a11d48b8cd
commit 7a7b2490c2
13 changed files with 686 additions and 13 deletions
+332
View File
@@ -0,0 +1,332 @@
# ctask Portability & Feature Readiness Audit
**Audit date:** 2026-05-07
**Scope:** Read-only audit of the ctask codebase against (a) running on Linux without behavioural drift from Windows and (b) the planned features `restore`, `info` on archived workspaces, `notes <ws>`, `path <ws>`, shell completion, and a centralized resolver.
**Headline:** The codebase is already in good shape for Linux. There are **no true blockers** to running ctask on Linux today — a `GOOS=linux go build` will produce a working binary, and the test suite is portable. The work falls into three buckets: distribution surface (justfile / install scripts / `.gitignore` are Windows-only), one piece of latent metadata (`WorkspacePath` stores absolute paths but nothing reads them), and a handful of small additions needed before completion can be wired up cleanly (`list --names` and `ValidArgsFunction` hooks).
---
## Section 1 — Path handling (portability)
### Finding: `WorkspacePath` persisted as absolute path in `task.yaml`
- **Files:** `internal/workspace/create.go:138`, `internal/workspace/metadata.go:29`
- **Current behavior:** `meta.WorkspacePath = wsDir` stores the absolute filesystem path (e.g. `C:\Users\Warren\ai-workspaces\general\2026-04-24_promptvolley` on Windows). Written to YAML by `WriteMeta`/`WriteMetaLocked`. Verified by grep: **the field is never read in production code** — only in test struct initializers (`internal/workspace/metadata_test.go`, `cmd/{archive,delete,list}_test.go`, `internal/workspace/query_test.go`). The runtime path used everywhere is `QueryResult.Path`, derived freshly from the filesystem walk.
- **Linux/feature impact:** No functional break today. But: (a) the field is misleading — task.yaml claims `workspace_path: C:\...` even when read on Linux; (b) any future code that decides to consume the field will instantly break cross-OS workspace sharing (Windows host ↔ WSL/Docker reading the same dir tree); (c) blocks any plan where ctask metadata travels between hosts.
- **Severity:** **should fix** — either drop the field entirely (it is dead) or convert to relative-to-root before persisting.
### Finding: Defensive dual-separator checks in manifest are dead code
- **Files:** `internal/session/manifest.go:27`, `internal/session/manifest.go:35`
- **Current behavior:** `ignoredPath` checks `strings.HasPrefix(relPath, ".ctask/") || strings.HasPrefix(relPath, ".ctask\\")` and `relPath == "logs/sessions.log" || relPath == "logs\\sessions.log"`. Manifest paths reach this function only after `filepath.ToSlash(rel)` at `manifest.go:65`, so the `\\`-form branches can never fire.
- **Linux/feature impact:** Fine functionally on Linux; just confusing/redundant.
- **Severity:** fine
### Finding: All path construction goes through `filepath` package
- **Files:** Verified across `internal/config/config.go`, `internal/workspace/{create.go, query.go, list.go, metadata.go, launchdir.go, slug.go}`, `internal/session/*.go`, `cmd/*.go`. No string concatenation with `\\` or `/` separators in path-building positions; `path.Join` (the URL/forward-slash variant) is not used; `os.PathSeparator` and `filepath.Separator` are used correctly where the platform separator is needed (e.g. `internal/workspace/launchdir.go:48` traversal check, `internal/workspace/create.go:189` display normalization).
- **Linux/feature impact:** Fine.
- **Severity:** fine
### Finding: `RelativePath` normalizes to forward slashes for display
- **Files:** `internal/workspace/create.go:184-190`
- **Current behavior:** `filepath.Rel(root, wsPath)` then `strings.ReplaceAll(rel, string(filepath.Separator), "/")`. Used in error messages and `cmd/archive.go:61`, `cmd/helpers.go:28`.
- **Linux/feature impact:** Fine — display strings will be identical across platforms.
- **Severity:** fine
### Finding: Config layer cleanly handles `~`, env-var defaults, Windows case-insensitive dedup
- **Files:** `internal/config/config.go:12-105`, `defaultSeedDir` at `:138-149`, `expandPath` at `:152-162`
- **Current behavior:** `ResolveRoot` defaults to `filepath.Join(home, "ai-workspaces")` (works on Linux: `/home/<user>/ai-workspaces`). `ResolveSeedDir`/`ResolveProjectSeedDir` switch on `runtime.GOOS`: `%APPDATA%\ctask\seed[-project]` on Windows, `~/.config/ctask/seed[-project]` on Unix. `searchRootKey`/`samePath` lower-case on Windows for case-insensitive dedup, leave alone on Linux. `expandPath` handles `~/` and converts to absolute via `filepath.Abs`.
- **Linux/feature impact:** Fine.
- **Severity:** fine
### Finding: Env var resolution is centralized
- **Files:** Grep confirms `os.Getenv("CTASK_ROOT" | "CTASK_PROJECT_ROOT" | "CTASK_SEED_DIR" | "CTASK_SEED_PROJECT_DIR")` appears only in `internal/config/config.go` (and tests). Commands always go via `config.Resolve*`.
- **Linux/feature impact:** Fine.
- **Severity:** fine
### Finding: No registry, syscall, or PowerShell invocations from core logic
- **Files:** No `golang.org/x/sys/windows` imports, no `import "C"`, no exec'ing of `powershell`/`cmd` from non-shell layers.
- **Linux/feature impact:** Fine.
- **Severity:** fine
**Section 1 summary:** 1 should-fix (`WorkspacePath`), 1 cosmetic dead-code note. Path-construction discipline is solid.
---
## Section 2 — Workspace identity model
### Finding: Canonical identity is the absolute filesystem `Path`; no separate ID type
- **Files:** `internal/workspace/query.go:14-18` (`QueryResult{Path, Root, Meta}`), `internal/workspace/metadata.go:19-32` (`TaskMeta.ID` is timestamp-derived, never compared)
- **Current behavior:** `QueryResult.Path` is always absolute (constructed via `filepath.Join` rooted at an absolute search root, `query.go:122,142,147-150`). `TaskMeta.ID` is created in `create.go:70` as a timestamp string and is never used for resolution. The user-facing identifier is `TaskMeta.Slug`.
- **Impact on planned features:** `path <ws>` is a one-liner against `QueryResult.Path`. `notes <ws>` and `restore <ws>` need only the `Path` and `Meta` already present in `QueryResult`. No identity-model refactor is needed.
- **Severity:** fine
### Finding: Slug ≠ directory basename — slug is the post-date suffix
- **Files:** `internal/workspace/slug.go`, `internal/workspace/create.go:83-85,129`, `internal/workspace/query.go:46-50,57-65`
- **Current behavior:** Directory basename has the form `YYYY-MM-DD_<slug>[-<N>]`. `TaskMeta.Slug` is the bare slug after stripping date and any numeric collision suffix. Resolution matches against (1) full directory basename, (2) exact `Meta.Slug`, (3) case-insensitive substring of `Meta.Slug`.
- **Impact on planned features:** Completion candidates should be slugs (or full dated basenames) — both forms are accepted by the resolver. Confirmed the relationship is consistent across the codebase.
- **Severity:** fine
### Finding: Display paths use `/` regardless of OS; filesystem operations use native separator
- **Files:** `internal/workspace/create.go:189` (display normalization), filesystem paths via `filepath.*` everywhere else
- **Impact on planned features:** Output of `path <ws>` should keep native separators (it is a real FS path). List/error output should keep using `RelativePath` for `/` display.
- **Severity:** fine
**Section 2 summary:** Identity model is clean and consistent. No work required.
---
## Section 3 — Workspace resolution: per-command consistency
### Finding: Resolution is already centralized; minor drift in `includeArchived` defaults
- **Files:**
- Central path: `cmd/helpers.go:13-35` (`resolveOne`), `internal/workspace/query.go:23-66` (`ResolveQuery`)
- `cmd/archive.go:29``resolveOne(roots, args[0], false)` — hardcoded
- `cmd/resume.go:50``resolveOne(roots, query, false)` — hardcoded
- `cmd/info.go:23,29``--all/-a` flag (`infoAll`), default false
- `cmd/delete.go:30,36``--all/-a` flag (`deleteAll`), default false
- `cmd/open.go:28,35``--all/-a` flag (`openAll`), default false
- `cmd/last.go` — uses `workspace.MostRecentActive(roots)` (purpose-specific helper)
- `cmd/new.go` — does not resolve; creates fresh
- **Current behavior:** Every command that takes a query goes through `resolveOne`, which goes through `ResolveQuery`. All callers pass `config.SearchRoots()`. 0-match exits 1; multi-match prints candidates and exits 1. No hand-rolled scans elsewhere.
- **Impact on planned features:** No "centralized resolver" rewrite is needed — it already exists. The only inconsistency is that some commands hardcode `includeArchived=false` while others gate it behind `--all`. New commands (`restore`, `notes`, `path`) can plug straight in. `restore` will need `includeArchived=true` (or to set the resolver to true and then verify `Meta.Status == "archived"`).
- **Severity:** **should fix** — flag inconsistency, not a structural problem. Decide a uniform policy (e.g., commands that act on metadata get `--all`; commands that need an active session don't).
**Section 3 summary:** Resolver is already shared. The "centralized resolver" item on the planned-features list is essentially done.
---
## Section 4 — Archive implementation
### Finding: Archive is a metadata-only flip; trivially reversible
- **Files:** `cmd/archive.go:27-65`
- **Current behavior:** Sets `Meta.Status = "archived"`, `Meta.ArchivedAt = &now`, `Meta.UpdatedAt = now`. Writes via `WriteMetaLocked`. **No directory move, no lease/manifest cleanup, no cache invalidation.** Active-session check at `:35-49` reads the lease and refuses (or prompts) if fresh.
- **Impact on planned features:** `restore` is mechanically symmetric: clear `Status` (or set to `"active"`), clear `ArchivedAt`, bump `UpdatedAt`, `WriteMetaLocked`. Should refuse if `Status != "archived"` (idempotency / user-error guard). No directory move needed.
- **Severity:** fine
### Finding: Archived workspaces stay in place; filtering is at the resolver layer
- **Files:** `internal/workspace/query.go:31-33` (filter), `internal/workspace/list.go:46-47` (filter), `internal/workspace/query.go:99-155` (`scanWorkspaces` ignores status)
- **Current behavior:** `scanWorkspaces` reads every `task.yaml` regardless of status. Status filtering is purely a `ResolveQuery`/`ListWorkspaces` decision based on the `IncludeArchived` flag.
- **Impact on planned features:** Cross-workspace reads on archived workspaces (`notes my-old-ws`, `path my-old-ws`) will work as long as the new commands pass `includeArchived=true` or expose an `--all` flag.
- **Severity:** fine
**Section 4 summary:** Clean. `restore` is a one-screen feature.
---
## Section 5 — List command and filtering
### Finding: `ctask list` has no machine-readable mode
- **Files:** `cmd/list.go:14-101`
- **Current behavior:** Default lists active tasks + projects. Flags: `--all/-a` (include archived), `--task`, `--projects`, `--category/-c`, `--limit/-n`. Output is `tabwriter`-aligned six-column human format: `status, type, mode, category, date, slug`.
```
active task local general 2026-04-05 task-active
active project local projects 2026-04-02 proj-active
```
- **Impact on planned features:** **Blocks clean shell-completion.** A completion script needs a stable, parseable list of slugs. Either add `--names` (one slug per line) or `--json`, or add a hidden `__complete-workspaces` helper. Parsing the tabwriter output is fragile (tab spacing, future column changes).
- **Severity:** **should fix** — directly blocks the completion feature being clean.
**Section 5 summary:** Functionality is fine; lack of a `--names` (or equivalent) is the gap.
---
## Section 6 — Agent launch & shell assumptions
### Finding: Agent and shell launch are os-aware and use `os/exec` directly
- **Files:** `internal/shell/launch.go` (entire), `cmd/archive.go:89-95` (`isStdinTerminal`)
- **Current behavior:**
- `DefaultShell()` → `powershell.exe` → `cmd.exe` on Windows; `$SHELL` → `bash` on Unix.
- `ExecAgent` → `exec.LookPath` + `exec.Command(path)`. No shell wrapping. PATHEXT resolution is automatic on Windows.
- `ExecShell` injects `function prompt {...}` via `-NoExit -Command` for PowerShell, `PROMPT=...$P$G` env var for cmd.exe, `PS1` for bash; all branches guarded by `runtime.GOOS`.
- `BuildEnvList` is the sole env-export mechanism (`os.Environ()` ++ provided map).
- `isStdinTerminal` uses `os.ModeCharDevice` (cross-platform).
- No registry, no console-mode manipulation, no `term.MakeRaw`/ANSI handling.
- **Impact on planned features:** Fine. Linux launches will work.
- **Severity:** fine
### Finding: Heartbeat / lease / manifest writers are pure in-process Go
- **Files:** `internal/session/heartbeat.go`, `internal/session/run.go`
- **Current behavior:** Goroutine heartbeat rewrites lease via `os.WriteFile`. No subprocess helpers.
- **Severity:** fine
### Finding: Statusline scripts ship to both platforms; install is Windows-only (see Section 8)
- **Files:** `scripts/ctask-statusline.sh`, `scripts/ctask-statusline.ps1`, `scripts/install.ps1:67-80`, `cmd/doctor.go:78-92`
- **Current behavior:** Both helper scripts are copied on Windows install. `doctor.go` searches for `.sh` first on non-Windows, `.ps1` first on Windows. The `.sh` script uses bash idioms but is invoked explicitly via `bash <path>` so should be fine.
- **Severity:** fine (the install-script-only-Windows issue is logged in Section 8).
**Section 6 summary:** Fully portable. No work required for Linux launch.
---
## Section 7 — File content & line endings
### Finding: All ctask-generated files use `\n`
- **Files:** `internal/workspace/metadata.go:50` (`yaml.Marshal`), `internal/session/manifest.go:87` (`json.MarshalIndent`), `internal/session/log.go:36-121` (manual `\n`), `internal/seed/templates.go` (backtick raw strings, `\n` only).
- **Current behavior:** No literal `\r\n` anywhere in `*.go`. Standard Go marshalers emit LF.
- **Severity:** fine
### Finding: Seed copy is byte-for-byte (preserves source line endings)
- **Files:** `internal/seed/copy.go:73-92`
- **Current behavior:** `io.Copy(out, in)` — no normalization. Seed files committed with CRLF on a Windows machine will be copied as CRLF on Linux too.
- **Linux impact:** Generally fine for Markdown / config files. Becomes a problem only if the seed contains a shell script the agent later executes; recommend a `.gitattributes` `* text=auto eol=lf` for the seed dirs if/when this matters.
- **Severity:** fine (pre-emptive note; not blocking)
**Section 7 summary:** Clean.
---
## Section 8 — Build system
### Finding: `justfile` has no Linux build/install/uninstall targets
- **Files:** `justfile:6-23`
- **Current behavior:** `build` outputs `ctask.exe`. `install` and `uninstall` invoke `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/...`. Running `just install` on Linux fails (no `powershell`).
- **Linux impact:** Linux developers can still run `GOOS=linux go build -o ctask .` directly, but there is no parallel install path.
- **Severity:** **should fix** — required for a clean Linux developer/install experience.
### Finding: No POSIX install/uninstall script
- **Files:** `scripts/install.ps1`, `scripts/uninstall.ps1` — PowerShell only.
- **Linux impact:** No `install.sh` to put the binary on `$PATH`, drop in the statusline helper, etc.
- **Severity:** **should fix** — required for distribution parity.
### Finding: `.gitignore` only ignores `*.exe`, not the Linux `ctask` binary
- **Files:** `.gitignore:1-2`
- **Current behavior:** `ctask.exe` and `*.exe` are ignored. A binary built on Linux as `ctask` is not.
- **Linux impact:** Easy to accidentally commit a built Linux binary.
- **Severity:** **should fix** — trivial.
### Finding: No build tags, no cgo, no `//go:embed` of OS-specific assets
- **Files:** Project-wide grep — clean.
- **Linux impact:** Cross-compile is `GOOS=linux GOARCH=amd64 go build .` with nothing else needed.
- **Severity:** fine
### Finding: `runtime.GOOS` use is contained and correctly branched
- **Files:** `internal/config/config.go:101,112,139`, `internal/workspace/query.go:158`, `internal/shell/launch.go` (multiple), `cmd/doctor.go:78,146`. All have an explicit Unix branch.
- **Severity:** fine
**Section 8 summary:** 3 should-fix items, all in distribution surface (justfile, install scripts, .gitignore). No code-level blockers.
---
## Section 9 — Test coverage
### Finding: 29 `_test.go` files, broad coverage, all spot-checks portable
- **Files:** Tests live in `cmd/` (5), `internal/config/` (2), `internal/workspace/` (8), `internal/session/` (10), `internal/shell/` (1), `internal/seed/` (2), `internal/lockfile/` (1).
- **Current behavior:** All path construction uses `t.TempDir()` + `filepath.Join`. `runtime.GOOS` checks guard Windows-specific assertions (`internal/config/config_test.go:57`, `internal/shell/launch_test.go:11-14`). `t.Skip("tilde expansion not typical on Windows")` patterns are present. No CRLF/LF assertions. No tests shell out to `cmd.exe`/`powershell.exe`.
- **Linux impact:** Spot-checked `internal/shell/launch_test.go`, `internal/workspace/query_test.go`, `internal/config/config_test.go`. All pass on Linux without modification.
- **Severity:** fine
### Finding: A few tests use Unix-style fixture string literals — but only as comparison values, not as path-construction targets
- **Files:** `internal/workspace/metadata_test.go:81,126,157,237` (e.g. `WorkspacePath: "/home/warren/..."`)
- **Current behavior:** These strings are stored in `WorkspacePath` and compared back; they never touch the filesystem.
- **Severity:** fine — and ironically further evidence that `WorkspacePath` is never resolved as a real path.
**Section 9 summary:** Tests are portable. No work required.
---
## Section 10 — Feature-specific gotchas
### `ctask restore`
- Archive (`cmd/archive.go:51-54`) flips `Status`, `ArchivedAt`, `UpdatedAt` only. No locks, sentinel files, or directory moves to undo. `restore` is a clean inverse: `resolveOne(roots, q, true)` → reject if `Status != "archived"` → clear `Status`+`ArchivedAt` → `WriteMetaLocked`.
- **Severity:** fine
### `ctask info` on archived workspaces directly
- Today: `cmd/info.go:23,29` — gated on `--all/-a`. The user wants direct lookup to also work without `-a`. Easiest path: change the default to `includeArchived=true` for `info` specifically (it's a read-only display command, no harm in always finding archived workspaces). No callers depend on the current default-off behaviour. Same logic could optionally extend to `delete` and `open` but those have stronger reasons to default to active-only.
- **Severity:** **should fix** — small UX/policy change.
### `ctask notes <workspace>`
- `notes.md` is unconditionally written by `internal/workspace/create.go:179-180`. Always at `<wsPath>/notes.md`. Edge cases: file deleted by user (treat as error, like `cat` of missing file), empty file (print empty, exit 0).
- **Severity:** fine
### `ctask path <workspace>`
- `QueryResult.Path` is already absolute. Implementation: `resolveOne` then `fmt.Println(ws.Path)`. No new function needed.
- **Severity:** fine
### Shell completion (`ctask completion {bash,powershell}`)
- The CLI is plain Cobra v1.10.2 (`cmd/root.go`), `main.go` does no hand-rolled flag parsing. Cobra ships `GenBashCompletionV2`, `GenPowerShellCompletionWithDesc`, `GenZshCompletionV2`, `GenFishCompletion` — and Cobra also auto-injects a `completion` subcommand in modern versions (`rootCmd.CompletionOptions`). **Adding the command itself is essentially free.**
- The *quality* of completion depends on `ValidArgsFunction` hooks on commands that accept a workspace query (`resume`, `open`, `info`, `archive`, `delete`, `last`, plus the new `restore`, `notes`, `path`). Each hook would call the resolver to enumerate slug candidates. Trivial wrapper.
- **Real dependency:** dynamic completion needs a way to enumerate workspaces — that's where Section 5's missing `--names`/`--json` mode comes in. The `ValidArgsFunction` can call `workspace.ListWorkspaces` directly (no shell-out), so this is solvable in-process without a new flag, but a flag is also useful for external tooling and bash-side hand-written completion.
- **Severity:** fine for the command itself; completion *content* depends on the should-fix in Section 5.
### Cross-workspace reads on archived workspaces
- `scanWorkspaces` (`query.go:99-155`) reads every `task.yaml` regardless of status. Filtering is at the `ResolveQuery` layer, controlled by `includeArchived`. So `notes <archived>` and `path <archived>` will work as long as those commands pass `includeArchived=true` (or expose an `--all` flag).
- **Severity:** fine
---
## Section 11 — Recommended implementation order
The codebase needs no Linux-portability bugfix to *run* on Linux. The work is best ordered by what unblocks the most other items downstream.
### Step 1 — Linux distribution surface
**What:** Add a `build-linux` / `build-all` target to `justfile`, add a `scripts/install.sh` and `scripts/uninstall.sh` mirroring the PowerShell scripts (probably installing to `~/.local/bin` with a check-and-warn for `$PATH`), add `ctask` and `ctask-*` to `.gitignore`.
**Why first:** Required for *Linux portability* end-to-end. None of the other planned work blocks on this, but a developer can't smoke-test the Linux build cleanly without it. Resolves Section 8 should-fixes.
**Required for:** Linux portability (distribution).
### Step 2 — Cross-compile and smoke test on Linux/WSL
**What:** Build the Linux binary from Step 1, run the test suite (`GOOS=linux go test ./...` from Windows is not a substitute — actually run on Linux), exercise `new`, `list`, `info`, `archive`, `resume` against a fresh `~/ai-workspaces` root.
**Why now:** Validates the audit's central claim ("the code is portable") with evidence rather than inference. Also surfaces any environmental issues before more code is added.
**Required for:** Linux portability (validation).
### Step 3 — Decide the fate of `WorkspacePath`
**What:** Either delete the field (cleanest — it has no readers and the persisted value is misleading on cross-OS shares) or convert it to a path relative to the resolved root before persisting (preserves the schema). Update `internal/workspace/create.go:138`, the YAML schema, and the test fixtures.
**Why before features ship:** Once `notes`/`path`/`restore` ship, users will start sharing workspaces between Windows and WSL/Docker. A latent-but-visible Windows path in `task.yaml` will be the first thing they ask about. Cheaper to fix while no consumer exists than after.
**Required for:** Cross-OS workspace sharing hygiene; resolves Section 1's only should-fix.
### Step 4 — Add `ctask list --names` (and/or `--json`)
**What:** Add a flag (or hidden helper subcommand `__complete-workspaces`) that emits one slug per line, ignoring filters/limit, suitable for completion sources.
**Why before completion:** Either the in-process `ValidArgsFunction` hooks (Step 6) or external bash completion scripts will want a stable enumeration source. Cheaper to do once than to retrofit per-shell.
**Required for:** Shell completion (clean implementation).
### Step 5 — Implement `ctask restore`, `ctask notes <ws>`, `ctask path <ws>`
**What:** Three small commands. Each: `resolveOne(roots, args[0], true)` → operate on `QueryResult`. `restore` adds an active-session check (mirror archive's lease guard).
**Why grouped:** All three follow the same template and exercise the same code paths. Doing them together amortizes the design conversation about "how should the new commands handle archived workspaces" — settle the policy in one PR.
**Required for:** restore + notes + path features.
### Step 6 — Decide and apply policy: `info` default for archived
**What:** Either flip `infoAll` default to `true` (so `ctask info <archived>` works without `-a`) or keep the flag and document. Confirm no test expects the current default-off behaviour.
**Why after Step 5:** Better to set the policy once with the new commands in mind. If `notes`/`path` default to including archived, `info` should match for consistency.
**Required for:** "info on archived workspaces directly" feature.
### Step 7 — Add `ctask completion <shell>` and `ValidArgsFunction` hooks
**What:** Add the `completion` subcommand (Cobra one-liner — or simply enable Cobra's built-in `CompletionOptions`). Add `ValidArgsFunction` to every command taking a workspace query: `resume`, `open`, `info`, `archive`, `delete`, `notes`, `path`, `restore`. Each hook calls a shared helper that enumerates slugs.
**Why last:** Completion exposes everything else. If commands or list output change later, the completion has to track them. Doing it after the new commands settle avoids churn.
**Required for:** Shell completion feature.
### Optional follow-ups (not on the planned-feature list, but cheap wins)
- Drop the dead `\\` branches in `internal/session/manifest.go:27,35`.
- Tighten the `--all` default policy across `archive`/`resume`/`delete`/`open`/`info` (Section 3 inconsistency) once the new commands have set the precedent.
- Add a `.gitattributes` covering `internal/seed/` to normalize EOL on the seed sources.
---
## Summary — blocking and should-fix items, grouped by feature
### Linux portability (cross-compile + run)
- **should fix:** `justfile` has no Linux targets — `justfile:11,18,23`
- **should fix:** No POSIX install/uninstall script — `scripts/install.ps1`, `scripts/uninstall.ps1`
- **should fix:** `.gitignore` does not cover the Linux `ctask` binary — `.gitignore:1-2`
- **should fix (cross-OS sharing):** `WorkspacePath` persisted as absolute path — `internal/workspace/create.go:138`, `internal/workspace/metadata.go:29`
### `ctask restore`
- *(no blocking or should-fix items — clean addition)*
### `ctask info` on archived workspaces directly
- **should fix:** Decide policy on `infoAll` default — `cmd/info.go:23`
### `ctask notes <ws>`
- *(no blocking or should-fix items — clean addition)*
### `ctask path <ws>`
- *(no blocking or should-fix items — clean addition)*
### Shell completion (`ctask completion {bash,powershell}`)
- **should fix:** No machine-readable list output (`--names`/`--json`) — `cmd/list.go:81-98`
### Centralized workspace resolver
- *(already centralized via `cmd/helpers.go` `resolveOne` → `internal/workspace/query.go` `ResolveQuery`)*
- **should fix (cosmetic):** `includeArchived` default is hardcoded in `archive`/`resume`, flag-controlled in `info`/`delete`/`open` — `cmd/archive.go:29`, `cmd/resume.go:50`, `cmd/info.go:23`, `cmd/delete.go:30`, `cmd/open.go:28`
### No-impact / housekeeping
- Dead defensive code in `internal/session/manifest.go:27,35` (fine; can be cleaned).
**Total: 0 blocking, 6 should-fix items.** None of them prevent ctask from running on Linux today; all of them either smooth the distribution surface, prevent latent cross-OS bugs, or unblock the cleanest implementation of the planned features.