LaunchOpts gains LaunchDir. session.Run resolves it via
workspace.ResolveLaunch, prints any fallback warning, and passes the
absolute path as the child process's working directory. Security
violations (absolute paths, .. escape) abort the session. The banner
gains a 'project dir: <name>/' line when launch_dir is set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
config.EnvVars gains a 7th launchDir argument. cmd/new, cmd/resume, and
cmd/open pass ws.Meta.LaunchDir. Child sessions can read CTASK_LAUNCH_DIR
to know which subdirectory ctask launched them into.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ctask new --project now creates an empty subdirectory named after the
final suffixed slug inside the workspace root, and sets meta.LaunchDir
to that slug. Task workspaces are unchanged. Seeds do not target the
subdirectory — the user populates it with their project code and their
own CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves a relative launch_dir into the absolute directory the child
process should cd into. Returns an error for absolute paths and paths
that escape the workspace via .. traversal. Returns (wsDir, warning, nil)
on os.IsNotExist or target-is-a-file so the caller prints a warning and
falls back. Non-IsNotExist stat errors (permission, invalid name, I/O)
propagate as real errors rather than being masked as warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a child-exit-code guard to handleProvisional so a `ctask new`
workspace is reclaimed only when the agent was actually canceled
before real work (trust prompt rejected, Esc during startup, Ctrl+C
mid-launch — all confirmed empirically as exit code 1). A zero exit
means the user entered the agent and exited cleanly, which is a
legitimate workflow that must preserve the workspace even with an
empty manifest diff — for example when the user wants the workspace
directory established so they can populate context/ before resuming.
The exit code reaches handleProvisional via a new childExitCode
helper that unwraps *exec.ExitError from cmd.Run's return. Non-exit
errors (agent not found, OS-level failure) map to -1 so the
workspace is still cleaned up — the child never actually ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Project CLAUDE.md template now includes a Git section stating that the
workspace uses a single git repo at the root and subdirectory git init
is not permitted. docs/commands.md picks up the same guidance under the
--project flag section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites scanWorkspaces to handle both category layout
(root/<category>/<workspace>/task.yaml) and flat layout
(root/<workspace>/task.yaml used under CTASK_PROJECT_ROOT).
Adds scanAllRoots to walk multiple roots with absolute-path dedupe.
ResolveQuery, ListWorkspaces, and MostRecentActive now accept []string.
QueryResult gains a Root field so callers can render display paths and
session env vars relative to the originating root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When `ctask new` launched the agent or shell and the child exited without
touching any files (e.g. the user canceled at Claude Code's trust prompt),
the workspace was left behind as empty clutter. Now, if the invocation
just created the workspace and the manifest diff is empty (zero added,
modified, deleted), the workspace directory is removed and finalize is
skipped. resume/open/last are unaffected (NewlyCreated defaults to false),
and --no-launch is unaffected (session.Run is never called).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the cross-type "most recently updated active workspace"
selector that previously lived inline in cmd/last and cmd/delete.
The helper takes only a root path and returns (nil, nil) when
nothing matches, which keeps the call sites trivial.
Eight focused tests cover:
- tasks-only fixture
- projects-only fixture
- project most-recent vs older task
- task most-recent vs older project
- legacy v0.2 workspace (no Type field) winning, treated as task
- newest workspace archived, older active project wins
- empty root returns (nil, nil)
- all archived returns (nil, nil)
A new createTestWorkspaceFull helper takes an explicit UpdatedAt
timestamp so the "which is newest" assertions don't depend on
wall-clock ordering.
cmd/last and cmd/delete are migrated onto the helper in the next
commit.
ListOpts now exposes a Type string field (TypeAny / TypeTask /
TypeProject). TypeAny is the new way to express "both tasks and
projects" in a single ListWorkspaces call -- which the next two
commits will use to consolidate cmd/last and cmd/delete onto a
single helper, and to make 'ctask list' default to showing both
types.
Invalid Type values now return an explicit error from
ListWorkspaces (defensive against typos in callers).
cmd/list, cmd/last, and cmd/delete are migrated to the new field.
External behavior is unchanged in this commit; the cleanup of
ctask list semantics happens in a follow-up commit so the diff
stays reviewable.
ListOpts gains a Projects bool that filters by EffectiveType.
Default behavior (Projects: false) now returns tasks only --
this is a deliberate semantic change that supports the new
'ctask list' (tasks) vs 'ctask list --projects' (projects)
spec.
The change silently regresses two cmd-level callers that scan
for "the most recently updated workspace": cmd/last.go (used by
'ctask last') and cmd/delete.go (used to print the "this was
your most recent workspace" note). Both are fixed by unioning a
tasks-scan with a projects-scan, so 'last' and 'delete' continue
to consider both types.
Test helper createTestWorkspaceTyped allows setting an explicit
type (or "" to simulate a v0.2 workspace with no type field).
EnsureGitignore writes a minimal .gitignore (.ctask/ +
logs/sessions.log) iff one does not already exist. This is the
file-system half of the v0.3 seed-wins rule for .gitignore: if
either the general or project seed copied a .gitignore into the
workspace, EnsureGitignore must be a no-op.
GitAvailable + RunGitInit wrap exec.LookPath("git") and `git init`
respectively, so the caller in cmd/new.go can decide whether to
print the informational note when git is missing.
Tests cover:
- missing -> created with the minimal body
- present -> preserved verbatim
- integration: general seed .gitignore preserved end-to-end
- integration: project seed .gitignore preserved end-to-end
- integration: no seed -> minimal body created
ctask new gains --project, which:
- records type=project on the workspace
- defaults the category to "projects"
- applies general + project seed overlays
- uses CTASK_PROJECT_ROOT when set, with no doubled "projects/"
path unless the user explicitly passes -c
- exports CTASK_TYPE=project into the child session
EnvVars now takes a taskType arg and exports CTASK_TYPE. Empty
type defaults to "task" for safety. resume/open also pass
EffectiveType so the env var is correct on resume of a v0.2
workspace.
Git init for project mode is wired in the next commit.
When SkipCategoryDir is true, Create places the workspace directly
under Root and does not append a Category subdirectory. This is the
mechanism that prevents doubled paths like
~/projects/projects/<slug> when the user sets CTASK_PROJECT_ROOT
without an explicit -c flag. The Category value is still recorded
on TaskMeta so list/info/filter still work.
CreateOpts gains IsProject, SeedDir, and ProjectSeedDir. The new
seeding flow is:
1. write built-in defaults (task or project CLAUDE.md + notes.md)
2. apply general seed if SeedDir != "" (overlay)
3. apply project seed if IsProject && ProjectSeedDir != "" (overlay)
4. write task.yaml (last, so seeds can never inject metadata)
The obsolete TestCreateDoesNotOverwriteSeedFiles test is removed:
its v0.2 invariant ("don't overwrite existing files") no longer
holds because v0.3 always lays defaults onto a fresh dir and seeds
are expected to overwrite.
TaskMeta now records type: task or type: project. EffectiveType
returns 'task' for missing/empty/unknown values and for nil meta,
so v0.2 workspaces continue to read as tasks without any
migration. The field is placed between Category and Mode in the
YAML output. Tests cover the round-trip and the legacy
no-type-field case.
CopySeedDir is the v0.3 user seed overlay primitive: recursive copy
from a seed directory to a workspace, overwriting destination
files. task.yaml and the .ctask metadata directory at the seed root
are intentionally skipped so a stale or hostile seed cannot
overwrite ctask-owned state. Missing src is a no-op (the seed
directory is optional).
ResolveSeedDir, ResolveProjectSeedDir, and ResolveProjectRoot read
the v0.3 environment variables (CTASK_SEED_DIR,
CTASK_SEED_PROJECT_DIR, CTASK_PROJECT_ROOT) and fall back to
platform defaults under %APPDATA%\ctask or ~/.config/ctask. The
expandPath helper handles tilde expansion + abs-path resolution
once for all resolvers.
ClaudeMDProject returns the project-oriented default used when
--project is passed (and no project seed overrides it). Adds tests
asserting required v0.3 spec sections and ASCII-only content.
Replaces the v0.2 task-scoped template with the v0.3 workspace
guidelines (file placement conventions + session handoff).
ClaudeMD signature is preserved for API compatibility; the
parameters are no longer interpolated.
Add "Repository and Package Identity" rule to both the project-level CLAUDE.md
and the seeded CLAUDE.md template (internal/seed/templates.go). Every new ctask
workspace now inherits guidance against fabricating repo URLs, module paths,
GitHub identities, or remote install commands.
Also adds local-only build/install instructions and data safety invariant
documentation to the project-level CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace box-drawing characters (U+2500) in session log with ASCII dashes.
Replace em dashes (U+2014) in CLAUDE.md template with double hyphens.
Remove em dash from comment in run.go.
Add ASCII-guard tests for session log output and seed templates.
Prevents mojibake on Windows terminals that misinterpret UTF-8 as CP1252.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Advisory instructions to append a brief summary to notes.md before ending a session. Also update logs/ description from "reserved" to "automatic session snapshots".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactor new/resume/open to use session.Run() which wraps child process launch with pre/post manifest capture and append-only session logging to logs/sessions.log. Bump version to 0.2.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DefaultShell, ExecAgent, ExecShell with prompt prefix. Banner, container notice. Tests for all helpers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create function produces full workspace layout (task.yaml, CLAUDE.md, notes.md, context/, output/, logs/). Seed files only written if missing. Collision suffixing tested.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slugify, AutoTitle, DirName, ResolveDir with -2, -3 suffixing. Full test coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskMeta struct matching task.yaml schema exactly. Tests for roundtrip, field presence, and archive state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go module, cobra root command, config resolution (CTASK_ROOT, CTASK_AGENT, EnvVars) with tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>