Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@
},
{
"name": "task-utils",
"description": "Task discipline hooks (0-or-1 in_progress invariant, write-tool gating, validation-steps + RESULT cites) + task-manage skill for atomic-task workflow. Blocking behaviour is configurable via plugins.settings.yaml.",
"version": "0.3.0",
"description": "Task discipline hooks (0-or-1 in_progress invariant on flat YAML store, write-tool gating, validation-steps + RESULT cites, PostToolUse legacy-sync, native-task advisory/block, pull sync) + task-manage skill + MCP task server (primary task path)",
"version": "0.4.0",
"author": {
"name": "Nathan Heaps"
},
Expand All @@ -116,7 +116,8 @@
"claude-code",
"agent-discipline",
"validation-steps",
"in-progress-invariant"
"in-progress-invariant",
"mcp"
]
}
]
Expand Down
26 changes: 25 additions & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,34 @@ run = "bunx nx run-many --target=lint"

[tasks.build]
description = "Build every workspace package via nx (root and the plugins package have no build target and are skipped)"
depends = ["build-task-mcp"]
run = "bunx nx run-many --target=build"

# The task-mcp-service lives under services/task-mcp-service and is NOT a
# workspace package (it ships as source and builds on-device via build.sh).
# These dedicated tasks let CI compile + test it without registering it in the
# root bun workspace. The generic lib/ (store-base, git-helper) is intended to
# back a future ticket-mcp-service.
[tasks.build-task-mcp]
description = "Compile the task-mcp-service native binary (dist/task-mcp) — local-dev/CI check; end users build on-device via build.sh"
dir = "services/task-mcp-service"
run = """
bun install
bun build --compile --outfile dist/task-mcp src/server.ts
echo "Built services/task-mcp-service/dist/task-mcp"
"""

[tasks.test-task-mcp]
description = "Run the task-mcp-service unit + integration tests + the hook write-gate integration test"
depends = ["build-task-mcp"]
run = """
cd services/task-mcp-service && bun test
cd "$MISE_PROJECT_ROOT" && bash services/task-mcp-service/test/hook-integration.test.sh
"""

[tasks.test]
description = "Test every workspace package via nx (TS packages: bun test; @nsheaps/agents-plugins: claude plugin validate)"
description = "Test every workspace package via nx (TS packages: bun test; @nsheaps/agents-plugins: claude plugin validate) + the task-utils MCP server"
depends = ["test-task-mcp"]
run = "bunx nx run-many --target=test"

[tasks.check]
Expand Down
7 changes: 4 additions & 3 deletions plugins/claude-code/task-utils/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "task-utils",
"version": "0.3.0",
"description": "Task discipline hooks (0-or-1 in_progress invariant, write-tool gating, validation-steps + RESULT cites) + task-manage skill for atomic-task workflow. Blocking behaviour is configurable via plugins.settings.yaml.",
"version": "0.4.0",
"description": "Task discipline hooks (0-or-1 in_progress invariant on flat YAML store, write-tool gating, validation-steps + RESULT cites, PostToolUse legacy-sync, native-task advisory/block, pull sync) + task-manage skill + MCP task server (primary task path)",
"author": {
"name": "Nathan Heaps",
"email": "nsheaps@gmail.com",
Expand All @@ -15,6 +15,7 @@
"claude-code",
"agent-discipline",
"validation-steps",
"in-progress-invariant"
"in-progress-invariant",
"mcp"
]
}
15 changes: 15 additions & 0 deletions plugins/claude-code/task-utils/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"mcpServers": {
"task-mcp": {
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT}/services/task-mcp-service/launch.sh"],
"env": {
"CLAUDE_PLUGIN_ROOT": "${CLAUDE_PLUGIN_ROOT}",
"CLAUDE_PLUGIN_DATA": "${CLAUDE_PLUGIN_DATA}",
"TASK_UTILS_TASK_DIR": "${TASK_UTILS_TASK_DIR}",
"CLAUDE_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}",
"CLAUDE_CODE_SESSION_ID": "${CLAUDE_CODE_SESSION_ID}"
}
}
}
}
112 changes: 73 additions & 39 deletions plugins/claude-code/task-utils/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# task-utils

Task discipline hooks and the `task-manage` skill (renamed from `manage-tasks` per noun-verb naming convention) — packages the workflow originally proven out in [nsheaps/.ai-agent-alex](https://github.com/nsheaps/.ai-agent-alex).
Task discipline hooks and the `task-manage` skill — packages the workflow originally proven out in [nsheaps/.ai-agent-alex](https://github.com/nsheaps/.ai-agent-alex).

## What it provides

Expand All @@ -12,58 +12,79 @@ Task discipline hooks and the `task-manage` skill (renamed from `manage-tasks` p
- Parses `<validation-steps>` blocks from task descriptions and requires `RESULT(...)` lines on every checked `- [x]` step before allowing `* → completed`.
- Emits lifecycle coach messages (STARTED / COMPLETED) after allowed transitions.
- On TaskCreate, emits a `BEHAVIOR_CHANGING_COACH` reminding the agent that skill/plugin/hook updates jump the queue rather than being end-of-list ticketed.
- Scans **only the flat MCP YAML store** (`<store-root>/<id>.yaml`); the legacy per-session JSON store is not consulted.

- **`require-task-in-progress.sh`** — gates `Write` / `Edit` / `MultiEdit` / `NotebookEdit`:
- Denies the write tool if no task is currently `in_progress`. Keeps file edits attached to a tracked unit of work.
- **Known soft spot:** the `Bash` tool is NOT in the PreToolUse write-tool set (Claude Code limitation — Bash isn't in the matched set). So `bash -c 'cat > foo.txt'`, `sed -i`, `tee`, etc. bypass this gate. The invariant is "edits via Claude's native write tools require an in_progress task"; file writes via shell commands are not gated and rely on agent discipline alone.
- Denies the write tool if no task is currently `in_progress` in the flat MCP YAML store.
- **Opt-outs:** set `TASK_UTILS_REQUIRE_TASK=0` (env var) or `requireTaskInProgress: false` in `plugins.settings.yaml` to disable the gate entirely.
- **Known soft spot:** the `Bash` tool is NOT in the PreToolUse write-tool set (Claude Code limitation — Bash isn't in the matched set). So `bash -c 'cat > foo.txt'`, `sed -i`, `tee`, etc. bypass this gate.

- **`task-native-warning.sh`** — fires on `TaskCreate` / `TaskUpdate` (native built-in Task tools):
- Advises or blocks use of native Task tools in favor of the MCP server equivalents.
- Behavior controlled by `nativeTaskMode` setting (see Configuration below). Default: `warn`.

**Hooks** (PostToolUse):

- **`task-sync-from-legacy.sh`** — fires after `TaskCreate` / `TaskUpdate` (native built-in Task tools):
- When built-in tasks are enabled, copies the resulting legacy task file (`~/.claude/tasks/<session_id>/<id>.{json,yaml}`) into the flat MCP store (`<store-root>/<id>.yaml`), converting JSON to minimal YAML if needed.
- Makes a best-effort `git add + commit + push` after the sync.
- Always exits 0 — PostToolUse errors never surface to the user. Skipped silently when built-in tasks are disabled (`CLAUDE_CODE_ENABLE_TASKS=0/false/off/no`).

**MCP server** (`task-mcp`):

- A bundled stdio MCP server exposing four tools — `task_create`, `task_update`, `task_list`, `task_get` — that mirror the built-in Claude Code Task tools. It is the **fallback** for contexts where the built-in Task tools are unavailable (notably Claude Code on the web), so the `require-task-in-progress.sh` write-gate stays satisfiable without resorting to `TASK_UTILS_REQUIRE_TASK=0`.
- The server re-implements the `task-invariant.sh` lifecycle invariants in-process (no born-`in_progress`, 0-or-1 `in_progress`, validation-steps required for `pending→in_progress`, `RESULT` lines required for `in_progress→completed`) — the PreToolUse hook does not match MCP tool names, so the server polices itself.
- See the `tool-task-mcp` skill for correct usage.

**Skill**:

- **`task-manage`** — doctrine for the atomicity check, breakdown pattern, status-transition table, validation-steps mechanism, background-subagent (`AGENT(<n>)`) prefix, MONITORING(<monitor-id>) prefix, and behavior-changing-jumps-the-queue rule. Forks into an isolated context via `context: fork` so the parent's window stays lean and returns a ≤5-sentence imperative instruction.
- **`tool-task-mcp`** — guides correct use of the `task-mcp` MCP tools when the built-in Task tools are unavailable.

## Configuration
## Task storage

All blocking behaviour is opt-in via `plugins.settings.yaml` in your project's `.claude/` directory. Add a `task-utils:` section to tune or disable enforcement:
| Layout | Location | Written by |
| -------------------- | ----------------------------------------------------------------------- | ------------------------------------------- |
| Flat (MCP) | `<store-root>/<task-id>.yaml` | the `task-mcp` MCP server |
| Legacy (per-session) | `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/tasks/<session_id>/<task-id>.json` | the built-in Task tools |
| Synced from legacy | `<store-root>/<task-id>.yaml` | `task-sync-from-legacy.sh` PostToolUse hook |

```yaml
# .claude/plugins.settings.yaml
task-utils:
# Master switch — set false to disable all task-utils hooks entirely.
# Default: true
enabled: true

# Enforce the 0-or-1 in_progress invariant (task-invariant.sh).
# Set false to allow multiple concurrent in_progress tasks (e.g. multi-workstream setups).
# Default: true
singleTaskBlocking: true

# Require a <validation-steps> block before a task can move to in_progress,
# and require RESULT(...) lines on checked steps before completing.
# Default: true
requireValidationSteps: true

# Require at least one task in in_progress before Write/Edit/MultiEdit/NotebookEdit
# tools are permitted (require-task-in-progress.sh).
# Default: true
requireInProgress: true
```
**The hooks scan only the flat MCP store** (R1 redesign, 2026-05-24). The legacy per-session store is written by the built-in Task tools; when those tools are used, `task-sync-from-legacy.sh` copies each task into the flat store automatically so the write-gate stays satisfied.

**Defaults are all `true`** — existing installs with no `task-utils:` section in their settings file continue to behave exactly as before. Agents that need to relax the rules can set any combination of the above to `false` without touching the plugin source.
`<store-root>` resolves in this order:

Example — disable all blocking while keeping coaching:
1. `TASK_UTILS_TASK_DIR` — an absolute path, used verbatim when set.
2. `<git-repo-root>/.claude/tasks` — the CWD git repo's tasks directory (default).
3. `<cwd>/.claude/tasks` — fallback when not in a git repo.

```yaml
task-utils:
singleTaskBlocking: false
requireValidationSteps: false
requireInProgress: false
```
On every task create / update / delete the MCP server makes a **best-effort** templated git commit (`chore(tasks): …`) + push of the task file when the store is inside a git working tree — any git failure is logged and swallowed; the task write always succeeds.

Example — disable everything (full pass-through):
## Configuration

Settings are resolved from three tiers (project > user > plugin defaults):

| Tier | Location |
| --------------- | ----------------------------------------------------------------------------- |
| Project | `$CLAUDE_PROJECT_DIR/.claude/plugins.settings.yaml` under `task-utils:` block |
| User | `~/.claude/plugins.settings.yaml` under `task-utils:` block |
| Plugin defaults | `task-utils.settings.yaml` in the plugin root |

Available settings (all camelCase):

| Key | Default | Description |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | `true` | Enable/disable the plugin entirely |
| `nativeTaskMode` | `warn` | What to do when built-in TaskCreate/TaskUpdate are used: `silent` (allow, no advisory), `warn` (allow with advisory), `block` (deny) |
| `requireTaskInProgress` | `true` | Whether the write-gate (`require-task-in-progress.sh`) is active. Set to `false` to disable entirely (also: `TASK_UTILS_REQUIRE_TASK=0` env var) |
| `syncPullEnabled` | `true` | Whether `task-sync-pull.sh` runs before write tools |
| `syncPullIntervalSecs` | `60` | Minimum seconds between pull syncs |

**Example project-level override** (`.claude/plugins.settings.yaml`):

```yaml
task-utils:
enabled: false
nativeTaskMode: silent # suppress advisory when using built-in Task tools
requireTaskInProgress: false # disable write-gate for this project
```

## Installation
Expand All @@ -89,10 +110,23 @@ Then in a fresh session:
claude plugin install task-utils@agents
```

The two PreToolUse hooks register automatically; the skill is available as `Skill(task-manage)` (was `Skill(manage-tasks)` pre-rename).
The two PreToolUse hooks register automatically; the skills are available as `Skill(task-manage)` and `Skill(tool-task-mcp)`. The `task-mcp` MCP server connects automatically when the plugin is enabled — verify with `/mcp`, and run `/reload-plugins` if you enabled the plugin mid-session.

### How the MCP server is built (on-device, on first use)

The MCP server is shipped as TypeScript **source only** (`mcp/src/`). The runnable artifact is a **native executable** produced by `bun build --compile`. Because a compiled binary is platform-specific, it cannot be committed — instead it is built **on the end user's machine, lazily, on first use**:

- The MCP server's launch command is `mcp/launch.sh`. On first run it invokes `mcp/build.sh`, which runs `bun install` then `bun build --compile`; subsequent runs `exec` the binary directly with no rebuild.
- A `SessionStart` hook (`mcp/prewarm.sh`) kicks the same build off in the background at session start, so the binary is usually ready before the MCP server connects.
- The compiled binary lives in the plugin's **persistent data dir** — `${CLAUDE_PLUGIN_DATA}/bin/task-mcp` — so it survives plugin updates. It is keyed by plugin version: a version bump triggers a one-time rebuild. `build.sh` is idempotent and lock-guarded, so concurrent sessions are safe.
- **Requirement:** `bun` must be on `PATH` (install from <https://bun.sh>). If it is missing, the build fails with an actionable message and the MCP server does not start.

For local development / CI, `mise run build-task-mcp` compiles the binary to `mcp/dist/task-mcp` (gitignored — not shipped); `mise run test-task-mcp` builds it and runs the full test suite against it.

See the build scripts (`mcp/build.sh`, `mcp/launch.sh`) for implementation details.

## Design

Worked example for the breakdown pattern: [nsheaps/agents/docs/journal/2026/05/16/entry002-managing-tasks-example.md](https://github.com/nsheaps/agents/blob/main/docs/journal/2026/05/16/entry002-managing-tasks-example.md).
Worked example for the breakdown pattern: [nsheaps/agents/docs/journal/2026/05/16/managing-tasks-example.md](https://github.com/nsheaps/agents/blob/main/docs/journal/2026/05/16/managing-tasks-example.md).

**Internal reference (private):** the full spec lives at `docs/specs/draft/task-discipline-plugin.md` and `docs/specs/draft/manage-tasks-skill.md` in the `nsheaps/.ai-agent-alex` private repo where the workflow was proven out. The behavior is fully documented in the SKILL.md doctrine sections in this plugin; the spec drafts are mainly useful for the design rationale + change history.
Loading
Loading