Files
pulse-memory/skills/taskflow/SKILL.md
T
pulse-agent bbdb68a6de feat(lib-core): biblioteca atomica @pulse-libs/core v1.0.0-beta.1
Esta commit conteudo a estrutura atomica completa:

- types:     Result<T,E>, AsyncState<T>, Paginated<T>, SortConfig<T>
- utils:     date, str, num, cn, debounce, throttle, storage, arr, obj
- validators: Zod schemas — email, password, uuid, url, phone, CPF/CNPJ, sanitizedStr, safeParse
- hooks:     useToggle, useAsync, useDebounce, useLocalStorage, useMedia, useInterval, useOnClickOutside, useClipboard, useFetch
- components: Button, Input, Alert, Card, Spinner (atomic design pattern)
- build:     tsup v8 ESM+CJS + DTS + sourcemaps — 0 erros
- tests:     57 testes 100% usuarios
- docker:    multi-stage Dockerfile (node 20-alpine)
- config:    vitest, tsup, tsconfig strict, .npmignore

Filosofia atomica:/utils ← /types ← /validators ← /hooks ← /components
Build: npm run build | Test: npm test | Publish: npm publish

🤖 Generated with Pulse (openclaw + nova-self-improver)
2026-05-19 21:43:03 -03:00

696 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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:**
<details>
<summary>Manual steps (if you prefer explicit control)</summary>
```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/<slug>-tasks.md manually (see templates/)
# Sync markdown → DB
taskflow sync files-to-db
# Verify
taskflow status
```
</details>
---
## 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/<slug>-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
```
<workspace>/
├── PROJECTS.md # Project registry (one ## block per project)
├── tasks/<slug>-tasks.md # Task list per project
├── plans/<slug>-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)
<workspace>/
└── 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
## <slug>
- Name: <Human-Readable 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/<slug>-tasks.md` and update the project name in the heading.
The file **must** contain these five section headers in this order:
```markdown
# <Project Name> — Tasks
## In Progress
## Pending Validation
## Backlog
## Blocked
## Done
```
### 3. Optionally create a plan file
Copy `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.
### 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:<id>) [<priority>] [<owner>] <title>
```
| Field | Details |
|---|---|
| `[ ]` / `[x]` | Open / completed. Sync drives status from section header, not this checkbox. |
| `(task:<id>)` | Task ID. Format: `<slug>-NNN` (zero-padded 3-digit). Sequential per project. |
| `[<priority>]` | **Required. Must come before owner tag.** See priority table below. |
| `[<owner>]` | Optional. Agent/model tag (e.g., `codex`, `sonnet`, `claude`). |
| `<title>` | 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 `<slug>-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/<slug>-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/<slug>-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/<slug>-tasks.md` — load current task state
3. `cat plans/<slug>-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/<slug>-tasks.md + optional plans/<slug>-plan.md
New task: taskflow add <project> "title" (or append manually to section)
Update status: Move line to correct ## section, flip checkbox if needed
Query simple: cat tasks/<slug>-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
```