Skip to content

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 tasks table 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 | show work 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.yaml is read from MMDEP_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, plus fake for tests. They are copied from legacy/mmdep_orchestrator.py; legacy itself stays untouched this iteration.
  • Every adapter has the same interface: run(prompt, workdir) -> AgentResult and health() -> (ok, reason).
  • mmdep agents lists 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) creates MMDEP_HOME/repos/<project>.git as a bare clone of the project's main if it doesn't exist yet.
  • create_worktree(task) creates MMDEP_HOME/workspaces/<project>/<task-id>/ on branch task/<id> from origin main.
  • commit_all(task) commits the worktree's changes and returns the sha; it returns None when nothing changed.
  • merge(task, approver, item_id) merges task/<id> into origin main, and the commit message names the approver and the item. A conflict leaves main unchanged 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.yaml has a role map per project listing which agents may act in which roles. For mmdep, only the test_runner and reviewer roles 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.refused to 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 -p runs with cwd= the worktree and --permission-mode acceptEdits, never with --dangerously-skip-permissions.
  • Each agent has an optional max_calls_per_day in agents.yaml, counted from today's agent.call ledger 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 an approve item linking branch: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 a review item 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, fake agent):
  • 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 main contains 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 run is 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 in backlog.md; I'll add it), and releasing to mmdep_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>.git and MMDEP_HOME/workspaces/<project>/<task-id>/. The spec's isolated_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 run pass after the approval, not at the moment of respond. That keeps respond a 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 their cwd directly. 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. pyyaml is added as a core dependency. The spec names agents.yaml; PyYAML is already installed through langchain-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 mmdep project 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 sandbox project 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_by records 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 updates main, and only after an approved item. Story 3 enforces this with a hook on the origin that refuses pushes to main; MMDEP updates main locally, 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.py conflict was resolved by keeping both sides. main has 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 is Store.resume_from_item(task_id).
  • resume_from_item reads the task's item_id and refuses if the item is still open. The target state comes from the person's response, not from the caller: approve, answer or done go to Ready, and reject goes 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.yaml under projects: <p>: roles: <role>: [agents], in preference order. It denies by default: an unlisted project or role has no agents. For mmdep the default lists only test_runner: [] (plain code, iteration 2) and reviewer: [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 main on 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 manual item that lists each agent, its reason and how to fix it. Responding done retries. A review item 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: mmdep forces 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 through resume_from_item) → Backlog to Ready → one slot per project → run one step of the highest-priority Ready task.
  • Releasing a task. An approved approve item sets step=merge. A released review or manual item retries the work with attempts reset; if the merge had failed, the task restarts from the latest main. 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_by records 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 review item.
  • Line endings. The origin now uses core.autocrlf=input (it was false). 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_item over MCP. get_item on item #4 took more than 2 minutes, while list_items returned at once. The likely cause is the branch-diff lookup, which runs two git 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_on field for the pre-scheduling dependency check. It goes with the backlog entry "Pre-scheduling dependency check".
  • The mcp v2 migration was added to backlog.md on 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