Add architecture specification and bump taskgraph to v0.0.2

Architecture docs for the open-tasks plugin covering the registry pattern
dispatch design, operation set, error handling, data flow, and constraints.
Includes four ADRs (registry pattern, no-cache policy, risk operation merge,
frontmatter normalization). The depends_on/dependsOn compatibility issue in
@alkdev/taskgraph is resolved in v0.0.2, so the dependency is bumped and
the docs reflect the fix.

AGENTS.md updated: canonical dependsOn field, dependents operation added,
hooks clarification, field naming note.
This commit is contained in:
2026-04-28 09:29:26 +00:00
parent fd59748a64
commit 307b8a2b54
8 changed files with 520 additions and 6 deletions

View File

@@ -52,7 +52,8 @@ Like open-memory, this plugin exposes **one tool** (`tasks`) with internal opera
tasks({tool: "help"}) → Show available operations tasks({tool: "help"}) → Show available operations
tasks({tool: "list"}) → List tasks in project tasks({tool: "list"}) → List tasks in project
tasks({tool: "show", args: {id: "..."}}) → Show task details tasks({tool: "show", args: {id: "..."}}) → Show task details
tasks({tool: "deps", args: {id: "..."}}) → Show task dependencies tasks({tool: "deps", args: {id: "..."}}) → Show task prerequisites
tasks({tool: "dependents", args: {id: "..."}}) → Show tasks that depend on a task
tasks({tool: "validate"}) → Validate all task files tasks({tool: "validate"}) → Validate all task files
... etc ... etc
``` ```
@@ -61,7 +62,7 @@ tasks({tool: "validate"}) → Validate all task files
``` ```
src/ src/
├── index.ts # Plugin entry: hooks + tool registration ├── index.ts # Plugin entry: tool registration (no hooks in v1)
├── tools.ts # Tool definitions (tasks router) ├── tools.ts # Tool definitions (tasks router)
├── registry.ts # Operation registry pattern (dispatch by tool name) ├── registry.ts # Operation registry pattern (dispatch by tool name)
├── operations/ # Individual operation implementations ├── operations/ # Individual operation implementations
@@ -140,7 +141,7 @@ Tasks are markdown files in `tasks/` with YAML frontmatter:
id: auth-setup id: auth-setup
name: Setup Authentication name: Setup Authentication
status: pending status: pending
depends_on: [] dependsOn: []
scope: moderate scope: moderate
risk: medium risk: medium
impact: component impact: component
@@ -165,6 +166,8 @@ Implement OAuth2 authentication with provider abstraction.
> Agent fills this on completion. > Agent fills this on completion.
``` ```
> **Note on field naming**: The `@alkdev/taskgraph` library uses camelCase (`dependsOn`, `scope`, `risk`, etc.) in its schema. The Rust CLI historically used snake_case (`depends_on`). As of `@alkdev/taskgraph` v0.0.2, the parser accepts both forms — but camelCase is the canonical form for new files.
## Build & Test Commands ## Build & Test Commands
```bash ```bash

View File

@@ -5,7 +5,7 @@
"": { "": {
"name": "@alkdev/open-tasks", "name": "@alkdev/open-tasks",
"dependencies": { "dependencies": {
"@alkdev/taskgraph": "^0.0.1", "@alkdev/taskgraph": "^0.0.2",
"@opencode-ai/plugin": "^1.1.3", "@opencode-ai/plugin": "^1.1.3",
}, },
"devDependencies": { "devDependencies": {
@@ -16,7 +16,7 @@
}, },
}, },
"packages": { "packages": {
"@alkdev/taskgraph": ["@alkdev/taskgraph@0.0.1", "", { "dependencies": { "@alkdev/typebox": "^0.34.49", "graphology": "^0.26.0", "graphology-components": "^1.5.4", "graphology-dag": "^0.4.1", "graphology-metrics": "^2.4.0", "graphology-operators": "^1.6.1", "yaml": "^2.8.3" } }, "sha512-U1EehRUXU1sTjHVgwOumU6EeIWsHHBoY5oxehs1iEZF05EO8uh+GaZfY6M5H0G2wzbSy7qw/exSq2DijWT0xwg=="], "@alkdev/taskgraph": ["@alkdev/taskgraph@0.0.2", "", { "dependencies": { "@alkdev/typebox": "^0.34.49", "graphology": "^0.26.0", "graphology-components": "^1.5.4", "graphology-dag": "^0.4.1", "graphology-metrics": "^2.4.0", "graphology-operators": "^1.6.1", "yaml": "^2.8.3" } }, "sha512-YwKit2CiNe32NHl/+WEr3Hw/o+DsJWe3MDN6Nz5RK4IyUiOZYAtaffFlTbZNlQosM64Pl9kRgsQnRG2b9ikr/g=="],
"@alkdev/typebox": ["@alkdev/typebox@0.34.49", "", {}, "sha512-hMidpI6GlMgQMlW9KEd8I3ywgewV6mva9iJaDuBfGtgeRAGrB8yyu6T/fHmgmyQineZ8l4/1PdH/VNr3S2er2g=="], "@alkdev/typebox": ["@alkdev/typebox@0.34.49", "", {}, "sha512-hMidpI6GlMgQMlW9KEd8I3ywgewV6mva9iJaDuBfGtgeRAGrB8yyu6T/fHmgmyQineZ8l4/1PdH/VNr3S2er2g=="],

View File

@@ -0,0 +1,39 @@
---
status: draft
last_updated: 2026-04-28
---
# ADR-001: Registry Pattern (Single Tool Dispatch)
## Context
The plugin exposes 14 distinct operations (list, show, deps, dependents, validate, topo, cycles, critical, parallel, bottleneck, risk, cost, decompose, help). OpenCode's tool system adds each tool's JSON schema to the system prompt. At ~200-300 tokens per tool definition, 14 individual tools would consume ~3500 tokens of context before the agent even starts working.
## Decision
Collapse all operations into a single `tasks` tool that dispatches by `{tool: string, args?: Record<string, unknown>}`. The agent calls `tasks({tool: "help"})` to discover available operations on demand.
This follows the pattern established by open-memory, which exposes 9 operations through a single `memory` tool.
## Consequences
**Positive:**
- Minimal context overhead (~250 tokens for one tool schema vs ~3500 for 14)
- Adding new operations never increases context bloat
- Agent always has access to the full operation set without schema pollution
- Consistent with the alk.dev ecosystem pattern (memory, coordinator all use this)
**Negative:**
- The `tool` and `args` fields are not validated by the outer Zod schema — validation happens inside the dispatch handler
- Agent must call help to discover operations; the tool description can only hint
- Slightly more overhead per call (string dispatch vs direct function call)
**Mitigation for negatives:**
- The `tool` field description enumerates all operation names, so the LLM can dispatch correctly
- Validation errors are clear and include usage guidance
- The help operation provides complete reference with examples
## References
- open-memory `src/tools.ts`: proven pattern in production
- OpenCode plugin SDK: `tool.schema` (Zod) for schema definition

View File

@@ -0,0 +1,42 @@
---
status: draft
last_updated: 2026-04-28
---
# ADR-002: No Caching — Fresh Graph Per Invocation
## Context
Task files change frequently during active work. Agents update task status (pending → in-progress → completed), add notes, modify acceptance criteria. A cached `TaskGraph` would become stale and produce misleading analysis.
Options considered:
1. **Fresh read per call** — parse files and build graph on every invocation
2. **Session-scoped cache** — cache the graph within a session, invalidate on file change detection
3. **Time-based TTL cache** — cache for N seconds, then re-parse
## Decision
Fresh read per call (Option 1). Each tool invocation reads the tasks directory and constructs a new `TaskGraph`.
## Consequences
**Positive:**
- Guaranteed correctness — analysis always reflects the current state of task files
- No invalidation logic to get wrong
- No cache coherence bugs
- Simple mental model for the agent — "what I see is what's on disk"
**Negative:**
- Redundant I/O for consecutive calls within a short time window
- Slight latency increase for each call
**Why this is acceptable:**
- Typical task directories contain 5-50 files. `parseTaskDirectory` + `TaskGraph.fromTasks` is sub-second for this scale.
- The plugin is read-only — there's no mutation to cache anyway
- File I/O is the plugin's only expensive operation, and it's inherently cheap for small task sets
- Open-memory makes no attempt to cache SQLite query results either; freshness trumps efficiency
## References
- `@alkdev/taskgraph` `parseTaskDirectory`: async file reading + YAML frontmatter parsing
- Open-memory pattern: stateless queries, no caching between calls

View File

@@ -0,0 +1,44 @@
---
status: draft
last_updated: 2026-04-28
---
# ADR-003: Merged `risk` Operation (Risk Path + Risk Distribution)
## Context
The taskgraph CLI exposes two separate risk-related subcommands:
- `taskgraph risk` — shows risk distribution (tasks grouped by risk level: trivial, low, medium, high, critical)
- `taskgraph risk-path` — shows the single highest-cumulative-risk path through the DAG
Both are about understanding risk in the task graph. An agent asking "what's the risk situation?" almost always wants both perspectives — which tasks are risky, and where does risk concentrate along paths.
## Decision
Merge into a single `risk` operation that returns:
1. **Risk distribution** — tasks grouped by risk level (trivial → critical), with counts and percentages
2. **Highest risk path** — the path through the DAG with maximum cumulative risk, showing per-task risk and impact
This maps to `riskDistribution(graph)` and `riskPath(graph)` from `@alkdev/taskgraph`.
## Consequences
**Positive:**
- One call gives the complete risk picture
- Agent doesn't need to correlate results from two separate calls
- The distribution provides context for understanding the risk path (e.g., "3 high-risk tasks, 2 of which are on the critical path")
**Negative:**
- Output is larger than individual calls
- An agent that only wants distribution or only wants the path gets extra content
- Slightly more complex formatting logic
**Mitigation for negatives:**
- The combined output is still well under typical markdown rendering limits
- Distribution is shown first (most likely to be actioned on), path second (deeper analysis)
- Both sections have clear headers so the agent can focus on what matters
## References
- taskgraph CLI: `taskgraph risk` and `taskgraph risk-path` subcommands
- `@alkdev/taskgraph`: `riskDistribution()` and `riskPath()` functions

View File

@@ -0,0 +1,55 @@
---
status: stable
last_updated: 2026-04-28
---
# ADR-004: Frontmatter Field Name Normalization (depends_on / dependsOn)
## Context
There was a naming divergence between the Rust CLI and the TypeScript core library for the dependency field:
| Source | Field name in YAML | Field name in struct |
|--------|--------------------|---------------------|
| Rust CLI (`taskgraph`) | `depends_on` | `depends_on` |
| TypeScript lib (`@alkdev/taskgraph`) | `dependsOn` | `dependsOn` |
The `yaml` npm package does **not** auto-convert snake_case to camelCase. A markdown file with `depends_on: [a, b]` would parse to `{depends_on: ["a", "b"]}`, which the `TaskInput` schema (expecting `dependsOn`) rejected as an unknown property. `Value.Clean()` would strip it, and `Value.Check()` would fail because the required field was missing.
This was a bug in `@alkdev/taskgraph`. The library's `parseFrontmatter()` function contract says it accepts "markdown with YAML frontmatter" — but the YAML convention established by the Rust CLI ecosystem was `depends_on`, and the parser silently discarded it.
**Broader point**: This was a textbook example of how issues upstream increase the surface area of issues downstream. A field naming convention in the Rust implementation created a compatibility fault line that propagated to every consumer. These are the "corners" that are hard to see around in linear text — exactly the kind of problem DAG-structured task analysis is designed to surface.
## Decision
**Fixed upstream in `@alkdev/taskgraph` v0.0.2**: A normalization step was added to `parseFrontmatter()` between YAML parsing and `Value.Clean()`. Known snake_case aliases are mapped to their camelCase canonical names.
The normalization map:
```typescript
const KEY_ALIASES: Record<string, string> = {
depends_on: "dependsOn",
}
```
Applied after YAML parse, before `Value.Clean()`. Both `depends_on` and `dependsOn` are now accepted in YAML frontmatter. The canonical form for new files is `dependsOn` (camelCase).
## Resolution
- `@alkdev/taskgraph` v0.0.2 includes the fix
- This plugin pins `^0.0.2` in its dependencies
- No plugin-level workaround needed
- The `depends_on` / `dependsOn` compatibility surface is resolved
## Impact on This Plugin
**Resolved**. Task files using either `depends_on` (Rust CLI convention) or `dependsOn` (TypeScript canonical) parse correctly. No preprocessing, workarounds, or special handling required in the plugin.
AGENTS.md documents `dependsOn` as the canonical form for new task files, with a note that both forms are accepted.
## References
- Rust CLI `struct TaskFrontmatter`: uses `depends_on` (snake_case, Serde default)
- TypeScript `TaskInput` schema: uses `dependsOn` (camelCase, JS convention)
- `yaml` npm package: preserves YAML key casing as-is (no auto-conversion)
- `Value.Clean()`: previously stripped `depends_on` as unknown property — now handled by normalization upstream

View File

@@ -0,0 +1,331 @@
---
status: draft
last_updated: 2026-04-28
---
# Open Tasks: Architecture Overview
Structured task management for OpenCode agents — graph analysis, dependency insight, decomposition guidance, and workflow cost estimation. Exposes a single `tasks` tool using a registry pattern to keep the agent's visible tool count minimal.
## Problem
The `taskgraph` Rust CLI provides task graph operations but requires shell invocation — agents must compose bash commands and parse plain-text output. This is error-prone, context-expensive, and gives no structural validation or rich formatting. The TypeScript core library (`@alkdev/taskgraph`) now provides all graph operations natively. This plugin wraps that library into an OpenCode tool interface so agents get first-class, structured access without leaving the conversation.
## What This Plugin Is
A **read-only analysis and query layer** on top of the project's `tasks/` directory. It:
- Reads task markdown files with YAML frontmatter via `@alkdev/taskgraph` parsing
- Constructs an in-memory `TaskGraph` per invocation
- Runs analysis functions (critical path, parallel groups, bottlenecks, risk, workflow cost, decomposition)
- Returns formatted markdown to the agent
## What This Plugin Is Not
- **Not a task editor** — it does not create, modify, or delete task files. Task creation and status updates are the agent's responsibility (Write/Edit tools).
- **Not a task runner** — it does not coordinate execution. That's the role of open-coordinator.
- **Not a persistence layer** — there is no database, no cache, no state between invocations. Each tool call reads files fresh.
## Architecture
### Single-Tool Registry Pattern
Following open-memory's proven approach, the plugin exposes **one tool** (`tasks`) with internal operation dispatch:
```
tasks({tool: "help"}) → Show available operations
tasks({tool: "list"}) → List tasks in project
tasks({tool: "show", args: {id: "..."}}) → Show task details
tasks({tool: "deps", args: {id: "..."}}) → Task prerequisites
tasks({tool: "dependents", args: {id: "..."}}) → Tasks depending on a task
tasks({tool: "validate"}) → Validate all task files
tasks({tool: "topo"}) → Topological ordering
tasks({tool: "cycles"}) → Circular dependency detection
tasks({tool: "critical"}) → Critical path
tasks({tool: "parallel"}) → Parallel execution groups
tasks({tool: "bottleneck"}) → Bottleneck analysis
tasks({tool: "risk"}) → Risk path + distribution
tasks({tool: "cost"}) → Workflow cost estimate
tasks({tool: "decompose", args: {id: "..."}}) → Decomposition guidance
```
**Why**: Each tool definition adds JSON schema to the system prompt (~200-300 tokens each). 14 operations as 14 separate tools = ~3500 tokens of tool definitions. The registry pattern collapses this to ~250 tokens (one tool schema) plus an on-demand help text the agent retrieves only when needed. This is the same math that drove open-memory's design.
### Component Structure
```
src/
├── index.ts # Plugin entry: tool registration (no hooks in v1)
├── tools.ts # Tool definition — single `tasks` tool with registry dispatch
├── registry.ts # Operation registry (dispatch table, arg validation)
├── operations/ # Individual operation implementations
│ ├── help.ts # Help reference and per-operation details
│ ├── list.ts # List and filter tasks
│ ├── show.ts # Show full task details
│ ├── deps.ts # Show prerequisites
│ ├── dependents.ts # Show dependents
│ ├── validate.ts # Validate task files
│ ├── topo.ts # Topological ordering
│ ├── cycles.ts # Cycle detection
│ ├── critical.ts # Critical path
│ ├── parallel.ts # Parallel execution groups
│ ├── bottleneck.ts # Bottleneck scores
│ ├── risk.ts # Risk path + risk distribution
│ ├── cost.ts # Workflow cost estimate
│ └── decompose.ts # Decomposition guidance
└── formatting.ts # Shared markdown formatting helpers
```
### Data Flow
Each operation follows the same pipeline:
```
Agent calls tasks({tool: "list", args: {status: "pending"}})
├─ registry.ts validates tool name and args
├─ Operation handler:
│ │
│ ├─ resolveTasksPath(ctx) → find project's tasks/ directory
│ │
│ ├─ parseTaskDirectory(tasksPath) → TaskInput[] from @alkdev/taskgraph
│ │
│ ├─ TaskGraph.fromTasks(inputs) → in-memory graph
│ │
│ ├─ Analysis function (e.g., parallelGroups(graph))
│ │
│ └─ format result as markdown
└─ Return formatted markdown to agent
```
There is no caching between calls. Each invocation reads files and builds a fresh graph. This is intentional — task files change as agents work, and stale data would be worse than redundant I/O.
### Task Discovery
The plugin needs to find the project's `tasks/` directory. Resolution order:
1. **Workspace root**`<workspace>/tasks/` (where `workspace` comes from the OpenCode plugin context)
2. **Fallback**`./tasks/` relative to CWD
The path is constrained: it must resolve to a directory named `tasks/` within the workspace. If a config-provided path escapes the workspace root (e.g., `../../etc/`), it is rejected. This prevents the plugin from reading arbitrary files outside the project.
If no tasks directory is found, operations return a clear error message explaining where they looked and how to create one.
## Operations Reference
### Query Operations
| Operation | Maps to | Key Args | Output |
|-----------|---------|----------|--------|
| `list` | `TaskGraph` iteration | `status`, `scope`, `risk` (filter) | Filtered task table |
| `show` | `graph.getTask()` | `id` (required) | Full task details + markdown body |
| `deps` | `graph.dependencies()` | `id` (required) | Prerequisite task list |
| `dependents` | `graph.dependents()` | `id` (required) | Dependent task list |
| `topo` | `graph.topologicalOrder()` | — | Ordered task list |
| `cycles` | `graph.findCycles()` | — | Cycle report or "no cycles" |
| `validate` | `graph.validate()` | — | Validation errors or "all valid" |
### Analysis Operations
| Operation | Maps to | Key Args | Output |
|-----------|---------|----------|--------|
| `critical` | `criticalPath()`, `weightedCriticalPath()` | — | Critical path with task names |
| `parallel` | `parallelGroups()` | — | Grouped task lists by generation |
| `bottleneck` | `bottlenecks()` | — | Ranked task list with scores |
| `risk` | `riskPath()`, `riskDistribution()` | — | Highest-risk path + distribution table |
| `cost` | `workflowCost()` | `propagationMode`, `defaultQualityRetention`, `includeCompleted` | Per-task EV + totals |
| `decompose` | `shouldDecomposeTask()` | `id` (required) | Decomposition verdict + reasons |
### Help Operation
`tasks({tool: "help"})` returns the full operation reference table. `tasks({tool: "help", args: {tool: "list"}})` returns detailed usage for one operation including argument shapes and example calls.
## Design Decisions
### D1: Registry Pattern (single tool, not 14)
- **Context**: 14 operations could each be a separate tool or collapsed into one router.
- **Choice**: Single `tasks` tool with `{tool, args}` dispatch.
- **Consequences**: Agent always has access to the help reference. Adding operations never increases context bloat. Trade-off: the `tool` and `args` fields are not individually validated by the outer schema — validation happens inside the dispatch.
- **Reference**: See [ADR-001](decisions/001-registry-pattern.md)
### D2: No Caching, Fresh Graph Per Call
- **Context**: Task files change as agents work (status updates, new tasks, removed tasks). A cached graph would become stale.
- **Choice**: Each tool invocation reads the tasks directory fresh and builds a new graph.
- **Consequences**: Slightly redundant I/O for consecutive calls, but guarantees correctness. The tasks directory is typically small (<50 files). The `parseTaskDirectory` + `TaskGraph.fromTasks` pipeline is fast (sub-second for typical task sets).
- **Reference**: See [ADR-002](decisions/002-no-cache.md)
### D3: `risk` Operation Merges `risk-path` and Risk Distribution
- **Context**: The CLI has separate `risk` (distribution) and `risk-path` (path) subcommands. Both are risk-related and an agent asking "what's the risk situation?" wants both.
- **Choice**: Single `risk` operation returns both risk distribution (grouped by category) and risk path (the highest-cumulative-risk path through the DAG).
- **Consequences**: One call gives the full risk picture. Saves the agent from needing two calls and correlating results.
- **Reference**: See [ADR-003](decisions/003-risk-merge.md)
### D4: `decompose` Takes Task ID, Not Raw Attributes
- **Context**: `shouldDecomposeTask()` in the core library accepts `TaskGraphNodeAttributes` directly (an object with id, name, risk, scope, impact, etc. — all categorical fields nullable). The plugin could expose this raw or resolve by task ID.
- **Choice**: The `decompose` operation takes a task `id`, looks up the task from the graph (`graph.getTask(id)`), and passes its attributes to `shouldDecomposeTask()`.
- **Consequences**: Agent-friendly — just pass the task ID rather than reconstructing attributes. If the task doesn't exist, a clear error is returned. The library function is still available for programmatic use; this is an interface convenience.
### D5: `cost` Defaults Match SDD Process
- **Context**: `workflowCost()` supports `propagationMode` (independent vs dag-propagate), `defaultQualityRetention`, and `includeCompleted`. Different defaults make sense for different workflows.
- **Choice**: Default to `propagationMode: "dag-propagate"`, `includeCompleted: false`, `defaultQualityRetention: 0.9` — matching the Spec-Driven Development (SDD) process's assumption that completed tasks are factored out of remaining cost, and that quality degrades probabilistically across dependencies. See [SDD Process](../../sdd_process.md) for the overall workflow.
- **Consequences**: The most common use case (active project planning) gets sensible defaults. Agents can override per-call.
### D6: Separate `registry.ts` From `tools.ts`
- **Context**: Open-memory puts all handler logic in `tools.ts` (~500 lines). That works for a single cohesive domain (SQL queries) but open-tasks has 14 operations that each wrap a distinct library function.
- **Choice**: `tools.ts` defines the tool schema and dispatch. `registry.ts` maps operation names to handler functions. Each operation is a separate file under `operations/`.
- **Consequences**: Each operation is independently understandable and testable. Adding a new operation means adding one file and one registry entry, not editing a growing monolith.
## Interfaces
### Plugin Entry (`src/index.ts`)
```typescript
import type { Plugin } from "@opencode-ai/plugin"
import { createTools } from "./tools.js"
const OpenTasksPlugin: Plugin = async (ctx) => {
return {
tool: createTools(ctx),
}
}
export default OpenTasksPlugin
```
No hooks in v1. Future: task status injection into system prompt (similar to open-memory's context awareness hook).
### Tool Definition (`src/tools.ts`)
Single tool with `{tool: string, args?: Record<string, unknown>}` schema. The `tool` field dispatches to an operation handler via the registry. Unknown tool names produce a friendly error directing to `tasks({tool: "help"})`.
### Operation Handler Signature
```typescript
import type { PluginInput } from "@opencode-ai/plugin"
type OperationHandler = (
args: Record<string, unknown>,
ctx: PluginInput,
) => string | Promise<string>
```
Each handler receives raw args (already validated by the handler itself) and the plugin context. `PluginInput` provides workspace path information needed by `resolveTasksPath()`. Returns formatted markdown string.
`resolveTasksPath(ctx)` in the registry handles path resolution and returns the absolute path to the tasks directory. Operations should call this rather than hardcoding paths.
## Compatibility Surface
This plugin depends on `@alkdev/taskgraph` for all graph and parsing operations. Any contract divergence between the library and existing task files surfaces as a runtime issue in the plugin — and these are easy to miss until they break.
**Resolved**: The Rust CLI uses `depends_on` (snake_case) in YAML frontmatter while the TypeScript library uses `dependsOn` (camelCase). This was a bug in the library's parser — `parseFrontmatter()` would silently strip `depends_on` and then fail on the missing required field. **Fixed in `@alkdev/taskgraph` v0.0.2**: a normalization step now maps `depends_on``dependsOn` before schema validation, so both forms are accepted transparently. See [ADR-004](decisions/004-frontmatter-field-normalization.md).
The broader lesson remains: **issues upstream increase the surface area of issues downstream**. A naming convention in the Rust tooling created a fault line that propagated to every consumer. These are the corners that are hard to see around in linear text — exactly what DAG-structured task analysis is designed to surface.
## Constraints
1. **Read-only** — the plugin never writes to the filesystem. Task mutations happen through Write/Edit tools.
2. **No network** — the plugin makes no HTTP calls. All data comes from local task files.
3. **No state between calls** — each invocation is independent. No caching, no session storage.
4. **Task files are the source of truth** — markdown files in `tasks/` directory. No database, no alternative storage.
5. **Depends on `@alkdev/taskgraph`** — all graph construction, analysis, and frontmatter parsing comes from the core library. This plugin is a thin consumer. Contract changes in the library (field naming, schema changes) propagate here — see [Compatibility Surface](#compatibility-surface).
6. **Task directory required** — operations fail gracefully if no `tasks/` directory is found, returning a clear message about where to create one.
7. **Circular dependency handling** — if `TaskGraph.fromTasks()` detects cycles via the `topologicalOrder()` path, the `cycles` operation surfaces the cycle details. Other operations that rely on topological ordering (topo, critical, parallel, cost) report the error and suggest running `cycles` first.
8. **Frontmatter key normalization resolved**`@alkdev/taskgraph` v0.0.2+ accepts both `depends_on` and `dependsOn` in YAML frontmatter. The plugin pins `^0.0.2`. See [ADR-004](decisions/004-frontmatter-field-normalization.md) and [Compatibility Surface](#compatibility-surface).
## Error Handling
Operations encounter two categories of errors:
### Infrastructure Errors (tasks directory / file I/O)
- **No tasks directory**: Return a clear message identifying the searched paths and how to create a `tasks/` directory
- **Empty tasks directory**: Return "No task files found in `<path>`"
- **Malformed task file**: Include the filename and parse error in the output. Other valid files are still processed — a single bad file does not block the entire operation
- **File permission errors**: Return the OS error with the file path. Operation continues processing remaining files
### Graph Errors (validation / cycles)
- **Cycle detection**: The `cycles` operation surfaces all cycles. Operations that require topological ordering (topo, critical, parallel, cost) catch `CircularDependencyError` and return a message suggesting `tasks({tool: "cycles"})` first
- **Validation errors**: The `validate` operation returns both schema errors (field-level: invalid enums, missing required fields) and graph errors (dangling references, duplicate edges). Other operations call `graph.validate()` only when structural correctness matters
- **Task not found**: Operations that take a task `id` return a clear "not found" message listing the available task IDs (up to 20)
### Error Format
All errors are returned as markdown-formatted strings (not thrown). The agent sees a helpful message, not a stack trace. This matches open-memory's pattern where every handler returns a string.
## Performance Budget
Each operation should complete within these targets (assumes ≤50 task files):
| Operation | Target | Reasoning |
|-----------|--------|-----------|
| `help`, `list`, `show`, `deps`, `dependents` | <200ms | Single-pass read + format |
| `validate`, `topo`, `cycles` | <300ms | Graph construction + traversal |
| `critical`, `parallel`, `bottleneck` | <400ms | Graph construction + analysis |
| `risk`, `cost` | <500ms | Graph construction + cost-benefit analysis |
| `decompose` | <200ms | Single task lookup + check |
At 100+ files, expect 2-3x slowdown. The dominant cost is file I/O (reading and parsing YAML), not graph algorithms.
## Versioning
The plugin pins `@alkdev/taskgraph` at `^0.0.2` in `package.json` dependencies. As the library stabilizes, the pin should be tightened to a minor version range to prevent unexpected contract changes. Major version bumps in the library require explicit review of this plugin's compatibility surface.
## Operation Lifecycle
New operations can be added freely — the registry pattern means no schema bloat. When an operation needs removal:
1. Mark as deprecated in the `help` text for one minor version
2. Return a deprecation notice from the handler for one minor version
3. Remove in the next major version
4. Any removal requires an ADR documenting the reason
## Test Strategy
- **Unit tests**: Each operation handler tested with mock `TaskGraph` inputs (no file I/O). `@alkdev/taskgraph` functions are mocked — we test formatting and dispatch, not the library's analysis.
- **Integration tests**: End-to-end tool dispatch with a fixture `tasks/` directory containing sample task files. Tests write temporary files, invoke operations, and assert on markdown output.
- **Error tests**: Missing `tasks/` directory, malformed YAML, cyclic graphs, missing task IDs — each error path has at least one test.
- Run with `bun test`. Test fixtures live in `test/fixtures/tasks/`.
## Formatting Conventions
- **Tables** for list, cost, bottleneck — pipe-delimited columns, sorted by relevance
- **Hierarchical lists** for deps, dependents — indented dependency chains
- **Sectioned output** for risk — distribution table followed by risk path
- **Header + detail** for show — frontmatter fields as labeled list, then markdown body
- **Status badges** for validate — ✓ valid / ✗ with error details
- **Grouped output** for parallel — numbered generations with task lists
## Relationship to Other Plugins
| Plugin | Relationship |
|--------|-------------|
| **open-memory** | Complementary — memory handles session introspection; tasks handles task graph analysis. Both use the registry pattern. |
| **open-coordinator** | Downstream consumer — coordinator uses `tasks` to identify parallelizable work, then spawns worktrees. The `parallel` and `critical` operations inform coordination decisions. |
| **taskgraph CLI** | Functional equivalent — the Rust CLI and this plugin expose the same operations, but this plugin is native TypeScript + in-process, while the CLI is a separate binary. |
| **@alkdev/taskgraph** | Core dependency — all graph operations. This plugin is a thin wrapper. |
## Open Questions
1. **Should `show` include the task's markdown body?** Task files can be long (especially with acceptance criteria and notes). Option A: always include full body. Option B: `show` returns frontmatter summary, `show --full` includes body. Recommendation: always include body — agents need the full context for implementation tasks, and `show` is on-demand (not in every call).
2. **Should `cost` accept `--format json`?** The CLI supports JSON output for programmatic consumption. Since the plugin returns to an agent (not a script), markdown is always appropriate. JSON output is out of scope.
3. **Future hook: task status injection?** Open-memory injects context percentage into the system prompt. Could open-tasks inject a brief task summary ("3 pending, 1 in-progress, 2 blocked")? This would require reading tasks on every message, which is cheap for small task sets but could be noisy. Defer to v2.
## References
- `@alkdev/taskgraph` API surface: see [`@alkdev/taskgraph` docs/architecture/api-surface.md](https://git.alk.dev/alkdev/taskgraph_ts) or the local clone at `/workspace/@alkdev/taskgraph_ts/docs/architecture/api-surface.md`
- `@alkdev/taskgraph` README: local clone at `/workspace/@alkdev/taskgraph_ts/README.md`
- open-memory architecture: `/workspace/@alkdev/open-memory/docs/architecture.md` (reference implementation for the registry pattern)
- open-memory tools.ts: `/workspace/@alkdev/open-memory/src/tools.ts` (reference for handler pattern)
- SDD process: [../sdd_process.md](../sdd_process.md)
- OpenCode plugin SDK: `@opencode-ai/plugin` npm package

View File

@@ -41,7 +41,7 @@
"decomposition" "decomposition"
], ],
"dependencies": { "dependencies": {
"@alkdev/taskgraph": "^0.0.1", "@alkdev/taskgraph": "^0.0.2",
"@opencode-ai/plugin": "^1.1.3" "@opencode-ai/plugin": "^1.1.3"
}, },
"devDependencies": { "devDependencies": {