Commit Graph

146 Commits

Author SHA1 Message Date
typebasedio 9070c4274c feat(v0.6): tri-state PID liveness probe (ProcessAlive/Dead/Unknown) 2026-05-15 14:25:31 -04:00
typebasedio 8d5243dce2 docs(v0.6): Phase 2 closeout in notes.md 2026-05-15 11:29:46 -04:00
typebasedio 0f96d202c7 feat(v0.6): ctask agents check + doctor integration
ctask agents check [workspace] validates that a workspace's agent
configuration can be launched without launching it: agent type known,
command resolvable on PATH, launch_dir valid, AGENTS.md present,
CLAUDE.md shim present (WARN only, claude type only). agent.env keys
are displayed informationally, with a WARN line when any key shadows a
ctask-exported CTASK_* var. Returns non-zero when any check FAILs.

ctask doctor includes the same sweep when a workspace context is
resolvable — the most-recently-active workspace via
workspace.MostRecentActive. When no active workspace exists, doctor
shows "Agent check: skipped (no workspace context)" without bumping
the failure counter. runAgentsCheckOnWorkspace is shared between the
standalone command and the doctor integration.

TestCompletionSubcommandViaExecute is made order-independent: cobra's
default completion command captures the root output writer on the first
Execute() in the process, and the new agents-check tests now run an
Execute() earlier in the suite.
2026-05-15 11:28:14 -04:00
typebasedio 0c6ed0c0cf feat(v0.6): AGENTS.md seed + CLAUDE.md shim + handoff + context-archive scaffold
New workspaces get:
  - AGENTS.md (always): canonical agent instructions — handoff workflow,
    notes-archive convention (~300-500 line trigger), cross-workspace
    discovery, do-not-touch warnings. Project variant adds workspace-
    structure and git-conventions sections.
  - CLAUDE.md shim (claude type only per v0.6 spec §6 — opencode shim
    deferred until its instruction-file convention is verified).
  - handoff.md: minimal current-state template the agent updates per
    session.
  - context/notes-archive/.gitkeep: directory pinned for git tracking,
    ready for the agent to populate per the archive convention.

seed.ClaudeMD and seed.ClaudeMDProject removed — no callers remain.
Existing workspaces are NOT modified; this is strictly a ctask-new code
path. The seed-wins overlay rule still applies — a user seed dir's
AGENTS.md/CLAUDE.md overrides the built-in.
2026-05-15 11:15:16 -04:00
typebasedio a61f900c86 feat(v0.6): --agent flag on ctask new selects agent type
Resolver gains SetCLIFlagAgent; DefaultAgent now layers CLIFlag above
EnvVar so doctor/info attribution renders the correct precedence chain
(CLIFlag overrides EnvVar overrides ConfigFile overrides Builtin).

ctask new --agent <type> writes agent.type into the new workspace's
task.yaml. Resolution and validation run before workspace.Create, so
--agent custom without a companion command refuses ("type custom
requires command") with no half-created workspace left on disk. The
deferred Phase 1 test TestCLIFlagOverridesEnvVar lands here.

--agent on resume/last/attach is unchanged (one-shot agent.command
override on the AgentSpec — Open Q 1).
2026-05-15 11:11:16 -04:00
typebasedio b75b82e676 feat(v0.6): launch path carries ResolvedAgent (command + args + env)
LaunchOpts.Agent (string) and WorkspaceEntryOptions.Agent (string) are
replaced by *agent.Resolved, carrying Command, Args, and Env. The five
entry commands (new, resume, last, open, attach) each construct an
AgentSpec from the workspace metadata, apply --agent as a one-shot
agent.command override (Open Q 1 — keeps muscle memory for users
passing executable paths), call agent.Resolve, and pass the result
through. resolveEntryAgent centralises the resume/last/open/attach path.

shell.ExecAgent and shell.ExecTmuxAgent gain an args parameter; agent.env
is merged into the env map at the session.Run launch switch, AFTER
ctask's exported CTASK_* vars (per spec §5: agent.env wins on collision).
mergeAgentEnv is the centralised merge.

Lease, manifest, write lock, heartbeat, summary, and provisional
cleanup are unchanged. The Agent string fields on Lease, SessionSummary,
and SessionInfo continue to record the launched command for diagnostics.
2026-05-15 11:08:03 -04:00
typebasedio 24f213449e feat(v0.6): internal/agent package — Resolve + BuiltinProfiles
Pure resolution logic combining a workspace's AgentSpec with the
user-level default_agent into a Resolved value carrying Command, Args,
and Env. No I/O — PATH lookup stays in shell.ExecAgent and ctask agents
check, so Resolve is trivially testable and reusable.

BuiltinProfiles enumerates claude and opencode; "custom" is the escape
hatch and requires command. Keep BuiltinProfiles in sync with
workspace.knownAgentTypes and workspace.IsBuiltinAgentType (Task 1).
2026-05-15 10:58:55 -04:00
typebasedio 8120c399df feat(v0.6): AgentSpec field on TaskMeta with backward-compat unmarshal
Replace TaskMeta.Agent (string) with TaskMeta.Agent (AgentSpec) carrying
type/command/args/env. Custom UnmarshalYAML preserves the legacy scalar
form: a built-in name (claude, opencode) maps to that type; any other
scalar maps to type=custom with the scalar as command. A missing agent
field leaves Type empty so the resolver fills in default_agent at launch.

ValidateAgentSpec enforces: known type (claude|opencode|custom),
type=custom requires command, command must be an executable name or
path with no whitespace or shell metacharacters.

Launch-path wiring (Task 3) and the --agent flag rework (Task 4) are
intentionally not part of this commit; cmd/* call sites are patched to
the minimum needed for the build to compile.
2026-05-15 10:58:06 -04:00
typebasedio 6c4c3e8df2 docs(v0.6): Phase 1 closeout in notes.md
Records:

- Branch feat/v0.6-multi-agent-config is the active surface for v0.6;
  Phase 1 (5 commits) is implemented, tested, and reviewed but the
  branch is NOT merged into main. Phase 2 and Phase 3 will continue on
  the same branch per the v0.6 spec's "one branch, three sequential
  milestones" policy. The eventual merge happens at the end of Phase 3
  alongside the version bump.
- The five Phase 1 commits with one-line summaries each:
    6f80c8b config file parser + resolver + source attribution
    0b21b8d schema_version and workspace.mode in task.yaml
    c918e5c doctor Settings section with source attribution
    937a1c8 info source attribution on Agent and Launch session mode
    6182d89 platform-override stderr warning on launch paths
- Per-commit detail: what each commit landed, where the load-bearing
  code lives, and what the test coverage is.
- Verification gate (all clean on tip 6182d89):
    go test ./... -count=1, go vet ./..., go build, just build-linux
    (statically linked ELF). Version stays v0.5.4 — bump is deferred
    to the end-of-Phase-3 commit.
- Phase 1 constraints held: no config auto-creation, no opportunistic
  schema writes, env vars remain overrides, task.yaml remains
  workspace state, no Phase 2 or Phase 3 work started.
- Native-Windows platform-override behavior across the three surfaces:
    * doctor + info — show source attribution
    * launch paths (new/resume/last/open) — emit one stderr warning
      per invocation and downgrade to direct
    * attach — does NOT downgrade; continues to refuse via
      preflightPersistentEntry because it has no direct-mode equivalent
- Architecture notes worth preserving for future Phase work:
  "one resolver per command", exported test seams, validation in
  ReadMeta rather than callers, omitempty as the load-bearing
  primitive for no-opportunistic-writes.
- Files-committed list and "How to resume" section both updated to
  point at Phase 2 as the next horizon and to enumerate the resume
  sanity checks against the unmerged branch.
2026-05-14 22:20:10 -04:00
typebasedio 6182d89135 feat(v0.6): platform-override stderr warning on launch paths
Closes the half-feature surfaced during Phase 1 review: doctor now
shows that persistent session_mode is overridden to direct on native
Windows, but the launch-time entry paths were silently downgrading
without telling the user. This commit adds the one-line warning
specified by the user, gated to the four entry commands where the
downgrade actually has an effect.

cmd/persistent.go:

- New package-level const platformOverrideWarning carries the exact
  spec-mandated text so tests don't have to retype the copy.
- New helper emitPlatformOverrideWarningIfNeeded(alwaysPersistent).
  Loads the resolver, reads SessionMode(), and emits the warning to
  stderr when:
    1. the call site is NOT AlwaysPersistent (attach has no
       direct-mode fallback and continues to refuse on native
       Windows via preflightPersistentEntry), AND
    2. the resolver's value resolved through PlatformOverride.
  Doctor and info do not call this helper — they render source
  attribution themselves and an extra stderr line from a diagnostic
  command would be noise. "Once per invocation" is provided
  implicitly by the call site running at most once per ctask
  command; no warn-once subsystem.

cmd/entry.go:

- defaultRunWorkspaceEntry calls the helper at the top, before any
  launch work. This covers all four entry paths (new / resume / last
  / open) since they all funnel through runWorkspaceEntry. attach
  also routes here but sets AlwaysPersistent=true, so the helper
  short-circuits.

cmd/platform_warning_test.go (5 cases):

- TestPlatformOverrideWarningEmittedOnLaunch — simulated native
  Windows + config session_mode: persistent + AlwaysPersistent=false
  → warning text appears on stderr.
- TestPlatformOverrideWarningNotEmittedWhenDirect — config
  session_mode: direct → no warning even on simulated Windows.
- TestPlatformOverrideWarningNotEmittedWithoutConfig — builtin
  default (direct) → no warning.
- TestPlatformOverrideWarningNotEmittedOnNonWindows — persistent
  config but isNativeWindows() returns false → no warning.
- TestPlatformOverrideWarningSkippedForAlwaysPersistent — attach's
  AlwaysPersistent=true gate prevents emission. attach's actual
  refusal contract on native Windows is already covered by
  TestPreflightRefusesNativeWindows in persistent_test.go.

Validation: `go test ./... -count=1`, `go vet ./...`, `go build`,
and `just build-linux` all clean.
2026-05-14 22:12:28 -04:00
typebasedio 937a1c8216 feat(v0.6): info source attribution on Agent and Launch session mode
Adds source-attribution rendering to `ctask info` for the two Phase 1
settings whose effective value depends on user-level defaults: the
agent recorded in task.yaml and the configured launch session mode.

cmd/info.go:

- runInfo loads the resolver once (config.LoadResolver) and reuses
  it across the rendering — matches the user's correction that "new
  doctor/info code should load the resolver once and reuse it".
- Agent line now reads `Agent: <value> (workspace)` when task.yaml
  has a non-empty agent (the common case), or `Agent: <value>
  (default)` / `Agent: <value> (default — <source label>)` when the
  field is empty and the value comes from the resolver fallback
  chain. The fallback path is informational only: every workspace
  created by recent ctask versions writes the resolved default into
  task.yaml at Create time, so the (workspace) branch is what users
  normally see.
- New "Launch session mode:" line lives immediately after Agent and
  before Created — outside the v0.5.4 Session block per the user's
  placement decision ("Keep it outside the Session block because it
  represents the configured launch default, not the current session
  lease mode"). Format: `Launch session mode: <value> (<source>)`.
- Two small helpers added: agentLineWithSource composes the agent
  payload + label; infoSourceLabel renders a single-row source
  string (CTASK_X env var / config file / built-in default /
  platform override). infoSourceLabel intentionally omits the
  override-chain suffix used by doctor — info's row layout has no
  room for the extra parenthetical.

cmd/info_attribution_test.go (5 cases):

- TestInfoAgentSourceWorkspace — task.yaml with agent set → "(workspace)"
- TestInfoAgentSourceDefaultForLegacy — empty agent → "(default)"
- TestInfoLaunchSessionModeFromConfig — config session_mode value +
  "config file" source label
- TestInfoLaunchSessionModeBuiltinDefault — no config, no env →
  "direct (built-in default)"
- TestInfoLaunchSessionModeAfterAgentBeforeCreated — placement check:
  Agent < Launch session mode < Created in the rendered output

Smoke-verified against an existing v0.5.x workspace on the installed
binary; render order and source labels match the spec example.
2026-05-14 21:57:20 -04:00
typebasedio c918e5ceab feat(v0.6): doctor Settings section with source attribution
Adds a new ── Settings ── block at the bottom of `ctask doctor` (after
the existing tmux INFO line, before the summary). The block reports
every user-level default tracked by the resolver — ctask_root,
project_root, seed_dir, default_agent, default_category, session_mode,
editor — together with the source it was drawn from.

Implementation:

- runDoctor calls config.LoadResolver() once and passes the resolver
  into checkSettings; no setting is re-resolved mid-block (the user's
  correction: "new doctor/info code should load the resolver once and
  reuse it").
- checkSettings prints the config-file lifecycle line first
  (`Config file: <path>` / `Config file: not found — using built-in
  defaults` / `[FAIL] unknown key: "..."` per the resolver's
  ConfigErr state), then iterates the resolver's ResolvedSetting
  accessors and feeds each one through printSettingLine.
- printSettingLine renders the key + value pair plus a `source: …`
  child line. When the resolver chains an Override, the source line
  embeds the overridden source + value in parens. For the
  PlatformOverride session_mode case, an additional
  `configured: <value>` line surfaces what the user asked for.
- formatSettingSource centralises the "EnvVar → CTASK_X env var" /
  "PlatformOverride → ... (persistent mode requires tmux; not
  available on native Windows)" / override-chain wording so doctor
  and info can share the same labels in commit 4.
- Only the invalid-config-file branch increments the failed counter.
  Missing file is INFO. The valid-file branch is purely informational.

Tests (cmd/doctor_settings_test.go, 6 cases):

- TestDoctorShowsSettingsSection — header present + all keys appear
- TestDoctorShowsSourceAttribution — every line has source: + at
  least one "built-in default"
- TestDoctorShowsOverrides — env vs config override chain rendered
- TestDoctorConfigNotFound — INFO, no failed bump
- TestDoctorConfigInvalid — FAIL bump + unknown key named
- TestDoctorSessionModePlatformOverrideRendered — direct value +
  "platform override" label + "configured: persistent" row

Smoke-verified end-to-end against the installed binary:
- no config + no env → all "built-in default"
- env CTASK_AGENT=aider + config default_agent=opencode →
  "CTASK_AGENT env var (overrides config file: opencode)"
- config session_mode=persistent on native Windows → "platform
  override (...)" plus "configured: persistent"
- config with typo "default_agnet: foo" → [FAIL] line names the key
  and the "settings not applied" advisory
2026-05-14 21:55:02 -04:00
typebasedio 0b21b8d3da feat(v0.6): schema_version and workspace.mode in task.yaml
Adds the two new metadata fields specified for Phase 1 of v0.6 plus
the validation and defaulting helpers around them.

internal/workspace/metadata.go:

- CurrentMetaSchemaVersion = 1 constant.
- WorkspaceSection struct {Mode string} with omitempty.
- SchemaVersion int and Workspace WorkspaceSection fields added to
  TaskMeta at the top of the struct. Both are omitempty so legacy
  task.yaml files (no schema_version, no workspace block) round-trip
  without acquiring these keys when an unrelated field is updated.
- EffectiveSchemaVersion(meta) — returns 1 for stored-value-0 legacy
  workspaces; non-zero stored values are returned verbatim.
- ValidateSchemaVersion(slug, meta) — rejects stored values higher
  than CurrentMetaSchemaVersion with the spec-mandated upgrade
  message. Accepts 0 (legacy missing).
- ValidateWorkspaceMode(slug, meta) — rejects modes other than ""
  and "native". "adopted" is reserved for v0.7.
- ReadMeta now runs both validators after unmarshal. The validation
  error includes the workspace slug (derived from task.yaml's slug
  field, falling back to the directory basename when the file itself
  is corrupt).

internal/workspace/create.go:

- workspace.Create stamps every new meta with
  SchemaVersion: CurrentMetaSchemaVersion and Workspace.Mode: "native".
  This is the ONLY write site for these fields in v0.6; resume,
  archive, restore, and any other path that rewrites task.yaml MUST
  NOT backfill them (the "no opportunistic schema writes" invariant).

internal/workspace/schema_test.go:

- 10 tests:
  * new meta written by Create contains schema_version: 1 +
    workspace.mode: native (both serialization and round-trip)
  * legacy meta without these fields loads with stored value 0 / ""
    and EffectiveSchemaVersion returns 1
  * task.yaml with schema_version: 2 is rejected with upgrade message
  * task.yaml with workspace.mode: adopted is rejected
  * the no-opportunistic-writes invariant is pinned for both WriteMeta
    and WriteMetaLocked: a legacy file rewritten with an updated
    UpdatedAt does NOT acquire schema_version or workspace: keys
  * ValidateSchemaVersion accepts {0, 1}; ValidateWorkspaceMode
    accepts {"", "native"}
2026-05-14 21:50:02 -04:00
typebasedio 6f80c8bf5c feat(v0.6): config file parser + resolver + source attribution
Phase 1 foundation. Adds:

- internal/config/configfile.go — strict-key YAML parser for the
  global config file. Unknown top-level keys invalidate the entire
  file (no partial apply); future schema versions are rejected with
  an upgrade message. Platform-appropriate path (XDG_CONFIG_HOME or
  ~/.config on Unix, %APPDATA% on native Windows).

- internal/config/resolver.go — Resolver / ResolvedSetting /
  SettingSource (Builtin, ConfigFileSrc, EnvVar, CLIFlag,
  PlatformOverride). Each setting carries provenance plus an
  Override chain so callers can render "env var (overrides config
  file: X)" lines. session_mode applies the native-Windows
  platform override at the resolver layer with the configured
  value chained as Override. Exports SetConfigPathForTest and
  SetIsNativeWindowsForTest so cross-package tests can isolate
  themselves from host config or simulate the override.

- internal/config/config.go — legacy ResolveRoot / ResolveAgent /
  ResolveSeedDir / ResolveProjectRoot / ResolveSessionMode now
  delegate to LoadResolver so config-file values take effect for
  entry commands. ResolveProjectRoot preserves the "" sentinel for
  built-in source so SearchRoots and doctor checkProjectRoot keep
  their existing fallback semantics. ResolveSessionMode preserves
  the v0.5.3 unknown-value stderr warning, distinguishing env-var
  and config-file sources in the message.

- Tests: 6 LoadConfigFile cases (missing, basic, partial, unknown
  key, future schema, malformed YAML), 3 ConfigFilePath cases
  (XDG / ~/.config / %APPDATA%), 11 resolver cases including
  layering, override chains, path expansion, platform override,
  and SettingSource string rendering.

- Test isolation: vulnerable tests in config_test.go and
  config_roots_test.go that asserted Builtin-source defaults now
  call SetConfigPathForTest to point at a nonexistent path,
  insulating them from developer-host config files. Three cmd
  tests that asserted persistent session_mode behavior now call
  SetIsNativeWindowsForTest to disable the platform override
  (Phase 1 introduces the override; the layering-only behavior
  these tests cover is tested separately in resolver_test.go).

No new dependencies (gopkg.in/yaml.v3 was already in go.mod).
No version bump (lands at the end of Phase 3 per the v0.6 spec).
2026-05-14 21:47:25 -04:00
typebasedio 8304545840 docs(v0.5.4): session handoff in notes.md (ship report + v0.6 Phase 1 scope)
Bump notes.md to anchor on v0.5.4:

- Top stanza + "Where we are": v0.5.4 shipped on main at merge commit
  10b7d5a, installed binary reports v0.5.4.
- Insert "What v0.5.4 delivered" with the 8-commit list and
  validation results.
- Insert "From v0.5.4 (new -- don't unlearn)" load-bearing design
  points covering SessionStatus's display-only contract, the
  session.json/mode naming preserved from v0.5.3, the column-14
  alignment in info, the SESSION column / list --names invariant,
  and the errArchivedWorkspace sentinel scoping.
- Append v0.5.4 Don't re-do entries (display-only SessionStatus, no
  PID liveness in the helper, --names contract, archived em-dash,
  sentinel scoping, cmd-vs-session boundary, no hardcoded "ctask").
- Replace "Next: v0.6 (planning)" with "Next: v0.6 Phase 1" listing
  config file + schema_version + workspace.mode + source attribution
  in doctor/info -- explicitly noting Phase 2 (agent profiles,
  AGENTS.md, PID liveness, lazy-cleanup adoption) is out of scope
  until Phase 1 is implemented and reviewed.
- Update Tree state, How to resume, Files to read first.
- Resolve closed limitations: v0.5.2 duplicate Cobra Error line, the
  v0.5.3 invocation-name asymmetry (audit codified the split as the
  right line), info/list session-state visibility, docs/commands.md
  staleness.
- Record spec deviations: v0.5.4 spec referred to .ctask/lease.json
  and session_mode but the actual code uses session.json and mode.
  Implementation correctly preserved the existing names per the
  spec's "no new metadata fields" constraint. Future specs touching
  this surface should use the actual names.
2026-05-14 20:22:06 -04:00
typebasedio 10b7d5ab82 Merge branch 'feat/v0.5.4-session-visibility-polish' into main 2026-05-14 20:07:38 -04:00
typebasedio 7704cd93fc release(v0.5.4): bump version to 0.5.4 2026-05-14 20:01:57 -04:00
typebasedio ae9bfafb1f polish(v0.5.4): suppress Cobra duplicate Error on archived resume
ctask resume <archived-workspace> printed both the helpful [ctask]
diagnostic + restore hint AND a redundant trailing "Error: workspace
archived" line from Cobra's default error rendering. Cosmetic but
unprofessional.

Add an errArchivedWorkspace sentinel and have runResume flip
SilenceErrors only when the inner error is that sentinel. All other
resume errors (lookup failure, metadata write failure, etc.) continue
to flow through Cobra's default rendering unchanged — we only silence
the case where we have already printed an equivalent diagnostic
ourselves.

Verified end-to-end through Cobra Execute (not just runResume direct
invocation) so the SilenceErrors-flip-from-RunE timing is exercised.
2026-05-14 20:01:19 -04:00
typebasedio 4fd0befee1 docs(v0.5.4): rewrite commands.md as a structured reference
Bring docs/commands.md up to current state. The document was last
substantively updated in v0.5; v0.5.2 (restore/notes/path/list --names/
completion + archived-inclusive lookup), v0.5.3 (attach + --direct +
persistent session mode + doctor tmux check), and v0.5.4 (info Session
block + list SESSION column) were all undocumented.

Format follows the spec section §3: each command has Purpose, Usage,
Scenarios, Examples, Flags, Notes, Related. Sections are kept short
(roughly one screen each) — if a command needed more, the command is
doing too much, not the docs.

New non-command sections: workspace layout (task and project), a
consolidated environment-variables table (previously scattered),
explicit query-resolution rules including archive-inclusive lookup
behavior, session modes (direct vs persistent), and a shell-completion
how-to.

Examples use the canonical "ctask" command name per spec §3 — docs
describe the product, not the user's local binary path.
2026-05-14 19:59:22 -04:00
typebasedio 0fb8de697b polish(v0.5.4): invocation-name audit + targeted regression tests
Audit walked every cmd/ and internal/ file that produces user-facing
output. All command-form hints (text the user is meant to type back)
were already routed through invocationName() in v0.5.3 — the audit is
a verification pass, not a code rewrite.

Additions:

- Extract formatResumeRestoreHint and formatDirectModeTmuxHint as
  testable string-only helpers. Production paths are unchanged
  behaviorally; the helpers exist purely so the audit can pin down
  the format strings without simulating tmux or stderr capture.

- Two new tests pinning the invocation name to a non-canonical value
  ("my-bin"). The pre-existing tests already protect these surfaces
  but pin the name to "ctask", so they cannot detect a regression
  that hard-codes "ctask" inside the format string. The new tests
  flush that out for the resume restore hint and the Layer-1
  active-session attach hint.

- Drop the duplicated withInvocationName helper accidentally added in
  the info-session tests; reuse the canonical helper from
  persistent_test.go.

Product-identity references ("ctask persistent mode requires tmux",
the SSH-remote `ssh -t <host> ctask <subcmd>` hint, doctor's "[ctask]"
diagnostic prefix, root-command Use:/Long:) deliberately remain
literal per spec §2.
2026-05-14 19:56:05 -04:00
typebasedio 0c8076aba9 feat(v0.5.4): list SESSION column
Add a SESSION column to ctask list output, inserted to the right of
STATUS per spec. Values: "direct", "persistent", "stale", or em dash
for no session. Populated by SessionStatus, so each workspace adds at
most one short lease-file read — negligible for typical workspace
counts.

Archived workspaces always render as the em dash regardless of any
lease file present. The spec calls this a display simplification, not
a lifecycle invariant: ctask info still surfaces the raw session
state on archived workspaces because info is the diagnostic command.

ctask list --names is unchanged: one basename per line, no header,
no SESSION column. Verified by a regression test that asserts every
emitted line is bare-basename whitespace-free and contains none of
the SESSION tokens.
2026-05-14 19:52:52 -04:00
typebasedio e0e9cd764e feat(v0.5.4): info Session block
Add a Session block to ctask info output, surfacing the workspace
session lease state derived from SessionStatus. Inserted between Path
and any launch-dir fields so the new content is visually distinct
from both blocks.

Format: state on the header line, then indented Mode / Owner / Attach
/ Note rows aligned at column 14. The Owner line omits the hostname
when it matches the local machine. The Attach hint surfaces only for
active+persistent sessions and uses invocationName() so the suggested
command reflects the user's actual invocation. Malformed leases
render as stale with a single-line diagnostic and no Mode/Owner/Attach
rows so we never display fields parsed from a broken file.

Exposes session.CurrentHostname() so the cmd layer has a single
source of truth for the local-vs-remote hostname check.
2026-05-14 19:51:21 -04:00
typebasedio 7f2c43d599 feat(v0.5.4): SessionStatus display-only helper
Add internal/session.SessionStatus(wsDir) that derives a display-only
view of the workspace session lease. Pure read of .ctask/session.json
with no tmux invocation, no PID liveness, no lock acquisition, and no
mutation of lease state.

States: none | active | stale. Mode defaults to "direct" when a
pre-v0.5.3 lease lacks the field. Malformed leases surface as stale
with a diagnostic so the present-but-broken case stays visible
instead of being silently classified as none.

Reused for ctask info and ctask list in subsequent commits. Lifecycle
code continues to use the existing primitives (ReadLease, IsFresh,
InspectLease).
2026-05-14 19:47:24 -04:00
typebasedio 1e9333254e Merge branch 'feat/v0.5.3-persistent-session-mode' into main
v0.5.3: persistent session mode via tmux.

- CTASK_SESSION_MODE env var: direct (default) | persistent
- ctask attach: always-tmux entry command
- --direct flag on new/resume/last/open to bypass persistent mode per invocation
- Deterministic session names: ctask-<category>-<slug>-<sha256_6>
- Three entry paths: owner-create, passive reattach, adopted reattach
- Adoption transfers ownership under metadata write lock with race-guard re-check
- v0.4 four-layer concurrency model preserved; Layer 3 selectively skipped on reattach
- Provisional cleanup bypassed in persistent mode (gate UX assumption doesn't translate)
- last-session-summary.json gains 4 optional fields (end_reason, detected_via,
  session_ownership, adopted_from_orphan_at)
- Native Windows refuses persistent mode with WSL recommendation; --direct bypass works
- Doctor reports session mode + tmux presence/version when persistent
- v0.4 lease prompt now suggests 'ctask attach <slug>' when a tmux session exists
- User-facing command suggestions use filepath.Base(os.Args[0])
- 21 commits including the polish patch (c204d87)
2026-05-14 18:25:13 -04:00
typebasedio c204d87b47 polish(v0.5.3): v0.4 lease-prompt hint, invocation-name in user-facing commands, smoke-checklist fixes
Three independent v0.5.3 polish items surfaced during manual WSL smoke testing,
bundled into one commit since they're all string/UX touches with no behavior change.

1. v0.4 lease-prompt tightening (`internal/session/{lease,run,run_preflight}.go`,
   `cmd/entry.go`): when ctask is about to enter direct mode on a workspace that
   already has a live tmux session, the "Continue anyway?" prompt now suggests
   `ctask attach <slug>` as the reattach path. Threaded via
   `PreflightOpts.ActiveLeaseHint` / `LaunchOpts.ActiveLeaseHint`; computed in
   `cmd/entry.go::directModeTmuxHint` (best-effort: silent when no tmux on PATH
   or no session for the workspace). Closes the footgun where a user forgot to
   `export CTASK_SESSION_MODE=persistent` in a second terminal and hit the v0.4
   coexistence prompt without realizing tmux passive-reattach was the intended
   path.

2. Invocation-name in user-facing command hints (`cmd/invocation.go` new,
   `cmd/{persistent,entry,resume,doctor}.go`): the binary name printed in
   bypass / restore / "create one with" suggestions now reflects
   `filepath.Base(os.Args[0])` instead of a hard-coded "ctask". Local-build
   PowerShell users running `.\ctask.exe` see `ctask.exe new <ws> --direct`,
   matching what they need to type. Installed contexts continue to see `ctask`.
   Test seam (`invocationNameOverride`) pins the name to "ctask" in unit tests
   so substring assertions stay stable across Go test binary names. Descriptive
   prose ("ctask persistent mode requires...") and the ssh-remote hint
   (`ssh -t <host> ctask <subcmd>`) intentionally keep the literal "ctask" —
   they refer to the program identity / remote invocation, not the local
   command form. Affected tests: `cmd/{persistent,resume}_test.go` tightened to
   check the full `"<binary> <subcmd> <workspace> --direct"` form.

3. Smoke-checklist fixes (`docs/.../2026-05-08-v0.5.3-smoke-test-checklist.md`):
   six issues caught during the run -- S2 now exports CTASK_SESSION_MODE in
   both terminals (previously only WSL-A had it, which routed WSL-B's
   secondary resume through direct mode instead of passive reattach); O3/A2
   tmux-ls expectations corrected (tmux doesn't emit a literal "(detached)"
   token); P2 expected behavior rewritten (passive reattach detach returns to
   prompt immediately -- AttachExisting calls only shell.AttachSession with
   no PollSessionEnd, the owner is responsible for finalize); A1 no longer
   asks for Ctrl-C in WSL-B; A6 path now uses a glob (was hardcoded to the
   checklist's authoring date, broke on v0.5.1 local-time directory naming);
   M1 PATH-hide rewritten to `$HOME/.local/bin` only (was `:/bin` which is a
   symlink to /usr/bin on usrmerge systems, did not hide tmux) and uses
   `command -v` instead of `which` (which is itself in /usr/bin, unreachable
   under the minimal PATH); M3/M4 reordered so the no-workspace verification
   runs after PATH is restored (was silently failing under minimal PATH);
   T1 wording made "substring match" explicit; T2 wording made N/A
   unambiguous; section 10 split into 10a-10f, each a separate input, to
   work around PSReadLine multi-line paste parsing.
2026-05-14 18:22:27 -04:00
typebasedio 548e292310 docs(v0.5.3): session handoff in notes.md (top stanza, tree state, resume, load-bearing, don't re-do) 2026-05-09 14:48:54 -04:00
typebasedio 659e318535 docs(v0.5.3): smoke-test checklist v2 -- explicit terminals and corrected expectations 2026-05-08 15:58:26 -04:00
typebasedio aee3a20012 docs(v0.5.3): manual WSL smoke-test checklist for the user 2026-05-08 14:30:30 -04:00
typebasedio cf2f43644c docs(v0.5.3): smoke-test log per executor constraint #5 2026-05-08 14:21:34 -04:00
typebasedio 4170a2849e docs(v0.5.3): record v0.5.3 completion in notes.md 2026-05-08 14:06:42 -04:00
typebasedio 45ea5beba6 docs(v0.5.3): persistent session mode reference 2026-05-08 14:06:02 -04:00
typebasedio dea64fcbb7 chore(v0.5.3): bump version to 0.5.3 2026-05-08 14:05:16 -04:00
typebasedio be508e2ec7 feat(v0.5.3): doctor -- checkTmux three-state helper 2026-05-08 14:04:14 -04:00
typebasedio 8dec5e08a4 feat(v0.5.3): cmd attach -- always-tmux entry via runWorkspaceEntry 2026-05-08 14:02:56 -04:00
typebasedio 5f76feecdf feat(v0.5.3): cmd open -- preserve --all resolution; delegate to runWorkspaceEntry with Shell:true 2026-05-08 14:02:03 -04:00
typebasedio 2d3ebfbc3a feat(v0.5.3): cmd new -- persistent preflight before workspace.Create; delegate to runWorkspaceEntry 2026-05-08 14:01:07 -04:00
typebasedio f5746df314 feat(v0.5.3): shared workspace-entry helper; resume + last delegate; fresh_remote prompt 2026-05-08 13:59:53 -04:00
typebasedio 08fb5bb1c3 feat(v0.5.3): AdoptExistingPersistentSession with race guard, UpdatedAt bump, attach-error propagation 2026-05-08 13:56:46 -04:00
typebasedio a1309b596e feat(v0.5.3): LaunchOpts.SessionMode/TmuxPath; shouldRunProvisional gate 2026-05-08 13:54:39 -04:00
typebasedio 8b82af1598 feat(v0.5.3): summary fields for end_reason / ownership / adoption 2026-05-08 13:53:02 -04:00
typebasedio 7697ec0507 feat(v0.5.3): AttachExisting passive reattach helper 2026-05-08 13:51:57 -04:00
typebasedio 08b0f1a6a7 feat(v0.5.3): shared persistent preflight (cmd/persistent.go) 2026-05-08 13:51:21 -04:00
typebasedio 53adba638e feat(v0.5.3): centralized tmux primitives (LookupTmux, HasSession, NewSession, AttachSession, PollSessionEnd, ExecTmux*) 2026-05-08 13:50:17 -04:00
typebasedio 1ab1cda111 feat(v0.5.3): InspectLease four-state classifier 2026-05-08 13:48:37 -04:00
typebasedio 120dc54337 feat(v0.5.3): ResolveSessionMode env var resolver 2026-05-08 13:47:49 -04:00
typebasedio 32fa5d0d21 feat(v0.5.3): SessionName deterministic tmux session naming 2026-05-08 13:47:01 -04:00
typebasedio e448effd2f docs(v0.5.2): record v0.5.2 completion in notes.md
Add the v0.5.2 round summary (commits a5e508b..3b6be0d + 5910100):
restore/notes/path commands, direct-lookup archived-inclusive policy,
resume archived hint, list --names, shell completion, cross-workspace
context seed section. Windows + WSL validation passed; Linux binary
statically linked.

Add load-bearing v0.5.2 invariants to the "don't unlearn" section
(archived-inclusive lookup policy, list --names emits basenames not
slugs, completion calls ListWorkspaces directly, etc.). Replace the
"Next: v0.5.2" pointer with a "Next: v0.6 (planning)" stub covering
config/agent profile work, resume-error polish, and flag-aware
completion for open/delete --all.

Drop v0.5.2-spec.md from the untracked-files list (committed in a5e508b).
2026-05-07 20:54:29 -04:00
typebasedio 5910100d88 chore(v0.5.2): bump version to 0.5.2 2026-05-07 20:45:54 -04:00
typebasedio 3b6be0d732 feat(v0.5.2): cross-workspace context section in seed CLAUDE.md
Adds a concise "## Cross-Workspace Context" section to both the
task and project CLAUDE.md seed templates. Teaches the agent to
inspect related workspaces with the new commands before making
changes:

  ctask list --all          discover all workspaces, including archived
  ctask info <workspace>    view metadata and status of any workspace
  ctask notes <workspace>   read another workspace's notes.md
  ctask path <workspace>    get the filesystem path to inspect files

Closes the v0.5.2 design loop: ctask exposes workspace context
through CLI commands and lets agents consume it themselves. The
seed text is read-only by convention — the section explicitly
asks the agent to treat other workspaces as read-only unless the
user grants modification rights.

Applies to newly created workspaces only. Existing workspaces
keep their current CLAUDE.md (per spec: no retroactive overwrite).
Users with custom seed directories see this only if they update
their seeds.
2026-05-07 19:47:43 -04:00
typebasedio 56d2e07716 feat(v0.5.2): list --names for machine-readable enumeration
Adds a --names flag to ctask list that emits one workspace
directory basename per line, no header, no decoration. Empty
result is empty stdout with zero exit code (no "No workspaces
found." placeholder).

Used by shell completion scripts and external tooling. Candidates
are directory basenames rather than bare slugs because basenames
are unique under the resolver's exact-match step while slugs can
collide across categories or dates.

Respects existing list filters: --all, --task, --projects,
--category, --limit. So:

  ctask list --names           active workspaces only
  ctask list --names --all     active and archived
  ctask list --names --projects active project workspaces

The new TestListNamesCandidatesResolveUniquely test enforces the
spec invariant: every line emitted by list --names must resolve
to exactly one workspace via the standard resolver.
2026-05-07 19:47:33 -04:00