---
name: taskflow
description: Structured project/task management for OpenClaw agents — markdown-first authoring, SQLite-backed querying, bidirectional sync, CLI, Apple Notes integration.
metadata:
{
"openclaw":
{
"emoji": "📋",
"os": ["darwin", "linux"],
"requires": { "bins": ["node"], "env": ["OPENCLAW_WORKSPACE"] },
},
}
---
# TaskFlow — Agent Skill Reference
TaskFlow gives any OpenClaw agent a **structured project/task/plan system** with markdown-first authoring, SQLite-backed querying, and bidirectional sync.
**Principle:** Markdown is canonical. Edit `tasks/*.md` directly. The SQLite DB is a derived index, not the source of truth.
---
## Security
### OPENCLAW_WORKSPACE Trust Boundary
`OPENCLAW_WORKSPACE` is a **high-trust value**. All TaskFlow scripts resolve file paths from it, and the CLI and sync daemon use it to locate the SQLite database, markdown task files, and log directory.
**Rules for safe use:**
1. **Set it only from trusted, controlled sources.** The value must come from:
- Your own shell profile (`.zshrc`, `.bashrc`, `/etc/environment`)
- The systemd user unit `Environment=` directive in a template you control
- The macOS LaunchAgent `EnvironmentVariables` dictionary you installed
**Never** accept `OPENCLAW_WORKSPACE` from:
- User-supplied CLI arguments or HTTP request parameters
- Untrusted config files read at runtime
- Any external input that has not been explicitly validated
2. **Validate the path exists before use.** Any script that reads `OPENCLAW_WORKSPACE` should confirm the directory exists before proceeding:
```js
import { existsSync } from 'node:fs'
import path from 'node:path'
const workspace = process.env.OPENCLAW_WORKSPACE
if (!workspace) {
console.error('OPENCLAW_WORKSPACE is not set. Aborting.')
process.exit(1)
}
if (!existsSync(workspace)) {
console.error(`OPENCLAW_WORKSPACE path does not exist: ${workspace}`)
process.exit(1)
}
// Resolve to absolute path to neutralize any relative-path tricks
const safeWorkspace = path.resolve(workspace)
```
3. **Do not construct paths from untrusted input.** Even with a valid `OPENCLAW_WORKSPACE`, never concatenate unvalidated user input onto it (e.g. `path.join(workspace, userSlug, '../../../etc/passwd')`). Use `path.resolve()` and check that the resolved path starts with the workspace root:
```js
function safeJoin(base, ...parts) {
const resolved = path.resolve(base, ...parts)
if (!resolved.startsWith(path.resolve(base) + path.sep)) {
throw new Error(`Path traversal attempt detected: ${resolved}`)
}
return resolved
}
```
4. **Treat `OPENCLAW_WORKSPACE` as a local system path only.** It must point to a directory on the local filesystem. Remote paths (NFS mounts, network shares) may work but are outside the tested configuration and could introduce TOCTOU (time-of-check/time-of-use) race conditions.
---
## Setup
### 1. Set environment variable
Add to your shell profile (`.zshrc`, `.bashrc`, etc.):
```bash
export OPENCLAW_WORKSPACE="/path/to/your/.openclaw/workspace"
```
All TaskFlow scripts and the CLI resolve paths from this variable. Without it, they fall back to `process.cwd()`, which is almost never what you want.
> **See also:** [OPENCLAW_WORKSPACE Trust Boundary](#openclaw_workspace-trust-boundary) above for security requirements.
### 2. Link the CLI
```bash
ln -sf {baseDir}/scripts/taskflow-cli.mjs /opt/homebrew/bin/taskflow # macOS (Apple Silicon)
# or: ln -sf {baseDir}/scripts/taskflow-cli.mjs /usr/local/bin/taskflow
```
### 3. Run the setup wizard
```bash
taskflow setup
```
The wizard handles the rest: creates workspace directories, walks you through adding your first project(s), initializes the database, syncs, and optionally installs the macOS LaunchAgent for periodic sync.
**Alternative — manual setup:**
Manual steps (if you prefer explicit control)
```bash
# Create workspace dirs
mkdir -p "$OPENCLAW_WORKSPACE/tasks" "$OPENCLAW_WORKSPACE/plans" "$OPENCLAW_WORKSPACE/memory" "$OPENCLAW_WORKSPACE/logs"
# Bootstrap the DB schema
taskflow init
# Create PROJECTS.md and tasks/-tasks.md manually (see templates/)
# Sync markdown → DB
taskflow sync files-to-db
# Verify
taskflow status
```
---
## First Run
### For agents (OpenClaw / AI)
When a user asks you to set up TaskFlow or you detect it has not been initialized:
1. **Detect state.** Check for `$OPENCLAW_WORKSPACE/PROJECTS.md` and `$OPENCLAW_WORKSPACE/memory/taskflow.sqlite`.
2. **If clean slate:** Ask the user for their first project name and description, then run:
```bash
taskflow setup --name "Project Name" --desc "One-liner description"
```
Follow up by running `taskflow status` to confirm.
3. **If PROJECTS.md exists but no DB:** Run `taskflow setup` (it detects the state automatically and offers to init + sync).
4. **If both exist:** Run `taskflow status` — already set up.
5. After setup, update `AGENTS.md` with the new project slug so future sessions discover it via `cat PROJECTS.md`.
### For humans (CLI)
```bash
taskflow setup
```
The interactive wizard will:
- Detect your existing workspace state
- Walk you through naming your first project(s)
- Create `PROJECTS.md` and `tasks/-tasks.md` from templates
- Initialize the SQLite database and sync
- Offer to install the periodic-sync daemon (LaunchAgent on macOS, systemd timer on Linux) for automatic 60s sync
**Non-interactive (scripted installs):**
```bash
taskflow setup --name "My Project" --desc "What it does"
```
Passing `--name` skips all interactive prompts (daemon install is also skipped in non-interactive mode).
---
## Directory Layout
```
/
├── PROJECTS.md # Project registry (one ## block per project)
├── tasks/-tasks.md # Task list per project
├── plans/-plan.md # Optional: architecture/design doc per project
└── taskflow/
├── SKILL.md # This file
├── scripts/
│ ├── taskflow-cli.mjs # CLI entry point (symlink target)
│ ├── task-sync.mjs # Bidirectional markdown ↔ SQLite sync
│ ├── init-db.mjs # Bootstrap SQLite schema (idempotent)
│ ├── export-projects-overview.mjs # JSON export of project/task state
│ └── apple-notes-export.mjs # Optional: project state → Apple Notes (macOS only)
├── templates/ # Starter files for new projects
├── schema/
│ └── taskflow.sql # Full DDL
└── system/
├── com.taskflow.sync.plist.xml # Periodic sync (macOS LaunchAgent)
├── taskflow-sync.service # Periodic sync (Linux systemd user unit)
└── taskflow-sync.timer # Systemd timer (60s interval)
/
└── taskflow.config.json # Apple Notes config (auto-created on first notes run)
```
---
## Creating a Project
Follow this full checklist when creating a new project:
### 1. Add a block to `PROJECTS.md`
```markdown
##
- Name:
- Status: active
- Description: One-sentence description of the project.
```
- `slug` is lowercase, hyphenated (e.g., `my-project`). It becomes the canonical project ID everywhere.
- Valid status values: `active`, `paused`, `done`.
### 2. Create the task file
Copy `taskflow/templates/tasks-template.md` → `tasks/-tasks.md` and update the project name in the heading.
The file **must** contain these five section headers in this order:
```markdown
# — Tasks
## In Progress
## Pending Validation
## Backlog
## Blocked
## Done
```
### 3. Optionally create a plan file
Copy `taskflow/templates/plan-template.md` → `plans/-plan.md` for architecture docs, design decisions, and phased roadmaps. Plan files are **not** synced to SQLite — they are reference-only for the agent.
### 4. DB row (auto-created on first sync)
You do **not** need to manually insert into the `projects` table. The sync engine auto-creates the project row from `PROJECTS.md` on the next `files-to-db` run. If you want to be explicit via Node.js, use a parameterized statement:
```js
// Safe: parameterized insert — no string interpolation in the SQL
db.prepare(`INSERT INTO projects (id, name, description, status)
VALUES (:id, :name, :description, 'active')`)
.run({ id: slug, name: projectName, description: projectDesc })
```
---
## Task Line Format
Every task line follows this exact format:
```
- [x| ] (task:) [] []
```
| Field | Details |
|---|---|
| `[ ]` / `[x]` | Open / completed. Sync drives status from section header, not this checkbox. |
| `(task:)` | Task ID. Format: `-NNN` (zero-padded 3-digit). Sequential per project. |
| `[]` | **Required. Must come before owner tag.** See priority table below. |
| `[]` | Optional. Agent/model tag (e.g., `codex`, `sonnet`, `claude`). |
| `` | Human-readable task title. |
### ⚠️ Tag Order Rule
**Priority tag MUST come before owner tag.** The sync parser is positional — it reads the first `[Px]` bracket as priority, and the next `[tag]` as owner. Swapping them will misparse the task.
### ⚠️ Title Sanitization Rules
Task titles must be **plain text only**. Before writing any user-supplied string as a task title, apply the following rules:
1. **Reject lines that look like section headers.** A title may not start with one or more `#` characters followed by a space (e.g. `# My heading`, `## Done`). These would corrupt the sync parser's section detection.
2. **Reject the exact section header strings** even without leading whitespace:
- `In Progress`, `Pending Validation`, `Backlog`, `Blocked`, `Done`
- Comparison must be case-insensitive.
3. **Escape or strip markdown special characters** that have structural meaning in the task file:
| Character | Risk | Safe action |
|-----------|------|-------------|
| `#` | Looks like a header | Strip or reject |
| `- ` (dash + space at line start) | Looks like a list item / task | Strip leading `- ` |
| `[ ]` / `[x]` | Looks like a checkbox | Escape brackets: `\[` `\]` |
| `]` / `[` alone | Can corrupt `(task:id)` parse | Escape: `\[` `\]` |
| Newlines (`\n`, `\r`) | Creates multi-line titles | Strip / reject |
4. **Maximum length.** Titles should be ≤ 200 characters. Truncate or reject longer strings.
**Example sanitization (Node.js):**
```js
// Safe: sanitize a user-supplied task title before writing to markdown
function sanitizeTitle(raw) {
if (typeof raw !== 'string') throw new TypeError('title must be a string')
// Strip newlines
let title = raw.replace(/[\r\n]+/g, ' ').trim()
// Reject lines that look like section headers (# Heading or bare header words)
if (/^#{1,6}\s/.test(title)) {
throw new Error('Title may not start with a markdown heading (#)')
}
const BANNED_HEADERS = /^(in progress|pending validation|backlog|blocked|done)$/i
if (BANNED_HEADERS.test(title)) {
throw new Error('Title may not be a reserved section header name')
}
// Escape structural markdown characters
title = title
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
// Enforce length limit
if (title.length > 200) {
throw new Error('Title exceeds 200 character limit')
}
return title
}
```
These rules apply whenever a task title comes from **any external or user-supplied source** (CLI args, API payloads, file imports). Titles hard-coded by agents in their own sessions are low-risk but should still avoid structural characters.
✅ Correct: `- [ ] (task:myproject-007) [P1] [codex] Implement search`
❌ Wrong: `- [ ] (task:myproject-007) [codex] [P1] Implement search`
### Priority Levels (Configurable)
| Tag | Default Meaning |
|---|---|
| `P0` | Critical — must do now, blocks everything |
| `P1` | High — important, do soon |
| `P2` | Normal — standard priority (default) |
| `P3` | Low — nice to have |
| `P9` | Someday — no urgency, parking lot |
Priorities are configurable per-installation but the tags themselves (`P0`–`P3`, `P9`) are what the sync engine validates.
### Optional Note Lines
A note can follow a task line as an indented `- note:` line:
```markdown
- [ ] (task:myproject-003) [P1] [codex] Implement auth flow
- note: blocked on API key from vendor
```
> **Known limitation (v1):** Notes are one-way. Removing or editing a note in markdown does not propagate to the DB. This is tracked for a post-MVP fix.
### Example Task File Section
```markdown
## In Progress
- [ ] (task:myproject-001) [P1] [codex] Wire up OAuth login
- note: PR open, needs review
## Backlog
- [ ] (task:myproject-002) [P2] Add rate limiting middleware
- [ ] (task:myproject-003) [P3] Write integration tests
```
---
## Adding a New Task
1. **Determine the next ID.** Scan the task file for the highest existing `-NNN` and increment by 1. Or query SQLite using a **parameterized statement** (never interpolate the slug into SQL strings):
```js
// Node.js — safe, parameterized
const db = new DatabaseSync(dbPath)
const row = db
.prepare(`SELECT MAX(CAST(SUBSTR(id, LENGTH(:slug) + 2) AS INTEGER)) AS max_seq
FROM tasks_v2
WHERE project_id = :slug`)
.get({ slug: projectSlug })
const nextSeq = (row.max_seq ?? 0) + 1
const nextId = `${projectSlug}-${String(nextSeq).padStart(3, '0')}`
```
> ⚠️ **Never construct SQL by string interpolation.** Use `db.prepare()` with named or positional parameters (`?` or `:name`) for all values that come from external input. This applies even for read-only queries.
2. **Append the task line** to the correct section (`## Backlog` for new work, `## In Progress` if starting immediately).
3. **Format the line** using the exact format above. No trailing spaces. Priority tag before owner tag.
---
## Updating Task Status
**Move the task line** from its current section to the target section in the markdown file.
| Target State | Move to Section |
|---|---|
| Started / picked up | `## In Progress` |
| Needs human review | `## Pending Validation` |
| Not started yet | `## Backlog` |
| Waiting on dependency | `## Blocked` |
| Finished | `## Done` |
Also flip the checkbox: `[ ]` for active states, `[x]` for `Done` (and optionally `Pending Validation`).
The periodic sync (60s) will pick up the change and update SQLite automatically. To force an immediate sync:
```bash
node taskflow/scripts/task-sync.mjs files-to-db
```
---
## Querying Tasks
### Simple: Read the markdown file directly
```bash
cat tasks/-tasks.md
```
For a quick in-session view, just read the relevant section.
### Advanced: Query SQLite
> ⚠️ **SQL Safety Rule:** Any query that incorporates a variable value (project slug, task ID, status string, etc.) **must** use parameterized statements — not string interpolation. The `sqlite3` CLI examples below use only **static, hardcoded literal values** and are shown as diagnostic/inspection tools only. For programmatic use, always use the Node.js `db.prepare()` API with bound parameters.
#### sqlite3 CLI (static queries — for manual inspection only)
```bash
# All in-progress tasks across all projects (by priority)
# Safe: 'in_progress' is a static literal, not a variable
sqlite3 "$OPENCLAW_WORKSPACE/memory/taskflow.sqlite" \
"SELECT id, project_id, priority, title
FROM tasks_v2
WHERE status = 'in_progress'
ORDER BY priority, project_id;"
# Task count by status per project (no variables — safe for CLI)
sqlite3 "$OPENCLAW_WORKSPACE/memory/taskflow.sqlite" \
"SELECT project_id, status, COUNT(*) AS count
FROM tasks_v2
GROUP BY project_id, status
ORDER BY project_id, status;"
```
> Do **not** embed shell variables directly in the SQL string (e.g. `WHERE project_id = '$SLUG'`). That pattern is SQL injection waiting to happen. Use the Node.js API with parameters instead.
#### Node.js API — parameterized queries (required for programmatic use)
```js
import { DatabaseSync } from 'node:sqlite'
import path from 'node:path'
const dbPath = path.join(process.env.OPENCLAW_WORKSPACE, 'memory', 'taskflow.sqlite')
const db = new DatabaseSync(dbPath)
db.exec('PRAGMA foreign_keys = ON')
// ── Backlog for a specific project ─────────────────────────────
// :slug is a named parameter — never interpolated into the SQL string
const backlog = db
.prepare(`SELECT id, priority, title
FROM tasks_v2
WHERE project_id = :slug AND status = 'backlog'
ORDER BY priority`)
.all({ slug: 'my-project' }) // value bound at runtime, never in SQL string
// ── Audit trail for a specific task ────────────────────────────
const transitions = db
.prepare(`SELECT from_status, to_status, actor, at
FROM task_transitions_v2
WHERE task_id = ?
ORDER BY at`)
.all('my-project-007') // positional parameter — also safe
// ── Write: update task status ───────────────────────────────────
// NEVER: db.exec(`UPDATE tasks_v2 SET status='${newStatus}' WHERE id='${id}'`)
// ALWAYS:
db.prepare(`UPDATE tasks_v2 SET status = :status, updated_at = datetime('now')
WHERE id = :id`)
.run({ status: 'done', id: 'my-project-007' })
```
### CLI Quick Reference
```bash
# Terminal summary: all projects + task counts by status
taskflow status
# Add a task in markdown with automatic next ID
taskflow add taskflow "Implement quick add command" --priority P1 --owner codex
# List current tasks for a project (excludes done by default)
taskflow list taskflow
taskflow list --project "TaskFlow" --all
taskflow list task --status backlog,pending_validation --json
# JSON export of full project/task state (for dashboards, integrations)
node taskflow/scripts/export-projects-overview.mjs
# Detect drift between markdown and DB (exit 1 if mismatch)
node taskflow/scripts/task-sync.mjs check
# Sync markdown → DB (normal direction; run after editing task files)
node taskflow/scripts/task-sync.mjs files-to-db
# Sync DB → markdown (run after programmatic DB updates)
node taskflow/scripts/task-sync.mjs db-to-files
```
### Apple Notes Export (Optional — macOS Only)
TaskFlow can maintain a live Apple Note with your current project status. The note is rendered as rich HTML and written via AppleScript.
```bash
# Push current status to Apple Notes (creates note on first run)
taskflow notes
```
On first run (or during `taskflow setup`), a new note is created in the configured folder and its Core Data ID is saved to:
```
$OPENCLAW_WORKSPACE/taskflow.config.json
```
Config schema:
```json
{
"appleNotesId": "x-coredata://...",
"appleNotesFolder": "Notes",
"appleNotesTitle": "TaskFlow - Project Status"
}
```
**Important — never delete the shared note.** The note is always edited in-place. Deleting and recreating it generates a new Core Data ID and breaks any existing share links. If the note is accidentally deleted, `taskflow notes` will create a new one and update the config automatically.
For hourly auto-refresh, add a cron entry:
```bash
# Run: crontab -e
0 * * * * OPENCLAW_WORKSPACE=/path/to/workspace /path/to/node /path/to/taskflow/scripts/apple-notes-export.mjs
```
Or install a dedicated LaunchAgent (macOS) targeting `apple-notes-export.mjs` with an hourly `StartInterval` of `3600`.
This feature is entirely optional and macOS-specific. On other platforms, `taskflow notes` exits gracefully with a message.
---
## Memory Integration Rules
These rules keep daily memory logs clean and prevent duplication.
### ✅ Do
- Reference task IDs in daily memory logs when you complete or advance work:
```
Completed `myproject-007` (OAuth login). Moved `myproject-008` to In Progress.
```
- Keep memory entries narrative — what happened, what you decided, what's next.
### ❌ Do Not
- **Never duplicate the backlog in daily memory files.** `tasks/-tasks.md` is the single source of truth for all pending work. Memory files should not list what's left to do.
- Do not track task state changes in memory (e.g., "Task 007 is now in progress"). Only note meaningful progress events or decisions.
- Do not create new tasks in memory files. Add them to the task file directly.
### Pattern: Loading Project Context
At the start of a session involving a project:
1. `cat PROJECTS.md` — identify the project slug and status
2. `cat tasks/-tasks.md` — load current task state
3. `cat plans/-plan.md` — load architecture context (if it exists)
4. Begin work. Record task ID references in memory at session end.
---
## Periodic Sync Daemon
The sync daemon runs `task-sync.mjs files-to-db` every **60 seconds** in the background. This means markdown edits are automatically reflected in SQLite within a minute.
- Logs: `logs/taskflow-sync.stdout.log` and `logs/taskflow-sync.stderr.log` (relative to workspace)
- Lock: Advisory TTL lock in `sync_state` table prevents concurrent syncs
- Conflict resolution: Last-write-wins per sync direction
### Quickest install (auto-detects OS)
```bash
taskflow install-daemon
```
This detects your platform and installs the appropriate unit. On macOS it installs and loads the LaunchAgent; on Linux it writes systemd user units and enables the timer.
### macOS — LaunchAgent (manual steps)
Templates: `taskflow/system/com.taskflow.sync.plist.xml`
1. Copy `taskflow/system/com.taskflow.sync.plist.xml` → `~/Library/LaunchAgents/com.taskflow.sync.plist`
2. Replace `{{workspace}}` with the absolute path to your workspace (no trailing slash)
3. Replace `{{node}}` with the path to your `node` binary (`which node`)
4. Load: `launchctl load ~/Library/LaunchAgents/com.taskflow.sync.plist`
5. Verify: `launchctl list | grep taskflow`
Uninstall:
```bash
launchctl unload ~/Library/LaunchAgents/com.taskflow.sync.plist
rm ~/Library/LaunchAgents/com.taskflow.sync.plist
```
### Linux — systemd user timer (manual steps)
Templates: `taskflow/system/taskflow-sync.service` and `taskflow/system/taskflow-sync.timer`
```bash
# Create the user unit directory
mkdir -p ~/.config/systemd/user
# Copy templates, replacing placeholders
sed -e "s|{{workspace}}|$OPENCLAW_WORKSPACE|g" \
-e "s|{{node}}|$(which node)|g" \
taskflow/system/taskflow-sync.service > ~/.config/systemd/user/taskflow-sync.service
sed -e "s|{{workspace}}|$OPENCLAW_WORKSPACE|g" \
-e "s|{{node}}|$(which node)|g" \
taskflow/system/taskflow-sync.timer > ~/.config/systemd/user/taskflow-sync.timer
# Enable and start
systemctl --user daemon-reload
systemctl --user enable --now taskflow-sync.timer
```
Verify:
```bash
systemctl --user status taskflow-sync.timer
journalctl --user -u taskflow-sync.service
```
Uninstall:
```bash
systemctl --user disable --now taskflow-sync.timer
rm ~/.config/systemd/user/taskflow-sync.{service,timer}
systemctl --user daemon-reload
```
> **Note:** systemd user units require a login session. To run them without an interactive session (e.g. on a server), enable lingering: `loginctl enable-linger $USER`
---
## Section Header → DB Status Map
| Markdown Header | DB `status` value |
|---|---|
| `## In Progress` | `in_progress` |
| `## Pending Validation` | `pending_validation` |
| `## Backlog` | `backlog` |
| `## Blocked` | `blocked` |
| `## Done` | `done` |
**Section headers are fixed.** Do not rename them. The sync parser maps these exact strings.
---
## Known Quirks
Things that work but might trip you up:
- **`MAX(id)` is lexicographic.** Task IDs are text, so `SELECT MAX(id)` works only because IDs are zero-padded (`-001`, `-002`). If you create `-1` instead of `-001`, sequencing breaks. Always zero-pad to 3 digits.
- **Checkbox state is decorative.** Status comes from which `##` section a task lives under, not whether it's `[x]` or `[ ]`. The sync engine ignores the checkbox on read. On write-back, `done` tasks get `[x]`, everything else gets `[ ]`.
- **Notes survive deletion.** If you remove a `- note:` line from markdown, the old note stays in the DB (COALESCE preserves it). This is intentional for v1 -- notes are one-way display. To truly clear a note, update the DB directly.
- **Lock TTL is 60 seconds.** If a sync crashes without releasing the lock, the next run will be blocked for up to 60s. The SIGTERM/SIGINT handlers try to clean up, but a `kill -9` won't. The lock auto-expires.
- **Auto-project creation derives names from slugs.** If sync encounters a task file with no matching `projects` row, it creates one with a name like "My Project" from slug "my-project". The name might not be what you want -- fix it in PROJECTS.md and re-sync.
- **Tag order is strict.** `[P1] [codex]` works. `[codex] [P1]` silently assigns `codex` as... nothing useful. Priority tag must come first.
---
## Known Limitations (v1)
- Notes are one-way (markdown → DB). Removing a note in markdown does not clear it in DB.
- `db-to-files` rewrites all project task files, even unchanged ones.
- One task file per project (1:1 mapping). Multiple files per project is post-MVP.
- Periodic sync daemon: macOS (LaunchAgent) and Linux (systemd user timer) are supported. Run `taskflow install-daemon` to install.
- Node.js 22.5+ required (`node:sqlite`). No Python fallback in v1.
---
## Quick Cheat Sheet
```
New project: PROJECTS.md block + tasks/-tasks.md + optional plans/-plan.md
New task: taskflow add "title" (or append manually to section)
Update status: Move line to correct ## section, flip checkbox if needed
Query simple: cat tasks/-tasks.md
Query complex: Use db.prepare('SELECT ... WHERE id = ?').all(id) — never interpolate variables into SQL
CLI status: taskflow status
CLI add: taskflow add dashboard "Fix cron panel" --priority P1 --owner codex
Force sync: node taskflow/scripts/task-sync.mjs files-to-db
Memory rule: Reference IDs in logs; never copy backlog into memory
```