qqq-cli 0.12.0

Local-first task queue for coding agent sessions
# Luau filters

`qqq list --filter EXPR` and `qqq next --filter EXPR` compile one Luau expression to SQLite. Quote expression so shell preserves spaces, operators, string literals.

```sh
qqq list --filter 'status == "new" and priority >= 5'
qqq list --filter 'like(task_name, "%auth%")'
qqq list --filter 'parent_id ~= nil' --query migration --status new
qqq list --watch --json --filter 'priority > 0'
qqq next --local --session worker-1 --filter 'priority >= 5'
qqq next --local --wait --session worker-1 --filter 'like(description, "%Rust%")'
qqq list --filter 'datetime(created_time) >= datetime("now", "-7 days")'
qqq list --filter '(if parent_id == nil then priority else priority + 1) >= 5'
qqq list --tag frontend --tag "UI review" --query layout --status new
qqq list --filter 'has_tag("frontend") and priority >= 5'
qqq next --local --wait --tag frontend --tag bug
qqq next --explain --tag "界 面"
```

See [all variables](filter-variables.md), [all functions](filter-functions.md).

## List behavior

`--tag`, `--filter`, `--query`, `--status`, and `--readiness` combine with AND across list, next and TUI. Every repeated `--tag LABEL` must match; repeated statuses combine with OR. Archive visibility and completed-task limits apply before matching. Use `--all` to bypass configured completed limit, `--include-archived` to include archived rows.

Tags match whole stored labels with exact case and Unicode, never description text or substrings. Surrounding spaces are trimmed; exact duplicate selectors are removed. Quote labels with internal spaces. Empty labels, controls, commas and square brackets fail before project preflight or DB access. Case and Unicode normalization remain unchanged: `UI` differs from `ui`, and composed/decomposed Unicode spellings remain distinct. For a label starting with `-`, use `--tag="-label"`.

`has_tag("LABEL")` returns boolean under the same matching and validation rules. It accepts exactly one string literal, including a parenthesized literal; dynamic text expressions and nil fail during compilation. It composes with `and`, `or`, `not` and priority/status predicates. Use it in saved-view filters or config aliases:

```toml
[alias]
ui = "list --filter 'has_tag(\"frontend\") and status == \"new\"'"
```

`qqq ui --tag bug` requires both the configured expression and the appended tag selector.

Save actual project views with `qqq view save NAME --tag frontend --readiness ready`.
`list --view NAME`, `next --view NAME` and `tui --view NAME` use the same
definition. Saved and explicit selectors combine with AND, preserving independent
Luau truthiness. Explicit visibility flags override saved defaults. See
[named view persistence and TUI picker](views.md). Aliases remain config command
shortcuts; their expansion rules are unchanged.

Visible ancestors of direct matches remain as context rows, even when predicate is false for parent. Human output marks `[context]`; JSON marks `context_only: true`. Ancestors excluded by archive/completed limits remain excluded. Task order, complete descriptions, JSON fields stay unchanged. No matches -> `[]` in JSON, `No matching tasks.` in human output.

`--watch` compiles once, evaluates predicate for every committed DB snapshot, retains existing idle behavior. Date functions using `"now"` reevaluate on those snapshots; watch does not refresh solely because wall-clock time passed.

## Claim behavior

`next` applies predicate to **new candidates** inside atomic claim transaction. Candidates must also be unarchived, ready, unclaimed. Completed parent dependency remains required. Highest priority wins, oldest ID breaks ties.

Without `--dry-run`, existing owned task always returns, even when predicate or tag selectors no longer match. Changing selectors never releases or transfers ownership. Complete or explicitly release current task before choosing another.

`next --dry-run --filter EXPR` previews matching queued candidate using same readiness/priority rules. Existing claims are skipped. Preview uses read-only snapshot; `--wait` waits for matching candidate without claiming or dispatching it. No session or Herdr identity required.

No match -> JSON `null` or human `No task available for pickup.`. `--wait` blocks until matching ready candidate exists, reevaluating predicate on each claim attempt. Herdr dispatch uses same filter for readiness and atomic claim; no matching candidate creates no tab.

`--tag` uses the same matching in normal/waiting claims, dry-run, explain, list watch and Herdr dispatch. `next --explain --tag LABEL` reports `tag_excluded` when explicit tags fail, alongside any parent/prerequisite blockers. If tags match but the Luau expression fails, the reason is `filter_excluded`. Tag predicates inside arbitrary Luau expressions retain `filter_excluded`, since OR/NOT expressions cannot attribute failure to individual positive tags. `matches_filter` represents all combined selectors; readiness remains independent.

## Supported expression language

Syntax follows [Luau expression grammar](https://luau.org/syntax/). This is a typed query subset: no Luau VM or arbitrary script execution.

| Form | Example |
| --- | --- |
| Task variables | `priority`, `task_name`, `created_time` |
| Boolean/nil literals | `true`, `false`, `nil` |
| String literals | `"auth"`, `'auth'`, `[=[auth]=]` |
| Numeric literals | `5`, `1.5`, `1e-2`, `0xFF`, `0b1_010`, `1_000` |
| Parentheses | `(priority + 1) > 5` |
| Equality | `status == "new"`, `parent_id ~= nil` |
| Ordering | `priority < 5`, `priority <= 5`, `created_at > "2026-01-01"`, `id >= 10` |
| Logic | `not archived`, `priority > 0 and status == "new"`, `parent_id == nil or priority > 5` |
| Numeric arithmetic | `priority + 1`, `priority - 1`, `priority * 2`, `priority / 2` |
| String concatenation | `"Fix " .. "auth"` |
| UTF-8 byte length | `#description` |
| Conditional expression | `if archived then 0 elseif priority > 5 then 2 else 1` |
| Allowlisted functions | `like(description, "%auth%")`, `coalesce(parent_id, 0)` |

Luau precedence applies. Only `false` and `nil` are false; `0` and `""` are true. Final result is converted using that truthiness. For numeric selection use `priority > 0`; `--filter 'priority'` also matches zero priorities.

`and`/`or` return selected operand, preserving short-circuit behavior. Operand types must agree, with nil accepted alongside any type. Conditional branches follow same type rule. `(parent_id or 0) == 0` is valid; `true or 3` fails type checking. `not` and conditional conditions accept every supported type.

Equality handles nil: `parent_id == nil` tests absence; two nil values compare equal. Distinct non-nil types compare unequal, including `false == 0`. Ordering requires compatible numeric/text operands; nil ordering returns false. Strings use SQLite binary ordering. Arithmetic requires numeric operands, concatenation requires text; nil propagates through arithmetic, concatenation, byte length. No implicit string/number/boolean conversions. SQLite arithmetic is used, with `/` forced to real division; division by zero yields nil. SQLite function NULL becomes nil. Guard nullable operands or use `coalesce` when needed.

`length(text)` counts Unicode code points before NUL; `#text` counts UTF-8 bytes. Quoted strings support `\a \b \f \n \r \t \v \\ \' \"`, decimal byte escapes (up to three digits), `\xHH`, `\u{HEX}`, `\z` whitespace skipping, escaped newlines. Other escaped characters retain character without backslash, following Luau. Long-bracket strings normalize CRLF to LF, omit initial CRLF/LF, preserve standalone CR. Resulting string must be valid UTF-8. Numeric literals must be finite, magnitude at most 2^53; decimal/exponent/hex spellings cannot bypass bound through rounding.

Unsupported forms fail explicitly: assignments, statements, loops, tables, anonymous functions, arbitrary globals, member/method calls, casts, interpolated strings, hexadecimal floats, `%`, `//`, `^`. Functions require parentheses and documented signatures.

## Compilation and errors

Literal values become bound SQL parameters. Task columns, operators, function names come from compiler allowlist. SQL text in a string stays a string. Invalid syntax/names/types fail with `Invalid --filter` before DB open, migration, recovery, claim, or dispatch. SQLite runtime errors abort query; claim transaction rolls back.

Limits: 16 KiB source, 128 non-trivia tokens, 32 AST levels, 128 AST nodes, 64 KiB compiled SQL. Comments and whitespace do not count toward token limit; each string literal counts once. Large expressions can reach generated-SQL bound earlier because value-returning logic repeats operand SQL. Split oversized expressions into smaller predicates. No messages, image contents, aggregates, subqueries, SQL extension loading, or user-defined functions are exposed.