{"skill":{"slug":"taskflow","displayName":"TaskFlow","summary":"Structured project/task management for OpenClaw agents — markdown-first authoring, SQLite-backed querying, bidirectional sync, CLI, Apple Notes integration.","description":"---\nname: taskflow\ndescription: Structured project/task management for OpenClaw agents — markdown-first authoring, SQLite-backed querying, bidirectional sync, CLI, Apple Notes integration.\nmetadata:\n  {\n    \"openclaw\":\n      {\n        \"emoji\": \"📋\",\n        \"os\": [\"darwin\", \"linux\"],\n        \"requires\": { \"bins\": [\"node\"], \"env\": [\"OPENCLAW_WORKSPACE\"] },\n      },\n  }\n---\n\n# TaskFlow — Agent Skill Reference\n\nTaskFlow gives any OpenClaw agent a **structured project/task/plan system** with markdown-first authoring, SQLite-backed querying, and bidirectional sync.\n\n**Principle:** Markdown is canonical. Edit `tasks/*.md` directly. The SQLite DB is a derived index, not the source of truth.\n\n---\n\n## Security\n\n### OPENCLAW_WORKSPACE Trust Boundary\n\n`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.\n\n**Rules for safe use:**\n\n1. **Set it only from trusted, controlled sources.** The value must come from:\n   - Your own shell profile (`.zshrc`, `.bashrc`, `/etc/environment`)\n   - The systemd user unit `Environment=` directive in a template you control\n   - The macOS LaunchAgent `EnvironmentVariables` dictionary you installed\n\n   **Never** accept `OPENCLAW_WORKSPACE` from:\n   - User-supplied CLI arguments or HTTP request parameters\n   - Untrusted config files read at runtime\n   - Any external input that has not been explicitly validated\n\n2. **Validate the path exists before use.** Any script that reads `OPENCLAW_WORKSPACE` should confirm the directory exists before proceeding:\n\n   ```js\n   import { existsSync } from 'node:fs'\n   import path from 'node:path'\n\n   const workspace = process.env.OPENCLAW_WORKSPACE\n   if (!workspace) {\n     console.error('OPENCLAW_WORKSPACE is not set. Aborting.')\n     process.exit(1)\n   }\n   if (!existsSync(workspace)) {\n     console.error(`OPENCLAW_WORKSPACE path does not exist: ${workspace}`)\n     process.exit(1)\n   }\n   // Resolve to absolute path to neutralize any relative-path tricks\n   const safeWorkspace = path.resolve(workspace)\n   ```\n\n3. **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:\n\n   ```js\n   function safeJoin(base, ...parts) {\n     const resolved = path.resolve(base, ...parts)\n     if (!resolved.startsWith(path.resolve(base) + path.sep)) {\n       throw new Error(`Path traversal attempt detected: ${resolved}`)\n     }\n     return resolved\n   }\n   ```\n\n4. **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.\n\n---\n\n## Setup\n\n### 1. Set environment variable\n\nAdd to your shell profile (`.zshrc`, `.bashrc`, etc.):\n\n```bash\nexport OPENCLAW_WORKSPACE=\"/path/to/your/.openclaw/workspace\"\n```\n\nAll 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.\n\n> **See also:** [OPENCLAW_WORKSPACE Trust Boundary](#openclaw_workspace-trust-boundary) above for security requirements.\n\n### 2. Link the CLI\n\n```bash\nln -sf {baseDir}/scripts/taskflow-cli.mjs /opt/homebrew/bin/taskflow  # macOS (Apple Silicon)\n# or: ln -sf {baseDir}/scripts/taskflow-cli.mjs /usr/local/bin/taskflow\n```\n\n### 3. Run the setup wizard\n\n```bash\ntaskflow setup\n```\n\nThe 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.\n\n**Alternative — manual setup:**\n\n<details>\n<summary>Manual steps (if you prefer explicit control)</summary>\n\n```bash\n# Create workspace dirs\nmkdir -p \"$OPENCLAW_WORKSPACE/tasks\" \"$OPENCLAW_WORKSPACE/plans\" \"$OPENCLAW_WORKSPACE/memory\" \"$OPENCLAW_WORKSPACE/logs\"\n\n# Bootstrap the DB schema\ntaskflow init\n\n# Create PROJECTS.md and tasks/<slug>-tasks.md manually (see templates/)\n\n# Sync markdown → DB\ntaskflow sync files-to-db\n\n# Verify\ntaskflow status\n```\n\n</details>\n\n---\n\n## First Run\n\n### For agents (OpenClaw / AI)\n\nWhen a user asks you to set up TaskFlow or you detect it has not been initialized:\n\n1. **Detect state.** Check for `$OPENCLAW_WORKSPACE/PROJECTS.md` and `$OPENCLAW_WORKSPACE/memory/taskflow.sqlite`.\n2. **If clean slate:** Ask the user for their first project name and description, then run:\n   ```bash\n   taskflow setup --name \"Project Name\" --desc \"One-liner description\"\n   ```\n   Follow up by running `taskflow status` to confirm.\n3. **If PROJECTS.md exists but no DB:** Run `taskflow setup` (it detects the state automatically and offers to init + sync).\n4. **If both exist:** Run `taskflow status` — already set up.\n5. After setup, update `AGENTS.md` with the new project slug so future sessions discover it via `cat PROJECTS.md`.\n\n### For humans (CLI)\n\n```bash\ntaskflow setup\n```\n\nThe interactive wizard will:\n- Detect your existing workspace state\n- Walk you through naming your first project(s)\n- Create `PROJECTS.md` and `tasks/<slug>-tasks.md` from templates\n- Initialize the SQLite database and sync\n- Offer to install the periodic-sync daemon (LaunchAgent on macOS, systemd timer on Linux) for automatic 60s sync\n\n**Non-interactive (scripted installs):**\n\n```bash\ntaskflow setup --name \"My Project\" --desc \"What it does\"\n```\n\nPassing `--name` skips all interactive prompts (daemon install is also skipped in non-interactive mode).\n\n---\n\n## Directory Layout\n\n```\n<workspace>/\n├── PROJECTS.md                      # Project registry (one ## block per project)\n├── tasks/<slug>-tasks.md            # Task list per project\n├── plans/<slug>-plan.md             # Optional: architecture/design doc per project\n└── taskflow/\n    ├── SKILL.md                     # This file\n    ├── scripts/\n    │   ├── taskflow-cli.mjs         # CLI entry point (symlink target)\n    │   ├── task-sync.mjs            # Bidirectional markdown ↔ SQLite sync\n    │   ├── init-db.mjs              # Bootstrap SQLite schema (idempotent)\n    │   ├── export-projects-overview.mjs  # JSON export of project/task state\n    │   └── apple-notes-export.mjs   # Optional: project state → Apple Notes (macOS only)\n    ├── templates/                   # Starter files for new projects\n    ├── schema/\n    │   └── taskflow.sql             # Full DDL\n    └── system/\n        ├── com.taskflow.sync.plist.xml  # Periodic sync (macOS LaunchAgent)\n        ├── taskflow-sync.service        # Periodic sync (Linux systemd user unit)\n        └── taskflow-sync.timer          # Systemd timer (60s interval)\n<workspace>/\n└── taskflow.config.json                 # Apple Notes config (auto-created on first notes run)\n```\n\n---\n\n## Creating a Project\n\nFollow this full checklist when creating a new project:\n\n### 1. Add a block to `PROJECTS.md`\n\n```markdown\n## <slug>\n- Name: <Human-Readable Name>\n- Status: active\n- Description: One-sentence description of the project.\n```\n\n- `slug` is lowercase, hyphenated (e.g., `my-project`). It becomes the canonical project ID everywhere.\n- Valid status values: `active`, `paused`, `done`.\n\n### 2. Create the task file\n\nCopy `taskflow/templates/tasks-template.md` → `tasks/<slug>-tasks.md` and update the project name in the heading.\n\nThe file **must** contain these five section headers in this order:\n\n```markdown\n# <Project Name> — Tasks\n\n## In Progress\n## Pending Validation\n## Backlog\n## Blocked\n## Done\n```\n\n### 3. Optionally create a plan file\n\nCopy `taskflow/templates/plan-template.md` → `plans/<slug>-plan.md` for architecture docs, design decisions, and phased roadmaps. Plan files are **not** synced to SQLite — they are reference-only for the agent.\n\n### 4. DB row (auto-created on first sync)\n\nYou 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:\n\n```js\n// Safe: parameterized insert — no string interpolation in the SQL\ndb.prepare(`INSERT INTO projects (id, name, description, status)\n            VALUES (:id, :name, :description, 'active')`)\n  .run({ id: slug, name: projectName, description: projectDesc })\n```\n\n---\n\n## Task Line Format\n\nEvery task line follows this exact format:\n\n```\n- [x| ] (task:<id>) [<priority>] [<owner>] <title>\n```\n\n| Field | Details |\n|---|---|\n| `[ ]` / `[x]` | Open / completed. Sync drives status from section header, not this checkbox. |\n| `(task:<id>)` | Task ID. Format: `<slug>-NNN` (zero-padded 3-digit). Sequential per project. |\n| `[<priority>]` | **Required. Must come before owner tag.** See priority table below. |\n| `[<owner>]` | Optional. Agent/model tag (e.g., `codex`, `sonnet`, `claude`). |\n| `<title>` | Human-readable task title. |\n\n### ⚠️ Tag Order Rule\n\n**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.\n\n### ⚠️ Title Sanitization Rules\n\nTask titles must be **plain text only**. Before writing any user-supplied string as a task title, apply the following rules:\n\n1. **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.\n\n2. **Reject the exact section header strings** even without leading whitespace:\n   - `In Progress`, `Pending Validation`, `Backlog`, `Blocked`, `Done`\n   - Comparison must be case-insensitive.\n\n3. **Escape or strip markdown special characters** that have structural meaning in the task file:\n\n   | Character | Risk | Safe action |\n   |-----------|------|-------------|\n   | `#`       | Looks like a header | Strip or reject |\n   | `- ` (dash + space at line start) | Looks like a list item / task | Strip leading `- ` |\n   | `[ ]` / `[x]` | Looks like a checkbox | Escape brackets: `\\[` `\\]` |\n   | `]` / `[` alone | Can corrupt `(task:id)` parse | Escape: `\\[` `\\]` |\n   | Newlines (`\\n`, `\\r`) | Creates multi-line titles | Strip / reject |\n\n4. **Maximum length.** Titles should be ≤ 200 characters. Truncate or reject longer strings.\n\n**Example sanitization (Node.js):**\n\n```js\n// Safe: sanitize a user-supplied task title before writing to markdown\nfunction sanitizeTitle(raw) {\n  if (typeof raw !== 'string') throw new TypeError('title must be a string')\n\n  // Strip newlines\n  let title = raw.replace(/[\\r\\n]+/g, ' ').trim()\n\n  // Reject lines that look like section headers (# Heading or bare header words)\n  if (/^#{1,6}\\s/.test(title)) {\n    throw new Error('Title may not start with a markdown heading (#)')\n  }\n  const BANNED_HEADERS = /^(in progress|pending validation|backlog|blocked|done)$/i\n  if (BANNED_HEADERS.test(title)) {\n    throw new Error('Title may not be a reserved section header name')\n  }\n\n  // Escape structural markdown characters\n  title = title\n    .replace(/\\[/g, '\\\\[')\n    .replace(/\\]/g, '\\\\]')\n\n  // Enforce length limit\n  if (title.length > 200) {\n    throw new Error('Title exceeds 200 character limit')\n  }\n\n  return title\n}\n```\n\nThese 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.\n\n✅ Correct: `- [ ] (task:myproject-007) [P1] [codex] Implement search`\n❌ Wrong:   `- [ ] (task:myproject-007) [codex] [P1] Implement search`\n\n### Priority Levels (Configurable)\n\n| Tag | Default Meaning |\n|---|---|\n| `P0` | Critical — must do now, blocks everything |\n| `P1` | High — important, do soon |\n| `P2` | Normal — standard priority (default) |\n| `P3` | Low — nice to have |\n| `P9` | Someday — no urgency, parking lot |\n\nPriorities are configurable per-installation but the tags themselves (`P0`–`P3`, `P9`) are what the sync engine validates.\n\n### Optional Note Lines\n\nA note can follow a task line as an indented `- note:` line:\n\n```markdown\n- [ ] (task:myproject-003) [P1] [codex] Implement auth flow\n  - note: blocked on API key from vendor\n```\n\n> **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.\n\n### Example Task File Section\n\n```markdown\n## In Progress\n- [ ] (task:myproject-001) [P1] [codex] Wire up OAuth login\n  - note: PR open, needs review\n\n## Backlog\n- [ ] (task:myproject-002) [P2] Add rate limiting middleware\n- [ ] (task:myproject-003) [P3] Write integration tests\n```\n\n---\n\n## Adding a New Task\n\n1. **Determine the next ID.** Scan the task file for the highest existing `<slug>-NNN` and increment by 1. Or query SQLite using a **parameterized statement** (never interpolate the slug into SQL strings):\n\n   ```js\n   // Node.js — safe, parameterized\n   const db = new DatabaseSync(dbPath)\n   const row = db\n     .prepare(`SELECT MAX(CAST(SUBSTR(id, LENGTH(:slug) + 2) AS INTEGER)) AS max_seq\n               FROM tasks_v2\n               WHERE project_id = :slug`)\n     .get({ slug: projectSlug })\n   const nextSeq = (row.max_seq ?? 0) + 1\n   const nextId  = `${projectSlug}-${String(nextSeq).padStart(3, '0')}`\n   ```\n\n   > ⚠️ **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.\n\n2. **Append the task line** to the correct section (`## Backlog` for new work, `## In Progress` if starting immediately).\n\n3. **Format the line** using the exact format above. No trailing spaces. Priority tag before owner tag.\n\n---\n\n## Updating Task Status\n\n**Move the task line** from its current section to the target section in the markdown file.\n\n| Target State | Move to Section |\n|---|---|\n| Started / picked up | `## In Progress` |\n| Needs human review | `## Pending Validation` |\n| Not started yet | `## Backlog` |\n| Waiting on dependency | `## Blocked` |\n| Finished | `## Done` |\n\nAlso flip the checkbox: `[ ]` for active states, `[x]` for `Done` (and optionally `Pending Validation`).\n\nThe periodic sync (60s) will pick up the change and update SQLite automatically. To force an immediate sync:\n\n```bash\nnode taskflow/scripts/task-sync.mjs files-to-db\n```\n\n---\n\n## Querying Tasks\n\n### Simple: Read the markdown file directly\n\n```bash\ncat tasks/<slug>-tasks.md\n```\n\nFor a quick in-session view, just read the relevant section.\n\n### Advanced: Query SQLite\n\n> ⚠️ **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.\n\n#### sqlite3 CLI (static queries — for manual inspection only)\n\n```bash\n# All in-progress tasks across all projects (by priority)\n# Safe: 'in_progress' is a static literal, not a variable\nsqlite3 \"$OPENCLAW_WORKSPACE/memory/taskflow.sqlite\" \\\n  \"SELECT id, project_id, priority, title\n   FROM tasks_v2\n   WHERE status = 'in_progress'\n   ORDER BY priority, project_id;\"\n\n# Task count by status per project (no variables — safe for CLI)\nsqlite3 \"$OPENCLAW_WORKSPACE/memory/taskflow.sqlite\" \\\n  \"SELECT project_id, status, COUNT(*) AS count\n   FROM tasks_v2\n   GROUP BY project_id, status\n   ORDER BY project_id, status;\"\n```\n\n> 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.\n\n#### Node.js API — parameterized queries (required for programmatic use)\n\n```js\nimport { DatabaseSync } from 'node:sqlite'\nimport path from 'node:path'\n\nconst dbPath = path.join(process.env.OPENCLAW_WORKSPACE, 'memory', 'taskflow.sqlite')\nconst db = new DatabaseSync(dbPath)\ndb.exec('PRAGMA foreign_keys = ON')\n\n// ── Backlog for a specific project ─────────────────────────────\n// :slug is a named parameter — never interpolated into the SQL string\nconst backlog = db\n  .prepare(`SELECT id, priority, title\n            FROM tasks_v2\n            WHERE project_id = :slug AND status = 'backlog'\n            ORDER BY priority`)\n  .all({ slug: 'my-project' })  // value bound at runtime, never in SQL string\n\n// ── Audit trail for a specific task ────────────────────────────\nconst transitions = db\n  .prepare(`SELECT from_status, to_status, actor, at\n            FROM task_transitions_v2\n            WHERE task_id = ?\n            ORDER BY at`)\n  .all('my-project-007')  // positional parameter — also safe\n\n// ── Write: update task status ───────────────────────────────────\n// NEVER: db.exec(`UPDATE tasks_v2 SET status='${newStatus}' WHERE id='${id}'`)\n// ALWAYS:\ndb.prepare(`UPDATE tasks_v2 SET status = :status, updated_at = datetime('now')\n            WHERE id = :id`)\n  .run({ status: 'done', id: 'my-project-007' })\n```\n\n### CLI Quick Reference\n\n```bash\n# Terminal summary: all projects + task counts by status\ntaskflow status\n\n# Add a task in markdown with automatic next ID\ntaskflow add taskflow \"Implement quick add command\" --priority P1 --owner codex\n\n# List current tasks for a project (excludes done by default)\ntaskflow list taskflow\ntaskflow list --project \"TaskFlow\" --all\ntaskflow list task --status backlog,pending_validation --json\n\n# JSON export of full project/task state (for dashboards, integrations)\nnode taskflow/scripts/export-projects-overview.mjs\n\n# Detect drift between markdown and DB (exit 1 if mismatch)\nnode taskflow/scripts/task-sync.mjs check\n\n# Sync markdown → DB (normal direction; run after editing task files)\nnode taskflow/scripts/task-sync.mjs files-to-db\n\n# Sync DB → markdown (run after programmatic DB updates)\nnode taskflow/scripts/task-sync.mjs db-to-files\n```\n\n### Apple Notes Export (Optional — macOS Only)\n\nTaskFlow can maintain a live Apple Note with your current project status. The note is rendered as rich HTML and written via AppleScript.\n\n```bash\n# Push current status to Apple Notes (creates note on first run)\ntaskflow notes\n```\n\nOn first run (or during `taskflow setup`), a new note is created in the configured folder and its Core Data ID is saved to:\n\n```\n$OPENCLAW_WORKSPACE/taskflow.config.json\n```\n\nConfig schema:\n\n```json\n{\n  \"appleNotesId\":     \"x-coredata://...\",\n  \"appleNotesFolder\": \"Notes\",\n  \"appleNotesTitle\":  \"TaskFlow - Project Status\"\n}\n```\n\n**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.\n\nFor hourly auto-refresh, add a cron entry:\n\n```bash\n# Run: crontab -e\n0 * * * * OPENCLAW_WORKSPACE=/path/to/workspace /path/to/node /path/to/taskflow/scripts/apple-notes-export.mjs\n```\n\nOr install a dedicated LaunchAgent (macOS) targeting `apple-notes-export.mjs` with an hourly `StartInterval` of `3600`.\n\nThis feature is entirely optional and macOS-specific. On other platforms, `taskflow notes` exits gracefully with a message.\n\n---\n\n## Memory Integration Rules\n\nThese rules keep daily memory logs clean and prevent duplication.\n\n### ✅ Do\n\n- Reference task IDs in daily memory logs when you complete or advance work:\n  ```\n  Completed `myproject-007` (OAuth login). Moved `myproject-008` to In Progress.\n  ```\n- Keep memory entries narrative — what happened, what you decided, what's next.\n\n### ❌ Do Not\n\n- **Never duplicate the backlog in daily memory files.** `tasks/<slug>-tasks.md` is the single source of truth for all pending work. Memory files should not list what's left to do.\n- Do not track task state changes in memory (e.g., \"Task 007 is now in progress\"). Only note meaningful progress events or decisions.\n- Do not create new tasks in memory files. Add them to the task file directly.\n\n### Pattern: Loading Project Context\n\nAt the start of a session involving a project:\n\n1. `cat PROJECTS.md` — identify the project slug and status\n2. `cat tasks/<slug>-tasks.md` — load current task state\n3. `cat plans/<slug>-plan.md` — load architecture context (if it exists)\n4. Begin work. Record task ID references in memory at session end.\n\n---\n\n## Periodic Sync Daemon\n\nThe 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.\n\n- Logs: `logs/taskflow-sync.stdout.log` and `logs/taskflow-sync.stderr.log` (relative to workspace)\n- Lock: Advisory TTL lock in `sync_state` table prevents concurrent syncs\n- Conflict resolution: Last-write-wins per sync direction\n\n### Quickest install (auto-detects OS)\n\n```bash\ntaskflow install-daemon\n```\n\nThis 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.\n\n### macOS — LaunchAgent (manual steps)\n\nTemplates: `taskflow/system/com.taskflow.sync.plist.xml`\n\n1. Copy `taskflow/system/com.taskflow.sync.plist.xml` → `~/Library/LaunchAgents/com.taskflow.sync.plist`\n2. Replace `{{workspace}}` with the absolute path to your workspace (no trailing slash)\n3. Replace `{{node}}` with the path to your `node` binary (`which node`)\n4. Load: `launchctl load ~/Library/LaunchAgents/com.taskflow.sync.plist`\n5. Verify: `launchctl list | grep taskflow`\n\nUninstall:\n```bash\nlaunchctl unload ~/Library/LaunchAgents/com.taskflow.sync.plist\nrm ~/Library/LaunchAgents/com.taskflow.sync.plist\n```\n\n### Linux — systemd user timer (manual steps)\n\nTemplates: `taskflow/system/taskflow-sync.service` and `taskflow/system/taskflow-sync.timer`\n\n```bash\n# Create the user unit directory\nmkdir -p ~/.config/systemd/user\n\n# Copy templates, replacing placeholders\nsed -e \"s|{{workspace}}|$OPENCLAW_WORKSPACE|g\" \\\n    -e \"s|{{node}}|$(which node)|g\" \\\n    taskflow/system/taskflow-sync.service > ~/.config/systemd/user/taskflow-sync.service\n\nsed -e \"s|{{workspace}}|$OPENCLAW_WORKSPACE|g\" \\\n    -e \"s|{{node}}|$(which node)|g\" \\\n    taskflow/system/taskflow-sync.timer  > ~/.config/systemd/user/taskflow-sync.timer\n\n# Enable and start\nsystemctl --user daemon-reload\nsystemctl --user enable --now taskflow-sync.timer\n```\n\nVerify:\n```bash\nsystemctl --user status taskflow-sync.timer\njournalctl --user -u taskflow-sync.service\n```\n\nUninstall:\n```bash\nsystemctl --user disable --now taskflow-sync.timer\nrm ~/.config/systemd/user/taskflow-sync.{service,timer}\nsystemctl --user daemon-reload\n```\n\n> **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`\n\n---\n\n## Section Header → DB Status Map\n\n| Markdown Header | DB `status` value |\n|---|---|\n| `## In Progress` | `in_progress` |\n| `## Pending Validation` | `pending_validation` |\n| `## Backlog` | `backlog` |\n| `## Blocked` | `blocked` |\n| `## Done` | `done` |\n\n**Section headers are fixed.** Do not rename them. The sync parser maps these exact strings.\n\n---\n\n## Known Quirks\n\nThings that work but might trip you up:\n\n- **`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.\n- **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 `[ ]`.\n- **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.\n- **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.\n- **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.\n- **Tag order is strict.** `[P1] [codex]` works. `[codex] [P1]` silently assigns `codex` as... nothing useful. Priority tag must come first.\n\n---\n\n## Known Limitations (v1)\n\n- Notes are one-way (markdown → DB). Removing a note in markdown does not clear it in DB.\n- `db-to-files` rewrites all project task files, even unchanged ones.\n- One task file per project (1:1 mapping). Multiple files per project is post-MVP.\n- Periodic sync daemon: macOS (LaunchAgent) and Linux (systemd user timer) are supported. Run `taskflow install-daemon` to install.\n- Node.js 22.5+ required (`node:sqlite`). No Python fallback in v1.\n\n---\n\n## Quick Cheat Sheet\n\n```\nNew project:   PROJECTS.md block + tasks/<slug>-tasks.md + optional plans/<slug>-plan.md\nNew task:      taskflow add <project> \"title\" (or append manually to section)\nUpdate status: Move line to correct ## section, flip checkbox if needed\nQuery simple:  cat tasks/<slug>-tasks.md\nQuery complex: Use db.prepare('SELECT ... WHERE id = ?').all(id) — never interpolate variables into SQL\nCLI status:    taskflow status\nCLI add:       taskflow add dashboard \"Fix cron panel\" --priority P1 --owner codex\nForce sync:    node taskflow/scripts/task-sync.mjs files-to-db\nMemory rule:   Reference IDs in logs; never copy backlog into memory\n```\n","topics":["Sqlite","Task Management","Sync"],"tags":{"latest":"1.1.1"},"stats":{"comments":0,"downloads":2639,"installsAllTime":90,"installsCurrent":63,"stars":0,"versions":5},"createdAt":1771650741589,"updatedAt":1778491597572},"latestVersion":{"version":"1.1.1","createdAt":1771801350988,"changelog":"Adds taskflow list command","license":null},"metadata":{"setup":[{"key":"OPENCLAW_WORKSPACE","required":true}],"os":["darwin","linux"],"systems":null},"owner":{"handle":"sm0ls","userId":"s171qvjqgbqz3sz4msh1rs1tj5885j0c","displayName":"Smols","image":"https://avatars.githubusercontent.com/u/105522896?v=4"},"moderation":null}