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:
39
docs/architecture/decisions/001-registry-pattern.md
Normal file
39
docs/architecture/decisions/001-registry-pattern.md
Normal 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
|
||||
42
docs/architecture/decisions/002-no-cache.md
Normal file
42
docs/architecture/decisions/002-no-cache.md
Normal 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
|
||||
44
docs/architecture/decisions/003-risk-merge.md
Normal file
44
docs/architecture/decisions/003-risk-merge.md
Normal 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user