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)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "d2-diagram-creator",
|
||||
"installedVersion": "1.0.0",
|
||||
"installedAt": 1779235744784,
|
||||
"fingerprint": "9063d4b6592b57d32454ec4a1a4a5ab501152f99c3ae3156816f235db21ab962"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# D2 Diagram Creator
|
||||
|
||||
D2 Diagram Creator is a skill that can directly convert natural language descriptions into high-quality D2 diagram code and image files. After installation, you only need to describe the desired diagram structure in text, and it will automatically generate it for you.
|
||||
|
||||
Whether it's a flowchart in academic writing, a model architecture diagram in scientific illustration, or a module relationship diagram when understanding a new open-source project, you no longer need to draw it manually step by step.
|
||||
|
||||
[中文版本(Chinese version)](README_zh.md)
|
||||
|
||||

|
||||
|
||||
## Quick Use
|
||||
|
||||
### Dependency Installation
|
||||
|
||||
The recommended and simplest installation method is to use D2's installation script, which will detect your operating system and architecture and use the best installation method.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://d2lang.com/install.sh | sh -s --
|
||||
|
||||
# TALA is a powerful diagram layout engine specifically designed for software architecture diagrams. It requires separate installation.
|
||||
curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala
|
||||
```
|
||||
|
||||
Please refer to the official D2 documentation for details:
|
||||
|
||||
+ https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md
|
||||
|
||||
+ https://github.com/terrastruct/tala
|
||||
|
||||
After successful installation, use the following command to check
|
||||
|
||||
```bash
|
||||
d2 --version
|
||||
d2 layout tala
|
||||
```
|
||||
|
||||
### Import Claude Code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/HuTa0kj/d2-diagram-creator ~/.claude/skills/d2-diagram-creator
|
||||
```
|
||||
|
||||
## Example Prompts
|
||||
|
||||
+ Please use D2 to draw the system flowchart for the current project.
|
||||
|
||||
+ Please use D2 SKILL to draw a TCP three-way handshake sequence diagram.
|
||||
|
||||
+ Please use D2 to draw the database ER diagram for the current project, in dark mode and a thatched cottage style, and export it as a PNG.
|
||||
@@ -0,0 +1,49 @@
|
||||
# D2 Diagram Creator
|
||||
|
||||
|
||||
|
||||
D2 Diagram Creator 是一个能将自然语言描述直接转换为高质量 D2 图表代码及图片文件的 SKILL。安装后,你只需用文字描述想要的图表结构,它就能帮你自动生成。
|
||||
|
||||
无论是论文写作中的方法流程图、科研绘图里的模型架构示意,还是理解一个新的开源项目时的模块关系图,你都不需要再一点一点手动绘制。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
## 快速使用
|
||||
|
||||
### 依赖安装
|
||||
|
||||
推荐且最简单的安装方法是使用 D2 的安装脚本,该脚本将检测您使用的操作系统和架构,并使用最佳安装方法
|
||||
|
||||
```bash
|
||||
curl -fsSL https://d2lang.com/install.sh | sh -s --
|
||||
|
||||
# TALA 是一款专为软件架构图设计的图表布局引擎,非常强大,该引擎需要单独安装
|
||||
curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala
|
||||
```
|
||||
|
||||
具体请参考 D2 的官方文档:
|
||||
|
||||
+ https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md
|
||||
+ https://github.com/terrastruct/tala
|
||||
|
||||
安装成功后使用下面的命令检查
|
||||
|
||||
```bash
|
||||
d2 --version
|
||||
d2 layout tala
|
||||
```
|
||||
|
||||
### 导入 Claude Code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/HuTa0kj/d2-diagram-creator ~/.claude/skills/d2-diagram-creator
|
||||
```
|
||||
|
||||
## 示例提示词
|
||||
|
||||
+ 帮我使用 D2 绘制当前项目的系统流程图
|
||||
+ 使用 D2 SKILL 帮我画一个 TCP 三次握手的时序图
|
||||
|
||||
+ 使用 D2 绘制当前项目的数据库ER图,深色模式、草屋风格,导出PNG
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
name: d2-diagram-creator
|
||||
description: "Generate D2 diagram code supporting flowcharts, system architecture diagrams, organizational charts, service topology diagrams, state machine diagrams, swimlane diagrams, sequence diagrams, SQL table relationship diagrams, and grid diagrams. Use when users need to: (1) create or generate flowcharts and process diagrams, (2) design system architecture or infrastructure diagrams, (3) build state machine or sequence diagrams, (4) visualize relationships between components, entities or services, (5) create swimlane diagrams for cross-functional processes, (6) design database table structures and ER diagrams, (7) create grid layouts or dashboard designs, or any diagram that can be represented with nodes, connections, and containers in D2 syntax."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Fur1na
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# D2 Diagram Creator
|
||||
|
||||
Three-agent pipeline for high-quality diagram generation:
|
||||
|
||||
```
|
||||
AskUserQuestion → Agent 1 (Analyzer) → Agent 2 (Generator) → Agent 3 (Validator)
|
||||
```
|
||||
|
||||
Each agent is a `general-purpose` subagent with its own focused instructions. You (the main agent) orchestrate the pipeline — ask the user questions, then spawn agents in sequence, passing outputs forward.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Ask Requirements
|
||||
|
||||
Use AskUserQuestion to ask all questions at once. Do not split into multiple rounds, do not skip any.
|
||||
|
||||
### First round (always required)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "How detailed should the diagram be?",
|
||||
"header": "Detail Level",
|
||||
"options": [
|
||||
{ "label": "Core Flow", "description": "5-8 nodes" },
|
||||
{ "label": "Moderate", "description": "10-15 nodes" },
|
||||
{ "label": "Full Detail", "description": "Include all details and exception branches" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "What is the layout direction of the diagram?",
|
||||
"header": "Layout Direction",
|
||||
"options": [
|
||||
{ "label": "Horizontal", "description": "direction: right, left to right" },
|
||||
{ "label": "Vertical", "description": "direction: down, top to bottom" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "What format to export as?",
|
||||
"header": "Export Format",
|
||||
"options": [
|
||||
{ "label": "D2 Code Only", "description": "No image export" },
|
||||
{ "label": "SVG", "description": "Vector graphics (recommended)" },
|
||||
{ "label": "PNG", "description": "Bitmap" },
|
||||
{ "label": "Preview", "description": "ASCII text" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Second round (only when SVG/PNG is selected)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which theme to use?",
|
||||
"header": "Theme",
|
||||
"options": [
|
||||
{ "label": "Light Theme", "description": "--theme 0, white background (default)" },
|
||||
{ "label": "Dark Theme", "description": "--theme 200, dark background" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Enable hand-drawn sketch style?",
|
||||
"header": "Sketch",
|
||||
"options": [
|
||||
{ "label": "No", "description": "Normal style (default)" },
|
||||
{ "label": "Yes", "description": "--sketch, hand-drawn effect" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Choose layout engine?",
|
||||
"header": "Layout Engine",
|
||||
"options": [
|
||||
{ "label": "dagre", "description": "Default, fast and efficient" },
|
||||
{ "label": "elk", "description": "Complex diagrams, 100+ nodes" },
|
||||
{ "label": "tala", "description": "Most powerful, SVG only, requires installation" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Only provide 2 theme options (Light/Dark). Do not add colorful, terminal, or other themes.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Spawn Analyzer Agent
|
||||
|
||||
Use the Agent tool to spawn the requirements analyzer. This agent deeply analyzes the user's request and produces a structured JSON document.
|
||||
|
||||
- **subagent_type**: `general-purpose`
|
||||
- **description**: `Analyze diagram requirements`
|
||||
|
||||
The prompt should be:
|
||||
|
||||
```
|
||||
Read the file at <skill-base-path>/agents/analyzer.md for your instructions.
|
||||
|
||||
Analyze this diagram request:
|
||||
|
||||
User request: <user's original description>
|
||||
|
||||
User preferences:
|
||||
- Detail level: <answer>
|
||||
- Layout direction: <answer>
|
||||
- Export format: <answer>
|
||||
- Theme: <answer, or "not selected">
|
||||
- Sketch: <answer, or "not selected">
|
||||
- Layout engine: <answer, or "not selected">
|
||||
|
||||
Return the structured requirements JSON.
|
||||
```
|
||||
|
||||
Save the returned JSON — you will pass it to the generator agent.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Spawn Generator Agent
|
||||
|
||||
Use the Agent tool to spawn the D2 code generator.
|
||||
|
||||
- **subagent_type**: `general-purpose`
|
||||
- **description**: `Generate D2 diagram code`
|
||||
|
||||
The prompt should be:
|
||||
|
||||
```
|
||||
Read the file at <skill-base-path>/agents/generator.md for your instructions.
|
||||
|
||||
Generate D2 code based on these requirements:
|
||||
|
||||
<the requirements JSON from Step 2>
|
||||
|
||||
Read the diagram type guide at:
|
||||
<skill-base-path>/references/diagram-types/<diagram_type>.md
|
||||
|
||||
Save the D2 code to a .d2 file (use a descriptive filename).
|
||||
Return the file path.
|
||||
```
|
||||
|
||||
Save the returned file path — you will pass it to the validator agent.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Spawn Validator Agent
|
||||
|
||||
Use the Agent tool to spawn the validator.
|
||||
|
||||
- **subagent_type**: `general-purpose`
|
||||
- **description**: `Validate and export diagram`
|
||||
|
||||
The prompt should be:
|
||||
|
||||
```
|
||||
Read the file at <skill-base-path>/agents/validator.md for your instructions.
|
||||
|
||||
Your skill base directory is: <skill-base-path>
|
||||
The watermark removal script is at: <skill-base-path>/scripts/remove_watermark.py
|
||||
|
||||
Validate and export:
|
||||
|
||||
D2 file: <path from Step 3>
|
||||
Requirements: <the requirements JSON>
|
||||
|
||||
Export preferences:
|
||||
- Format: <export_format>
|
||||
- Theme: <theme or "default">
|
||||
- Sketch: <sketch or false>
|
||||
- Layout engine: <engine or "dagre">
|
||||
|
||||
Fix any issues and export the final diagram.
|
||||
```
|
||||
|
||||
Report the validator's summary to the user.
|
||||
|
||||
---
|
||||
|
||||
## Diagram Types Reference
|
||||
|
||||
| Type | Use Case |
|
||||
|------|----------|
|
||||
| **Flowchart** | Business processes, decision trees, algorithm flows |
|
||||
| **System Architecture** | Component relationships, infrastructure, service interactions |
|
||||
| **Organizational Chart** | Hierarchical structures, reporting relationships |
|
||||
| **Service Topology** | Cloud architecture, network topology |
|
||||
| **State Machine** | State transitions, workflow states, lifecycles |
|
||||
| **Swimlane Diagram** | Cross-functional processes, multi-role workflows |
|
||||
| **Sequence Diagram** | Time-based interactions, protocol flows |
|
||||
| **SQL Table Diagram** | Database schemas, ER diagrams |
|
||||
| **Grid Diagram** | Dashboard layouts, UI prototypes |
|
||||
|
||||
---
|
||||
|
||||
## Prohibitions
|
||||
|
||||
1. Do not skip requirement questions — always ask first
|
||||
2. Do not add visual styles unless the user explicitly requests them
|
||||
3. Do not add a diagram title unless the user explicitly requests one
|
||||
4. Do not skip any of the three agents — the pipeline must run to completion
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7cm8jm5xnvtaj28v6b4y9t7s842d5t",
|
||||
"slug": "d2-diagram-creator",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1775119154697
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Requirements Analyzer Agent
|
||||
|
||||
You are a specialized requirements analyzer for D2 diagram generation. Your job is to deeply understand the user's request and produce a structured requirements document that the generator agent can use directly.
|
||||
|
||||
## What you receive
|
||||
|
||||
- The user's original description of what diagram they want
|
||||
- Their answers to preference questions (detail level, layout direction, export format, theme, sketch, layout engine)
|
||||
|
||||
## What to do
|
||||
|
||||
1. Read the diagram type reference files to understand what structures each type supports. Start with the most likely type based on the user's description:
|
||||
- `references/diagram-types/flowchart.md` — processes, decision trees
|
||||
- `references/diagram-types/architecture.md` — system components, infrastructure
|
||||
- `references/diagram-types/org-chart.md` — hierarchies, reporting
|
||||
- `references/diagram-types/topology.md` — cloud, network
|
||||
- `references/diagram-types/state-machine.md` — states, transitions
|
||||
- `references/diagram-types/swimlane.md` — cross-functional processes
|
||||
- `references/diagram-types/sequence.md` — time-based interactions
|
||||
- `references/diagram-types/sql-table.md` — database schemas, ER
|
||||
- `references/diagram-types/grid.md` — dashboards, layouts
|
||||
2. Identify the diagram type that best matches the user's request
|
||||
3. Extract all entities (nodes, components, tables, states, actors, etc.) the user mentioned
|
||||
4. Extract all relationships and connections between entities
|
||||
5. Identify any natural groupings or containers
|
||||
6. Produce a structured requirements document
|
||||
|
||||
## Output format
|
||||
|
||||
Return ONLY a JSON object with this structure (no markdown wrapping):
|
||||
|
||||
```json
|
||||
{
|
||||
"diagram_type": "flowchart|architecture|org-chart|topology|state-machine|swimlane|sequence|sql-table|grid",
|
||||
"title": null,
|
||||
"entities": [
|
||||
{
|
||||
"id": "short_english_id",
|
||||
"label": "User's Original Label",
|
||||
"shape": "rectangle",
|
||||
"container": "parent_id or null"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from": "entity_id or container.entity_id",
|
||||
"to": "entity_id or container.entity_id",
|
||||
"label": "connection label or null",
|
||||
"arrow": "->"
|
||||
}
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"id": "short_english_id",
|
||||
"label": "Display Label"
|
||||
}
|
||||
],
|
||||
"preferences": {
|
||||
"detail_level": "core|moderate|full",
|
||||
"layout_direction": "right|down",
|
||||
"export_format": "d2|svg|png|preview",
|
||||
"theme": 0,
|
||||
"sketch": false,
|
||||
"layout_engine": "dagre"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Entity IDs must be short, descriptive, in English, and free of special characters (no hyphens, dots, colons). Use underscores if needed.
|
||||
- Labels should preserve the user's original wording and language.
|
||||
- Use quotes around any ID that contains special characters.
|
||||
- The `arrow` field supports: `->`, `<-`, `<->`, `--`.
|
||||
- If a connection crosses container boundaries, use dot-separated paths in from/to (e.g., `"frontend.ui"` → `"api.gateway"`).
|
||||
- Don't invent entities the user didn't mention.
|
||||
- Don't omit entities the user explicitly described.
|
||||
- Think about logical grouping — related entities belong in the same container.
|
||||
- If the user's request doesn't fit neatly into one diagram type, pick the closest one.
|
||||
- `title` should be null unless the user explicitly asked for a diagram title.
|
||||
- Shape should be a valid D2 shape. Use `rectangle` as default when unsure.
|
||||
@@ -0,0 +1,90 @@
|
||||
# D2 Diagram Generator Agent
|
||||
|
||||
You are a specialized D2 diagram code generator. Your job is to produce clean, correct D2 code from a structured requirements document.
|
||||
|
||||
## What you receive
|
||||
|
||||
- A structured JSON requirements document (from the analyzer agent)
|
||||
- The path to the relevant diagram type reference file
|
||||
|
||||
## What to do
|
||||
|
||||
1. Read the diagram type guide specified by the `diagram_type` field in the requirements:
|
||||
- `references/diagram-types/<diagram_type>.md`
|
||||
2. If you need detailed syntax info, read `references/syntax.md`
|
||||
3. Generate D2 code that faithfully implements every entity and connection in the requirements
|
||||
4. Write the D2 code to the specified output file
|
||||
5. Return the file path
|
||||
|
||||
## Design principles
|
||||
|
||||
These principles matter because D2 auto-layouts everything — bad code structure means bad diagrams.
|
||||
|
||||
1. **Simplicity first** — show core content, avoid excessive nesting
|
||||
2. **Minimal styling** — only use semantic attributes (shape, label, direction). No colors, shadows, or icons unless the user explicitly asked
|
||||
3. **Automatic layout** — use `direction` to control flow, never try manual positioning
|
||||
4. **Semantic naming** — IDs should clearly describe what the node represents
|
||||
5. **Reasonable grouping** — use containers for logically related entities, but don't over-nest
|
||||
6. **No title by default** — unless `title` in the requirements is non-null
|
||||
|
||||
### Allowed attributes
|
||||
|
||||
```d2
|
||||
Node: { shape: circle } # Shape
|
||||
Container: { label: "Label" } # Label
|
||||
Connection: { label: "Note" } # Connection label
|
||||
direction: right # Layout direction
|
||||
```
|
||||
|
||||
### Forbidden attributes (unless user explicitly requested)
|
||||
|
||||
```d2
|
||||
style.fill: "#color"
|
||||
style.stroke: "#color"
|
||||
style.opacity: 0.5
|
||||
style.shadow: true
|
||||
icon: https://...
|
||||
```
|
||||
|
||||
## Critical syntax rules
|
||||
|
||||
### Container node references — this is the #1 source of bugs
|
||||
|
||||
When connecting nodes that live inside containers, you MUST use the full dot-separated path. Otherwise D2 creates duplicate orphan nodes and the diagram falls apart.
|
||||
|
||||
Wrong:
|
||||
```d2
|
||||
Frontend Layer: {
|
||||
User Interface: { label: "Web UI" }
|
||||
}
|
||||
API Layer: {
|
||||
Gateway: { label: "API Gateway" }
|
||||
}
|
||||
User Interface -> Gateway
|
||||
```
|
||||
|
||||
Correct:
|
||||
```d2
|
||||
Frontend Layer.User Interface -> API Layer.Gateway
|
||||
```
|
||||
|
||||
### Attribute separation — no semicolons, no commas
|
||||
|
||||
Wrong: `Node: { shape: circle; style.fill: red }`
|
||||
Correct:
|
||||
```d2
|
||||
Node: {
|
||||
shape: circle
|
||||
style.fill: red
|
||||
}
|
||||
```
|
||||
|
||||
### Special characters in IDs
|
||||
|
||||
IDs containing `-`, `:`, `.` must be quoted:
|
||||
Wrong: `my-node: { shape: circle }`
|
||||
Correct: `"my-node": { shape: circle }`
|
||||
|
||||
## Output
|
||||
|
||||
Write the complete D2 code to the specified output file path. Then return the file path.
|
||||
@@ -0,0 +1,81 @@
|
||||
# D2 Output Validator Agent
|
||||
|
||||
You are a specialized D2 code validator and exporter. Your job is to catch mistakes, fix them, and produce the final output file.
|
||||
|
||||
## What you receive
|
||||
|
||||
- Path to the generated `.d2` file
|
||||
- The structured requirements JSON (to verify completeness)
|
||||
- Export preferences (format, theme, sketch, layout engine)
|
||||
|
||||
## What to do — in this order
|
||||
|
||||
### Step 1: Read the D2 file and do a manual code review
|
||||
|
||||
Check every one of these:
|
||||
|
||||
- **Container node references**: Cross-container connections use full dot-separated paths (e.g., `Frontend.UI -> Backend.API`). This is the most common bug — if you see a bare node name that also appears inside a container, it's wrong.
|
||||
- **No orphan nodes**: Every node participates in at least one connection. Orphans mean something got disconnected.
|
||||
- **No semicolons or commas**: Attributes are separated by newlines only.
|
||||
- **Special characters**: IDs with `-`, `:`, `.` are wrapped in quotes.
|
||||
- **Bracket matching**: Every `{` has a matching `}`.
|
||||
- **Arrow syntax**: Connections use `->`, `<-`, `<->`, or `--` (nothing else).
|
||||
|
||||
If you find any issues, fix them directly in the file.
|
||||
|
||||
### Step 2: Verify requirements completeness
|
||||
|
||||
Compare the D2 code against the requirements JSON:
|
||||
|
||||
- Every entity in `entities` is present in the D2 code
|
||||
- Every connection in `connections` is present
|
||||
- Container/grouping structure matches what was specified
|
||||
- Layout direction is set correctly (`direction: right` or `direction: down`)
|
||||
|
||||
If entities or connections are missing, add them. If extras were added that weren't in the requirements, remove them unless they're clearly implied by the diagram type's structure (e.g., sequence diagrams need lifecycle bars).
|
||||
|
||||
### Step 3: Syntax validation
|
||||
|
||||
```bash
|
||||
d2 validate <file_path>
|
||||
```
|
||||
|
||||
If it fails, read the error message, fix the code, and re-validate. Repeat until it passes.
|
||||
|
||||
### Step 4: Export (if not D2-only)
|
||||
|
||||
If `export_format` is `d2`, skip this step — the .d2 file is the final output.
|
||||
|
||||
Otherwise, build and run the export command:
|
||||
|
||||
```bash
|
||||
d2 [options] input.d2 output.<format>
|
||||
```
|
||||
|
||||
Options based on preferences:
|
||||
- Theme: `--theme 0` (light) or `--theme 200` (dark)
|
||||
- Sketch: `--sketch` (if enabled)
|
||||
- Engine: `-l dagre` | `-l elk` | `-l tala`
|
||||
|
||||
#### Export format mapping
|
||||
|
||||
| Format | Command |
|
||||
|--------|---------|
|
||||
| SVG | `d2 [opts] input.d2 output.svg` |
|
||||
| PNG | `d2 [opts] input.d2 output.png` |
|
||||
| Preview | `d2 input.d2 output.txt` |
|
||||
|
||||
#### Tala engine special handling
|
||||
|
||||
- Tala only supports SVG. If the user wanted PNG, fall back to dagre/elk and note this in your output.
|
||||
- Check installation: `d2 layout tala`
|
||||
- If not installed, tell the user: "Tala engine is not installed. See https://github.com/terrastruct/tala"
|
||||
- **After SVG export with tala, you MUST remove the watermark.** The script is at `<skill-base-path>/scripts/remove_watermark.py`. Run: `python <skill-base-path>/scripts/remove_watermark.py output.svg`. This step is not optional — tala always adds a watermark and the user does not want it.
|
||||
|
||||
### Step 5: Return summary
|
||||
|
||||
Report back with:
|
||||
1. Issues found during code review (and what you fixed)
|
||||
2. Whether `d2 validate` passed (and any fixes applied)
|
||||
3. The export command you ran (if any)
|
||||
4. Path to the final output file(s)
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
```
|
||||
d2 0.7.1
|
||||
Usage:
|
||||
d2 [--watch=false] [--theme=0] file.d2 [file.svg | file.png | file.pdf | file.pptx | file.gif | file.txt]
|
||||
d2 layout [name]
|
||||
d2 fmt file.d2 ...
|
||||
d2 play [--theme=0] [--sketch] file.d2
|
||||
d2 validate file.d2
|
||||
|
||||
d2 compiles and renders file.d2 to file.svg | file.png | file.pdf | file.pptx | file.gif | file.txt
|
||||
It defaults to file.svg if an output path is not provided.
|
||||
|
||||
Use - to have d2 read from stdin or write to stdout.
|
||||
|
||||
See man d2 for more detailed docs.
|
||||
|
||||
Flags:
|
||||
-w, --watch $D2_WATCH watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.
|
||||
(default localhost:0, which will open on a randomly available local port). (default false)
|
||||
-h, --host string $HOST host listening address when used with watch (default "localhost")
|
||||
-p, --port string $PORT port listening address when used with watch (default "0")
|
||||
-b, --bundle $D2_BUNDLE when outputting SVG, bundle all assets and layers into the output file (default true)
|
||||
--force-appendix $D2_FORCE_APPENDIX an appendix for tooltips and links is added to PNG exports since they are not interactive. --force-appendix adds an appendix to SVG exports as well (default false)
|
||||
-d, --debug $DEBUG print debug logs. (default false)
|
||||
--img-cache $IMG_CACHE in watch mode, images used in icons are cached for subsequent compilations. This should be disabled if images might change. (default true)
|
||||
-l, --layout string $D2_LAYOUT the layout engine used (default "dagre")
|
||||
-t, --theme int $D2_THEME the diagram theme ID (default 0)
|
||||
--dark-theme int $D2_DARK_THEME the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831. (default -1)
|
||||
--pad int $D2_PAD pixels padded around the rendered diagram (default 100)
|
||||
--animate-interval int $D2_ANIMATE_INTERVAL if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports. (default 0)
|
||||
--timeout int $D2_TIMEOUT the maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value (default 120)
|
||||
-v, --version get the version (default false)
|
||||
-s, --sketch $D2_SKETCH render the diagram to look like it was sketched by hand (default false)
|
||||
--stdout-format string output format when writing to stdout (svg, png, ascii, txt, pdf, pptx, gif). Usage: d2 input.d2 --stdout-format png - > output.png (default "")
|
||||
--browser string $BROWSER browser executable that watch opens. Setting to 0 opens no browser. (default "")
|
||||
-c, --center $D2_CENTER center the SVG in the containing viewbox, such as your browser screen (default false)
|
||||
--scale float $SCALE scale the output. E.g., 0.5 to halve the default size. Default -1 means that SVG's will fit to screen and all others will use their default render size. Setting to 1 turns off SVG fitting to screen. (default -1)
|
||||
--target string target board to render. Pass an empty string to target root board. If target ends with '*', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. --target='' to render root board only or --target='layers.x.*' to render layer 'x' with all of its children. (default "*")
|
||||
--font-regular string $D2_FONT_REGULAR path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used. (default "")
|
||||
--font-italic string $D2_FONT_ITALIC path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used. (default "")
|
||||
--font-bold string $D2_FONT_BOLD path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used. (default "")
|
||||
--font-semibold string $D2_FONT_SEMIBOLD path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used. (default "")
|
||||
--font-mono string $D2_FONT_MONO path to .ttf file to use for the monospace font. If none provided, Source Code Pro Regular is used. (default "")
|
||||
--font-mono-bold string $D2_FONT_MONO_BOLD path to .ttf file to use for the monospace bold font. If none provided, Source Code Pro Bold is used. (default "")
|
||||
--font-mono-italic string $D2_FONT_MONO_ITALIC path to .ttf file to use for the monospace italic font. If none provided, Source Code Pro Italic is used. (default "")
|
||||
--font-mono-semibold string $D2_FONT_MONO_SEMIBOLD path to .ttf file to use for the monospace semibold font. If none provided, Source Code Pro Semibold is used. (default "")
|
||||
--check $D2_CHECK check that the specified files are formatted correctly. (default false)
|
||||
--no-xml-tag $D2_NO_XML_TAG omit XML tag (<?xml ...?>) from output SVG files. Useful when generating SVGs for direct HTML embedding (default false)
|
||||
--salt string Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate IDs do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output. (default "")
|
||||
--omit-version $OMIT_VERSION omit D2 version from generated image (default false)
|
||||
--ascii-mode string $D2_ASCII_MODE ASCII rendering mode for text outputs. Options: 'standard' (basic ASCII chars) or 'extended' (Unicode chars) (default "extended")
|
||||
|
||||
|
||||
Subcommands:
|
||||
d2 layout - Lists available layout engine options with short help
|
||||
d2 layout [name] - Display long help for a particular layout engine, including its configuration options
|
||||
d2 themes - Lists available themes
|
||||
d2 fmt file.d2 ... - Format passed files
|
||||
d2 play file.d2 - Opens the file in playground, an online web viewer (https://play.d2lang.com)
|
||||
d2 validate file.d2 - Validates file.d2
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# System Architecture
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Software system component relationships
|
||||
- Infrastructure planning
|
||||
- Service interaction design
|
||||
- Technical architecture documentation
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Database | `shape: cylinder` | Data storage |
|
||||
| External System | `shape: cloud` | Third-party services, external APIs |
|
||||
| Service Component | Default rectangle | Microservices, application services |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Frontend App
|
||||
Backend Service: { shape: rectangle }
|
||||
Database: { shape: cylinder }
|
||||
|
||||
Frontend App -> Backend Service: HTTP
|
||||
Backend Service -> Database: SQL Query
|
||||
```
|
||||
|
||||
## Cloud Architecture Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Load Balancer
|
||||
Web Server: { shape: rectangle }
|
||||
Database: { shape: cylinder }
|
||||
|
||||
Load Balancer -> Web Server: "Distribute traffic"
|
||||
Web Server -> Database: "Query data"
|
||||
```
|
||||
|
||||
## Microservices Architecture
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Frontend Layer: {
|
||||
Web App
|
||||
Mobile App
|
||||
Admin Console
|
||||
}
|
||||
|
||||
Gateway Layer: {
|
||||
API Gateway: { shape: rectangle }
|
||||
Load Balancer: { shape: rectangle }
|
||||
}
|
||||
|
||||
Service Layer: {
|
||||
User Service: { shape: rectangle }
|
||||
Order Service: { shape: rectangle }
|
||||
Payment Service: { shape: rectangle }
|
||||
}
|
||||
|
||||
Data Layer: {
|
||||
User Database: { shape: cylinder }
|
||||
Order Database: { shape: cylinder }
|
||||
Cache: { shape: cylinder }
|
||||
}
|
||||
|
||||
External Services: {
|
||||
Third-party Payment: { shape: cloud }
|
||||
Logistics Service: { shape: cloud }
|
||||
}
|
||||
|
||||
Frontend Layer -> Gateway Layer: HTTPS
|
||||
Gateway Layer -> Service Layer: gRPC
|
||||
User Service -> User Database: SQL
|
||||
Order Service -> Order Database: SQL
|
||||
Service Layer -> Cache: Redis
|
||||
Payment Service -> Third-party Payment: API
|
||||
Order Service -> Logistics Service: API
|
||||
```
|
||||
|
||||
## Multi-tier Application Architecture
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Presentation Layer: {
|
||||
Web Frontend
|
||||
Mobile Client
|
||||
Desktop App
|
||||
}
|
||||
|
||||
Application Layer: {
|
||||
API Gateway
|
||||
Business Services: {
|
||||
direction: right
|
||||
Auth Service
|
||||
Data Service
|
||||
Report Service
|
||||
}
|
||||
}
|
||||
|
||||
Middleware Layer: {
|
||||
Message Queue: { shape: queue }
|
||||
Cache Layer: { shape: cylinder }
|
||||
}
|
||||
|
||||
Data Layer: {
|
||||
Primary Database: { shape: cylinder }
|
||||
Replica Database: { shape: cylinder }
|
||||
Data Warehouse: { shape: cylinder }
|
||||
}
|
||||
|
||||
Presentation Layer -> API Gateway: HTTPS
|
||||
API Gateway -> Business Services: REST
|
||||
Business Services -> Cache Layer: Redis
|
||||
Business Services -> Message Queue: AMQP
|
||||
Business Services -> Primary Database: SQL
|
||||
Primary Database -> Replica Database: Replication
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Clear Layering** - Use containers to distinguish different tiers
|
||||
2. **Label Protocols** - Annotate communication protocols on connection lines
|
||||
3. **Proper Layout** - Use `direction` to control flow direction
|
||||
4. **Complete Connections** - Every node must have at least one connection, avoid orphan nodes
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Wrong: Orphan Nodes (Unconnected Modules)
|
||||
|
||||
```d2
|
||||
# Problem: Logging System, Report Generator not connected to other components
|
||||
Service Layer: {
|
||||
API Service
|
||||
Auth Service
|
||||
}
|
||||
Utility Layer: {
|
||||
Logging System # Orphan!
|
||||
Report Generator # Orphan!
|
||||
}
|
||||
API Service -> Auth Service
|
||||
```
|
||||
|
||||
### ✅ Correct: Ensure All Nodes Are Connected
|
||||
|
||||
```d2
|
||||
Service Layer: {
|
||||
API Service
|
||||
Auth Service
|
||||
}
|
||||
Utility Layer: {
|
||||
Logging System
|
||||
Report Generator
|
||||
}
|
||||
API Service -> Auth Service
|
||||
API Service -> Logging System: "Log records"
|
||||
Auth Service -> Logging System: "Log records"
|
||||
API Service -> Report Generator: "Generate report"
|
||||
```
|
||||
|
||||
### ✅ Better: Remove Non-core Dependencies, Keep It Simple
|
||||
|
||||
If a component (like logging) relates to all modules, consider omitting it and only keeping core data flows in the diagram:
|
||||
|
||||
```d2
|
||||
Service Layer: {
|
||||
API Service
|
||||
Auth Service
|
||||
}
|
||||
Database: { shape: cylinder }
|
||||
|
||||
API Service -> Auth Service
|
||||
API Service -> Database
|
||||
Auth Service -> Database
|
||||
# Auxiliary components like logging omitted to keep diagram clean
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# Flowchart
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Business process flows
|
||||
- Decision trees and branching logic
|
||||
- Algorithm flow descriptions
|
||||
- Operation step instructions
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Start/End | `shape: circle` | Beginning and end of process |
|
||||
| Decision Node | `shape: diamond` | Conditional branches, decision points |
|
||||
| Process Step | Default rectangle | Operations, processes, actions |
|
||||
| Input/Output | `shape: parallelogram` | Data input and output |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Start: { shape: circle }
|
||||
Step One
|
||||
Condition: { shape: diamond }
|
||||
End: { shape: circle }
|
||||
|
||||
Start -> Step One -> Condition
|
||||
Condition -> Step Two -> End
|
||||
Condition -> End: "End directly"
|
||||
```
|
||||
|
||||
## Example with Input/Output
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Start: { shape: circle }
|
||||
Input Data: { shape: parallelogram }
|
||||
Process Data
|
||||
Validate Result: { shape: diamond }
|
||||
Output Report: { shape: parallelogram }
|
||||
End: { shape: circle }
|
||||
|
||||
Start -> Input Data -> Process Data -> Validate Result
|
||||
Validate Result -> Output Report: "Success"
|
||||
Validate Result -> Process Data: "Failed, reprocess"
|
||||
Output Report -> End
|
||||
```
|
||||
|
||||
## Complex Decision Branching
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Start: { shape: circle }
|
||||
Enter Credentials
|
||||
Validate Credentials: { shape: diamond }
|
||||
Credentials Valid: { shape: diamond }
|
||||
Check Account Status: { shape: diamond }
|
||||
Generate Token
|
||||
End: { shape: circle }
|
||||
Error Handling
|
||||
Lock Handling
|
||||
|
||||
Start -> Enter Credentials
|
||||
Enter Credentials -> Validate Credentials
|
||||
Validate Credentials -> Error Handling: "Invalid"
|
||||
Validate Credentials -> Check Account Status: "Valid"
|
||||
Check Account Status -> Lock Handling: "Locked"
|
||||
Check Account Status -> Generate Token: "Normal"
|
||||
Generate Token -> End
|
||||
Lock Handling -> End: "Notify user"
|
||||
Error Handling -> End: "Return error"
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Simplicity First** - Show core processes, avoid over-detailing
|
||||
2. **Use Decision Nodes Wisely** - Only use diamond at key branching points
|
||||
3. **Clear Labeling** - Label branch conditions on connection lines
|
||||
4. **Avoid Displaying Loops** - Use text descriptions for loops instead of drawing cycles
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
❌ Over-detailing
|
||||
```
|
||||
Start → Validate Input Format → Format Correct? → Query User → User Exists? →
|
||||
Validate Password → Password Correct? → Create Session → Generate Token → Redirect to Home
|
||||
```
|
||||
|
||||
✅ Core Process
|
||||
```
|
||||
Start → Enter Credentials → Query User → User Exists? → Validate Password → Password Correct? →
|
||||
Create Session → Login Success
|
||||
```
|
||||
@@ -0,0 +1,308 @@
|
||||
# Grid Diagram
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Dashboard layout design
|
||||
- UI prototype design
|
||||
- Object matrix display
|
||||
- Heatmap-style visualization
|
||||
- Structured component arrangement
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Row Grid | `grid-rows: N` | Specify N rows |
|
||||
| Column Grid | `grid-columns: N` | Specify N columns |
|
||||
| Vertical Gap | `vertical-gap: N` | Row spacing |
|
||||
| Horizontal Gap | `horizontal-gap: N` | Column spacing |
|
||||
| Unified Gap | `grid-gap: N` | Set both row and column spacing |
|
||||
| Cell Width | `width: N` | Control cell width |
|
||||
| Cell Height | `height: N` | Control cell height |
|
||||
|
||||
## Basic Examples
|
||||
|
||||
### Rows Only
|
||||
|
||||
```d2
|
||||
grid-rows: 3
|
||||
Executive
|
||||
Legislative
|
||||
Judicial
|
||||
```
|
||||
|
||||
### Columns Only
|
||||
|
||||
```d2
|
||||
grid-columns: 3
|
||||
Executive
|
||||
Legislative
|
||||
Judicial
|
||||
```
|
||||
|
||||
### Both Rows and Columns
|
||||
|
||||
```d2
|
||||
grid-rows: 2
|
||||
grid-columns: 2
|
||||
Executive
|
||||
Legislative
|
||||
Judicial
|
||||
Fourth Item
|
||||
```
|
||||
|
||||
## Controlling Cell Size
|
||||
|
||||
Use `width` and `height` to create specific structures:
|
||||
|
||||
```d2
|
||||
grid-rows: 2
|
||||
Executive
|
||||
Legislative
|
||||
Judicial
|
||||
The American Government.width: 400
|
||||
```
|
||||
|
||||
When only one of rows or columns is defined, objects will expand automatically:
|
||||
|
||||
```d2
|
||||
grid-rows: 3
|
||||
Executive
|
||||
Legislative
|
||||
Judicial
|
||||
The American Government.width: 400
|
||||
Voters
|
||||
Non-voters
|
||||
```
|
||||
|
||||
## Grid Spacing
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `grid-gap` | Set both vertical and horizontal spacing |
|
||||
| `vertical-gap` | Row spacing (can override grid-gap) |
|
||||
| `horizontal-gap` | Column spacing (can override grid-gap) |
|
||||
|
||||
### Tight Grid (Heatmap Style)
|
||||
|
||||
```d2
|
||||
grid-gap: 0
|
||||
grid-columns: 3
|
||||
|
||||
Cell1
|
||||
Cell2
|
||||
Cell3
|
||||
Cell4
|
||||
Cell5
|
||||
Cell6
|
||||
```
|
||||
|
||||
## Nested Grids
|
||||
|
||||
Grid diagrams can be nested within other grids to create complex layouts:
|
||||
|
||||
```d2
|
||||
grid-gap: 0
|
||||
grid-columns: 1
|
||||
|
||||
header: "Header"
|
||||
|
||||
body: {
|
||||
grid-gap: 0
|
||||
grid-columns: 2
|
||||
content: "Main Content"
|
||||
sidebar: "Sidebar"
|
||||
}
|
||||
|
||||
footer: "Footer"
|
||||
```
|
||||
|
||||
## Complete Example: Dashboard Layout
|
||||
|
||||
```d2
|
||||
grid-rows: 5
|
||||
style.fill: black
|
||||
|
||||
classes: {
|
||||
white square: {
|
||||
label: ""
|
||||
width: 120
|
||||
style: {
|
||||
fill: white
|
||||
stroke: cornflowerblue
|
||||
stroke-width: 10
|
||||
}
|
||||
}
|
||||
block: {
|
||||
style: {
|
||||
text-transform: uppercase
|
||||
font-color: white
|
||||
fill: darkcyan
|
||||
stroke: black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flow1.class: white square
|
||||
flow2.class: white square
|
||||
flow3.class: white square
|
||||
flow4.class: white square
|
||||
flow5.class: white square
|
||||
flow6.class: white square
|
||||
flow7.class: white square
|
||||
flow8.class: white square
|
||||
flow9.class: white square
|
||||
|
||||
dagger engine: {
|
||||
width: 800
|
||||
class: block
|
||||
style: {
|
||||
fill: beige
|
||||
stroke: darkcyan
|
||||
font-color: blue
|
||||
stroke-width: 8
|
||||
}
|
||||
}
|
||||
|
||||
any docker compatible runtime: {
|
||||
width: 800
|
||||
class: block
|
||||
style: {
|
||||
fill: lightcyan
|
||||
stroke: darkcyan
|
||||
font-color: black
|
||||
stroke-width: 8
|
||||
}
|
||||
}
|
||||
|
||||
any ci: {
|
||||
class: block
|
||||
style: {
|
||||
fill: gold
|
||||
stroke: maroon
|
||||
font-color: maroon
|
||||
stroke-width: 8
|
||||
}
|
||||
}
|
||||
windows.class: block
|
||||
linux.class: block
|
||||
macos.class: block
|
||||
kubernetes.class: block
|
||||
```
|
||||
|
||||
## Complete Example: User Access Architecture
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
users -- via -- teleport
|
||||
|
||||
teleport -> jita: "all connections audited and logged"
|
||||
teleport -> infra
|
||||
teleport -> identity provider
|
||||
teleport <- identity provider
|
||||
|
||||
users: {
|
||||
grid-columns: 1
|
||||
|
||||
Engineers: {
|
||||
shape: circle
|
||||
}
|
||||
Machines: {
|
||||
shape: circle
|
||||
}
|
||||
}
|
||||
|
||||
via: {
|
||||
grid-columns: 1
|
||||
|
||||
https: "HTTPS://"
|
||||
kubectl: "> kubectl"
|
||||
tsh: "> tsh"
|
||||
api: "> api"
|
||||
db clients: "DB Clients"
|
||||
}
|
||||
|
||||
teleport: Teleport {
|
||||
grid-rows: 2
|
||||
|
||||
inp: |md
|
||||
# Identity Native Proxy
|
||||
| {
|
||||
width: 300
|
||||
}
|
||||
|
||||
Audit Log
|
||||
Cert Authority
|
||||
}
|
||||
|
||||
jita: "Just-in-time Access via" {
|
||||
grid-rows: 1
|
||||
|
||||
Slack
|
||||
Mattermost
|
||||
Jira
|
||||
Pagerduty
|
||||
Email
|
||||
}
|
||||
|
||||
infra: Infrastructure {
|
||||
grid-rows: 2
|
||||
|
||||
ssh
|
||||
Kubernetes
|
||||
My SQL
|
||||
MongoDB
|
||||
PSQL
|
||||
Windows
|
||||
}
|
||||
|
||||
identity provider: Indentity Provider
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Row/Column Priority** - Determine the primary direction first (rows or columns)
|
||||
2. **Appropriate Spacing** - Use `grid-gap` to control visual rhythm
|
||||
3. **Nested Composition** - Achieve complex layouts through nesting
|
||||
4. **Consistent Styling** - Use `class` to unify styles for similar cells
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Equal Grid
|
||||
|
||||
```d2
|
||||
grid-columns: 3
|
||||
grid-gap: 20
|
||||
|
||||
Module A
|
||||
Module B
|
||||
Module C
|
||||
```
|
||||
|
||||
### Unequal Layout
|
||||
|
||||
```d2
|
||||
grid-columns: 3
|
||||
grid-gap: 10
|
||||
|
||||
Sidebar.width: 200
|
||||
Main Content.width: 400
|
||||
Right Panel.width: 150
|
||||
```
|
||||
|
||||
### Compact Heatmap
|
||||
|
||||
```d2
|
||||
grid-gap: 0
|
||||
grid-columns: 4
|
||||
|
||||
1.style.fill: "#ff0000"
|
||||
2.style.fill: "#ff4444"
|
||||
3.style.fill: "#ff8888"
|
||||
4.style.fill: "#ffcccc"
|
||||
5.style.fill: "#00ff00"
|
||||
6.style.fill: "#44ff44"
|
||||
7.style.fill: "#88ff88"
|
||||
8.style.fill: "#ccffcc"
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
# Organization Chart
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Company organizational structure
|
||||
- Team composition display
|
||||
- Reporting relationships
|
||||
- Department structure diagrams
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Person Node | `shape: person` | Employees, roles |
|
||||
| Department Container | `{}` nested | Organizational units |
|
||||
| Hierarchical Relationship | `->` connection | Reporting, management relationships |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
CEO: { shape: person }
|
||||
|
||||
Tech Department: {
|
||||
CTO: { shape: person }
|
||||
Frontend Team: {
|
||||
Frontend Lead: { shape: person }
|
||||
Frontend Engineer: { shape: person }
|
||||
}
|
||||
Backend Team: {
|
||||
Backend Lead: { shape: person }
|
||||
Backend Engineer: { shape: person }
|
||||
}
|
||||
}
|
||||
|
||||
Marketing Department: {
|
||||
Marketing Director: { shape: person }
|
||||
Marketing Specialist: { shape: person }
|
||||
}
|
||||
|
||||
CEO -> CTO
|
||||
CEO -> Marketing Director
|
||||
CTO -> Frontend Team.Frontend Lead
|
||||
CTO -> Backend Team.Backend Lead
|
||||
```
|
||||
|
||||
## Complete Company Organization Structure
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Board: {
|
||||
Chairman
|
||||
}
|
||||
|
||||
Executive Team: {
|
||||
CEO
|
||||
CFO
|
||||
CTO
|
||||
}
|
||||
|
||||
Tech Department: {
|
||||
Frontend Team: {
|
||||
direction: right
|
||||
Frontend Lead
|
||||
Frontend Engineer1: { shape: person }
|
||||
Frontend Engineer2: { shape: person }
|
||||
}
|
||||
Backend Team: {
|
||||
direction: right
|
||||
Backend Lead
|
||||
Backend Engineer1: { shape: person }
|
||||
Backend Engineer2: { shape: person }
|
||||
}
|
||||
DevOps Team: {
|
||||
direction: right
|
||||
DevOps Lead
|
||||
DevOps Engineer: { shape: person }
|
||||
}
|
||||
}
|
||||
|
||||
Business Department: {
|
||||
Product Manager: { shape: person }
|
||||
Operations Team: {
|
||||
direction: right
|
||||
Operations Lead
|
||||
Operations Specialist: { shape: person }
|
||||
}
|
||||
}
|
||||
|
||||
Finance Department: {
|
||||
Finance Lead
|
||||
Accountant: { shape: person }
|
||||
Cashier: { shape: person }
|
||||
}
|
||||
|
||||
Board -> CEO: "Appoint"
|
||||
CEO -> CFO
|
||||
CEO -> CTO
|
||||
CTO -> Tech Department
|
||||
CEO -> Business Department
|
||||
CFO -> Finance Department
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Vertical Layout Preferred** - `direction: down` works best
|
||||
2. **Container Nesting** - Use containers to represent department hierarchy
|
||||
3. **Person Shape** - Use `shape: person` to identify person nodes
|
||||
4. **Clear Role Labels** - Include position information in node names
|
||||
|
||||
## Team Internal Layout
|
||||
|
||||
Members within the same team can use horizontal layout:
|
||||
|
||||
```d2
|
||||
Frontend Team: {
|
||||
direction: right
|
||||
Frontend Lead
|
||||
Frontend Engineer1: { shape: person }
|
||||
Frontend Engineer2: { shape: person }
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,166 @@
|
||||
# Sequence Diagram
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Time-based interaction flows
|
||||
- API call sequences
|
||||
- Protocol flows
|
||||
- Message passing
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Sequence Diagram Shape | `shape: sequence_diagram` | Top-level declaration |
|
||||
| Participant | Node declaration | Interacting roles/systems |
|
||||
| Message | `->` or `<-` | Call direction |
|
||||
| Phase Grouping | Container `{}` | Logical grouping |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
Client
|
||||
Server
|
||||
Database
|
||||
|
||||
Client -> Server: Request data
|
||||
Server -> Database: Query
|
||||
Database <- Server: Return result
|
||||
Client <- Server: Response data
|
||||
```
|
||||
|
||||
## User Login Sequence
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
User Browser
|
||||
Backend Service
|
||||
Auth Service
|
||||
Database
|
||||
Cache Service
|
||||
|
||||
Login Request: {
|
||||
User Browser -> Backend Service: POST /login {username, password}
|
||||
Backend Service -> Cache Service: Check login limit
|
||||
Cache Service <- Backend Service: Return limit status
|
||||
Backend Service -> Auth Service: Verify credentials
|
||||
Auth Service -> Database: Query user info
|
||||
Database <- Auth Service: Return user data
|
||||
Auth Service <- Backend Service: Verification result
|
||||
Backend Service -> Cache Service: Store session
|
||||
Cache Service <- Backend Service: Store success
|
||||
User Browser <- Backend Service: Return JWT Token
|
||||
}
|
||||
|
||||
Subsequent Request: {
|
||||
User Browser -> Backend Service: GET /api/resource {Authorization: Bearer}
|
||||
Backend Service -> Cache Service: Validate session
|
||||
Cache Service <- Backend Service: Session valid
|
||||
User Browser <- Backend Service: Return resource data
|
||||
}
|
||||
```
|
||||
|
||||
## Payment Flow Sequence
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
User
|
||||
Merchant System
|
||||
Payment Platform
|
||||
Bank System
|
||||
Notification Service
|
||||
|
||||
Create Order: {
|
||||
User -> Merchant System: Place order
|
||||
Merchant System -> Payment Platform: Create payment order
|
||||
Merchant System <- Payment Platform: Return payment link
|
||||
User <- Merchant System: Redirect to payment page
|
||||
}
|
||||
|
||||
User Payment: {
|
||||
User -> Payment Platform: Enter payment password
|
||||
Payment Platform -> Bank System: Deduct request
|
||||
Bank System <- Payment Platform: Deduction success
|
||||
User <- Payment Platform: Payment success page
|
||||
}
|
||||
|
||||
Async Notification: {
|
||||
Payment Platform -> Notification Service: Payment result notification
|
||||
Notification Service -> Merchant System: Webhook callback
|
||||
Merchant System <- Notification Service: Acknowledge receipt
|
||||
Notification Service <- Merchant System: Return 200 OK
|
||||
}
|
||||
```
|
||||
|
||||
## API Call Sequence
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
Client
|
||||
API Gateway
|
||||
User Service
|
||||
Order Service
|
||||
Message Queue
|
||||
|
||||
Client -> API Gateway: POST /orders
|
||||
API Gateway -> User Service: Validate user
|
||||
User Service <- API Gateway: User valid
|
||||
API Gateway -> Order Service: Create order
|
||||
Order Service -> Message Queue: Send order event
|
||||
Message Queue <- Order Service: Acknowledge receipt
|
||||
Order Service <- API Gateway: Order created successfully
|
||||
Client <- API Gateway: Return order ID
|
||||
```
|
||||
|
||||
## Styled Sequence Diagram
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
Client
|
||||
Server
|
||||
|
||||
Request Phase: {
|
||||
Client -> Server: Request data
|
||||
Server -> Server: Process data {
|
||||
style.stroke: blue
|
||||
}
|
||||
Client <- Server: Return result {
|
||||
style.stroke: green
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Declare Participants at Top** - List all roles before interactions
|
||||
2. **Phase Grouping** - Use containers to group related messages
|
||||
3. **Consistent Direction** - `->` for requests, `<-` for responses
|
||||
4. **Clear Labels** - Message labels describe specific operations
|
||||
|
||||
## Message Direction Explanation
|
||||
|
||||
- `A -> B: message` - A sends message to B (request)
|
||||
- `A <- B: message` - A receives message from B (response)
|
||||
- Typically use `->` for requests and `<-` for responses
|
||||
|
||||
## Animation Effects
|
||||
|
||||
Add animation to key messages:
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
Client
|
||||
Server
|
||||
|
||||
Client -> Server: Normal request
|
||||
Server -> Client: Important response {
|
||||
style.animated: true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,315 @@
|
||||
# SQL Table Diagram
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Database table structure design
|
||||
- ER Diagrams (Entity-Relationship)
|
||||
- Data model documentation
|
||||
- Table relationship visualization
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Table Shape | `shape: sql_table` | Define SQL table |
|
||||
| Field Definition | `field_name: type` | Table columns |
|
||||
| Primary Key | `constraint: primary_key` | Primary key field |
|
||||
| Foreign Key | `constraint: foreign_key` | Foreign key field |
|
||||
| Unique Constraint | `constraint: unique` | Unique field |
|
||||
| Table Connection | `TableA.field -> TableB.field` | Foreign key relationship |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
users: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
username: varchar(50)
|
||||
email: varchar(100) {constraint: unique}
|
||||
created_at: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Table with Constraints
|
||||
|
||||
### Single Constraint
|
||||
|
||||
```d2
|
||||
products: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
price: decimal(10,2)
|
||||
sku: varchar(20) {constraint: unique}
|
||||
created_at: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Constraints
|
||||
|
||||
Use an array to specify multiple constraint conditions:
|
||||
|
||||
```d2
|
||||
items: {
|
||||
shape: sql_table
|
||||
id: int {constraint: [primary_key; unique]}
|
||||
code: varchar(20) {constraint: [unique; not_null]}
|
||||
name: varchar(100)
|
||||
}
|
||||
```
|
||||
|
||||
## Table Relationships (Foreign Keys)
|
||||
|
||||
### One-to-Many Relationship
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
users: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
username: varchar(50)
|
||||
email: varchar(100)
|
||||
}
|
||||
|
||||
orders: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
user_id: int {constraint: foreign_key}
|
||||
total: decimal(10,2)
|
||||
status: varchar(20)
|
||||
}
|
||||
|
||||
orders.user_id -> users.id
|
||||
```
|
||||
|
||||
### Multi-table Relationships
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
users: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
username: varchar(50)
|
||||
email: varchar(100)
|
||||
}
|
||||
|
||||
posts: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
user_id: int {constraint: foreign_key}
|
||||
title: varchar(200)
|
||||
content: text
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
comments: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
post_id: int {constraint: foreign_key}
|
||||
user_id: int {constraint: foreign_key}
|
||||
content: text
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
posts.user_id -> users.id
|
||||
comments.post_id -> posts.id
|
||||
comments.user_id -> users.id
|
||||
```
|
||||
|
||||
## Nested in Containers
|
||||
|
||||
Organize tables into logical groups:
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
cloud: {
|
||||
disks: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
size_gb: int
|
||||
}
|
||||
|
||||
blocks: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
disk_id: int {constraint: foreign_key}
|
||||
data: blob
|
||||
}
|
||||
|
||||
blocks.disk_id -> disks.id
|
||||
}
|
||||
|
||||
AWS S3 -> cloud.disks
|
||||
```
|
||||
|
||||
## Complete E-commerce Data Model
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
User Module: {
|
||||
users: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
username: varchar(50)
|
||||
email: varchar(100) {constraint: unique}
|
||||
password_hash: varchar(255)
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
addresses: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
user_id: int {constraint: foreign_key}
|
||||
province: varchar(50)
|
||||
city: varchar(50)
|
||||
address: varchar(200)
|
||||
}
|
||||
|
||||
addresses.user_id -> users.id
|
||||
}
|
||||
|
||||
Product Module: {
|
||||
categories: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
parent_id: int {constraint: foreign_key}
|
||||
}
|
||||
|
||||
products: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
category_id: int {constraint: foreign_key}
|
||||
name: varchar(200)
|
||||
price: decimal(10,2)
|
||||
stock: int
|
||||
}
|
||||
|
||||
categories.parent_id -> categories.id
|
||||
products.category_id -> categories.id
|
||||
}
|
||||
|
||||
Order Module: {
|
||||
orders: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
user_id: int {constraint: foreign_key}
|
||||
address_id: int {constraint: foreign_key}
|
||||
total: decimal(10,2)
|
||||
status: varchar(20)
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
order_items: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
order_id: int {constraint: foreign_key}
|
||||
product_id: int {constraint: foreign_key}
|
||||
quantity: int
|
||||
price: decimal(10,2)
|
||||
}
|
||||
|
||||
orders.user_id -> User Module.users.id
|
||||
orders.address_id -> User Module.addresses.id
|
||||
order_items.order_id -> orders.id
|
||||
order_items.product_id -> Product Module.products.id
|
||||
}
|
||||
```
|
||||
|
||||
## Reserved Keyword Handling
|
||||
|
||||
If a field name is a reserved keyword, wrap it in quotes:
|
||||
|
||||
```d2
|
||||
my_table: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
"label": string
|
||||
"order": int
|
||||
"group": varchar(50)
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Data Types
|
||||
|
||||
Common SQL data types:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `int` / `integer` | Integer |
|
||||
| `varchar(n)` | Variable-length string |
|
||||
| `text` | Long text |
|
||||
| `decimal(m,n)` | Exact decimal |
|
||||
| `float` | Floating point |
|
||||
| `boolean` / `bool` | Boolean |
|
||||
| `date` | Date |
|
||||
| `timestamp` | Timestamp |
|
||||
| `json` / `jsonb` | JSON data |
|
||||
| `blob` | Binary data |
|
||||
| `uuid` | UUID identifier |
|
||||
|
||||
## Constraint Types
|
||||
|
||||
| Constraint | Description |
|
||||
|------------|-------------|
|
||||
| `primary_key` | Primary key |
|
||||
| `foreign_key` | Foreign key |
|
||||
| `unique` | Unique value |
|
||||
| `not_null` | Not null |
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Direction Layout** - Use `direction: right` for clearer relationship lines
|
||||
2. **Logical Grouping** - Organize related tables into containers
|
||||
3. **Label Relationships** - Add relationship descriptions on connection lines
|
||||
4. **Naming Conventions** - Use consistent naming (snake_case recommended)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Self-referencing Relationship
|
||||
|
||||
```d2
|
||||
categories: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
parent_id: int {constraint: foreign_key}
|
||||
}
|
||||
|
||||
categories.parent_id -> categories.id: "Parent category"
|
||||
```
|
||||
|
||||
### Many-to-Many Junction Table
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
students: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
}
|
||||
|
||||
courses: {
|
||||
shape: sql_table
|
||||
id: int {constraint: primary_key}
|
||||
name: varchar(100)
|
||||
}
|
||||
|
||||
enrollments: {
|
||||
shape: sql_table
|
||||
student_id: int {constraint: foreign_key}
|
||||
course_id: int {constraint: foreign_key}
|
||||
enrolled_at: timestamp
|
||||
}
|
||||
|
||||
enrollments.student_id -> students.id
|
||||
enrollments.course_id -> courses.id
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
# State Machine
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Object state transitions
|
||||
- Workflow state management
|
||||
- Lifecycle diagrams
|
||||
- Business process states
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| State Node | `shape: circle` or rectangle | Represents state |
|
||||
| Transition Arrow | `->` with label | State change |
|
||||
| Initial State | Style distinction | Starting state |
|
||||
| Final State | Style distinction | Ending state |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Pending -> Processing: "Start processing"
|
||||
Processing -> Completed: "Success"
|
||||
Processing -> Cancelled: "Failed"
|
||||
```
|
||||
|
||||
## Order State Machine
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Awaiting Payment: {
|
||||
style.stroke-width: 3
|
||||
}
|
||||
Paid
|
||||
Processing
|
||||
Shipped
|
||||
Completed: {
|
||||
style.stroke-width: 3
|
||||
}
|
||||
Cancelled: {
|
||||
style.stroke-width: 3
|
||||
}
|
||||
Refunding
|
||||
Refunded: {
|
||||
style.stroke-width: 3
|
||||
}
|
||||
|
||||
Awaiting Payment -> Paid: "Payment success"
|
||||
Awaiting Payment -> Cancelled: "Timeout/Cancel"
|
||||
Paid -> Processing: "Confirm order"
|
||||
Processing -> Shipped: "Warehouse ships"
|
||||
Shipped -> Completed: "Confirm receipt"
|
||||
Paid -> Cancelled: "User cancel"
|
||||
Processing -> Cancelled: "Cancel order"
|
||||
Shipped -> Refunding: "Request refund"
|
||||
Refunding -> Refunded: "Refund complete"
|
||||
Completed -> Refunding: "After-sale request"
|
||||
```
|
||||
|
||||
## User Session State Machine
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Not Logged In: {
|
||||
style.stroke-width: 3
|
||||
}
|
||||
Logging In
|
||||
Logged In
|
||||
Session Expired
|
||||
Locked
|
||||
Logged Out
|
||||
|
||||
Not Logged In -> Logging In: "Submit credentials"
|
||||
Logging In -> Logged In: "Auth success"
|
||||
Logging In -> Locked: "Multiple failures"
|
||||
Logging In -> Not Logged In: "Auth failed"
|
||||
Logged In -> Session Expired: "Timeout"
|
||||
Logged In -> Logged Out: "Active logout"
|
||||
Session Expired -> Not Logged In: "Re-login"
|
||||
Logged Out -> Not Logged In
|
||||
Locked -> Not Logged In: "Admin unlock"
|
||||
```
|
||||
|
||||
## Styled State Machine
|
||||
|
||||
Use colors to distinguish state types:
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
# Initial state - Green
|
||||
Initial State: {
|
||||
shape: circle
|
||||
style.fill: "#c8e6c9"
|
||||
style.stroke: "#2e7d32"
|
||||
}
|
||||
|
||||
# Intermediate state - Blue
|
||||
Processing: {
|
||||
style.fill: "#bbdefb"
|
||||
}
|
||||
|
||||
# Final state - Red
|
||||
Final State: {
|
||||
shape: circle
|
||||
style.fill: "#ffcdd2"
|
||||
style.stroke: "#c62828"
|
||||
}
|
||||
|
||||
Initial State -> Processing: "Event A"
|
||||
Processing -> Final State: "Event B"
|
||||
```
|
||||
|
||||
## Bidirectional State Toggle
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
On <-> Off: "Toggle"
|
||||
|
||||
# Or use two one-way arrows
|
||||
# On -> Off: "Turn off"
|
||||
# Off -> On: "Turn on"
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Label Transitions Clearly** - Every transition arrow should have a trigger condition
|
||||
2. **Distinguish State Types** - Use styles to differentiate initial/final states
|
||||
3. **Avoid Over-complexity** - Consider splitting if more than 10 states
|
||||
4. **Horizontal Layout** - `direction: right` usually works better
|
||||
@@ -0,0 +1,158 @@
|
||||
# Swimlane Diagram
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Cross-functional processes
|
||||
- Multi-role workflows
|
||||
- Inter-departmental collaboration
|
||||
- Approval workflows
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Swimlane Container | `{}` | One container per role/department |
|
||||
| Cross-lane Connection | `RoleA.Node -> RoleB.Node` | Cross-role interaction |
|
||||
| Decision Node | `shape: diamond` | Branch decisions |
|
||||
|
||||
## Basic Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
User: {
|
||||
Submit Request
|
||||
View Result
|
||||
}
|
||||
|
||||
System: {
|
||||
Validate Request
|
||||
Process Data
|
||||
}
|
||||
|
||||
User.Submit Request -> System.Validate Request
|
||||
System.Validate Request -> System.Process Data
|
||||
System.Process Data -> User.View Result
|
||||
```
|
||||
|
||||
## E-commerce Order Processing Swimlane
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Customer: {
|
||||
Place Order
|
||||
Pay for Order
|
||||
Confirm Receipt
|
||||
Request Refund
|
||||
}
|
||||
|
||||
Order System: {
|
||||
Validate Order
|
||||
Create Payment
|
||||
Update Inventory
|
||||
Generate Shipment
|
||||
Process Refund
|
||||
}
|
||||
|
||||
Warehouse: {
|
||||
Pick Items
|
||||
Pack Items
|
||||
Ship Order
|
||||
}
|
||||
|
||||
Logistics: {
|
||||
Transport
|
||||
Deliver
|
||||
}
|
||||
|
||||
Finance System: {
|
||||
Confirm Payment
|
||||
Process Refund
|
||||
}
|
||||
|
||||
Customer.Place Order -> Order System.Validate Order
|
||||
Order System.Validate Order -> Order System.Create Payment
|
||||
Order System.Create Payment -> Customer.Pay for Order
|
||||
Customer.Pay for Order -> Finance System.Confirm Payment
|
||||
Finance System.Confirm Payment -> Order System.Update Inventory
|
||||
Order System.Update Inventory -> Order System.Generate Shipment
|
||||
Order System.Generate Shipment -> Warehouse.Pick Items
|
||||
Warehouse.Pick Items -> Warehouse.Pack Items
|
||||
Warehouse.Pack Items -> Warehouse.Ship Order
|
||||
Warehouse.Ship Order -> Logistics.Transport
|
||||
Logistics.Transport -> Logistics.Deliver
|
||||
Logistics.Deliver -> Customer.Confirm Receipt
|
||||
Customer.Request Refund -> Order System.Process Refund
|
||||
Order System.Process Refund -> Finance System.Process Refund
|
||||
```
|
||||
|
||||
## Approval Workflow Swimlane
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Applicant: {
|
||||
Submit Application
|
||||
Provide Additional Info
|
||||
Receive Result
|
||||
}
|
||||
|
||||
Department Manager: {
|
||||
Initial Review
|
||||
Approve: { shape: diamond }
|
||||
Reject
|
||||
}
|
||||
|
||||
Finance Department: {
|
||||
Verify Amount
|
||||
Confirm Budget: { shape: diamond }
|
||||
}
|
||||
|
||||
CEO: {
|
||||
Final Review
|
||||
Final Approval: { shape: diamond }
|
||||
}
|
||||
|
||||
Applicant.Submit Application -> Department Manager.Initial Review
|
||||
Department Manager.Initial Review -> Department Manager.Approve
|
||||
Department Manager.Approve -> Department Manager.Reject: "Not approved"
|
||||
Department Manager.Approve -> Finance Department.Verify Amount: "Approved"
|
||||
Department Manager.Reject -> Applicant.Provide Additional Info
|
||||
Applicant.Provide Additional Info -> Department Manager.Initial Review
|
||||
Finance Department.Verify Amount -> Finance Department.Confirm Budget
|
||||
Finance Department.Confirm Budget -> CEO.Final Review: "Over budget"
|
||||
Finance Department.Confirm Budget -> Applicant.Receive Result: "Within budget"
|
||||
CEO.Final Review -> CEO.Final Approval
|
||||
CEO.Final Approval -> Applicant.Receive Result: "Approved"
|
||||
CEO.Final Approval -> Applicant.Receive Result: "Rejected"
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Clear Roles** - Each swimlane represents a role or department
|
||||
2. **Horizontal Layout** - `direction: right` suits most swimlane diagrams
|
||||
3. **Cross-lane Connections** - Use full paths `Role.Node`
|
||||
4. **Concise Naming** - Keep swimlane names short and clear
|
||||
|
||||
## Vertical Swimlanes
|
||||
|
||||
Vertical layout may be more suitable in some scenarios:
|
||||
|
||||
```d2
|
||||
direction: down
|
||||
|
||||
Frontend: {
|
||||
Page Render
|
||||
User Interaction
|
||||
}
|
||||
|
||||
Backend: {
|
||||
API Processing
|
||||
Data Query
|
||||
}
|
||||
|
||||
Frontend.Page Render -> Backend.API Processing
|
||||
Backend.API Processing -> Backend.Data Query
|
||||
Backend.Data Query -> Frontend.User Interaction
|
||||
```
|
||||
@@ -0,0 +1,167 @@
|
||||
# Service Topology
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Cloud infrastructure planning
|
||||
- Kubernetes cluster architecture
|
||||
- Network topology design
|
||||
- Resource dependency relationships
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Element | Syntax | Purpose |
|
||||
|---------|--------|---------|
|
||||
| Container Grouping | `{}` | Group by environment or type |
|
||||
| Dashed Connection | `style.stroke-dash: 3` | Optional or indirect relationships |
|
||||
| Database | `shape: cylinder` | Data storage |
|
||||
| Cloud Service | `shape: cloud` | External services |
|
||||
|
||||
## Kubernetes Cluster Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Ingress Controller: {
|
||||
Nginx: { shape: rectangle }
|
||||
}
|
||||
|
||||
Service Mesh: {
|
||||
API Service: {
|
||||
direction: down
|
||||
Pod1
|
||||
Pod2
|
||||
Pod3
|
||||
}
|
||||
User Service: {
|
||||
direction: down
|
||||
Pod1
|
||||
Pod2
|
||||
}
|
||||
Order Service: {
|
||||
direction: down
|
||||
Pod1
|
||||
Pod2
|
||||
}
|
||||
}
|
||||
|
||||
Data Storage: {
|
||||
Redis Cluster: { shape: cylinder }
|
||||
PostgreSQL: { shape: cylinder }
|
||||
}
|
||||
|
||||
Monitoring: {
|
||||
Prometheus
|
||||
Grafana
|
||||
}
|
||||
|
||||
External Traffic -> Ingress Controller.Nginx
|
||||
Ingress Controller.Nginx -> Service Mesh.API Service
|
||||
Service Mesh.API Service -> Service Mesh.User Service
|
||||
Service Mesh.API Service -> Service Mesh.Order Service
|
||||
Service Mesh.User Service -> Data Storage.Redis Cluster
|
||||
Service Mesh.Order Service -> Data Storage.PostgreSQL
|
||||
Service Mesh -> Prometheus: Metrics
|
||||
Prometheus -> Grafana
|
||||
```
|
||||
|
||||
## Cloud Infrastructure Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
VPC: {
|
||||
Public Subnet: {
|
||||
Load Balancer: { shape: rectangle }
|
||||
NAT Gateway: { shape: rectangle }
|
||||
}
|
||||
|
||||
Private Subnet: {
|
||||
Web Servers: {
|
||||
direction: down
|
||||
Instance1
|
||||
Instance2
|
||||
}
|
||||
App Servers: {
|
||||
direction: down
|
||||
Instance1
|
||||
Instance2
|
||||
Instance3
|
||||
}
|
||||
}
|
||||
|
||||
Data Subnet: {
|
||||
Primary DB: { shape: cylinder }
|
||||
Replica DB: { shape: cylinder }
|
||||
}
|
||||
}
|
||||
|
||||
External Services: {
|
||||
CDN: { shape: cloud }
|
||||
Monitoring Service: { shape: cloud }
|
||||
}
|
||||
|
||||
User Traffic -> CDN
|
||||
CDN -> VPC.Public Subnet.Load Balancer
|
||||
Load Balancer -> Private Subnet.Web Servers
|
||||
Web Servers -> Private Subnet.App Servers
|
||||
App Servers -> Data Subnet.Primary DB
|
||||
Primary DB -> Data Subnet.Replica DB: Replication
|
||||
Private Subnet.App Servers -> NAT Gateway
|
||||
NAT Gateway -> External Services.Monitoring Service
|
||||
```
|
||||
|
||||
## Network Topology Example
|
||||
|
||||
```d2
|
||||
direction: right
|
||||
|
||||
Internet: { shape: cloud }
|
||||
|
||||
DMZ Zone: {
|
||||
Firewall
|
||||
Web Server
|
||||
}
|
||||
|
||||
Internal Zone: {
|
||||
App Server
|
||||
DB Server: { shape: cylinder }
|
||||
}
|
||||
|
||||
Management Zone: {
|
||||
Monitoring Server
|
||||
Log Server
|
||||
}
|
||||
|
||||
Internet -> DMZ Zone.Firewall
|
||||
DMZ Zone.Firewall -> DMZ Zone.Web Server
|
||||
DMZ Zone.Web Server -> Internal Zone.App Server
|
||||
Internal Zone.App Server -> Internal Zone.DB Server
|
||||
DMZ Zone.Web Server -> Management Zone.Monitoring Server: Logs
|
||||
Internal Zone.App Server -> Management Zone.Log Server
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Group by Environment** - Use containers to distinguish production/test/development environments
|
||||
2. **Label Dependencies** - Annotate protocols and purposes on connection lines
|
||||
3. **Dashed for Optional** - Use dashed lines for non-essential connections
|
||||
4. **Use Icons Wisely** - Use icons for cloud services to enhance recognition
|
||||
|
||||
## Class Style Reuse
|
||||
|
||||
Unify styles for the same type of resources:
|
||||
|
||||
```d2
|
||||
class: server
|
||||
server: {
|
||||
style.fill: "#e3f2fd"
|
||||
style.stroke: "#1976d2"
|
||||
}
|
||||
|
||||
Web Server: {
|
||||
class: server
|
||||
}
|
||||
App Server: {
|
||||
class: server
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,532 @@
|
||||
# D2 Syntax Reference
|
||||
|
||||
## Node Shapes
|
||||
|
||||
### Basic Shapes
|
||||
|
||||
- `rectangle` - Rectangle (default)
|
||||
- `square` - Square
|
||||
- `circle` - Circle
|
||||
- `oval` - Oval
|
||||
- `ellipse` - Ellipse (alias for oval)
|
||||
|
||||
### Flowchart Shapes
|
||||
|
||||
- `diamond` - Diamond (conditional)
|
||||
- `parallelogram` - Parallelogram (input/output)
|
||||
- `trapezoid` - Trapezoid
|
||||
- `rounded_rectangle` - Rounded rectangle
|
||||
- `stroke_rectangle` - Stroked rectangle
|
||||
|
||||
### Data and Storage
|
||||
|
||||
- `cylinder` - Cylinder (database)
|
||||
- `queue` - Queue (cylinder variant)
|
||||
- `disk` - Disk
|
||||
- `storage` - Storage
|
||||
|
||||
### System and Network
|
||||
|
||||
- `cloud` - Cloud (external system/service)
|
||||
- `hexagon` - Hexagon
|
||||
- `octagon` - Octagon
|
||||
- `callout` - Callout box
|
||||
- `note` - Note
|
||||
|
||||
### People and Organization
|
||||
|
||||
- `person` - Person
|
||||
- `queue` - Queue (waiting line)
|
||||
|
||||
### Documents and Pages
|
||||
|
||||
- `page` - Document page
|
||||
- `document` - Document
|
||||
|
||||
### Special Shapes
|
||||
|
||||
- `image` - Image node
|
||||
- `sql_table` - SQL table
|
||||
- `class` - Class diagram (UML)
|
||||
|
||||
**Note**: `shape: label` is not supported in some versions of D2. It's recommended to use the node's `label` property or the direct label syntax:
|
||||
```
|
||||
node: "Label text" # Recommended approach
|
||||
```
|
||||
|
||||
## Connection Types
|
||||
|
||||
### Basic Connections
|
||||
|
||||
```
|
||||
A -> B # Directed arrow
|
||||
A <- B # Reverse arrow
|
||||
A <-> B # Bidirectional arrow
|
||||
A -- B # Undirected line
|
||||
A - B # Undirected line (shorthand)
|
||||
```
|
||||
|
||||
### Connection Labels
|
||||
|
||||
```
|
||||
A -> B: Label text # Single-line label
|
||||
# Note: Multi-line labels use block syntax or |md syntax
|
||||
```
|
||||
|
||||
### Arrowhead Styles
|
||||
|
||||
Customize arrows with `source-arrowhead` and `target-arrowhead`:
|
||||
|
||||
```
|
||||
source-arrowhead: triangle # Triangle (default)
|
||||
source-arrowhead: arrow # Arrow
|
||||
source-arrowhead: diamond # Diamond
|
||||
source-arrowhead: circle # Circle
|
||||
source-arrowhead: box # Box
|
||||
source-arrowhead: cross # Cross
|
||||
source-arrowhead: none # No arrowhead
|
||||
```
|
||||
|
||||
### Connection Line Styles
|
||||
|
||||
```
|
||||
A -> B {
|
||||
style.stroke-dash: 3 # Dashed line
|
||||
style.stroke-width: 2 # Line width
|
||||
style.stroke: red # Line color
|
||||
style.animated: true # Animation effect
|
||||
}
|
||||
```
|
||||
|
||||
## Style Properties
|
||||
|
||||
### Colors
|
||||
|
||||
```
|
||||
style.fill: "#ff0000" # Fill color (hexadecimal)
|
||||
style.fill: red # Fill color (color name)
|
||||
style.stroke: blue # Border color
|
||||
style.opacity: 0.5 # Opacity (0-1)
|
||||
```
|
||||
|
||||
### Borders
|
||||
|
||||
```
|
||||
style.stroke-width: 2 # Border width
|
||||
style.stroke-dash: 5 # Dash pattern (number is spacing)
|
||||
style.border-radius: 8 # Border radius
|
||||
```
|
||||
|
||||
### Fonts
|
||||
|
||||
```
|
||||
style.font-size: 14 # Font size
|
||||
style.font-color: "#333" # Font color
|
||||
# Note: style.font-weight and style.font-style are not supported in some versions
|
||||
```
|
||||
|
||||
**Important**: Multiple style properties **must be separated by newlines, do not use semicolons or commas**.
|
||||
|
||||
Correct example:
|
||||
```
|
||||
node: {
|
||||
style.fill: red
|
||||
style.stroke: blue
|
||||
style.stroke-width: 2
|
||||
}
|
||||
```
|
||||
|
||||
Incorrect example:
|
||||
```
|
||||
node: { style.fill: red; style.stroke: blue }
|
||||
```
|
||||
|
||||
### Fill Patterns
|
||||
|
||||
```
|
||||
style.fill-pattern: dots # Dots pattern
|
||||
style.fill-pattern: lines # Lines pattern
|
||||
style.fill-pattern: crosshatch # Crosshatch pattern
|
||||
```
|
||||
|
||||
### Multiplicity
|
||||
|
||||
```
|
||||
style.multiple: true # Display multiple nodes (e.g., multiple databases)
|
||||
```
|
||||
|
||||
### Animation
|
||||
|
||||
```
|
||||
style.animated: true # Enable animation
|
||||
```
|
||||
|
||||
## Container Syntax
|
||||
|
||||
### Basic Container
|
||||
|
||||
```
|
||||
container_name: {
|
||||
nodeA
|
||||
nodeB
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Containers
|
||||
|
||||
```
|
||||
outer_container: {
|
||||
inner_container: {
|
||||
nodeC
|
||||
}
|
||||
nodeA
|
||||
nodeB
|
||||
}
|
||||
```
|
||||
|
||||
### Container Styles
|
||||
|
||||
```
|
||||
container: {
|
||||
style.fill: "#f0f0f0"
|
||||
style.stroke: "#333"
|
||||
nodeA
|
||||
nodeB
|
||||
}
|
||||
```
|
||||
|
||||
### Container Layout
|
||||
|
||||
```
|
||||
container: {
|
||||
direction: right # Layout direction within container
|
||||
grid-columns: 3 # Number of grid columns
|
||||
nodeA
|
||||
nodeB
|
||||
nodeC
|
||||
}
|
||||
```
|
||||
|
||||
### Referencing Parent Containers
|
||||
|
||||
```
|
||||
outer: {
|
||||
middle: {
|
||||
node -> _ # _ references outer container
|
||||
}
|
||||
parent_node
|
||||
}
|
||||
|
||||
middle.node -> outer.parent_node # Full path reference
|
||||
```
|
||||
|
||||
## Classes and Style Reuse
|
||||
|
||||
### Defining Classes
|
||||
|
||||
```
|
||||
class: class_name
|
||||
class_name: {
|
||||
style.fill: yellow
|
||||
style.stroke: orange
|
||||
}
|
||||
```
|
||||
|
||||
### Applying Classes
|
||||
|
||||
```
|
||||
nodeA: {
|
||||
class: class_name
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Classes
|
||||
|
||||
```
|
||||
nodeA: {
|
||||
class: [class1, class2] # Applied in order
|
||||
}
|
||||
```
|
||||
|
||||
### Class Inheritance
|
||||
|
||||
Object properties override class properties:
|
||||
|
||||
```
|
||||
class_name: {
|
||||
style.fill: yellow
|
||||
}
|
||||
|
||||
node: {
|
||||
class: class_name
|
||||
style.fill: red # Overrides class fill property
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Control
|
||||
|
||||
### Global Layout Direction
|
||||
|
||||
```
|
||||
direction: right # Left to right (default)
|
||||
direction: down # Top to bottom
|
||||
direction: left # Right to left
|
||||
direction: up # Bottom to top
|
||||
```
|
||||
|
||||
### Layout Engine
|
||||
|
||||
Set in `vars`:
|
||||
|
||||
```
|
||||
vars: {
|
||||
d2-config: {
|
||||
layout-engine: dagre # dagre (default)
|
||||
layout-engine: elk # ELK layout
|
||||
layout-engine: tala # TALA layout
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
```
|
||||
vars: {
|
||||
d2-config: {
|
||||
theme-id: 0 # Default theme
|
||||
theme-id: 1 # Neutral theme
|
||||
theme-id: 3 # Terrastruct theme
|
||||
theme-id: 100 # Custom theme number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sketch Mode
|
||||
|
||||
```
|
||||
vars: {
|
||||
d2-config: {
|
||||
sketch: true # Enable hand-drawn style
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
### Defining Variables
|
||||
|
||||
```
|
||||
vars: {
|
||||
variable_name: value
|
||||
colors: {
|
||||
primary: "#007bff"
|
||||
secondary: "#6c757d"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Variables
|
||||
|
||||
```
|
||||
nodeA: {
|
||||
style.fill: ${vars.colors.primary}
|
||||
}
|
||||
|
||||
title: ${vars.variable_name}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Tooltips
|
||||
|
||||
```
|
||||
node: { tooltip: "Hover tooltip text" }
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
```
|
||||
node: {
|
||||
link: "https://example.com"
|
||||
tooltip: "Click to visit"
|
||||
}
|
||||
```
|
||||
|
||||
### SQL Tables
|
||||
|
||||
```
|
||||
table: {
|
||||
shape: sql_table
|
||||
column1: type1
|
||||
column2: type2
|
||||
column3: type3
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-line Labels
|
||||
|
||||
Use block syntax to support multi-line text:
|
||||
|
||||
```
|
||||
node: |md
|
||||
# Markdown Heading
|
||||
Supports multi-line text
|
||||
Supports **bold** and *italic*
|
||||
|
|
||||
```
|
||||
|
||||
Or use simple line breaks:
|
||||
|
||||
```
|
||||
node: |md
|
||||
First line
|
||||
Second line
|
||||
Third line
|
||||
|
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
```
|
||||
container: {
|
||||
grid-columns: 3 # 3-column grid
|
||||
nodeA
|
||||
nodeB
|
||||
nodeC
|
||||
nodeD
|
||||
nodeE
|
||||
nodeF
|
||||
}
|
||||
```
|
||||
|
||||
### Wildcard Styles
|
||||
|
||||
```
|
||||
*.style.fill: blue # All nodes
|
||||
*.*.style.fill: green # All nested nodes
|
||||
(nodeA -- *)[*].style.stroke: red # All connections of nodeA
|
||||
```
|
||||
|
||||
### Title
|
||||
|
||||
```
|
||||
title: "Diagram Title" # Simple title
|
||||
title: |md # Markdown title
|
||||
# Heading 1
|
||||
Supports **Markdown** format
|
||||
| {near: top-center} # Title position
|
||||
```
|
||||
|
||||
### Position Control (Use Sparingly)
|
||||
|
||||
```
|
||||
node: {
|
||||
position: absolute
|
||||
x: 100
|
||||
y: 200
|
||||
}
|
||||
```
|
||||
|
||||
Note: Prefer automatic layout; only use manual positioning when necessary.
|
||||
|
||||
## Comments
|
||||
|
||||
### Line Comments
|
||||
|
||||
```
|
||||
# This is a single-line comment
|
||||
nodeA -- nodeB # End-of-line comment
|
||||
```
|
||||
|
||||
### Multi-line Comments
|
||||
|
||||
Use multiple line comments for multi-line comments:
|
||||
|
||||
```
|
||||
# This is the first line of comment
|
||||
# This is the second line of comment
|
||||
# This is the third line of comment
|
||||
nodeA -- nodeB
|
||||
```
|
||||
|
||||
## Special Syntax
|
||||
|
||||
### Implicit Connections
|
||||
|
||||
Multiple nodes on the same line are automatically connected:
|
||||
|
||||
```
|
||||
A B C D # Equivalent to A -> B -> C -> D
|
||||
```
|
||||
|
||||
### Connection Grouping
|
||||
|
||||
```
|
||||
(A B C) -> (D E F) # A, B, C each connect to D, E, F
|
||||
```
|
||||
|
||||
### Conditional Styles
|
||||
|
||||
```
|
||||
node: {
|
||||
style.fill?: red # Conditional style (if not set)
|
||||
}
|
||||
```
|
||||
|
||||
### Label Shorthand
|
||||
|
||||
```
|
||||
node: Label text # Node name is "node", label is "Label text"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Database Connection
|
||||
|
||||
```
|
||||
app <-> database: {
|
||||
style.stroke-dash: 3
|
||||
source-arrowhead: none
|
||||
target-arrowhead: triangle
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Branch
|
||||
|
||||
```
|
||||
decision: {
|
||||
shape: diamond
|
||||
style.fill: lightyellow
|
||||
}
|
||||
```
|
||||
|
||||
### Loop Process
|
||||
|
||||
```
|
||||
stepA -> stepB -> stepC
|
||||
stepC -> stepA: "Loop"
|
||||
```
|
||||
|
||||
### Parallel Processing
|
||||
|
||||
```
|
||||
parallel_tasks: {
|
||||
direction: right
|
||||
task1
|
||||
task2
|
||||
task3
|
||||
}
|
||||
|
||||
start -> parallel_tasks
|
||||
parallel_tasks -> end
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```
|
||||
main_process -> error_handler: "Exception"
|
||||
error_handler: {
|
||||
style.fill: "#ffcccc"
|
||||
style.stroke: red
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python remove_watermark.py <svg_file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
svg_path = Path(sys.argv[1])
|
||||
|
||||
if not svg_path.exists():
|
||||
print(f"Error: File not found - {svg_path}")
|
||||
sys.exit(1)
|
||||
|
||||
content = svg_path.read_text(encoding='utf-8')
|
||||
new_content = content.replace('UNLICENSED COPY', '')
|
||||
|
||||
if content == new_content:
|
||||
print("'UNLICENSED COPY' text not found")
|
||||
return
|
||||
|
||||
svg_path.write_text(new_content, encoding='utf-8')
|
||||
print(f"Watermark removed: {svg_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "obsidian-vault-linker",
|
||||
"installedVersion": "1.0.4",
|
||||
"installedAt": 1779235805616,
|
||||
"fingerprint": "390ca7b1b40e565ffff46e65d35ae350bcb8448765246b9a37fc9a26f23b8cd1"
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
---
|
||||
name: obsidian-vault-linker
|
||||
description: Discover and write typed relationships between Obsidian vault notes. Uses plain Markdown and YAML — no plugins required. Works with any AI agent that has file access.
|
||||
homepage: https://github.com/penfieldlabs/obsidian-wikilink-types
|
||||
metadata: {"clawhub":{"emoji":"🔗","tags":["obsidian","knowledge-graph","pkm","relationships", "penfield", "wikilinks"]}}
|
||||
---
|
||||
|
||||
# Obsidian Vault Linker
|
||||
|
||||
Discover meaningful relationships between notes in an Obsidian vault. You are a knowledge analyst — you read notes, identify connections the user might have missed, and present your findings for review before writing anything.
|
||||
|
||||
Relationships are stored as plain Markdown and YAML frontmatter. No plugins, databases, or external tools are required — just files on disk.
|
||||
|
||||
## How You Work
|
||||
|
||||
You are a thinking partner, not an autopilot. The user directs you:
|
||||
|
||||
- **Targeted investigation** — "I think my notes on X might relate to Y, dig into it"
|
||||
- **Focused curation** — "Find everything about ABC and show me what connects"
|
||||
- **Open exploration** — "Look at this folder and tell me what patterns you see"
|
||||
|
||||
By default: **read first, report findings, write only on approval.** If the user explicitly grants autonomous mode (e.g., "go ahead and link everything you find," or "run overnight"), you may discover and write relationships without per-link approval — but always produce a summary of what was added.
|
||||
|
||||
## Reading the Vault
|
||||
|
||||
An Obsidian vault is a folder of Markdown files. Notes may have YAML frontmatter and `[[wikilinks]]` to other notes.
|
||||
|
||||
**If Obsidian CLI is available** (check with `which obsidian` or `obsidian version`), prefer it for discovery:
|
||||
|
||||
```bash
|
||||
obsidian search query="topic" # Find notes about a topic
|
||||
obsidian orphans # Notes with no links (good candidates)
|
||||
obsidian backlinks file="Note.md" # What already links to this note
|
||||
obsidian links file="Note.md" # What this note links to
|
||||
obsidian tags # All tags in the vault
|
||||
obsidian files format=json # Full file listing
|
||||
```
|
||||
|
||||
Obsidian CLI requires Obsidian v1.12+ with CLI enabled in Settings. If not available, read files directly from disk. Look at folder structure, filenames, frontmatter tags, and content.
|
||||
|
||||
## Relationship Types
|
||||
|
||||
These 24 relationship types are the standard set used by the [Penfield](https://penfield.app) memory system. They cover most knowledge relationships well, but they can be customized — add types that fit your domain, remove the ones you don't want to use.
|
||||
|
||||
Pick the most specific type that applies. If none fit precisely, don't force it — leave it unlinked.
|
||||
|
||||
**Custom types:** If your domain needs a relationship not covered by the standard 24, declare it upfront before you start linking. Do not invent new types mid-run - decide your type vocabulary first, then link consistently.
|
||||
|
||||
### Knowledge Evolution
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `supersedes` | This replaces an outdated understanding | Same subject, different conclusion, later date |
|
||||
| `updates` | This adds to or refines existing knowledge | Same subject, additional detail |
|
||||
| `evolution_of` | This shows how thinking changed over time | Same subject, shifted framing |
|
||||
|
||||
### Evidence
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `supports` | This provides evidence for another claim | Shared conclusion from different angle |
|
||||
| `contradicts` | This challenges another claim | Opposite conclusion on same subject |
|
||||
| `disputes` | This questions the reasoning of another | Methodological or logical disagreement |
|
||||
|
||||
### Hierarchy
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `parent_of` | This is a broader topic containing the other | General → specific |
|
||||
| `child_of` | This is a subtopic of the other | Specific → general |
|
||||
| `sibling_of` | These are peers under the same parent topic | Same level, same domain |
|
||||
| `composed_of` | This is made up of the other | Whole → part |
|
||||
| `part_of` | This is a component of the other | Part → whole |
|
||||
|
||||
### Causation
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `causes` | This leads to or produces the other | Action → consequence |
|
||||
| `influenced_by` | This was shaped by the other | Consequence ← influence |
|
||||
| `prerequisite_for` | This must come before the other | Dependency ordering |
|
||||
|
||||
### Implementation
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `implements` | This is a concrete realization of the other | Concept → code/action |
|
||||
| `documents` | This describes or records the other | Description → subject |
|
||||
| `tests` | This validates or verifies the other | Test → claim |
|
||||
| `example_of` | This is an instance of a general pattern | Instance → pattern |
|
||||
|
||||
### Conversation
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `responds_to` | This is a reply or reaction to the other | Dialogue thread |
|
||||
| `references` | This cites or points to the other | Attribution |
|
||||
| `inspired_by` | This was sparked by the other | Creative lineage |
|
||||
|
||||
### Sequence
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `follows` | This comes after the other in a process | Step N+1 → Step N |
|
||||
| `precedes` | This comes before the other in a process | Step N → Step N+1 |
|
||||
|
||||
### Dependencies
|
||||
| Type | Meaning | Signal |
|
||||
|------|---------|--------|
|
||||
| `depends_on` | This requires the other to function | Runtime dependency |
|
||||
|
||||
## What Makes a Good Discovery
|
||||
|
||||
**High value (prioritize these):**
|
||||
|
||||
- Contradictions — two notes that reach opposite conclusions about the same thing.
|
||||
- Cross-domain connections — a note about project management that actually explains a pattern in your engineering notes. Different folders, different tags, shared insight.
|
||||
- Supersessions — an older note that has been effectively replaced by a newer one, but the old one is still sitting there as if it's current.
|
||||
- Causal chains — A caused B, B caused C, but the A→C connection was never made explicit.
|
||||
|
||||
**Low value (be cautious):**
|
||||
|
||||
- Two notes about the same topic that say similar things. The user already knows these are related. Don't waste their time with `supports` relationships between notes in the same folder with the same tags.
|
||||
- Vague thematic similarity. "Both mention technology" is not a relationship.
|
||||
- Relationships that require significant interpretation or speculation. If you have to stretch, skip it.
|
||||
|
||||
## Analysis Process
|
||||
|
||||
### Step 1: Understand the Request
|
||||
|
||||
The user will tell you what to look at. Clarify if needed:
|
||||
- Which folders, tags, or topics?
|
||||
- Looking for something specific, or open exploration?
|
||||
- How many notes are involved?
|
||||
|
||||
### Hub-and-Spoke Vaults
|
||||
|
||||
Many knowledge bases have **hub notes** (concepts, topics, MOCs) that act as central nodes, with **spoke notes** (articles, chapters, meeting notes, transcripts) linking into them. If the vault has this architecture:
|
||||
|
||||
1. Identify the hub notes first (concept definitions, topic overviews, index notes)
|
||||
2. Link spoke notes into hubs before looking for lateral spoke-to-spoke connections
|
||||
3. Hub-to-hub relationships (e.g., one concept is `prerequisite_for` another) are often the highest-value links in the vault
|
||||
|
||||
If the vault doesn't have hub notes but should, suggest creating them — but don't create them without approval.
|
||||
|
||||
### Step 2: Read and Summarize
|
||||
|
||||
Read the relevant notes. For each, extract:
|
||||
- Core claim or subject
|
||||
- Key entities (people, projects, technologies)
|
||||
- Date context
|
||||
- Existing tags and links
|
||||
|
||||
For large sets (50+ notes), triage first: read the frontmatter and first 20 lines of each note to extract title, tags, dates, and core subject. Use this to identify candidate pairs for deep reading. Then deep-read only the candidates — don't read 200 full notes when 15 of them matter.
|
||||
|
||||
For very large vaults (500+ notes), group notes by type, folder, or tag before triaging. Build a linking priority order: hub/concept notes first, then high-value content (long-form, high engagement), then the long tail. Process in batches — don't try to hold the entire vault in context at once.
|
||||
|
||||
### Step 3: Identify Candidates
|
||||
|
||||
Look for pairs where:
|
||||
- Same subject, different conclusions (contradiction/supersession)
|
||||
- Same entities mentioned in different contexts (cross-domain)
|
||||
- Causal language ("because", "led to", "resulted in") pointing to another note's subject
|
||||
- Temporal progression on the same topic (evolution)
|
||||
- One note is a specific instance of another note's general pattern
|
||||
|
||||
**Be strict.** Only flag pairs where you can point to specific evidence in both notes. "These feel related" is not enough.
|
||||
|
||||
### Step 4: Present Findings
|
||||
|
||||
Report your findings as a structured list. For each discovered relationship:
|
||||
|
||||
```
|
||||
**[relationship_type]**: Note A → Note B
|
||||
Evidence: [specific text from Note A] connects to [specific text from Note B]
|
||||
Confidence: high/medium
|
||||
Why it matters: [one sentence]
|
||||
```
|
||||
|
||||
Confidence levels:
|
||||
|
||||
- **High** — Specific text in both notes directly supports the relationship. You can quote the evidence.
|
||||
- **Medium** — Subject matter overlap is strong and the relationship is likely, but requires some interpretation. You're connecting dots, not quoting direct evidence.
|
||||
|
||||
Only include medium and high confidence findings. If you'd rate something as low confidence, skip it. If you found nothing meaningful, say so — that's a valid result.
|
||||
|
||||
### Step 5: Write on Approval
|
||||
|
||||
After the user reviews and approves, write relationships in the format below. Only write what was approved. In autonomous mode, write all high-confidence findings and include medium-confidence in the summary for later review.
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
After writing relationships, re-read each modified note to confirm:
|
||||
- Frontmatter keys and inline `@type` links match (same relationships in both places)
|
||||
- Every wikilink target resolves to an actual file in the vault (no broken links) — unless the calling prompt defers broken-link checking to a separate verify pass (e.g., when running in parallel with other agents whose work may not be committed yet)
|
||||
- Existing content is preserved — nothing was deleted or overwritten
|
||||
- No duplicate relationships were introduced
|
||||
- YAML frontmatter is valid (proper quoting, no syntax errors)
|
||||
- Only declared relationship types were used (no types invented mid-run)
|
||||
|
||||
If anything is wrong, fix it immediately. File edits are the most error-prone step. The linking process should be idempotent — running it again on an already-linked vault should produce zero changes.
|
||||
|
||||
## Writing Format
|
||||
|
||||
Relationships are stored as plain Markdown and YAML. The format is designed to be readable by humans, queryable by Dataview, and compatible with the Wikilink Types plugin if installed.
|
||||
|
||||
### On the source note (where the relationship originates):
|
||||
|
||||
**YAML frontmatter** — add the relationship type as a key with wikilink targets:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: My Note
|
||||
supports:
|
||||
- "[[Other Note]]"
|
||||
contradicts:
|
||||
- "[[Another Note]]"
|
||||
---
|
||||
```
|
||||
|
||||
Each relationship type becomes a YAML key. The value is an array of wikilinks, each quoted with double quotes. Multiple targets under the same type are separate array entries.
|
||||
|
||||
**Inline link** — in the note body, use `@type` inside a wikilink alias:
|
||||
|
||||
```markdown
|
||||
## Relationships
|
||||
|
||||
- → [[Other Note|Other Note @supports]]
|
||||
- → [[Another Note|Another Note @contradicts]]
|
||||
```
|
||||
|
||||
The `@type` must be preceded by a space or appear at the start of the alias (right after `|`). The wikilink target (before `|`) is the note filename. The alias (after `|`) is the display text containing the `@type` tag.
|
||||
|
||||
Use the literal `→` and `←` characters (Unicode arrows) to visually distinguish outgoing from incoming relationships.
|
||||
|
||||
### Direction matters
|
||||
|
||||
Only write relationships on the **source** note — the note that does the action:
|
||||
|
||||
- "Note A supports Note B" → write `@supports` on Note A, pointing to Note B
|
||||
- "Note A is supported by Note B" → write `@supports` on Note B, pointing to Note A
|
||||
|
||||
Do NOT write `@type` on the receiving end. The `@type` syntax means "this note has this relationship to the target."
|
||||
|
||||
### Incoming relationships (informational only)
|
||||
|
||||
If you want to note an incoming relationship for reference, use bold type without `@`:
|
||||
|
||||
```markdown
|
||||
- ← **supports** [[Source Note]]
|
||||
```
|
||||
|
||||
This is informational only. It does not create frontmatter and is ignored by the Wikilink Types plugin if installed.
|
||||
|
||||
### Rules
|
||||
|
||||
1. Always write both frontmatter AND inline `@type` links — they must match
|
||||
2. If the note already has a `## Relationships` section, append to it. If the note has typed links woven into other sections (e.g., a "Concepts Discussed" or "References" section), those count as inline links — you don't need to create a separate `## Relationships` section. Frontmatter must still match.
|
||||
3. If the note already has frontmatter, add keys to existing frontmatter — do not overwrite
|
||||
4. Do not duplicate existing relationships
|
||||
5. Preserve all existing content — you are adding, not replacing
|
||||
6. Only use relationship types from the standard 24 or your declared custom types — do not invent new types during a linking pass
|
||||
|
||||
## Plugin (Optional — For Human Editing)
|
||||
|
||||
The [Wikilink Types](https://github.com/penfieldlabs/obsidian-wikilink-types) plugin enhances the human editing experience:
|
||||
- Autocomplete for `@type` when editing wikilinks
|
||||
- Automatic sync between inline `@type` links and YAML frontmatter
|
||||
- Visual relationship rendering in the graph view
|
||||
- Compatibility with Dataview, Graph Link Types, and Breadcrumbs
|
||||
|
||||
**The plugin is NOT required.** Without it:
|
||||
- YAML frontmatter works with Dataview queries
|
||||
- `@type` text is visible in notes (just not styled)
|
||||
- All relationships are fully preserved and functional
|
||||
- Any AI agent can read and write the format with no plugin installed
|
||||
|
||||
The plugin should be installed if human users will be hand-editing, reviewing or authoring relationships. Skip it if relationships are managed entirely by AI.
|
||||
|
||||
## Examples
|
||||
|
||||
### Targeted Investigation
|
||||
|
||||
**User:** "I think my notes on microservices might contradict some of my earlier notes about monolith architecture. Can you check?"
|
||||
|
||||
**You:**
|
||||
1. Search for notes about microservices and monolith architecture
|
||||
2. Read them, compare claims
|
||||
3. Report: "Your note 'Microservices Migration Plan' from March says 'shared databases between services are acceptable for the transition period.' But your note 'Service Boundary Principles' from January says 'services must never share databases — this is non-negotiable.' These contradict each other on database sharing."
|
||||
4. Wait for approval, then write the `contradicts` relationship
|
||||
|
||||
### Focused Curation
|
||||
|
||||
**User:** "Look at everything tagged #project-alpha and map out the relationships."
|
||||
|
||||
**You:**
|
||||
1. Find all notes with #project-alpha
|
||||
2. Read them, identify the narrative arc
|
||||
3. Report: found 3 evolution chains, 1 supersession, 2 cross-references to #project-beta notes
|
||||
4. Present each with evidence
|
||||
5. Write approved relationships
|
||||
|
||||
### Open Exploration
|
||||
|
||||
**User:** "I have 200 notes from this year in my Research folder. What patterns do you see?"
|
||||
|
||||
**You:**
|
||||
1. Triage: read frontmatter and first 20 lines of all 200 notes
|
||||
2. Identify candidate pairs from summaries
|
||||
3. Deep-read candidates, confirm relationships
|
||||
4. Report the most interesting 10-15 findings
|
||||
5. Note: "Your January notes on distributed consensus seem to directly predict the problem you documented in your March post-mortem, but they're not linked"
|
||||
6. Write approved relationships
|
||||
|
||||
## Limitations
|
||||
|
||||
- You can only find relationships in notes you can read. If the vault is very large, the user should direct you to relevant areas.
|
||||
- Your judgment is probabilistic. Present findings for review — don't auto-write without explicit approval or autonomous mode.
|
||||
- Some relationships require domain expertise you may not have. When uncertain, say so and let the user decide.
|
||||
- Relationship typing is subjective. `supports` vs `references` vs `inspired_by` can be a judgment call. When in doubt, pick the more conservative type or ask.
|
||||
|
||||
## Links
|
||||
|
||||
- **Plugin**: [Wikilink Types](https://github.com/penfieldlabs/obsidian-wikilink-types) — optional Obsidian plugin for human authoring and editing with autocomplete
|
||||
- **Penfield**: [penfield.app](https://penfield.app) — cloud memory system utilizing the same 24 relationship types
|
||||
- **Penfield OpenClaw Plugin**: [penfieldlabs/openclaw-penfield](https://github.com/penfieldlabs/openclaw-penfield)
|
||||
- **Pairs well with**: The [Obsidian skill by steipete](https://clawhub.ai/steipete/obsidian) (vault search, create, move, rename operations)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7e6rd0nc2b5r8j09bbcrz3m5800t3m",
|
||||
"slug": "obsidian-vault-linker",
|
||||
"version": "1.0.4",
|
||||
"publishedAt": 1776188187573
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "taskflow",
|
||||
"installedVersion": "1.1.1",
|
||||
"installedAt": 1779235737501,
|
||||
"fingerprint": "c59f89eeb1ba41f2d984cdcf28fef14c99d764d4078d756ba33525dd9533cb1a"
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# TaskFlow
|
||||
|
||||
Structured project and task management for OpenClaw agents — markdown-first, SQLite-backed, zero dependencies.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Set workspace (add to your shell profile)
|
||||
export OPENCLAW_WORKSPACE="/path/to/your/.openclaw/workspace"
|
||||
|
||||
# 2. Link the CLI
|
||||
ln -sf "$(pwd)/scripts/taskflow-cli.mjs" /opt/homebrew/bin/taskflow # macOS
|
||||
# ln -sf "$(pwd)/scripts/taskflow-cli.mjs" /usr/local/bin/taskflow # Linux
|
||||
|
||||
# 3. Run setup
|
||||
taskflow setup
|
||||
```
|
||||
|
||||
The setup wizard creates your workspace structure, walks you through your first project, initializes the SQLite database, syncs your markdown files, and optionally installs a macOS LaunchAgent for automatic 60-second sync.
|
||||
|
||||
**Non-interactive (scripted installs):**
|
||||
|
||||
```bash
|
||||
taskflow setup --name "My Project" --desc "What it does"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Markdown-first** — `PROJECTS.md` and `tasks/<slug>-tasks.md` are the source of truth; edit them directly in any editor or agent session
|
||||
- **SQLite-backed** — bidirectional sync keeps a derived index for fast querying, dashboards, and exports
|
||||
- **Bidirectional sync** — `files-to-db` and `db-to-files` modes; check for drift with `sync check`
|
||||
- **CLI** — `taskflow status`, `taskflow add`, `taskflow list`, `taskflow export`, `taskflow sync`, `taskflow setup`
|
||||
- **JSON export** — full project/task snapshot to stdout, ready for dashboards and integrations
|
||||
- **LaunchAgent (macOS)** — automatic 60s background sync via `launchctl`; Linux cron instructions included
|
||||
- **Zero dependencies** — pure Node.js, uses the built-in `node:sqlite` module (no npm install)
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
|
||||

|
||||
|
||||
```
|
||||
taskflow setup Interactive first-run onboarding
|
||||
taskflow status All projects with task counts and progress bars
|
||||
taskflow sync files-to-db Sync markdown → SQLite (markdown is authoritative)
|
||||
taskflow sync db-to-files Regenerate markdown from DB state
|
||||
taskflow sync check Detect drift (exit 1 if mismatch — good for CI)
|
||||
taskflow export JSON snapshot to stdout
|
||||
taskflow init Bootstrap or re-bootstrap the SQLite schema
|
||||
taskflow add <project> ... Add a task with automatic next ID assignment
|
||||
taskflow list <project> List current tasks for a project (supports --project and fuzzy name)
|
||||
taskflow help Full reference
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js 22.5+** (uses `node:sqlite` / `DatabaseSync`)
|
||||
- macOS or Linux
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
<workspace>/
|
||||
├── PROJECTS.md # Project registry (one ## block per project)
|
||||
├── tasks/<slug>-tasks.md # Task list per project (five fixed sections)
|
||||
├── plans/<slug>-plan.md # Optional: architecture / design docs
|
||||
└── memory/
|
||||
└── taskflow.sqlite # Derived index — never edit directly
|
||||
```
|
||||
|
||||
Tasks live in five fixed sections: `## In Progress`, `## Pending Validation`, `## Backlog`, `## Blocked`, `## Done`. A task line looks like:
|
||||
|
||||
```
|
||||
- [ ] (task:myproject-007) [P1] [codex] Implement search endpoint
|
||||
```
|
||||
|
||||
The sync engine reads section headers (not checkboxes) to determine task status. Move a line between sections to change status; the LaunchAgent or a manual sync picks it up within 60 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Agent Integration
|
||||
|
||||
TaskFlow is designed to be used by OpenClaw agents as a skill. The agent reads `PROJECTS.md` to discover projects, reads `tasks/<slug>-tasks.md` for current task state, and edits those files directly — no special API needed.
|
||||
|
||||
See [SKILL.md](SKILL.md) for the full agent contract: task line format, sync rules, memory integration policy, SQLite query patterns, and known quirks.
|
||||
|
||||
---
|
||||
|
||||
## Apple Notes Sync (macOS)
|
||||
|
||||
`taskflow notes` pushes a live project status summary to Apple Notes -- share it with your team or just keep it on your phone.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Use at your own risk. We are not liable for any issues you encounter. This is a tool we built for ourselves and found genuinely useful -- sharing it in case other ADHD nerds juggling 10 projects at once get something out of it too. PRs and feedback welcome.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
Built by [Aux](https://github.com/auxclawdbot) (an [OpenClaw](https://openclaw.ai) agent) and [@sm0ls](https://github.com/sm0ls).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,695 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn70t1yqncqxf8ajcssx37gc2x81ajqt",
|
||||
"slug": "taskflow",
|
||||
"version": "1.1.1",
|
||||
"publishedAt": 1771801350988
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# Building a Dashboard on TaskFlow (taskflow-016)
|
||||
|
||||
TaskFlow is **UI-agnostic**. It ships data; you pick the surface.
|
||||
|
||||
The export script (`taskflow export`) writes a single, self-contained JSON document
|
||||
to stdout. That's the only contract. Everything else is up to you.
|
||||
|
||||
---
|
||||
|
||||
## The Core Loop
|
||||
|
||||
```
|
||||
TaskFlow DB → export JSON → your UI
|
||||
```
|
||||
|
||||
Run `taskflow export > /path/to/projects.json` on any schedule and your dashboard
|
||||
gets fresh data without any server, polling API, or WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## How We Built Ours
|
||||
|
||||
We run a **Vite + React** single-page app that reads a static JSON file.
|
||||
The JSON is refreshed every few minutes by a macOS **LaunchAgent watchdog** that
|
||||
calls `taskflow export` and writes the result to the app's public directory.
|
||||
|
||||
No backend. No database exposed over HTTP. No caching layer.
|
||||
The browser just `fetch()`es a local file — or, in prod, an S3/CDN path.
|
||||
|
||||
The LaunchAgent entry looks like this (conceptually):
|
||||
|
||||
```
|
||||
ProgramArguments:
|
||||
/usr/bin/env node
|
||||
/path/to/taskflow/bin/taskflow export
|
||||
> /path/to/vite-app/public/data/projects.json
|
||||
StartInterval: 300 # every 5 minutes
|
||||
```
|
||||
|
||||
On the React side, a `useProjects` hook fetches `data/projects.json` at mount
|
||||
(and optionally on an interval if you want live-ish updates in dev). The rest is
|
||||
just components: progress rings, kanban columns, transition timelines.
|
||||
|
||||
---
|
||||
|
||||
## What the JSON Gives You
|
||||
|
||||
- **Every project** with its name, status, and description
|
||||
- **Task counts** in every bucket (`backlog`, `in_progress`, `pending_validation`,
|
||||
`blocked`, `done`) — ready to drive a status bar, ring, or number badge
|
||||
- **Progress percentage** pre-computed per project
|
||||
- **Last 20 transitions** across all projects — ready for an activity feed
|
||||
|
||||
See `export-schema.json` in this directory for the full JSON Schema.
|
||||
|
||||
---
|
||||
|
||||
## Why This Works Well
|
||||
|
||||
- **Static export = zero attack surface.** No live DB connection in the UI.
|
||||
- **Works offline.** The UI renders whatever is in the file, no network required.
|
||||
- **Framework-neutral.** Svelte, Vue, plain HTML + Chart.js — the JSON doesn't care.
|
||||
- **Cheap to refresh.** A 5-minute cron is fine. Sub-second latency is never needed
|
||||
for a task dashboard.
|
||||
- **Composable.** Pipe the JSON into `jq`, into a Notion integration, into a Slack
|
||||
bot — the export is just text.
|
||||
|
||||
---
|
||||
|
||||
## Other Ideas
|
||||
|
||||
| Surface | Approach |
|
||||
|---|---|
|
||||
| Terminal widget | `taskflow status` (built in) |
|
||||
| Apple Notes | `scripts/apple-notes-export.sh` (macOS) |
|
||||
| Raycast | Script Command reading the export JSON |
|
||||
| Obsidian | Dataview plugin querying the same SQLite directly |
|
||||
| Linear-style board | React + export JSON, one column per status |
|
||||
| CI badge | `taskflow sync check` exits 1 on drift — wire it to a status check |
|
||||
|
||||
---
|
||||
|
||||
The data is already there. Build whatever feels right.
|
||||
|
||||
## Validation Workflow Coupling (Projects Panel)
|
||||
|
||||
In our dashboard, validation actions are wired directly into TaskFlow task state.
|
||||
`decision-store.ts` updates `taskflow.sqlite` (`tasks_v2`) when reviewers act in the
|
||||
Projects panel:
|
||||
|
||||
- Confirm: `pending_validation` -> `done`
|
||||
- Reject: `pending_validation` -> `in_progress` (with reviewer feedback note)
|
||||
|
||||
This is an integration pattern for dashboards, not a required part of TaskFlow core.
|
||||
TaskFlow remains UI-agnostic and does not assume any specific frontend store.
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://clawhub.dev/schemas/taskflow/export-projects-overview/v1",
|
||||
"title": "TaskFlow Export — Projects Overview",
|
||||
"description": "Output schema for scripts/export-projects-overview.mjs (taskflow-009). Emitted to stdout as a single JSON object.",
|
||||
"type": "object",
|
||||
"required": ["exported_at", "projects", "recent_transitions"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
"exported_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp when the export was generated (UTC)."
|
||||
},
|
||||
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"description": "All projects in the TaskFlow DB, ordered by slug.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "description", "status", "task_counts", "progress_pct"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Project slug — primary key in the `projects` table.",
|
||||
"examples": ["dashboard", "trading", "smart-home"]
|
||||
},
|
||||
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable project name.",
|
||||
"examples": ["Dashboard", "Trading System"]
|
||||
},
|
||||
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "One-liner description (may be empty string)."
|
||||
},
|
||||
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "paused", "done"],
|
||||
"description": "Project lifecycle status."
|
||||
},
|
||||
|
||||
"task_counts": {
|
||||
"type": "object",
|
||||
"description": "Count of tasks in each status bucket for this project.",
|
||||
"required": ["in_progress", "pending_validation", "backlog", "blocked", "done"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"in_progress": { "type": "integer", "minimum": 0 },
|
||||
"pending_validation": { "type": "integer", "minimum": 0 },
|
||||
"backlog": { "type": "integer", "minimum": 0 },
|
||||
"blocked": { "type": "integer", "minimum": 0 },
|
||||
"done": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
|
||||
"progress_pct": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "Completion percentage: done / total * 100, rounded to 2 decimal places. 0 when there are no tasks."
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"recent_transitions": {
|
||||
"type": "array",
|
||||
"description": "Last 20 task status transitions across all projects, ordered oldest-first within the window. Sourced from task_transitions_v2.",
|
||||
"maxItems": 20,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["task_id", "to_status", "at"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "Task identifier in <slug>-NNN format.",
|
||||
"examples": ["dashboard-025", "trading-003"]
|
||||
},
|
||||
|
||||
"from_status": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["backlog", "in_progress", "pending_validation", "blocked", "done", null],
|
||||
"description": "Previous status. Null for task creation events."
|
||||
},
|
||||
|
||||
"to_status": {
|
||||
"type": "string",
|
||||
"enum": ["backlog", "in_progress", "pending_validation", "blocked", "done"],
|
||||
"description": "Status the task transitioned into."
|
||||
},
|
||||
|
||||
"reason": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Optional free-text reason recorded at transition time."
|
||||
},
|
||||
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of the transition (UTC)."
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@taskflow/core",
|
||||
"version": "0.1.0",
|
||||
"description": "TaskFlow — ClawHub skill for structured project/task management with markdown-first authoring and SQLite-backed querying.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.5"
|
||||
},
|
||||
"scripts": {
|
||||
"init-db": "node scripts/init-db.mjs",
|
||||
"sync": "node scripts/task-sync.mjs files-to-db",
|
||||
"export": "node scripts/export-projects-overview.mjs",
|
||||
"status": "node bin/taskflow status"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
-- TaskFlow SQLite Schema
|
||||
-- All statements use IF NOT EXISTS so this file is safe to re-run (idempotent).
|
||||
-- Generated: 2026-02-20
|
||||
-- Node requirement: >=22.5 (node:sqlite / DatabaseSync)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- PRAGMA tweaks (run once at connection time, not schema objects)
|
||||
-- ---------------------------------------------------------------------------
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- projects
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY, -- slug, e.g. "dashboard"
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'paused', 'done')),
|
||||
source_file TEXT NOT NULL DEFAULT 'PROJECTS.md',
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- tasks_v2
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS tasks_v2 (
|
||||
id TEXT PRIMARY KEY, -- <slug>-NNN, e.g. "dashboard-001"
|
||||
project_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL
|
||||
CHECK (status IN (
|
||||
'backlog',
|
||||
'in_progress',
|
||||
'pending_validation',
|
||||
'done',
|
||||
'blocked'
|
||||
)),
|
||||
priority TEXT NOT NULL DEFAULT 'P2'
|
||||
CHECK (priority IN ('P0', 'P1', 'P2', 'P3', 'P9')),
|
||||
owner_model TEXT, -- agent/model tag (optional)
|
||||
notes TEXT,
|
||||
source_file TEXT NOT NULL,
|
||||
legacy_key TEXT, -- for migrations from older systems
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_v2_project_status
|
||||
ON tasks_v2 (project_id, status, priority);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- task_transitions_v2 (audit log — every status change is recorded here)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS task_transitions_v2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
from_status TEXT, -- NULL on first insertion
|
||||
to_status TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
actor TEXT, -- 'sync', 'agent', 'human', etc.
|
||||
actor_model TEXT, -- which model made the change
|
||||
at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
FOREIGN KEY (task_id) REFERENCES tasks_v2(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transitions_task_id
|
||||
ON task_transitions_v2 (task_id, at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transitions_at
|
||||
ON task_transitions_v2 (at DESC);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- sync_state (singleton row — id must be 1)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sync_state (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
files_hash TEXT, -- hash of all task file contents
|
||||
db_hash TEXT, -- hash of serialized DB task state
|
||||
last_sync_at TEXT,
|
||||
lock_owner TEXT, -- process/session that holds the lock
|
||||
lock_until TEXT, -- TTL-based lock expiry (ISO8601, 60s TTL)
|
||||
last_result TEXT -- 'ok' | 'error: ...'
|
||||
);
|
||||
|
||||
-- Ensure the singleton row exists (safe to run multiple times).
|
||||
INSERT OR IGNORE INTO sync_state (id) VALUES (1);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- legacy_key_map (migration bridge — maps old task keys to new tasks_v2 ids)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS legacy_key_map (
|
||||
legacy_key TEXT PRIMARY KEY,
|
||||
new_id TEXT NOT NULL,
|
||||
FOREIGN KEY (new_id) REFERENCES tasks_v2(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_legacy_key_map_new_id
|
||||
ON legacy_key_map (new_id);
|
||||
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* apple-notes-export.mjs (taskflow-012)
|
||||
*
|
||||
* Generates an HTML project-status summary from TaskFlow markdown files and
|
||||
* writes it to an Apple Note via osascript.
|
||||
*
|
||||
* Config: $OPENCLAW_WORKSPACE/taskflow.config.json
|
||||
* {
|
||||
* "appleNotesId": "x-coredata://...", // persisted after first run
|
||||
* "appleNotesFolder": "Notes", // Notes folder name
|
||||
* "appleNotesTitle": "TaskFlow - Project Status"
|
||||
* }
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/apple-notes-export.mjs
|
||||
* taskflow notes
|
||||
*
|
||||
* macOS only — exits gracefully on other platforms.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
||||
import { execSync } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
|
||||
// ── Platform guard ─────────────────────────────────────────────────────────
|
||||
if (process.platform !== 'darwin') {
|
||||
console.log('[apple-notes-export] Skipping: Apple Notes sync is macOS only.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ── Paths ──────────────────────────────────────────────────────────────────
|
||||
const workspace = process.env.OPENCLAW_WORKSPACE || process.cwd()
|
||||
const configPath = path.join(workspace, 'taskflow.config.json')
|
||||
const tasksDir = path.join(workspace, 'tasks')
|
||||
const projectsFile = path.join(workspace, 'PROJECTS.md')
|
||||
|
||||
// ── Config helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Read taskflow.config.json (returns {} if missing or unparseable).
|
||||
*/
|
||||
function readConfig() {
|
||||
if (!existsSync(configPath)) return {}
|
||||
try {
|
||||
return JSON.parse(readFileSync(configPath, 'utf8'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `updates` into taskflow.config.json (non-destructive patch).
|
||||
*/
|
||||
function writeConfig(updates) {
|
||||
const current = readConfig()
|
||||
const merged = { ...current, ...updates }
|
||||
writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8')
|
||||
}
|
||||
|
||||
// ── Parse PROJECTS.md ──────────────────────────────────────────────────────
|
||||
|
||||
function parseProjects() {
|
||||
if (!existsSync(projectsFile)) return {}
|
||||
const lines = readFileSync(projectsFile, 'utf8').split('\n')
|
||||
const projects = {}
|
||||
let currentSlug = null
|
||||
|
||||
for (const line of lines) {
|
||||
const h2 = line.match(/^## (.+)/)
|
||||
if (h2) {
|
||||
currentSlug = h2[1].trim()
|
||||
projects[currentSlug] = { name: currentSlug, desc: '' }
|
||||
continue
|
||||
}
|
||||
if (!currentSlug) continue
|
||||
const nameMatch = line.match(/^- Name: (.+)/)
|
||||
if (nameMatch) { projects[currentSlug].name = nameMatch[1].trim(); continue }
|
||||
const descMatch = line.match(/^- Description: (.+)/)
|
||||
if (descMatch) { projects[currentSlug].desc = descMatch[1].trim() }
|
||||
}
|
||||
return projects
|
||||
}
|
||||
|
||||
// ── Parse task files ───────────────────────────────────────────────────────
|
||||
|
||||
function parseTasks(projects) {
|
||||
const result = { in_progress: [], pending: [], backlog: [], done: [], blocked: [] }
|
||||
|
||||
if (!existsSync(tasksDir)) return result
|
||||
|
||||
const files = readdirSync(tasksDir)
|
||||
.filter(f => f.endsWith('-tasks.md'))
|
||||
.sort()
|
||||
|
||||
for (const file of files) {
|
||||
const slug = file.replace('-tasks.md', '')
|
||||
const projName = projects[slug]?.name ?? slug
|
||||
const lines = readFileSync(path.join(tasksDir, file), 'utf8').split('\n')
|
||||
|
||||
let section = null
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('## ')) {
|
||||
const h = trimmed.slice(3).toLowerCase()
|
||||
if (h.includes('in progress')) section = 'in_progress'
|
||||
else if (h.includes('pending')) section = 'pending'
|
||||
else if (h.includes('backlog')) section = 'backlog'
|
||||
else if (h.includes('done')) section = 'done'
|
||||
else if (h.includes('blocked')) section = 'blocked'
|
||||
else section = null
|
||||
continue
|
||||
}
|
||||
if (!trimmed.startsWith('- [') || section === null) continue
|
||||
|
||||
// Strip checkbox, task ID, priority/owner tags
|
||||
let text = trimmed
|
||||
.replace(/^- \[.\]\s*/, '')
|
||||
.replace(/\(task:\S+\)\s*/g, '')
|
||||
.replace(/\[P\d\]\s*/g, '')
|
||||
.replace(/\[\S+\]\s*/g, '')
|
||||
.trim()
|
||||
if (!text) continue
|
||||
|
||||
result[section].push(`${projName}: ${text}`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── HTML generation ────────────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function ul(items) {
|
||||
if (!items || items.length === 0) {
|
||||
return '<ul><li><span style="color:#666;">None</span></li></ul>'
|
||||
}
|
||||
return '<ul>' + items.map(i => `<li>${escapeHtml(i)}</li>`).join('') + '</ul>'
|
||||
}
|
||||
|
||||
function generateHtml(tasks, title) {
|
||||
const { in_progress, pending, backlog, done, blocked } = tasks
|
||||
const top = [...in_progress, ...pending].slice(0, 5)
|
||||
const next3 = in_progress.length > 0 ? in_progress.slice(0, 3) : backlog.slice(0, 3)
|
||||
|
||||
const now = new Date()
|
||||
const stamp = now.toLocaleString('en-US', {
|
||||
timeZone: 'America/Chicago',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hour12: true,
|
||||
}) + ' CST'
|
||||
|
||||
return `<div style="font-family:-apple-system,Helvetica,Arial,sans-serif; line-height:1.35;">
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<h2>🎯 Top Priorities</h2>${ul(top)}
|
||||
<h2>🚧 In Progress (${in_progress.length})</h2>${ul(in_progress)}
|
||||
<h2>⏳ Pending Validation (${pending.length})</h2>${ul(pending)}
|
||||
<h2>📥 Backlog (top 12)</h2>${ul(backlog.slice(0, 12))}
|
||||
<h2>✅ Recently Done</h2>${ul(done.slice(0, 10))}
|
||||
<h2>🧱 Blockers</h2>${ul(blocked)}
|
||||
<h2>▶️ Next 3 Actions</h2>${ul(next3)}
|
||||
<p style="color:#888; font-size:0.85em;"><b>Updated:</b> ${escapeHtml(stamp)} · Source: tasks/*.md</p>
|
||||
</div>`
|
||||
}
|
||||
|
||||
// ── AppleScript helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Escape a string for embedding inside an AppleScript string literal.
|
||||
*/
|
||||
function asEscape(str) {
|
||||
// In AppleScript, backslash and double-quote need to be escaped for shell
|
||||
// We'll write HTML to a temp file and read it in AppleScript to avoid quoting issues.
|
||||
return str
|
||||
}
|
||||
|
||||
const TMP_HTML = '/tmp/taskflow-apple-notes.html'
|
||||
|
||||
/**
|
||||
* Check whether a note with the given Core Data ID exists.
|
||||
* Returns true/false.
|
||||
*/
|
||||
function noteExists(noteId) {
|
||||
const script = `
|
||||
tell application "Notes"
|
||||
try
|
||||
set n to note id "${noteId}"
|
||||
return (name of n) as text
|
||||
on error
|
||||
return "NOT_FOUND"
|
||||
end try
|
||||
end tell
|
||||
`
|
||||
try {
|
||||
const result = execSync(`/usr/bin/osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
||||
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim()
|
||||
return result !== 'NOT_FOUND'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing note by Core Data ID.
|
||||
*/
|
||||
function updateNote(noteId, title, html) {
|
||||
writeFileSync(TMP_HTML, html, 'utf8')
|
||||
const script = `
|
||||
set noteBody to (do shell script "cat /tmp/taskflow-apple-notes.html")
|
||||
tell application "Notes"
|
||||
try
|
||||
set targetNote to note id "${noteId}"
|
||||
set name of targetNote to "${title.replace(/"/g, '\\"')}"
|
||||
set body of targetNote to noteBody
|
||||
return (id of targetNote) as text
|
||||
on error errMsg
|
||||
error errMsg
|
||||
end try
|
||||
end tell
|
||||
`
|
||||
const result = execSync(`/usr/bin/osascript << 'OSASCRIPT'\n${script}\nOSASCRIPT`, {
|
||||
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new note in the specified folder.
|
||||
* Returns the new note's Core Data ID.
|
||||
*/
|
||||
function createNote(title, html, folder) {
|
||||
writeFileSync(TMP_HTML, html, 'utf8')
|
||||
const script = `
|
||||
set noteBody to (do shell script "cat /tmp/taskflow-apple-notes.html")
|
||||
tell application "Notes"
|
||||
launch
|
||||
delay 0.5
|
||||
set targetFolder to missing value
|
||||
try
|
||||
set targetFolder to folder "${folder.replace(/"/g, '\\"')}"
|
||||
on error
|
||||
-- folder not found, use default account
|
||||
end try
|
||||
if targetFolder is missing value then
|
||||
set newNote to make new note with properties {name:"${title.replace(/"/g, '\\"')}", body:noteBody}
|
||||
else
|
||||
set newNote to make new note at targetFolder with properties {name:"${title.replace(/"/g, '\\"')}", body:noteBody}
|
||||
end if
|
||||
return (id of newNote) as text
|
||||
end tell
|
||||
`
|
||||
const result = execSync(`/usr/bin/osascript << 'OSASCRIPT'\n${script}\nOSASCRIPT`, {
|
||||
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim()
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const config = readConfig()
|
||||
const folder = config.appleNotesFolder ?? 'Notes'
|
||||
const title = config.appleNotesTitle ?? 'TaskFlow - Project Status'
|
||||
let noteId = config.appleNotesId ?? null
|
||||
|
||||
// Parse content
|
||||
const projects = parseProjects()
|
||||
const tasks = parseTasks(projects)
|
||||
const html = generateHtml(tasks, title)
|
||||
|
||||
const { in_progress, pending, backlog, done, blocked } = tasks
|
||||
console.log(`[apple-notes-export] ${in_progress.length} in-progress, ${backlog.length} backlog, ${done.length} done`)
|
||||
|
||||
// Retry loop (up to 3 attempts)
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
if (noteId && noteExists(noteId)) {
|
||||
// Update existing note
|
||||
console.log(`[apple-notes-export] Updating existing note…`)
|
||||
updateNote(noteId, title, html)
|
||||
console.log(`[apple-notes-export] ✓ Note updated (${noteId})`)
|
||||
break
|
||||
} else {
|
||||
// Create new note (either no ID configured, or note was deleted)
|
||||
if (noteId) {
|
||||
console.log(`[apple-notes-export] Previous note not found — creating new note…`)
|
||||
} else {
|
||||
console.log(`[apple-notes-export] No note configured — creating new note in "${folder}"…`)
|
||||
}
|
||||
const newId = createNote(title, html, folder)
|
||||
if (!newId) throw new Error('osascript returned empty note ID')
|
||||
|
||||
noteId = newId
|
||||
writeConfig({
|
||||
appleNotesId: noteId,
|
||||
appleNotesFolder: folder,
|
||||
appleNotesTitle: title,
|
||||
})
|
||||
console.log(`[apple-notes-export] ✓ Note created and ID saved to taskflow.config.json`)
|
||||
console.log(`[apple-notes-export] ID: ${noteId}`)
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[apple-notes-export] Attempt ${attempt} failed: ${err.message}`)
|
||||
if (attempt < 3) {
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
} else {
|
||||
console.error('[apple-notes-export] All retries exhausted. Note not updated.')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* export-projects-overview.mjs (taskflow-009)
|
||||
*
|
||||
* Reads the TaskFlow SQLite DB and emits a clean JSON summary to stdout.
|
||||
*
|
||||
* Output shape:
|
||||
* {
|
||||
* exported_at: ISO string,
|
||||
* projects: [ { id, name, description, status, task_counts, progress_pct } ],
|
||||
* recent_transitions: [ { task_id, from_status, to_status, reason, at } ]
|
||||
* }
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/export-projects-overview.mjs
|
||||
* node scripts/export-projects-overview.mjs | jq .
|
||||
*/
|
||||
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import path from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
|
||||
// ── DB path resolution ─────────────────────────────────────────────────────
|
||||
const workspace = process.env.OPENCLAW_WORKSPACE || process.cwd()
|
||||
const dbPath = path.join(workspace, 'memory', 'taskflow.sqlite')
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
process.stderr.write(
|
||||
`[taskflow-export] DB not found at: ${dbPath}\n` +
|
||||
` Run 'taskflow init' to bootstrap the database.\n`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new DatabaseSync(dbPath)
|
||||
db.exec('PRAGMA foreign_keys = ON')
|
||||
db.exec('PRAGMA journal_mode = WAL')
|
||||
|
||||
// ── Fetch projects ─────────────────────────────────────────────────────────
|
||||
const projects = db
|
||||
.prepare('SELECT id, name, description, status FROM projects ORDER BY id')
|
||||
.all()
|
||||
|
||||
// ── Task counts per project ────────────────────────────────────────────────
|
||||
const ALL_STATUSES = ['in_progress', 'pending_validation', 'backlog', 'blocked', 'done']
|
||||
|
||||
const countRows = db
|
||||
.prepare(`
|
||||
SELECT project_id, status, COUNT(*) AS cnt
|
||||
FROM tasks_v2
|
||||
GROUP BY project_id, status
|
||||
`)
|
||||
.all()
|
||||
|
||||
// Build lookup: { project_id: { status: count } }
|
||||
const countsByProject = {}
|
||||
for (const row of countRows) {
|
||||
if (!countsByProject[row.project_id]) countsByProject[row.project_id] = {}
|
||||
countsByProject[row.project_id][row.status] = row.cnt
|
||||
}
|
||||
|
||||
// ── Assemble project objects ───────────────────────────────────────────────
|
||||
const projectsOut = projects.map((p) => {
|
||||
const counts = countsByProject[p.id] || {}
|
||||
|
||||
const task_counts = {
|
||||
in_progress: counts.in_progress ?? 0,
|
||||
pending_validation: counts.pending_validation ?? 0,
|
||||
backlog: counts.backlog ?? 0,
|
||||
blocked: counts.blocked ?? 0,
|
||||
done: counts.done ?? 0,
|
||||
}
|
||||
|
||||
const total = Object.values(task_counts).reduce((s, n) => s + n, 0)
|
||||
const progress_pct = total > 0
|
||||
? Math.round((task_counts.done / total) * 10000) / 100 // 2 decimal places
|
||||
: 0
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
status: p.status,
|
||||
task_counts,
|
||||
progress_pct,
|
||||
}
|
||||
})
|
||||
|
||||
// ── Recent transitions (last 20) ───────────────────────────────────────────
|
||||
const recentTransitions = db
|
||||
.prepare(`
|
||||
SELECT task_id, from_status, to_status, reason, at
|
||||
FROM task_transitions_v2
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
`)
|
||||
.all()
|
||||
|
||||
// Reverse so oldest-first within the window (more natural for consumers)
|
||||
recentTransitions.reverse()
|
||||
|
||||
// ── Output ─────────────────────────────────────────────────────────────────
|
||||
const output = {
|
||||
exported_at: new Date().toISOString(),
|
||||
projects: projectsOut,
|
||||
recent_transitions: recentTransitions,
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(output, null, 2) + '\n')
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* init-db.mjs — TaskFlow SQLite bootstrap (idempotent)
|
||||
*
|
||||
* Reads schema/taskflow.sql and executes it against the TaskFlow SQLite DB.
|
||||
* Safe to run multiple times — all DDL uses IF NOT EXISTS.
|
||||
*
|
||||
* DB path resolution (in order):
|
||||
* 1. $OPENCLAW_WORKSPACE/memory/taskflow.sqlite
|
||||
* 2. process.cwd()/memory/taskflow.sqlite
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/init-db.mjs
|
||||
* npm run init-db
|
||||
*
|
||||
* Requires: Node >=22.5 (node:sqlite / DatabaseSync)
|
||||
*/
|
||||
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const schemaPath = resolve(__dirname, '..', 'schema', 'taskflow.sql');
|
||||
|
||||
const workspaceRoot = process.env.OPENCLAW_WORKSPACE
|
||||
? process.env.OPENCLAW_WORKSPACE
|
||||
: process.cwd();
|
||||
|
||||
const memoryDir = join(workspaceRoot, 'memory');
|
||||
const dbPath = join(memoryDir, 'taskflow.sqlite');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function log(msg) {
|
||||
const ts = new Date().toISOString();
|
||||
console.log(`[${ts}] ${msg}`);
|
||||
}
|
||||
|
||||
function err(msg) {
|
||||
const ts = new Date().toISOString();
|
||||
console.error(`[${ts}] ERROR: ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a SQL file into individual statements, stripping comments and blanks.
|
||||
* Handles multi-line statements and PRAGMA directives.
|
||||
*/
|
||||
function splitStatements(sql) {
|
||||
// Remove -- line comments
|
||||
const stripped = sql.replace(/--[^\n]*/g, '');
|
||||
// Split on semicolons, filter blank/whitespace-only entries
|
||||
return stripped
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the statement type for human-readable logging.
|
||||
*/
|
||||
function describeStatement(stmt) {
|
||||
const upper = stmt.trimStart().toUpperCase();
|
||||
if (upper.startsWith('CREATE TABLE IF NOT EXISTS')) {
|
||||
const m = stmt.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/i);
|
||||
return m ? `CREATE TABLE IF NOT EXISTS ${m[1]}` : 'CREATE TABLE (IF NOT EXISTS)';
|
||||
}
|
||||
if (upper.startsWith('CREATE INDEX IF NOT EXISTS')) {
|
||||
const m = stmt.match(/CREATE INDEX IF NOT EXISTS\s+(\S+)/i);
|
||||
return m ? `CREATE INDEX IF NOT EXISTS ${m[1]}` : 'CREATE INDEX (IF NOT EXISTS)';
|
||||
}
|
||||
if (upper.startsWith('CREATE')) {
|
||||
return stmt.trimStart().split(/\s+/).slice(0, 4).join(' ');
|
||||
}
|
||||
if (upper.startsWith('INSERT OR IGNORE')) {
|
||||
const m = stmt.match(/INTO\s+(\S+)/i);
|
||||
return m ? `INSERT OR IGNORE INTO ${m[1]}` : 'INSERT OR IGNORE';
|
||||
}
|
||||
if (upper.startsWith('PRAGMA')) {
|
||||
return stmt.trimStart().split('\n')[0].trim();
|
||||
}
|
||||
return stmt.trimStart().split(/\s+/).slice(0, 3).join(' ');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
log('TaskFlow init-db starting');
|
||||
log(`Schema: ${schemaPath}`);
|
||||
log(`DB: ${dbPath}`);
|
||||
|
||||
// Ensure memory directory exists
|
||||
if (!existsSync(memoryDir)) {
|
||||
mkdirSync(memoryDir, { recursive: true });
|
||||
log(`Created directory: ${memoryDir}`);
|
||||
} else {
|
||||
log(`Directory exists: ${memoryDir}`);
|
||||
}
|
||||
|
||||
// Read schema
|
||||
let schemaSQL;
|
||||
try {
|
||||
schemaSQL = readFileSync(schemaPath, 'utf8');
|
||||
} catch (e) {
|
||||
err(`Cannot read schema file: ${schemaPath}\n${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Open (or create) DB
|
||||
let db;
|
||||
try {
|
||||
db = new DatabaseSync(dbPath);
|
||||
log(`Opened DB (created if new): ${dbPath}`);
|
||||
} catch (e) {
|
||||
err(`Cannot open SQLite DB at ${dbPath}\n${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Execute each statement
|
||||
const statements = splitStatements(schemaSQL);
|
||||
log(`Executing ${statements.length} SQL statement(s)...`);
|
||||
console.log('');
|
||||
|
||||
let ok = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const stmt of statements) {
|
||||
const label = describeStatement(stmt);
|
||||
try {
|
||||
db.exec(stmt);
|
||||
log(` ✓ ${label}`);
|
||||
ok++;
|
||||
} catch (e) {
|
||||
// PRAGMA errors on read-only pragmas are non-fatal; everything else is fatal.
|
||||
if (stmt.trim().toUpperCase().startsWith('PRAGMA')) {
|
||||
log(` ~ ${label} (skipped — ${e.message})`);
|
||||
skipped++;
|
||||
} else {
|
||||
err(`Failed: ${label}\n ${e.message}`);
|
||||
db.close?.();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify expected tables exist
|
||||
const EXPECTED_TABLES = [
|
||||
'projects',
|
||||
'tasks_v2',
|
||||
'task_transitions_v2',
|
||||
'sync_state',
|
||||
'legacy_key_map',
|
||||
];
|
||||
|
||||
console.log('');
|
||||
log('Verifying tables...');
|
||||
|
||||
const tableRows = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
.all();
|
||||
const existingTables = new Set(tableRows.map(r => r.name));
|
||||
|
||||
let allPresent = true;
|
||||
for (const t of EXPECTED_TABLES) {
|
||||
if (existingTables.has(t)) {
|
||||
log(` ✓ table: ${t}`);
|
||||
} else {
|
||||
log(` ✗ MISSING table: ${t}`);
|
||||
allPresent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify singleton sync_state row
|
||||
const syncRow = db.prepare('SELECT id FROM sync_state WHERE id = 1').get();
|
||||
if (syncRow) {
|
||||
log(` ✓ sync_state singleton row present`);
|
||||
} else {
|
||||
log(` ✗ sync_state singleton row MISSING`);
|
||||
allPresent = false;
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('');
|
||||
if (allPresent) {
|
||||
log(`init-db complete — ${ok} statement(s) executed, ${skipped} skipped`);
|
||||
log(`TaskFlow DB is ready at: ${dbPath}`);
|
||||
} else {
|
||||
err('init-db completed with warnings — some expected tables are missing');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* task-sync.mjs -- Sync task markdown files to/from SQLite
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/task-sync.mjs files-to-db # parse markdown, update DB
|
||||
* node scripts/task-sync.mjs check # detect drift, exit 1 if mismatch
|
||||
* node scripts/task-sync.mjs db-to-files # project DB state into markdown
|
||||
*
|
||||
* Environment:
|
||||
* OPENCLAW_WORKSPACE Path to the workspace root (defaults to process.cwd())
|
||||
*/
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
// === PATHS (taskflow-001: portable, no hardcoded paths) ===
|
||||
const workspace = process.env.OPENCLAW_WORKSPACE || process.cwd()
|
||||
const dbPath = path.join(workspace, 'memory', 'taskflow.sqlite')
|
||||
const tasksDir = path.join(workspace, 'tasks')
|
||||
|
||||
const mode = process.argv[2] || 'check'
|
||||
if (!['files-to-db', 'check', 'db-to-files'].includes(mode)) {
|
||||
console.error('Usage: node scripts/task-sync.mjs [files-to-db|check|db-to-files]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// === STARTUP CHECKS (taskflow-020: clear errors for missing paths) ===
|
||||
if (!existsSync(workspace)) {
|
||||
console.error(`ERROR: Workspace directory not found: ${workspace}`)
|
||||
console.error('Set OPENCLAW_WORKSPACE to your workspace root and try again.')
|
||||
process.exit(1)
|
||||
}
|
||||
if (!existsSync(tasksDir)) {
|
||||
console.error(`ERROR: Tasks directory not found: ${tasksDir}`)
|
||||
console.error(`Create it with: mkdir -p "${tasksDir}"`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!existsSync(dbPath)) {
|
||||
console.error(`ERROR: Database file not found: ${dbPath}`)
|
||||
console.error('Run init-db.mjs first to create the database schema.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new DatabaseSync(dbPath)
|
||||
db.exec('PRAGMA foreign_keys = ON')
|
||||
|
||||
// === LOCK (taskflow-002: atomic single-UPDATE acquisition) ===
|
||||
let lockAcquired = false
|
||||
|
||||
function acquireLock() {
|
||||
const owner = `task-sync:${mode}`
|
||||
const until = new Date(Date.now() + 60_000).toISOString() // 60s TTL
|
||||
|
||||
// Ensure the singleton row exists
|
||||
db.exec(`INSERT OR IGNORE INTO sync_state (id, lock_owner, lock_until) VALUES (1, NULL, NULL)`)
|
||||
|
||||
// Single atomic UPDATE: succeeds only when no valid lock is held
|
||||
const result = db.prepare(`
|
||||
UPDATE sync_state
|
||||
SET lock_owner = ?, lock_until = ?
|
||||
WHERE id = 1
|
||||
AND (lock_owner IS NULL OR lock_until < datetime('now'))
|
||||
`).run(owner, until)
|
||||
|
||||
if (result.changes === 0) {
|
||||
const row = db.prepare('SELECT lock_owner, lock_until FROM sync_state WHERE id = 1').get()
|
||||
console.error(`Sync locked by ${row?.lock_owner} until ${row?.lock_until}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
lockAcquired = true
|
||||
}
|
||||
|
||||
function releaseLock(result) {
|
||||
const now = new Date().toISOString()
|
||||
db.prepare(
|
||||
'UPDATE sync_state SET lock_owner = NULL, lock_until = NULL, last_sync_at = ?, last_result = ? WHERE id = 1'
|
||||
).run(now, result)
|
||||
lockAcquired = false
|
||||
}
|
||||
|
||||
// === SIGNAL HANDLERS (taskflow-019: release lock on exit) ===
|
||||
function handleSignal(signal) {
|
||||
if (lockAcquired) {
|
||||
try { releaseLock(`interrupted: ${signal}`) } catch (_) {}
|
||||
}
|
||||
process.exit(signal === 'SIGTERM' ? 0 : 130)
|
||||
}
|
||||
process.on('SIGTERM', () => handleSignal('SIGTERM'))
|
||||
process.on('SIGINT', () => handleSignal('SIGINT'))
|
||||
|
||||
// === PARSE MARKDOWN ===
|
||||
const STATUS_MAP = {
|
||||
'in progress': 'in_progress',
|
||||
'pending validation': 'pending_validation',
|
||||
'backlog': 'backlog',
|
||||
'done': 'done',
|
||||
'blocked': 'blocked',
|
||||
}
|
||||
|
||||
const TASK_RE = /^- \[([ x])\] \(task:([a-z0-9-]+)\)\s*(?:\[([^\]]*)\])?\s*(?:\[([^\]]*)\])?\s*(.+)$/
|
||||
|
||||
function parseTaskFile(filePath) {
|
||||
const content = readFileSync(filePath, 'utf8')
|
||||
const lines = content.split(/\r?\n/)
|
||||
const tasks = []
|
||||
let currentStatus = null
|
||||
const slug = path.basename(filePath).replace(/-tasks\.md$/, '')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// Section header
|
||||
const headerMatch = line.match(/^## (.+)$/)
|
||||
if (headerMatch) {
|
||||
const normalized = headerMatch[1].trim().toLowerCase()
|
||||
currentStatus = STATUS_MAP[normalized] || null
|
||||
continue
|
||||
}
|
||||
|
||||
if (!currentStatus) continue
|
||||
|
||||
const taskMatch = line.match(TASK_RE)
|
||||
if (taskMatch) {
|
||||
const [, , id, tag1, tag2, title] = taskMatch
|
||||
|
||||
// Parse priority and model from tags
|
||||
let priority = 'P2', model = null
|
||||
for (const tag of [tag1, tag2]) {
|
||||
if (!tag) continue
|
||||
if (/^P\d$/.test(tag)) priority = tag
|
||||
else model = tag
|
||||
}
|
||||
|
||||
// Check for note on next line
|
||||
let notes = null
|
||||
if (i + 1 < lines.length) {
|
||||
const noteMatch = lines[i + 1].match(/^\s+- note:\s*(.+)$/)
|
||||
if (noteMatch) notes = noteMatch[1].trim()
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
id,
|
||||
project_id: slug,
|
||||
title: title.trim(),
|
||||
status: currentStatus,
|
||||
priority,
|
||||
owner_model: model,
|
||||
notes,
|
||||
source_file: `tasks/${slug}-tasks.md`,
|
||||
})
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
function parseAllFiles() {
|
||||
const files = readdirSync(tasksDir).filter(f => f.endsWith('-tasks.md'))
|
||||
const allTasks = []
|
||||
for (const f of files) {
|
||||
allTasks.push(...parseTaskFile(path.join(tasksDir, f)))
|
||||
}
|
||||
return allTasks
|
||||
}
|
||||
|
||||
// === DB READ ===
|
||||
function getDbTasks() {
|
||||
return db.prepare(
|
||||
'SELECT id, project_id, title, status, priority, owner_model, notes, source_file FROM tasks_v2 ORDER BY id'
|
||||
).all()
|
||||
}
|
||||
|
||||
// === HASH (taskflow-012: include notes field) ===
|
||||
function hashTasks(tasks) {
|
||||
const sorted = [...tasks].sort((a, b) => a.id.localeCompare(b.id))
|
||||
const data = sorted
|
||||
.map(t => `${t.id}|${t.status}|${t.priority}|${t.owner_model || ''}|${t.title}|${t.notes || ''}`)
|
||||
.join('\n')
|
||||
return createHash('sha256').update(data).digest('hex').slice(0, 16)
|
||||
}
|
||||
|
||||
// === DIFF ===
|
||||
function diffTasks(fileTasks, dbTasks) {
|
||||
const fileMap = new Map(fileTasks.map(t => [t.id, t]))
|
||||
const dbMap = new Map(dbTasks.map(t => [t.id, t]))
|
||||
const diffs = []
|
||||
|
||||
for (const [id, ft] of fileMap) {
|
||||
const dt = dbMap.get(id)
|
||||
if (!dt) {
|
||||
diffs.push({ type: 'file_only', id, task: ft })
|
||||
} else {
|
||||
const changes = []
|
||||
if (ft.status !== dt.status) changes.push(`status: ${dt.status} → ${ft.status}`)
|
||||
if (ft.priority !== dt.priority) changes.push(`priority: ${dt.priority} → ${ft.priority}`)
|
||||
if ((ft.owner_model || null) !== (dt.owner_model || null)) changes.push(`model: ${dt.owner_model} → ${ft.owner_model}`)
|
||||
if (ft.title !== dt.title) changes.push('title changed')
|
||||
if (changes.length) diffs.push({ type: 'changed', id, changes, fileTask: ft, dbTask: dt })
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id] of dbMap) {
|
||||
if (!fileMap.has(id)) {
|
||||
diffs.push({ type: 'db_only', id, task: dbMap.get(id) })
|
||||
}
|
||||
}
|
||||
|
||||
return diffs
|
||||
}
|
||||
|
||||
// === WRITE TO DB ===
|
||||
function syncFilesToDb(fileTasks, dbTasks, diffs) {
|
||||
// Auto-create projects referenced in files but missing from the projects table.
|
||||
const knownProjects = new Set(db.prepare('SELECT id FROM projects').all().map(r => r.id))
|
||||
const upsertProject = db.prepare(`
|
||||
INSERT OR IGNORE INTO projects (id, name, description, status, source_file)
|
||||
VALUES (?, ?, '', 'active', ?)
|
||||
`)
|
||||
const missingProjects = new Set()
|
||||
|
||||
for (const t of fileTasks) {
|
||||
if (!knownProjects.has(t.project_id) && !missingProjects.has(t.project_id)) {
|
||||
missingProjects.add(t.project_id)
|
||||
const name = t.project_id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
upsertProject.run(t.project_id, name, `tasks/${t.project_id}-tasks.md`)
|
||||
console.log(` Auto-created missing project: ${t.project_id} ("${name}")`)
|
||||
}
|
||||
}
|
||||
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO tasks_v2 (id, project_id, title, status, priority, owner_model, notes, source_file, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
status = excluded.status,
|
||||
priority = excluded.priority,
|
||||
owner_model = excluded.owner_model,
|
||||
notes = COALESCE(excluded.notes, tasks_v2.notes),
|
||||
source_file = excluded.source_file,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
`)
|
||||
const insertTransition = db.prepare(`
|
||||
INSERT INTO task_transitions_v2 (task_id, from_status, to_status, reason, actor)
|
||||
VALUES (?, ?, ?, ?, 'sync')
|
||||
`)
|
||||
|
||||
let updated = 0, added = 0
|
||||
|
||||
for (const diff of diffs) {
|
||||
if (diff.type === 'file_only') {
|
||||
const t = diff.task
|
||||
upsert.run(t.id, t.project_id, t.title, t.status, t.priority, t.owner_model, t.notes, t.source_file)
|
||||
insertTransition.run(t.id, null, t.status, 'added via file sync')
|
||||
added++
|
||||
} else if (diff.type === 'changed') {
|
||||
const t = diff.fileTask
|
||||
upsert.run(t.id, t.project_id, t.title, t.status, t.priority, t.owner_model, t.notes, t.source_file)
|
||||
if (diff.dbTask.status !== t.status) {
|
||||
insertTransition.run(t.id, diff.dbTask.status, t.status, `file sync: ${diff.changes.join(', ')}`)
|
||||
}
|
||||
updated++
|
||||
}
|
||||
// db_only tasks stay in DB (no silent drops)
|
||||
}
|
||||
|
||||
// Update hashes
|
||||
const allFileTasks = parseAllFiles()
|
||||
const allDbTasks = getDbTasks()
|
||||
db.prepare('UPDATE sync_state SET files_hash = ?, db_hash = ? WHERE id = 1').run(
|
||||
hashTasks(allFileTasks), hashTasks(allDbTasks)
|
||||
)
|
||||
|
||||
return { added, updated, dbOnly: diffs.filter(d => d.type === 'db_only').length }
|
||||
}
|
||||
|
||||
// === WRITE TO FILES (taskflow-003: blocked is first-class) ===
|
||||
const STATUS_ORDER = ['in_progress', 'pending_validation', 'blocked', 'backlog', 'done']
|
||||
const STATUS_HEADERS = {
|
||||
in_progress: 'In Progress',
|
||||
pending_validation: 'Pending Validation',
|
||||
blocked: 'Blocked',
|
||||
backlog: 'Backlog',
|
||||
done: 'Done',
|
||||
}
|
||||
|
||||
function projectDbToFiles(dbTasks) {
|
||||
const byProject = new Map()
|
||||
for (const t of dbTasks) {
|
||||
if (!byProject.has(t.project_id)) byProject.set(t.project_id, [])
|
||||
byProject.get(t.project_id).push(t)
|
||||
}
|
||||
|
||||
// Preserve existing file headers (everything before the first ## section)
|
||||
const files = readdirSync(tasksDir).filter(f => f.endsWith('-tasks.md'))
|
||||
const headers = new Map()
|
||||
for (const f of files) {
|
||||
const content = readFileSync(path.join(tasksDir, f), 'utf8')
|
||||
const slug = f.replace(/-tasks\.md$/, '')
|
||||
const lines = content.split(/\r?\n/)
|
||||
const headerLines = []
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('## ')) break
|
||||
headerLines.push(line)
|
||||
}
|
||||
headers.set(slug, headerLines.join('\n').trimEnd())
|
||||
}
|
||||
|
||||
// All project slugs from DB
|
||||
const slugs = db.prepare('SELECT id FROM projects ORDER BY id').all().map(r => r.id)
|
||||
let filesWritten = 0
|
||||
|
||||
for (const slug of slugs) {
|
||||
const tasks = byProject.get(slug) || []
|
||||
const header = headers.get(slug) || `# Tasks: ${slug}`
|
||||
|
||||
let md = header + '\n\n'
|
||||
for (const section of STATUS_ORDER) {
|
||||
md += `## ${STATUS_HEADERS[section]}\n`
|
||||
const sectionTasks = tasks
|
||||
.filter(t => t.status === section)
|
||||
.sort((a, b) => a.priority.localeCompare(b.priority) || a.id.localeCompare(b.id))
|
||||
|
||||
if (sectionTasks.length === 0) {
|
||||
md += '\n'
|
||||
} else {
|
||||
for (const t of sectionTasks) {
|
||||
const checkbox = t.status === 'done' ? '[x]' : '[ ]'
|
||||
const modelTag = t.owner_model ? ` [${t.owner_model}]` : ''
|
||||
md += `- ${checkbox} (task:${t.id}) [${t.priority}]${modelTag} ${t.title}\n`
|
||||
if (t.notes) md += ` - note: ${t.notes}\n`
|
||||
}
|
||||
md += '\n'
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(path.join(tasksDir, `${slug}-tasks.md`), md)
|
||||
filesWritten++
|
||||
}
|
||||
|
||||
return { filesWritten }
|
||||
}
|
||||
|
||||
// === MAIN ===
|
||||
try {
|
||||
if (mode !== 'check') acquireLock()
|
||||
|
||||
const fileTasks = parseAllFiles()
|
||||
const dbTasks = getDbTasks()
|
||||
const diffs = diffTasks(fileTasks, dbTasks)
|
||||
|
||||
if (mode === 'check') {
|
||||
const fileHash = hashTasks(fileTasks)
|
||||
const dbHash = hashTasks(dbTasks)
|
||||
if (diffs.length === 0) {
|
||||
console.log(`OK: ${fileTasks.length} tasks in sync (hash: ${fileHash})`)
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.log(`DRIFT DETECTED: ${diffs.length} differences`)
|
||||
console.log(` File hash: ${fileHash}`)
|
||||
console.log(` DB hash: ${dbHash}`)
|
||||
for (const d of diffs) {
|
||||
if (d.type === 'file_only') console.log(` + ${d.id} (in files, not DB)`)
|
||||
else if (d.type === 'db_only') console.log(` - ${d.id} (in DB, not files)`)
|
||||
else console.log(` ~ ${d.id}: ${d.changes.join(', ')}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
} else if (mode === 'files-to-db') {
|
||||
const result = syncFilesToDb(fileTasks, dbTasks, diffs)
|
||||
console.log(`Synced files → DB: ${result.added} added, ${result.updated} updated, ${result.dbOnly} DB-only preserved`)
|
||||
releaseLock('ok')
|
||||
} else if (mode === 'db-to-files') {
|
||||
const freshDbTasks = getDbTasks()
|
||||
const result = projectDbToFiles(freshDbTasks)
|
||||
console.log(`Projected DB → files: ${result.filesWritten} files written`)
|
||||
releaseLock('ok')
|
||||
}
|
||||
} catch (err) {
|
||||
if (mode !== 'check') {
|
||||
try { releaseLock(`failed: ${err.message}`) } catch (_) {}
|
||||
}
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!--
|
||||
TaskFlow Periodic Sync — macOS LaunchAgent
|
||||
==========================================
|
||||
Label: com.taskflow.sync
|
||||
Action: Runs task-sync.mjs files-to-db every 60 seconds.
|
||||
Markdown files are canonical; DB is kept in sync automatically.
|
||||
|
||||
INSTALLATION:
|
||||
1. Copy this file to ~/Library/LaunchAgents/com.taskflow.sync.plist
|
||||
2. Replace ALL occurrences of {{workspace}} with the absolute path
|
||||
to your workspace directory (no trailing slash).
|
||||
Example: /Users/you/.openclaw/workspace
|
||||
2b. Replace {{node}} with the path to your node binary.
|
||||
Find it with: which node
|
||||
Example: /opt/homebrew/bin/node
|
||||
3. Load the agent:
|
||||
launchctl load ~/Library/LaunchAgents/com.taskflow.sync.plist
|
||||
4. Verify it is running:
|
||||
launchctl list | grep taskflow
|
||||
|
||||
LOGS:
|
||||
stdout → {{workspace}}/logs/taskflow-sync.stdout.log
|
||||
stderr → {{workspace}}/logs/taskflow-sync.stderr.log
|
||||
Create the logs/ directory if it does not exist:
|
||||
mkdir -p {{workspace}}/logs
|
||||
|
||||
UNINSTALL:
|
||||
launchctl unload ~/Library/LaunchAgents/com.taskflow.sync.plist
|
||||
rm ~/Library/LaunchAgents/com.taskflow.sync.plist
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<!-- Unique label for this agent -->
|
||||
<key>Label</key>
|
||||
<string>com.taskflow.sync</string>
|
||||
|
||||
<!-- Command to run -->
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{node}}</string>
|
||||
<string>{{workspace}}/taskflow/scripts/task-sync.mjs</string>
|
||||
<string>files-to-db</string>
|
||||
</array>
|
||||
|
||||
<!-- Run every 60 seconds -->
|
||||
<key>StartInterval</key>
|
||||
<integer>60</integer>
|
||||
|
||||
<!-- Working directory (workspace root, where PROJECTS.md and tasks/ live) -->
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{workspace}}</string>
|
||||
|
||||
<!-- Environment variables passed to the script -->
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>OPENCLAW_WORKSPACE</key>
|
||||
<string>{{workspace}}</string>
|
||||
</dict>
|
||||
|
||||
<!-- Standard output log -->
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{workspace}}/logs/taskflow-sync.stdout.log</string>
|
||||
|
||||
<!-- Standard error log -->
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{workspace}}/logs/taskflow-sync.stderr.log</string>
|
||||
|
||||
<!-- Start on load (do not wait for first interval) -->
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,30 @@
|
||||
# Projects
|
||||
|
||||
<!-- ============================================================
|
||||
PROJECTS.md — Project Registry
|
||||
============================================================
|
||||
|
||||
FORMAT: One ## block per project. The slug (## heading text)
|
||||
is the canonical project ID used in task IDs, file names,
|
||||
and SQLite foreign keys. Keep it lowercase and hyphenated.
|
||||
|
||||
FIELDS:
|
||||
Name Human-readable display name (any capitalization).
|
||||
Status One of: active | paused | done
|
||||
Description One-sentence summary of the project's purpose.
|
||||
|
||||
FILE CONVENTIONS:
|
||||
Task file: tasks/<slug>-tasks.md (required)
|
||||
Plan file: plans/<slug>-plan.md (optional)
|
||||
|
||||
NOTE: Do not add task counts or progress bars here. Those are
|
||||
computed by the sync/export scripts and belong in dashboards,
|
||||
not in this registry file.
|
||||
============================================================ -->
|
||||
|
||||
## example-project
|
||||
- Name: Example Project
|
||||
- Status: active
|
||||
- Description: A starter project to demonstrate the TaskFlow format — replace with your own.
|
||||
|
||||
<!-- Add additional ## blocks below. Each block is one project. -->
|
||||
@@ -0,0 +1,61 @@
|
||||
# {{Project Name}} — Plan
|
||||
|
||||
<!-- ============================================================
|
||||
PLAN FILE
|
||||
============================================================
|
||||
File name: plans/<slug>-plan.md
|
||||
|
||||
This is a freeform architecture/design document. It is NOT
|
||||
synced to SQLite. Agents read it at session start for context.
|
||||
It is the right place for:
|
||||
- High-level vision and goals
|
||||
- Architecture decisions (with rationale)
|
||||
- Phase breakdowns and milestones
|
||||
- Open questions and tradeoffs
|
||||
- Anything too long or structural for a task line
|
||||
|
||||
There is no required format. The sections below are suggested
|
||||
starting points — delete or rename them freely.
|
||||
============================================================ -->
|
||||
|
||||
## Vision
|
||||
|
||||
<!-- What is this project trying to accomplish? What does success look like?
|
||||
One paragraph. Be concrete. -->
|
||||
|
||||
## Architecture
|
||||
|
||||
<!-- How is the system structured? Key components, data flows, external dependencies.
|
||||
Diagrams (ASCII or Mermaid) welcome. -->
|
||||
|
||||
## Decisions
|
||||
|
||||
<!-- Resolved design decisions. Format: short title + rationale.
|
||||
Example:
|
||||
| # | Decision | Resolution |
|
||||
|---|---|---|
|
||||
| 1 | Auth strategy | Use OAuth2 PKCE — avoids storing client secrets server-side |
|
||||
| 2 | Storage | SQLite for local, S3 for media — keeps infra simple |
|
||||
-->
|
||||
|
||||
## Open Questions
|
||||
|
||||
<!-- Unresolved questions or tradeoffs still being evaluated.
|
||||
Remove items as they are resolved (move to Decisions above).
|
||||
Example:
|
||||
- Should we paginate the feed at 20 or 50 items?
|
||||
- Redis vs. in-process cache for rate limiting?
|
||||
-->
|
||||
|
||||
## Phases / Milestones
|
||||
|
||||
<!-- Optional. Break down the project into phases or milestones.
|
||||
Example:
|
||||
### Phase 1 — MVP
|
||||
- Core sync logic
|
||||
- Basic CLI
|
||||
|
||||
### Phase 2 — Polish
|
||||
- Error handling
|
||||
- Tests
|
||||
-->
|
||||
@@ -0,0 +1,57 @@
|
||||
# {{Project Name}} — Tasks
|
||||
|
||||
<!-- ============================================================
|
||||
TASK FILE FORMAT
|
||||
============================================================
|
||||
|
||||
File name: tasks/<slug>-tasks.md
|
||||
One file per project. Slug matches the ## heading in PROJECTS.md.
|
||||
|
||||
TASK LINE FORMAT:
|
||||
- [x| ] (task:<slug>-NNN) [<priority>] [<owner>] <title>
|
||||
|
||||
FIELDS:
|
||||
[ ] / [x] Open / completed. The sync engine drives status
|
||||
from the section header, not this checkbox.
|
||||
Use [x] for Done (and optionally Pending Validation).
|
||||
|
||||
(task:<id>) Task ID. Format: <slug>-NNN (zero-padded, sequential).
|
||||
Custom prefixes are allowed but tooling assumes this pattern.
|
||||
|
||||
[<priority>] REQUIRED. Must come BEFORE the owner tag.
|
||||
Defaults: P0 (critical) | P1 (high) | P2 (normal) | P3 (low) | P9 (someday)
|
||||
|
||||
[<owner>] Optional. Agent or model tag (e.g., codex, sonnet, claude).
|
||||
|
||||
<title> Human-readable task title.
|
||||
|
||||
NOTES (optional, immediately below task line):
|
||||
- note: <text>
|
||||
⚠️ Notes are one-way in v1: removing a note in markdown does
|
||||
not clear it in the DB. This is a known limitation.
|
||||
|
||||
SECTION HEADERS (fixed — do not rename):
|
||||
## In Progress → DB status: in_progress
|
||||
## Pending Validation → DB status: pending_validation
|
||||
## Backlog → DB status: backlog
|
||||
## Blocked → DB status: blocked
|
||||
## Done → DB status: done
|
||||
|
||||
TAG ORDER RULE:
|
||||
Priority tag MUST come before owner tag. The parser is positional.
|
||||
✅ - [ ] (task:myproject-007) [P1] [codex] Implement search
|
||||
❌ - [ ] (task:myproject-007) [codex] [P1] Implement search
|
||||
============================================================ -->
|
||||
|
||||
## In Progress
|
||||
|
||||
## Pending Validation
|
||||
|
||||
## Backlog
|
||||
|
||||
<!-- Example task — replace with your own and assign the next sequential ID -->
|
||||
- [ ] (task:{{slug}}-001) [P2] First task for this project
|
||||
|
||||
## Blocked
|
||||
|
||||
## Done
|
||||
Reference in New Issue
Block a user