Agent-driven task decomposition via the aeos skill - #2
Merged
Conversation
TASK_BREAKDOWN previously stopped at AWAITING_DECOMPOSITION and required a human to retype the breakdown into `ticket create --parent` calls. The architect now creates the child tickets itself. Approach: rather than parsing tasks out of the artifact, ship an `aeos` skill that teaches an agentic executor to call the CLI. One canonical copy lives at .aeos/skills/aeos/, relative-symlinked into each executor's skills dir (.claude/skills, .augment/skills) by `project init`, so a single definition serves whichever backend runs — the executor abstraction stays intact. Falls back to a copy where symlinks are unavailable. TASK_BREAKDOWN becomes agentic; the architect writes tasks.md and calls `aeos ticket create "<title>" --parent <epic>` per task. Three supporting changes make that safe: - `ticket create` is idempotent by (parent, title): a matching child short-circuits to the existing ticket. The review loop may retry the breakdown, and a retry must not duplicate tasks. - New column-spec flag `requiresRepoDiff` (default true) decouples "needs agentic/tool execution" from "must leave a repo diff". TASK_BREAKDOWN sets it false — creating tickets and writing a gitignored artifact is not a repo edit, so the IMPLEMENTATION diff guarantee must not fail it. - The orchestrator needs no decomposition step: children created during the run are picked up by its existing drive-all-children logic on the next tick. AWAITING_DECOMPOSITION remains the fallback when none appear (AEOS_EXECUTOR=stub can't shell out, so offline runs still halt there). Verified end to end: a fresh `project init` links the skill into both executor dirs, and creating the same task twice under an epic short-circuits to the existing ticket. 620 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four findings from the review of the decomposition PR. 1. Portability (skill-source.ts): the finding was that opencode-cli gets no skill link and would silently stall. In fact the decomposition instruction is carried by the architect's taskInstruction (the prompt), which reaches every executor — the skill is only reinforcement. Corrected the comment to document the portable carrier so this isn't misread again. 2. Idempotency keyed on title, but the retry it guards against is exactly when titles drift: the review loop re-runs the agent, which may rephrase a task's wording, and title-only dedup would then duplicate it. Added a stable decomposition key — `aeos ticket create --key T-NNN`, persisted as task_key (migration v4, idempotent), matched under a parent in preference to the title. The architect and skill now pass the T-NNN label. Falls back to title when no key is given. 3. Tickets are created mid-run before review, so an abandoned breakdown leaves orphan BACKLOG children that block the epic's DONE-join. The key fix stops retries from accumulating (same keys reuse); a genuinely removed task is cleared by the operator via `ticket move <id> DONE`, which the join's blocker list points at. Auto-deleting user tickets on a breakdown change is deliberately avoided. Documented. 4. skill-source had no packaging guard, unlike the sibling template-source: a missing skills/ dir would throw a cryptic ENOENT mid-scaffold. Added an explicit packaging-fault error. Verified end to end: a task re-created with a rephrased title but the same --key short-circuits to the existing ticket; migration v4 applies cleanly to a v3 database and is idempotent. 624 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four findings from reviewing the --key fix itself. 1. The prompt claimed a keyed retry "updates in place," but the code returns the existing ticket unchanged — a reworded title was silently discarded. Aligned the architect prompt and skill with the actual behaviour: a key match leaves the task unchanged, so keep titles stable across attempts too. 2. A reused key silently drops a task (the second create matches the first by key). The example hardcoded `--key T-001`, inviting copy-paste. Now shows two distinct keys and states plainly that each task needs its own key. 3. A keyed lookup ignored keyless children, so switching a task from unkeyed to keyed across attempts duplicated it. Added a keyless-title bridge: when a key finds no match, a keyless child with the same title (a pre-key attempt) is matched instead. It only matches keyless children, so it can't collide with a differently-keyed task. 4. `taskKey` was persisted even without a parent, contradicting the port's "ignored without a parent". Now gated: an epic never carries a key. Verified end to end: an unkeyed create then a keyed retry of the same title does not duplicate; an epic created with a stray --key stores no key. 627 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of five review findings applied; three skipped with reason. Applied: - TASK_BREAKDOWN is agentic, which grants the agent Write/Edit tools, and the prompt told it to "write the tasks.md artifact at your output path". But AEOS captures the agent's stdout as the artifact and never passes an output path — a file the agent writes is ignored. If the agent wrote a file and printed only a summary, tasks.md would be a thin stub (failing minWordCount, or misreviewed). Reworded the architect prompt and skill: produce the breakdown as response text; a written file is ignored, only printed output is captured. - skill-source: replaced the hand-rolled recursive copyDir with fs.cpSync (recursive), available on the required Node 22. Skipped: - Keyless-bridge match doesn't backfill the key onto the matched child, so a keyless-first task that is later rephrased under a key can still duplicate. Fixing needs a new repo mutator and the template always keys from the start — rare mixed-mode edge, left as a defensive-only bridge. - The idempotency read is outside createAtomic's transaction, so concurrent same-key creates could duplicate. Enforcing needs a UNIQUE constraint that would throw on the race; the agent creates tasks sequentially. Out of scope. - linkOrCopy skips a dangling executor symlink rather than repairing it. Repairing changes the intentional skip-existing behaviour for a rare case. 627 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`orchestrator run` printed only plain step lines; a standalone `ticket run` shows a live split-pane view. Now the orchestrator forwards each inner ticket run's event stream to that same display, so an orchestrated epic renders the same interface per ticket. - OrchestratorObserver gains an optional ticketRunObserver; the use case forwards every inner ticket-run event to it (alongside the lock heartbeat). - The command reuses createTicketRunDisplay: started lazily on the first event (so an immediate halt prints its summary rather than flashing a blank screen), torn down before the end-of-run summary. In a TTY the summary replays the step lines the live pane suppressed; without a TTY, steps print inline as before. - ticket-run-display gained the two cases it was missing — ticket-run.escalated and run.attempt.started — and now resets its per-run panels on ticket-run.started so a display reused across an epic's tickets reflects the current ticket (raw log kept for scroll-back). This also fixes escalation not showing in a standalone `ticket run`. 628 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The orchestrator had no interrupt path, so Ctrl+C mid-run killed the process ungracefully (and, before the terminal-restore fix, left the alt-screen active). It now stops like `ticket run` does. - OrchestratorPort gains interrupt(): sets a flag and forwards to the in-flight ticket run's interrupt() (which kills its executor child). No-op when idle. - The run loop checks the flag before scheduling the next action and again right after each one, so it stops as soon as the interrupted ticket unwinds rather than scheduling more work. New HaltReason.INTERRUPTED; the epic is left IDLE (not RUNNING), so it is not locked out and re-running resumes. - The flag is reset at the start of each run, so a stray interrupt with nothing running does not poison the next run. - The command captures the orchestrator instance and wires SIGINT -> display.requestInterrupt() + orchestrator.interrupt(), removing the handler in both success and error paths. Exit code 130 on interrupt (conventional). Verified end to end: SIGINT mid-run halts with INTERRUPTED, exit 130, epic left IDLE with a resume message. 631 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A ticket left WORKING or IN_REVIEW (a run killed before it could transition — a crash or a pre-graceful interrupt) fell through to the generic halt: "…which the orchestrator has no action for," which tells the operator nothing. The orchestrator still can't tell a live run from an orphan, so it correctly declines to touch the ticket — but the recovery is concrete. It now names it: "reset it with `aeos ticket ready <id>` and re-run." Applies to both an epic stuck in its own column and a child task. 633 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… CLI The pipeline computed a precise escalation reason (ITERATIONS_EXHAUSTED, NOT_CONVERGING, UNPARSEABLE_VERDICT, PREFLIGHT_BLOCKERS) and a human-readable message, then threw both away at end-of-run — they lived only in the live run event. An operator asking "why did this stall?" later had to reverse-engineer it from the review artifact on disk, and `orchestrator status` could only say "needs a human", which is exactly what escalation.ts promises it won't. Now the escalation is persisted on the ticket (migration v5: escalation_reason /message/artifact) and surfaced: - `aeos ticket show` → a Reason: line + the artifact to read first - `aeos orchestrator status` / halt messages → the reason inline The reason is cleared automatically the moment the ticket leaves ESCALATED/ BLOCKED — the clear lives in updateSubState, the single chokepoint for sub-state changes, so every path (run start, `ticket ready`, advance) drops a stale reason with no call-site churn. Tests: repo round-trip + retain/clear invariant, policy message includes the reason. 639 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TECH_SPEC runs in artifact mode: the agent's stdout IS the artifact (the adapter writes it), and `claude --print` grants no file-write permission. But the architect's TECH_SPEC instruction said "Produce a detailed technical specification as a single Markdown file" — so the agent reached for the Write tool, had it denied in headless mode, narrated "file write permissions aren't being granted. Let me output directly instead", and dumped a *partial summary* to stdout. That truncated, error-prefixed text became the artifact and the reviewer rejected it (2 blockers: leaked-tool-error, spec-content-not-reproduced). Mirror the capture contract already stated for TASK_BREAKDOWN: print the full spec as the response, never use Write/Edit, and don't hedge into a summary. TASK_BREAKDOWN (agentic, does need Bash to create tickets) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
During a run — standalone or orchestrator-driven — the display named the stage
("Worker") and the executor/model, but not *which agent* was working: architect,
pm, and reviewer all showed up as "Worker". You couldn't tell what the pipeline
was actually doing at a glance.
Thread the agent spec name through the stage.started event (worker, reviewer,
preflight) and render it:
- live meta line + summary pane → "Agent: architect-agent"
- plain/log mode → "[stage:worker] started | … | agent=architect-agent"
The column (pipeline stage) was already in the header; this fills the other half
of "where are we and who's working". 640 tests, typecheck, lint, build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n TASK_BREAKDOWN TASK_BREAKDOWN is agentic and the executor grants the Bash tool, so `aeos ticket create` runs without any approval step — verified by reproducing the exact `claude -p … --permission-mode acceptEdits --allowedTools Bash,…` invocation, which created a ticket cleanly. Yet the architect (opus) sometimes narrates "aeos ticket create requires approval and was not granted in this non-interactive session… re-approve aeos and I'll create the tickets" and creates nothing — a fabricated permission block, the same excuse-narration failure mode as the TECH_SPEC "file write permissions aren't being granted" one. Tell the agent explicitly that the CLI is pre-authorized, to run it directly, never claim it needs approval, and that a breakdown with no child tickets is a failed run. Same discipline guard already added for TECH_SPEC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rrent templates A project scaffolded by an older `project init` keeps its original column specs, agents, and rubrics forever — template fixes never reach it, and the drift only surfaces when a ticket reaches the affected column (e.g. TASK_BREAKDOWN still on executorMode: artifact, so the architect had no Bash tool to create child tickets). There was no way to see or close that gap short of hand-diffing files. `aeos project sync`: - default: dry run — lists missing / drifted / up-to-date files - --diff: line diff (LCS, context-collapsed) for each drifted file - --apply: writes missing files (always safe) - --force: also overwrites drifted files, backing each up to <file>.bak first No three-way merge: AEOS doesn't store the template version a file came from, so it can't distinguish a local edit from a template change within one file. Sync is honest about that — it never silently overwrites a local edit; --force keeps a .bak so a change like `advanceMode: auto` is recoverable. Hexagonal: TemplateCatalog (driven) → ProjectSyncUseCase → ProjectSyncPort → command; FsTemplateCatalog reuses readTemplates(). 648 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…content Two gaps in agentic decomposition made child tickets nearly useless to the engineer that later implements them: 1. No lineage in the ticket document. The AEOS:METADATA block recorded only Column and Sub-state — a task's own document never named its parent epic or kind. Add Kind, and Parent/Task key when present. 2. Only a title, no content. `aeos ticket create` accepted just a title, so the architect's `create "<title>" --parent … --key …` dropped every task's Description, Acceptance criteria, Touches, and Out-of-scope from tasks.md. The child then ran IMPLEMENTATION from a bare title. Add `--body`/`--body-file` to `ticket create`; the body becomes the ticket's Description verbatim (placeholders remain for hand-created tickets). The architect prompt and the aeos skill now write each task's block to a temp file and pass `--body-file`, so the full breakdown reaches the child. Verified end-to-end: a created task shows Kind/Parent/Task-key metadata and the task body in its Description. 653 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
During an orchestrator run it was hard to tell which task was being driven and at what pipeline stage. The live pane's header already showed ticket + column, but the orchestrator's own step lines and the plain-mode/scrollback output did not. - OrchestratorStep now carries the column it acted on (captured before the action, so an 'advance' reports the stage it drove, not the destination). - Progress lines render it: `STAN-2 [IMPLEMENTATION]: run succeeded …` — both the inline non-TTY prints and the end-of-run replay. - The run-started log line now includes the column too, so plain mode and live scrollback show `[run] started | STAN-2 | IMPLEMENTATION | executor=…`. 653 tests, typecheck, lint, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agent-driven task decomposition, plus a run of fixes and observability
improvements found while dogfooding the epic pipeline end-to-end.
Decomposition
TASK_BREAKDOWNis agentic: the architect callsaeos ticket create --parent --keyto create child tasks itself (idempotent on--key).--body/--body-file)and parent lineage in the document metadata (Kind / Parent / Task key) —
previously they were created title-only.
TECH_SPEC (artifact mode) and TASK_BREAKDOWN (agentic).
Orchestrator & escalation
WORKING/IN_REVIEW.and surfaced by
aeos ticket showand the orchestrator halt — no more"NEEDS_HUMAN with no explanation".
Observability
output shows task + column (
STAN-2 [IMPLEMENTATION]: …).Project maintenance
aeos project syncreconciles a project's scaffolded column specs,agents, and rubrics with the current templates (dry-run /
--diff/--apply/--forcewith.bakbackups). Closes the "project scaffoldedfrom an old template stays stale forever" gap.
All green: typecheck, lint, build, 653 tests.
🤖 Generated with Claude Code