From 392c7c1ada310b2f928dca89b75ba628478f7694 Mon Sep 17 00:00:00 2001 From: Claude Sonnet 5 Date: Fri, 3 Jul 2026 23:46:26 +0000 Subject: feat(story): add Epic/Story data model + REST CRUD (Phase 7a) Pure data model + CRUD -- no orchestration behavior yet (that's Phase 7b). Revives ADR-007's stories concept (adapted; ADR-007 itself was deleted as "more machinery than the usage pattern needed") while staying lean: reuses the existing events/projects/task-tree machinery rather than building a parallel hierarchy. - internal/story: Epic/Story types. Epic is a loosely-scoped initiative (OPEN/CLOSED, no execution semantics). Story is a shippable slice of work realized by a task tree rooted at root_task_id; epic_id/project_id/ root_task_id are loose references (no FK enforcement, matching the codebase's existing tolerance for unmatched reference strings like tasks.project). - storage: new epics/stories tables (additive migrations) + CRUD methods. Story status is intentionally unvalidated pass-through in this phase -- no task.ValidTransition-style state machine, since there's no orchestrator yet to make transitions meaningful. - event: 9 new Kind constants for the ceremony this layer will eventually drive (epic_proposed, discovery_proposed, framing_decided, groomed, prioritized, eval_verdict, arbitration_decided, retro_captured, human_accepted) -- defined but nothing emits them yet. - api: GET/POST /api/epics, GET/PUT /api/epics/{id}, GET /api/epics/{id}/stories, GET/POST /api/stories, GET/PUT /api/stories/{id}, and GET /api/stories/{id}/task-tree -- BFS walk from root_task_id following both parent_task_id children and depends_on-edge dependents (visited-set guarded), returning a flat node list for a later UI phase to render as a graph. Unauthenticated, matching the existing projects/tasks endpoints' posture. go build/vet/test -race -count=1 all pass, full suite. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs --- CLAUDE.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md index 6313f1d..90356e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,7 @@ Config defaults to `~/.claudomator/config.toml`. Data is stored in `~/.claudomat | `internal/sandbox` | `Sandbox` interface (`HostSandbox`/`DockerSandbox`) + pre-tool-use guardrail hooks | | `internal/role` | `RoleConfig`/`Tier`/`Rung` — per-role system prompt + provider/model escalation ladder | | `internal/scheduler` | `Scheduler` — polls role-typed FAILED tasks and retries/escalates them per their role's ladder | +| `internal/story` | `Epic`/`Story` — planning layer above the flat task tree; data model only (Phase 7a), no orchestration | | `web` | Embedded static UI (`embed.go`) | ### Key Data Flows @@ -196,6 +197,12 @@ events: id, task_id, seq, ts, kind, actor, payload_json role_configs: id, role, version, status, config_json, created_at, activated_at, retired_at, proposed_by (UNIQUE(role, version)) + +epics: id, name, description, status, discovery_source, created_at, updated_at + +stories: id, epic_id, project_id, name, branch_name, status, discovery_source, + spec, acceptance_criteria_json, validation_json, deploy_config, + priority, root_task_id, created_at, updated_at ``` The `events` table is the per-task observability stream (`internal/event`); see @@ -212,7 +219,12 @@ enforces "at most one active row per role" transactionally (retire-then-activate in one transaction), the same way `UpdateTaskState` enforces the task state machine in a transaction rather than in schema — see `internal/storage/roleconfig.go`. -JSON blobs: `config_json` (AgentConfig on `tasks`; RoleConfig on `role_configs`), `retry_json`, `tags_json`, `depends_on_json`, `interactions_json`, `changestats_json`, `commits_json`. +JSON blobs: `config_json` (AgentConfig on `tasks`; RoleConfig on `role_configs`), `retry_json`, `tags_json`, `depends_on_json`, `interactions_json`, `changestats_json`, `commits_json`, `acceptance_criteria_json`. + +`epics`/`stories` (`internal/story.Epic`/`internal/story.Story`) are the +planning layer that sits above the flat task tree — see "Planning layer: +epics & stories" below. This phase (7a) is pure data model + CRUD: no +orchestration reads or writes these tables yet. --- @@ -312,6 +324,45 @@ Two prerequisites for fan-out patterns (e.g. a Builder task with several role-ty - **Auto-cascade-fail** (`internal/executor.Pool.cascadeFail`): the moment a task lands in a terminal failure state (`FAILED`/`TIMED_OUT`/`CANCELLED`/`BUDGET_EXCEEDED` — checked in `handleRunResult` plus the two direct-cancel paths in `execute()` for budget-gate and dependency-failure), the pool looks up `Store.ListDependents(taskID)` (a full-table scan over `depends_on_json` — no reverse index, same tradeoff already accepted elsewhere for JSON-blob columns) and cancels every direct dependent still waiting (`PENDING`/`QUEUED`), recording a `CANCELLED` execution whose `error_msg` references the upstream task and writing the ordinary `KindStateChange` event via `UpdateTaskState` (no new event kind). It recurses into each cancelled dependent's own dependents (visited-set guarded against a pathological dependency cycle — task creation does not itself reject cycles), so a multi-level chain cascades all the way down. `execute()`'s dependency-wait requeue loop re-checks the task's fresh DB state before dispatching so a task cascade-cancelled out from under it is never actually run. - **Role-typed subtask spawning** (`agentchannel.SubtaskSpec.Role`, threaded through `storeChannel.SpawnSubtask` in `internal/executor/channel.go`): when a spawning agent sets `role` (available as an optional `spawn_subtask` tool parameter on both transports — `internal/agentloop/tools.go` for the native tool-use loop, `internal/executor/agentmcp.go` for the MCP transport), the child task gets `Agent.Role` set and `Agent.Type`/`Agent.Model` left empty, so Phase 5's role-based dispatch resolves them from the role's escalation ladder on the child's own first dispatch. Omitting `role` (every pre-Phase-6 caller) preserves the exact prior behavior (`Agent.Type: "claude"`, `Agent.Model` from the `model` argument). +### Planning layer: epics & stories (data model only) + +`internal/story` defines `Epic` and `Story` — a planning layer above the flat +task tree (`epics`/`stories` tables, see Storage Schema above). This is +Phase 7a: **pure data model + CRUD**. Nothing creates, transitions, or reads +these rows automatically yet — no watcher spawns Builder/Evaluator/ +Arbitration tasks from a story, no deploy-gating, no epic-proposal agent +tooling. A later phase builds that orchestration on top of what's stored +here. + +- **Epic** (`epics` table): a loosely-scoped initiative that decomposes into + Stories. `status` is `OPEN`/`CLOSED`, unvalidated pass-through (`UpdateEpic` + writes whatever it's given, same trust level as `UpdateProject`). +- **Story** (`stories` table): a shippable slice of work realized by a tree + of `internal/task` Tasks rooted at `root_task_id`. `epic_id`/`project_id`/ + `root_task_id` are loose references (plain strings, no FK enforcement) — + same tolerance the codebase already has for `tasks.project`. `status` is + one of `DISCOVERY|FRAMING|BACKLOG|PRIORITIZED|IN_PROGRESS|SHIPPABLE| + DEPLOYED|VALIDATING|REVIEW_READY|NEEDS_FIX|DONE|CANCELLED`, also + unvalidated pass-through in this phase (no `task.ValidTransition`-style + enforcement — there's no orchestrator yet to make transitions meaningful). +- **`GET /api/stories/{id}/task-tree`** walks the task graph realizing a + story: starting at `root_task_id`, it follows both `parent_task_id` + children (`ListSubtasks`) and `depends_on_json` edges in either direction + (`ListDependents`), so a task that merely depends on something already in + the tree is discovered even if it isn't a literal `parent_task_id` child. + Returns a flat node list (`{story_id, root_task_id, nodes: [{id, name, + state, agent_type, role, parent_task_id, depends_on}]}`); clients + reconstruct the graph from each node's `parent_task_id`/`depends_on`. An + unset `root_task_id` returns an empty node list, not an error. +- New `event.Kind` constants for the ceremony this layer will eventually + drive (`epic_proposed`, `discovery_proposed`, `framing_decided`, + `groomed`, `prioritized`, `eval_verdict`, `arbitration_decided`, + `retro_captured`, `human_accepted`) are defined in `internal/event` but + nothing emits them yet. +- REST endpoints are not gated by `api_token`, consistent with + `/api/projects`/`/api/tasks` (only chatbot MCP, agent MCP, and WebSocket + require it). + --- @@ -411,6 +462,16 @@ In `executor.go`, `withFailureHistory` creates a copy of the task struct (`copy | POST | `/api/roles/{role}/versions` | Create a new draft `role_configs` version | | GET | `/api/roles/{role}/versions` | List all versions for a role | | POST | `/api/roles/{role}/activate?version=N` | Activate a version (atomically retires the prior active one) | +| GET | `/api/epics` | List epics; `?status=OPEN` | +| POST | `/api/epics` | Create epic | +| GET | `/api/epics/{id}` | Get epic | +| PUT | `/api/epics/{id}` | Update epic (name/description/status; unvalidated pass-through) | +| GET | `/api/epics/{id}/stories` | List stories whose `epic_id` matches `{id}` | +| GET | `/api/stories` | List stories; `?status=X&epic_id=Y` | +| POST | `/api/stories` | Create story | +| GET | `/api/stories/{id}` | Get story | +| PUT | `/api/stories/{id}` | Update story (all fields but id/created_at; unvalidated status pass-through) | +| GET | `/api/stories/{id}/task-tree` | Walk the task graph realizing a story (parent_task_id + depends_on edges) | --- -- cgit v1.2.3