tasks
tasks
¶
Task metadata, lifecycle, and query operations.
This package is the single import surface for task operations —
from terok.lib.orchestration.tasks import … resolves every public
name below. The implementation is split into focused submodules:
meta— the on-disk I/O boundary: the dossier/bookkeeping split-write protocol, path helpers, and the directories task state lives in.identity— task ID generation, validation, and prefix resolution.naming— task name sanitization and random-name generation.query— theTaskMetaread model and the functions that hydrate it from disk + live container state.lifecycle— create, rename, delete (with archive-on-delete), stop, login, and status.archive— reading the immutable snapshots a deletion leaves behind.
Container runner functions (task_run_cli, task_run_headless,
task_restart) live in the companion task_runners module.
Status computation and domain value objects live in task_state;
their presentation tables live in task_display. Log viewing lives
in task_logs.
CONTAINER_MODES = ('cli', 'web', 'run', 'toad')
module-attribute
¶
All valid container mode suffixes used in container naming.
CONTAINER_TEROK_CONFIG = '/home/dev/.terok'
module-attribute
¶
In-container mount point for the per-task agent-config dir.
TASK_NAME_MAX_LEN = 60
module-attribute
¶
Maximum length of a sanitized task name.
__all__ = ['ArchivedTask', 'CONTAINER_MODES', 'CONTAINER_TEROK_CONFIG', 'TASK_NAME_MAX_LEN', 'TaskDeleteResult', 'TaskMeta', 'agent_config_dir', 'capture_task_logs', 'container_name', 'dossier_path', 'generate_task_name', 'get_all_task_states', 'get_login_command', 'get_task_container_state', 'get_task_meta', 'get_tasks', 'get_workspace_git_diff', 'is_task_id', 'iter_task_ids', 'list_archived_tasks', 'load_task_meta', 'lookup_container_by_pt', 'mark_task_deleting', 'meta_path', 'normalize_task_id_input', 'read_task_meta', 'resolve_task_id', 'sanitize_task_name', 'task_archive_list', 'task_archive_logs', 'task_delete', 'task_exists', 'task_list', 'task_login', 'task_new', 'task_rename', 'task_status', 'task_stop', 'tasks_archive_dir', 'tasks_meta_dir', 'update_task_exit_code', 'validate_task_name', 'wait_for_container_exit', 'write_task_meta']
module-attribute
¶
ArchivedTask(archive_dir, archived_at, task_id, name, mode, exit_code)
dataclass
¶
TaskDeleteResult(task_id, warnings)
dataclass
¶
Outcome of a task deletion — always completes, collects warnings.
The task is considered deleted regardless (metadata and workspace
removed), but individual cleanup steps may fail. warnings
carries human-readable descriptions of any steps that did not
complete cleanly.
TaskMeta(container_state=None, exit_code=None, deleting=False, initialized=False, starting=False, *, task_id, project_id='', mode, workspace, web_port, web_token=None, backend=None, preset=None, name='', provider=None, unrestricted=None, work_status=None, work_message=None, shield_state=None, created_at=None)
dataclass
¶
Bases: TaskState
Lightweight metadata snapshot for a single task.
Inherits lifecycle fields (container_state, exit_code,
deleting, initialized) from TaskState.
task_id
instance-attribute
¶
project_id = ''
class-attribute
instance-attribute
¶
Project the task belongs to.
Carried in the meta JSON so consumers that don't already know the
project (e.g. terok-shield's host-side dossier resolver, which only
has the meta-path pointer) can render full project/task identity
without re-deriving it from the on-disk path. Empty string for
pre-this-release meta files; the lifecycle backfills it on the
next mutation.
mode
instance-attribute
¶
workspace
instance-attribute
¶
web_port
instance-attribute
¶
web_token = None
class-attribute
instance-attribute
¶
backend = None
class-attribute
instance-attribute
¶
preset = None
class-attribute
instance-attribute
¶
name = ''
class-attribute
instance-attribute
¶
provider = None
class-attribute
instance-attribute
¶
unrestricted = None
class-attribute
instance-attribute
¶
work_status = None
class-attribute
instance-attribute
¶
work_message = None
class-attribute
instance-attribute
¶
shield_state = None
class-attribute
instance-attribute
¶
created_at = None
class-attribute
instance-attribute
¶
status
property
¶
Compute effective status from live container state + metadata.
load(project_id, task_id)
classmethod
¶
Load a TaskMeta from disk, with live container state hydrated.
Raises SystemExit if the task metadata file is not found.
Equivalent to the free-fn
get_task_meta,
kept as the canonical entry point so callers reach the class
through its own factory.
Source code in src/terok/lib/orchestration/tasks/query.py
container_name(project_id, mode, task_id)
¶
list_archived_tasks(project_id)
¶
Return archived tasks for project_id, sorted newest-first.
Source code in src/terok/lib/orchestration/tasks/archive.py
task_archive_list(project_id)
¶
Print archived tasks for project_id.
Source code in src/terok/lib/orchestration/tasks/archive.py
task_archive_logs(project_id, archive_id)
¶
Return the log file path for an archived task identified by archive_id.
archive_id is matched against archive directory names (prefix match).
Returns the log file path if found, or None.
Source code in src/terok/lib/orchestration/tasks/archive.py
is_task_id(text)
¶
normalize_task_id_input(raw)
¶
Collapse user-input variants to the canonical lowercase form.
Strips hyphens, lowercases, and applies the Crockford
I/L → 1, O → 0 substitutions. The result is still subject
to _TASK_ID_PREFIX_RE downstream — this only widens what
we accept, never what we emit.
Call-site discipline: only call this at user-interactive CLI
boundaries — argparse dispatch handlers and argcomplete completers.
Internal code paths (lib/*, tui/*, TUI pickers, clearance,
anything reading task IDs from disk, OCI annotations, or runtime
state) always work with canonical lowercase IDs and must never
re-normalise. Leaking this tolerance inward quietly defeats the
"we encode only in canonical form" invariant.
Source code in src/terok/lib/orchestration/tasks/identity.py
resolve_task_id(project_id, prefix)
¶
Resolve a (possibly partial) task ID to its full form.
The prefix is first run through normalize_task_id_input,
so callers may pass uppercase, hyphenated, or ambiguous-letter
variants (K3-V8H, k3v8I) — they collapse to the canonical
lowercase form before validation and lookup.
Raises SystemExit with an actionable message on zero or multiple matches.
Source code in src/terok/lib/orchestration/tasks/identity.py
capture_task_logs(project, task_id, mode)
¶
Capture container logs to the task's logs/ directory on the host.
Writes stdout/stderr from podman logs to
<tasks_root>/<task_id>/logs/container.log. Returns the log file
path on success, or None if the container doesn't exist or podman
fails.
project may be a ProjectConfig or a project-ID string
(the string form loads the config internally for backward compat).
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
get_login_command(project_id, task_id)
¶
Return the podman exec command to log into a task container.
task_delete(project_id, task_id)
¶
Delete a task's workspace, metadata, and any associated containers.
Before removal, captures container logs and archives the task metadata
and logs to archive/<project_id>/tasks/. Containers are stopped
best-effort via podman using the <project.id>-<mode>-<task_id>
naming scheme. Returns a TaskDeleteResult so the caller can
present any warnings from cleanup steps that failed.
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
task_login(project_id, task_id)
¶
task_new(project_id, *, name=None)
¶
Create a new task with a fresh workspace for a project.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str
|
The project to create the task under. |
required |
name
|
str | None
|
Optional human-readable name. Allowed characters are
lowercase letters, digits, hyphens, and underscores.
If |
None
|
Workspace Initialization Protocol:¶
Each task gets its own workspace directory that persists across container runs. When a container starts, the init script (init-ssh-and-repo.sh) needs to know whether this is:
- A NEW task that should be reset to the latest remote HEAD
- A RESTARTED task where local changes should be preserved
We use a marker file (.new-task-marker) to signal intent:
- task_new() creates the marker in the workspace directory
- init-ssh-and-repo.sh checks for the marker:
- If marker exists: reset to origin/HEAD, then delete marker
- If no marker: fetch only, preserve local state
- Subsequent container runs on the same task won't see the marker, so local work is preserved
This handles edge cases like: - Stale workspace from incompletely deleted previous task with same ID - Ensuring new tasks always start with latest code
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
task_rename(project_id, task_id, new_name)
¶
Rename a task by updating its metadata file.
Sanitizes new_name and writes the result to the task's metadata file.
Raises SystemExit if the task is unknown or the sanitized name is invalid.
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
task_status(project_id, task_id)
¶
Show live task status with container state diagnostics.
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
task_stop(project_id, task_id, *, timeout=None)
¶
Gracefully stop a running task container.
Uses podman stop --time <N> to give the container timeout seconds
before SIGKILL. When timeout is None the project's
run.shutdown_timeout setting is used (default 10 s).
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
wait_for_container_exit(container_name, project_id, task_id, timeout=7200)
¶
Wait for container_name to exit and record its code in task metadata.
Returns (exit_code, error_message). On a successful wait
error_message is None and the real exit code is persisted
— including a legitimate exit code of 124, which is no longer
conflated with the watcher's own timeout. On timeout exit_code
is None and the error message describes it.
Source code in src/terok/lib/orchestration/tasks/lifecycle.py
agent_config_dir(project_id, task_id)
¶
Host path of the agent-config dir bind-mounted at CONTAINER_TEROK_CONFIG.
Source code in src/terok/lib/orchestration/tasks/meta.py
dossier_path(meta_dir, task_id)
¶
Path to the wire-dossier JSON file — what shield consumers read.
The OCI dossier.meta_path annotation points operators at this
file. Companion bookkeeping lives at the _meta.yml sibling.
Source code in src/terok/lib/orchestration/tasks/meta.py
iter_task_ids(meta_dir)
¶
Yield every task ID with at least one meta file in meta_dir.
Source code in src/terok/lib/orchestration/tasks/meta.py
load_task_meta(project_id, task_id, expected_mode=None)
¶
Load task metadata and optionally validate mode.
Returns (meta, dossier_path): the merged in-memory dict (in
its internal storage shape — project_id / task_id / …)
plus the canonical dossier-file handle for write-back via
write_task_meta(dossier_path, meta). Raises SystemExit if
the task is unknown or its mode conflicts with expected_mode.
Source code in src/terok/lib/orchestration/tasks/meta.py
mark_task_deleting(project_id, task_id)
¶
Persist deleting: true to the task's metadata file.
Source code in src/terok/lib/orchestration/tasks/meta.py
meta_path(meta_dir, task_id)
¶
Path to the orchestrator-bookkeeping YAML — terok-internal state.
Holds everything except the wire-dossier triple. Single consumer (terok itself), so ruamel round-tripping is fine.
Source code in src/terok/lib/orchestration/tasks/meta.py
read_task_meta(meta_dir, task_id)
¶
Compose the orchestrator's logical task-meta dict from the on-disk pair.
Reads the wire-dossier JSON and the internal YAML, translates the
JSON's wire keys back to internal storage names, and returns the
union. Either file may be absent. Returns None only when
neither file is on disk.
Source code in src/terok/lib/orchestration/tasks/meta.py
task_exists(project_id, task_id)
¶
Return True if any task-meta file exists for (project_id, task_id).
Source code in src/terok/lib/orchestration/tasks/meta.py
tasks_archive_dir(project_id)
¶
Return the directory containing archived task data for project_id.
Lives under the namespace archive tree (archive/<pid>/tasks/) so
operators can find all archived data in one location. On project
deletion the entire archive/<pid>/ subtree is bundled into the
project snapshot and removed.
Source code in src/terok/lib/orchestration/tasks/meta.py
tasks_meta_dir(project_id)
¶
Return the directory containing task metadata files for project_id.
update_task_exit_code(project_id, task_id, exit_code)
¶
Update task metadata with exit code and final status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str
|
The project ID |
required |
task_id
|
str
|
The task ID |
required |
exit_code
|
int | None
|
The exit code from the task, or None if unknown/failed |
required |
Source code in src/terok/lib/orchestration/tasks/meta.py
write_task_meta(dossier_handle, meta)
¶
Atomic split-write: _dossier.json + _meta.yml in one atomic step each.
dossier_handle is a dossier-file handle — what
dossier_path returns
and what load_task_meta
hands back. The companion bookkeeping path is derived by swapping
the suffix. Atomic-rename writes (.tmp + os.replace) on each
file mean a partial write under EINTR / power loss can leave one
stale and the other fresh, but never a half-written file — readers
always get a parseable shape.
Source code in src/terok/lib/orchestration/tasks/meta.py
generate_task_name(project_id=None)
¶
Generate a random human-readable task name (e.g. talented-toucan).
When project_id is given, name categories are resolved from config:
project tasks.name_categories → global tasks.name_categories
→ deterministic 3-category selection based on project ID hash.
Source code in src/terok/lib/orchestration/tasks/naming.py
sanitize_task_name(raw)
¶
Sanitize a raw task name into a slug-style identifier.
Strips whitespace, lowercases, replaces spaces with hyphens,
removes characters outside [a-z0-9_-], collapses consecutive
hyphens, strips trailing hyphens, and truncates to
TASK_NAME_MAX_LEN. Returns None if the result is empty.
Leading hyphens are preserved so callers can detect and reject them
(a name starting with - looks like a CLI flag).
Source code in src/terok/lib/orchestration/tasks/naming.py
validate_task_name(sanitized)
¶
Return an error message if sanitized is not a valid task name, else None.
A name is invalid if it starts with a hyphen (looks like a CLI flag).
Callers should first check for None from sanitize_task_name
(which indicates the name was empty after sanitization).
Source code in src/terok/lib/orchestration/tasks/naming.py
get_all_task_states(project_id, tasks)
¶
Map each task to its live container state via a single batch query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str
|
The project whose containers to query. |
required |
tasks
|
list[TaskMeta]
|
List of |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str | None]
|
|
Source code in src/terok/lib/orchestration/tasks/query.py
get_task_container_state(project_id, task_id, mode)
¶
Get actual container state for a task (TUI helper).
Container state queries are runtime-agnostic — podman inspect
returns the same shape regardless of OCI runtime — so this short
cuts to PodmanRuntime rather than walking through the
per-project resolver (which would force a load_project for a
one-shot state probe).
Source code in src/terok/lib/orchestration/tasks/query.py
get_task_meta(project_id, task_id)
¶
Return metadata for a single task with live container state.
Hydrates container_state from the running container so that
TaskMeta.status reflects current reality rather than stale YAML.
Raises SystemExit if the task metadata file is not found.
Source code in src/terok/lib/orchestration/tasks/query.py
get_tasks(project_id, reverse=False)
¶
Return all task metadata for project_id, sorted by task ID.
get_workspace_git_diff(project_id, task_id, against='HEAD')
¶
Get git diff from a task's workspace via container exec.
Runs git diff inside the task container rather than on the host,
so that even poisoned git hooks only execute within the container sandbox.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str
|
The project ID |
required |
task_id
|
str
|
The task ID |
required |
against
|
str
|
What to diff against ( |
'HEAD'
|
Returns:
| Type | Description |
|---|---|
str | None
|
The git diff output as a string, or |
Source code in src/terok/lib/orchestration/tasks/query.py
lookup_container_by_pt(project_id, task_id)
¶
Resolve a project/task pair to the current container name, or None.
Powers the slash-form identity acceptance on per-container
terok executor * verbs (stop, future exec / logs /
state / login). Reads the recorded mode from the task's
meta file; returns None when the task is unknown or has never
been launched (no mode recorded), or when either input would
escape the project task store (path-traversal guard). The caller
decides whether to treat None as "pass the input through
verbatim" (raw container id) or "fail with an actionable error"
(unknown project/task).
Source code in src/terok/lib/orchestration/tasks/query.py
task_list(project_id, *, status=None, mode=None, agent=None)
¶
List tasks for a project, optionally filtered by status, mode, or agent preset.
Status is computed live from podman container state + task metadata.