Iteration 1 — one task, end to end, from MMDEP's own store¶
Goal¶
MMDEP runs one task end to end from its own store. A task is added to the store, the dispatcher picks an agent from the registry that is allowed in that project and role, and the agent works in the task's own git worktree on task/<id>. The result is committed there and an approve item is raised, then the task parks and the dispatcher returns at once. When the person responds in the inbox, the next dispatcher pass merges to main (or cancels the task) and removes the worktree. Every step is in the ledger.
Done when: the end-to-end test (story 5) passes offline in CI-style pytest -q, and one live run on a throwaway project (see item for S-A below) shows the same ledger trail.
Stories (in build order)¶
Stories 1–3 don't depend on each other, so I can build them one after another without waiting on merge approvals. Story 4 needs 1–3, and story 5 needs all of them.
1. iter-01/tasks — Tasks in the store · M¶
- A
taskstable has: id, project, title, description, role, assigned_agent, executed_by, state, attempts, item_id, branch, spec_version, risk, created, updated. - The states are exactly the spec's lifecycle: Backlog, Ready, Running, AwaitingHuman, Blocked, Done, Cancelled. A transition that isn't in the spec diagram raises
MMDEPError. - Every transition writes a
task.<state>ledger entry with the actor. mmdep task add | list | showwork against the same store.- The schema change is additive only (
CREATE TABLE IF NOT EXISTS). A test opens a database with the seed schema, and existing items and ledger rows are untouched.
2. iter-01/registry — Agent registry · M¶
agents.yamlis read fromMMDEP_HOME. If it isn't there, a packaged default is used (mmdep/agents/agents.default.yaml).- There is one adapter per kind in
mmdep/agents/:ollama,gemini,claude_cli,chatgpt_cli(a placeholder that reports "not installed"),human, plusfakefor tests. They are copied fromlegacy/mmdep_orchestrator.py; legacy itself stays untouched this iteration. - Every adapter has the same interface:
run(prompt, workdir) -> AgentResultandhealth() -> (ok, reason). mmdep agentslists each agent with its name, kind, hosting and health. Health checks are cheap and time-boxed.- An unknown kind or a missing field in the YAML gives a clear error that names the entry.
- All tests run without network, Ollama or API keys.
3. iter-01/workspaces — Git workspaces · M¶
ensure_origin(project)createsMMDEP_HOME/repos/<project>.gitas a bare clone of the project'smainif it doesn't exist yet.create_worktree(task)createsMMDEP_HOME/workspaces/<project>/<task-id>/on branchtask/<id>from originmain.commit_all(task)commits the worktree's changes and returns the sha; it returns None when nothing changed.merge(task, approver, item_id)mergestask/<id>into originmain, and the commit message names the approver and the item. A conflict leavesmainunchanged and raises an error.cleanup(task)removes the worktree and the branch. It is safe to call twice.- Each of these operations writes a ledger entry. The tests use temporary git repos, and paths go through
pathlib(Windows first).
4. iter-01/isolation — Isolation rules · M¶
agents.yamlhas a role map per project listing which agents may act in which roles. Formmdep, only thetest_runnerandreviewerroles are listed, and no Claude agent appears in them.- The dispatcher refuses an agent that isn't allowed for the project and role. It writes
task.refusedto the ledger and the task stays Ready. - The agent runner refuses any workdir that isn't the task's own worktree under
MMDEP_HOME/workspaces/. The main project folder is explicitly refused, and this is tested for each adapter kind. claude -pruns withcwd=the worktree and--permission-mode acceptEdits, never with--dangerously-skip-permissions.- Each agent has an optional
max_calls_per_dayinagents.yaml, counted from today'sagent.callledger entries. An agent over its cap reports unhealthy with the reason "cap reached" and is skipped.
5. iter-01/run-task — Run a task end to end · L¶
mmdep run <project>is one dispatcher pass and never blocks on a person.- It first handles each AwaitingHuman task whose item is closed: an approval means merge, Done and cleanup; a rejection means Cancelled and cleanup.
- It then takes the highest-priority Ready task, provided nothing is already Running in the project. That is the one-slot rule, and the slot times out after 30 minutes.
- For that task it creates the worktree, runs the agent, and commits on
task/<id>. It then raises anapproveitem linkingbranch:task/<id>(priority taken from the project), sets the task to AwaitingHuman, and returns. - If an agent fails, the task goes back to Ready with
attempts+1. After 3 failed attempts the task parks with areviewitem instead of retrying for ever. There is no fallback chain yet. - If the agent produced no changes, the task goes straight to Done with the ledger note "no changes"; no item is raised.
- A
branch:task/<id>link resolves its diff from the MMDEP origin repo, so the person sees the diff in chat or the CLI as usual. - End-to-end test (temporary hub, temporary project repo,
fakeagent): - Add a task.
- Run a pass: the task becomes AwaitingHuman and an item is open.
- Add and run a second task in the same project while the first is parked. It must also reach AwaitingHuman, which shows nobody waits.
- Approve the first item through the store and reject the second.
- Run a pass: origin
maincontains the first change with the approver and item id in the merge commit, the second task is Cancelled, and neither worktree remains. - The ledger trail lists add → running → agent.call → commit → item.raise → awaiting_human → item.approve → merge → done, in order.
Not this iteration (back to backlog)¶
- Test Runner role. It is first in line for iteration 2, and the registry and role map from stories 2 and 4 are its prerequisites.
- LangGraph checkpointer and
interrupt(). Parking here is a state plus an item id in the tasks table (assumption A2). - Fallback chain, circuit breakers and review debt.
- Blocked detection by file overlap. The Blocked state exists, but nothing sets it yet.
- A dispatcher daemon or scheduler.
mmdep runis run by hand or from Task Scheduler. - MCP tools for tasks. Atul sees tasks through the items they raise, which is enough for now.
- Deleting
legacy/(it goes once the fallback chain and benchmark are absorbed), the mcp v2 migration (not yet inbacklog.md; I'll add it), and releasing tommdep_runtime, which gets its own release item after this iteration's review.
Assumptions (proceeding on these; approving the plan confirms them)¶
- A1. Hub layout is
MMDEP_HOME/repos/<project>.gitandMMDEP_HOME/workspaces/<project>/<task-id>/. The spec'sisolated_workspaces/sits under the hub rather than next to it. - A2. For iteration 1, parking means a task state plus an item id in the store rather than a LangGraph
interrupt(). Single-step tasks don't need a graph yet (principle 7). The backlog already lists the checkpointer under "Later". - A3. Merging happens on the next
mmdep runpass after the approval, not at the moment ofrespond. That keepsresponda pure inbox action with no git side effects in the chat process. - A4. Text agents (Ollama, Gemini) keep the legacy pattern: they return a Python script that passes a compile gate and is then run with
cwd=the worktree, with a timeout. CLI agents edit files in theircwddirectly. Either way, MMDEP commits what changed. - A5. The Claude cap counts calls per agent per UTC day from the ledger. There's no token accounting yet.
- A6.
pyyamlis added as a core dependency. The spec namesagents.yaml; PyYAML is already installed throughlangchain-core, is pure Python on Windows, and is the most common YAML reader. The alternative is TOML from the standard library, which needs Python 3.11 (we declare 3.10+), or JSON, which is harder to edit by hand.
Open questions (raised as specify items)¶
- S-A. Where does the live end-to-end run happen, and with which agent? The
mmdepproject can't have code-changing agents. - S-B. How does the MMDEP origin (
repos/<project>.git) relate to the project's own working repo, which people also commit to?
Decisions and changes after approval¶
- Item #1 (plan): approved by atul on 2026-09-25, including A1–A6 and pyyaml.
- Item #2 (S-A): the live run uses a throwaway
sandboxproject with DeepSeek via Ollama, with Qwen as the fallback. Claude is capped at 0 calls per day. Scope change: story 5 gains a minimal fallback. When an agent fails, the task goes Running → Ready (as in the spec diagram) and the next attempt uses the next agent in the role's allowed list;executed_byrecords who did it. The full fallback chain, circuit breakers and review debt stay out of this iteration. - Item #3 (S-B): option (a) with a narrower rule. The MMDEP origin is the source of truth, and working repos add it as remote
mmdep. Anyone may push branches to it, but only MMDEP updatesmain, and only after an approved item. Story 3 enforces this with a hook on the origin that refuses pushes tomain; MMDEP updatesmainlocally, where the hook doesn't apply. Story 3 also proposes the matching change to the spec section "Workspaces and git" for approval. -
Refinement (story 1): approval follows the spec diagram exactly. On approval the task goes AwaitingHuman → Ready with
step=merge, and the next pass runs it (Ready → Running) to merge and then Done. The merge therefore takes the project slot like any other step. Story 5's ledger trail becomes: … item.approve → task.ready → task.running → task.merge → task.done. -
Items #4, #5, #6: stories 1–3 approved by atul on 2026-09-25 and merged in that order. The expected
cli.pyconflict was resolved by keeping both sides.mainhas 48 tests passing, and 23 in legacy.
Answer to the note on #4: who can move a task out of AwaitingHuman¶
Only a response to the task's linked item can move it, never an agent directly. Story 4 makes this a rule in the store, not a convention:
Store.transition()refuses any move out of AwaitingHuman. The only way out isStore.resume_from_item(task_id).resume_from_itemreads the task'sitem_idand refuses if the item is still open. The target state comes from the person's response, not from the caller:approve,answerordonego to Ready, andrejectgoes to Cancelled. The ledger entry records the item number and who responded.- Agents can't reach either function. Agents get only a prompt and a workdir, never the store. The MCP server has no task tools. An agent's MCP identity can raise items but can't
respond(the seed guard), so it can't produce the response either.
Answer to the note on #6: how MMDEP updates main past the hook, and whether an agent could do the same¶
How MMDEP does it. The pre-receive hook runs only when someone pushes to the origin. MMDEP never pushes main. It writes the merge commit directly into the origin repo (merge-tree, commit-tree, then update-ref refs/heads/main <new> <old>) as a local git operation, and git doesn't run receive hooks for local operations.
Could an agent use the same route? Yes, in principle, and the hook alone doesn't stop it. A task worktree shares its refs with the origin. Every agent runs as the same Windows user as MMDEP. So a script an agent produced and MMDEP ran in the worktree (A4) could call git update-ref refs/heads/main …, or edit the hook, exactly as MMDEP does. Headless Claude has Bash disallowed and couldn't do it. Ollama and Gemini output, though, is Python that MMDEP runs.
What story 4 adds:
1. Detect and restore. Before every agent run, including running a text agent's script, the runner records every ref in the origin and a hash of the hook. Afterwards, any change other than to the task's own task/<id> branch is rolled back: refs are reset or deleted and the hook is rewritten. The run then fails with an isolation.violation ledger entry, and the task doesn't reach the inbox.
2. Fewer tools. The Claude CLI adapter adds --disallowedTools Bash on top of acceptEdits.
3. Merge check. merge() refuses if the origin's main is not the last value MMDEP itself recorded in the ledger, at origin creation or its last merge. A change that slipped past (1) therefore stops merges and is visible, instead of being built on.
Residual risk. Detection happens after the fact. Code running as your Windows user can reach anything that user can reach, including other folders. Real prevention needs agents to run under a separate OS account or in a container, which is the runner work in Phase 1. I'm raising this as a specify item: should the billing project wait for that?
Story 4 as built (additions to the plan's acceptance criteria)¶
- Parking also needs a fresh link: a task enters AwaitingHuman only with an open item, so a second park can't reuse an old, closed approval.
- The role map lives in
agents.yamlunderprojects: <p>: roles: <role>: [agents], in preference order. It denies by default: an unlisted project or role has no agents. Formmdepthe default lists onlytest_runner: [](plain code, iteration 2) andreviewer: [gemini]. -
Script agents (A4) run inside the guarded runner, so the tamper check covers the script as well as the model call. The script file sits beside the worktree and is never committed.
-
Item #7: story 4 approved by atul on 2026-09-26 and merged. Detect-and-roll-back is accepted for iteration 1.
- Item #8: answered. An iteration 2 story, finished before billing starts: agents that run generated code or scripts use a dedicated low-privilege Windows account that can reach only the task workspaces. The proposal must list what that account needs (git, Ollama over HTTP, CLI logins) and any licence or seat implications. The spec is also to record that the long-term enforcement is a protected
mainon the server once origins move to GitLab; that spec change goes on its own branch for approval. - Story 5 changes agreed after the backlog review (atul, 2026-09-26):
- Service down: if every allowed agent is unhealthy, the task parks with a
manualitem that lists each agent, its reason and how to fix it. Respondingdoneretries. Areviewitem is still used when agents ran and failed 3 times. - Crash recovery: the 30-minute slot timeout returns a stuck Running task to Ready, recorded as
task.recovered. - UTF-8 CLI output:
mmdepforces UTF-8 on stdout and stderr, which fixes the crash on "→" when output is piped on Windows.
Story 5 as built¶
- A pass (
mmdep run <project>) goes: recover stuck tasks → release parked tasks whose item has a response (only throughresume_from_item) → Backlog to Ready → one slot per project → run one step of the highest-priority Ready task. - Releasing a task. An approved
approveitem setsstep=merge. A releasedreviewormanualitem retries the work with attempts reset; if the merge had failed, the task restarts from the latestmain. A rejection cancels the task and removes its worktree. - Fallback (item #2). Within a pass, a failed agent sends the task Running → Ready, with the error in the ledger, and the next agent in the role map is tried.
executed_byrecords who did the work. The worktree is reset before each attempt, so the next agent never sees a failed agent's leftovers. - Other outcomes. No changes means Done with no item. An isolation violation or a failed merge parks the task with a
reviewitem. - Line endings. The origin now uses
core.autocrlf=input(it wasfalse). The live run showed that a script on Windows writes CRLF, which turned every line into a change; git now stores LF. - Ledger trail of an approved task: task.backlog → task.ready → workspace.create → task.running → agent.call → workspace.commit → item.raise → task.awaiting_human → item.approve → task.ready → task.update (step=merge) → task.running → workspace.merge → workspace.cleanup → task.done.
Live run (item #2: sandbox, DeepSeek with Qwen as fallback)¶
This ran on a scratch hub, not the real mmdep_hub: the installed runtime has none of this iteration's code, and upgrading it needs a release item.
- Run 1 (63 s): DeepSeek changed calc.py, and the task parked with approve item #1. The diff rewrote every line because of CRLF, which led to the autocrlf=input fix.
- Run 2 (35 s, fresh hub after the fix): a clean diff of 6 insertions, and the task parked with approve item #1. DeepSeek repeated the module docstring. That's a quality problem for the approver, not an MMDEP defect.
- Waiting: Atul's response in the demo hub, then the merge pass.
- Seen along the way: chatgpt (codex) is now on PATH and reports ok. It isn't in any role map.
Findings for the iteration review¶
- Slow
get_itemover MCP.get_itemon item #4 took more than 2 minutes, whilelist_itemsreturned at once. The likely cause is the branch-diff lookup, which runs twogit diff main...<branch>calls in the MCP server process. To be measured and fixed. - An early run of the workspace tests took 68 s instead of about 11 s once. The suspected cause is antivirus scanning new git folders; it hasn't recurred.
Backlog notes¶
- From #4: tasks need a
depends_onfield for the pre-scheduling dependency check. It goes with the backlog entry "Pre-scheduling dependency check". - The mcp v2 migration was added to
backlog.mdon 2026-09-26.
Risks¶
| Risk | Impact | Mitigation |
|---|---|---|
| Store schema change (PROJECT.md: high risk) | Could break the live inbox after a release | Additive table only; test against a seed-schema DB; no runtime release until the iteration review |
| Git worktrees on Windows: long paths, file locks, CRLF | Flaky cleanup or merges | Short paths (workspaces/<p>/<id>), git worktree remove --force + retry, core.autocrlf set explicitly on origin |
| An agent writes outside its worktree | Changes in the wrong repo | cwd guard (S4), no permission bypass for Claude, text agents run as a child process with cwd= the worktree |
| Ollama on CPU is slow for the live run | Demo takes minutes | The live run is a tiny task; the offline test is the real gate |
| Story 5 is L and depends on 1–4 | Iteration slips | 1–3 are independent and done first; if needed, the 3-attempt rule and slot timeout move to iteration 2 |
Order and size¶
| # | Story | Size | Depends on |
|---|---|---|---|
| 1 | tasks | M | — |
| 2 | registry | M | — |
| 3 | workspaces | M | — |
| 4 | isolation | M | 1, 2, 3 |
| 5 | run-task | L | 1–4 |