Appearance
task_create.py is where a Task is born with its contract attached. Two flags — --files-expected and --required-reads — turn a task row into a hook-enforced obligation.
Implementation
task_create.py
.claude/tools/task-management/task_create.py
What it is
The Task's birth point.
A ~12-flag CLI that wraps TaskManager.create_task() and writes one row to the task list.
⚡
/planning
/build
the invoker
→
🔧
task_create.py
parses flags, writes row
→
📋
task-list.json
one new Task row with contract attached
Every Task in the planner-workflow system came through this script.
Which means every Task's contract was declared here, not negotiated later.
The two flags that matter
Two flags turn a task row into an obligation.
--files-expected
repeatable JSON object
"The agent will create, modify, or delete exactly these files with these symbols."
🚨
Enforced by product-spec-stop-verify.
Agent cannot emit Stop until disk state matches.
--required-reads
repeatable path
"The agent must read these files before any Write or Edit."
🚧
Enforced by context-gate.
Blocks Write/Edit until all required reads have happened.
Without these two flags, Task is just subject + description.
With them, Task is a hook-enforced obligation.
The JSON contract shape
Strict parsing. Malformed JSON exits with error.
--files-expected examples
create
{"path": "src/foo.ts", "op": "create"}modify
{"path": "src/bar.ts", "op": "modify",
"symbolsRequired": ["exportedFn"],
"symbolsForbidden": ["oldPattern"]}delete
{"path": "src/old.ts", "op": "delete"}No silent acceptance of typo'd flags — malformed JSON exits 1 with an error. The contract is declared or the task isn't created.
The layer gate
Unknown layers rejected at argparse, before any row is written.
--layerschema
--layerroute
--layerbuisness-logic
→
🛑
choices=[...]
at argparse · before write
schemaserviceroutecomponenthooktestconfiginfracontractagent
Downstream layer-dispatched verification always gets a known value to key on.
A typo like buisness-logic never reaches disk.
The output contract
JSON envelope means agents parse, not scrape.
Every invocation emits parseable JSON. No downstream regex.
$python3 task_create.py--subject "Add auth middleware" ...
↓
stdout · success {"success": true, "taskId": "P1-T1", "task": {...}}
stderr · failure {"success": false, "error": "..."}
# wiring the next task's dependency:
TASK_ID=$(python3 task_create.py ... | jq -r .taskId)
python3 task_create.py ... --depends-on$TASK_ID
Both success and failure are JSON-parseable. taskId flows into the next call's --depends-on.
No brittle text parsing between phases.
The invocation rule
python3, not uv run. Here's why.
Task tools run inside hooks, inside subprocesses, mid-session
called from many places, many times, under time pressure
uv run task_create.py
- resolves dependencies every call
- slow — caller waits for env setup
- resource-hungry inside subprocess chains
- fails if uv isn't on PATH in this subprocess
python3 task_create.py
- pure stdlib + one local import
- instant — no env resolution
- safe to call from anywhere
- works inside any subprocess, hook, or agent session
The single import beyond stdlib is task_manager.TaskManager — itself pure-Python, reading/writing JSON directly. Zero-dependency by discipline.
The role
A Task starts life with its contract attached.
The CLI is the single gate. Every flag declared here becomes an enforcement point downstream — in hooks that block writes, in verifiers that block Stop, in dispatchers that route by layer. One script, many downstream enforcers.