diff --git a/engine/lld_repo_pool.py b/engine/lld_repo_pool.py new file mode 100644 index 0000000..edc6707 --- /dev/null +++ b/engine/lld_repo_pool.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Repo-scoped LLD dispatch — the deterministic half of "one LLD per repo, any repo count." + +`parallel` workflow nodes have a FIXED branch count declared in the YAML — they cannot spin up +one branch per discovered repo. So design.yaml instead declares a small, fixed pool of +"slot" branches; each slot loops: claim the next unclaimed repo from this run's selection, +author its LLD, then claim again. This script owns the two pieces of state a swappable skill +must never control: WHICH repos exist (reuses codebase_scan.discover_repos) and the atomic +claim queue (so two slots can never claim the same repo — same fcntl lock the engine itself +uses for state.yaml, via state.locked, so a claim can never race a run-state write either). + +Commands +-------- + list [--root .] + Discovered repo names (read-only). Prints {"names_csv", "count"}. + + init [--root .] --slug --choice all|pick [--repos-text "..."] + Validate the human's scope choice against the discovered repos and write the claim + queue to .maestro/runs//lld-repos.json. `pick` parses --repos-text as a + comma/whitespace-separated list, case-insensitive, and fails on any name that doesn't + match a discovered repo (never silently drops a typo). Prints {"selected_csv", "count"}. + + claim [--root .] --slug + Atomically pop the next unclaimed repo. Prints {"repo": "", "done": false} or + {"repo": "", "done": true} once the queue is empty. +""" +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import codebase_scan # noqa: E402 +import state as statemod # noqa: E402 + + +def _queue_path(slug, root): + return os.path.join(statemod.feature_dir(slug, root), "lld-repos.json") + + +def _load_queue(slug, root): + path = _queue_path(slug, root) + if not os.path.exists(path): + return None + with open(path, encoding="utf-8") as fh: + return json.load(fh) + + +def _save_queue(slug, root, doc): + path = _queue_path(slug, root) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + json.dump(doc, fh, indent=2) + fh.write("\n") + + +def cmd_list(args): + names = [name for name, _path in codebase_scan.discover_repos(args.root)] + print(json.dumps({"names_csv": ",".join(names), "count": len(names)})) + return 0 + + +def _parse_repos_text(text, discovered): + by_lower = {n.lower(): n for n in discovered} + wanted = [t.strip() for t in text.replace(",", " ").split() if t.strip()] + picked, unknown = [], [] + for w in wanted: + match = by_lower.get(w.lower()) + (picked if match else unknown).append(match or w) + return picked, unknown + + +def cmd_init(args): + discovered = [name for name, _path in codebase_scan.discover_repos(args.root)] + if not discovered: + print(f"FAIL: no repos discovered under {args.root!r} (codebase/* or the root itself)", + file=sys.stderr) + return 1 + if args.choice == "all": + selected = discovered + else: + if not args.repos_text: + print("FAIL: --choice pick requires --repos-text", file=sys.stderr) + return 1 + selected, unknown = _parse_repos_text(args.repos_text, discovered) + if unknown: + print(f"FAIL: unknown repo name(s) {unknown} — discovered repos are {discovered}", + file=sys.stderr) + return 1 + if not selected: + print("FAIL: no repos selected", file=sys.stderr) + return 1 + with statemod.locked(args.slug, args.root): + _save_queue(args.slug, args.root, {"selected": selected, "remaining": list(selected)}) + print(json.dumps({"selected_csv": ",".join(selected), "count": len(selected)})) + return 0 + + +def cmd_claim(args): + with statemod.locked(args.slug, args.root): + doc = _load_queue(args.slug, args.root) + if doc is None: + print(f"FAIL: no lld-repos.json for slug {args.slug!r} — run init first", + file=sys.stderr) + return 1 + remaining = doc.get("remaining") or [] + if not remaining: + print(json.dumps({"repo": "", "done": True})) + return 0 + repo = remaining.pop(0) + doc["remaining"] = remaining + _save_queue(args.slug, args.root, doc) + print(json.dumps({"repo": repo, "done": False})) + return 0 + + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("list") + p.add_argument("--root", default=".") + p.set_defaults(fn=cmd_list) + + p = sub.add_parser("init") + p.add_argument("--root", default=".") + p.add_argument("--slug", required=True) + p.add_argument("--choice", required=True, choices=["all", "pick"]) + p.add_argument("--repos-text", default="") + p.set_defaults(fn=cmd_init) + + p = sub.add_parser("claim") + p.add_argument("--root", default=".") + p.add_argument("--slug", required=True) + p.set_defaults(fn=cmd_claim) + + args = parser.parse_args(argv[1:]) + return args.fn(args) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/engine/tests/test_sdlc_e2e.py b/engine/tests/test_sdlc_e2e.py index cf2391c..843579b 100644 --- a/engine/tests/test_sdlc_e2e.py +++ b/engine/tests/test_sdlc_e2e.py @@ -25,8 +25,10 @@ def canned_agent_outputs(step, action): "rq_fold": {"refined_summary": "folded 1 answer"}, "author_hld": {"hld_summary": "3 services, 2 new tables"}, "refine_hld": {"refined_summary": "folded 1 answer"}, - "backend_design": {"lld_path": "lld/backend.md", "contract_notes": "rest+cursor"}, - "frontend_design": {"lld_path": "lld/frontend.md", "contract_notes": "uses GET /searches"}, + "slot_1_design": {"lld_path": "lld/x.md", "contract_notes": "rest+cursor"}, + "slot_2_design": {"lld_path": "lld/x.md", "contract_notes": "uses GET /searches"}, + "slot_3_design": {"lld_path": "lld/x.md", "contract_notes": "n/a"}, + "slot_4_design": {"lld_path": "lld/x.md", "contract_notes": "n/a"}, "contract": {"contract_summary": "5 endpoints"}, "test_cases": {"test_cases_path": "test-cases.md", "case_count": 12}, "arch_review": {"review_path": "reviews/architecture.md", "blocking": False, @@ -72,6 +74,13 @@ def setUp(self): os.makedirs(req) with open(os.path.join(req, "requirement.md"), "w") as fh: fh.write("Build the demo feature.\n") + # lld_repo_pool.py discovers repos under codebase/* — give it exactly backend+frontend + # so the real script (run for real below, not stubbed) matches every other assumption + # in this file (backend.md/frontend.md artifacts, per-stack tasks/impl/review). + for stack in ("backend", "frontend"): + path = os.path.join(self.tmp, "codebase", stack) + os.makedirs(path) + subprocess.run(["git", "init", "-q"], cwd=path, check=True) # -- driver ---------------------------------------------------------- @@ -110,7 +119,8 @@ def drive(self, gate_script, max_steps=200, agent_overrides=None): elif act["action"] == "run_script": # actually run the real script where it's an engine helper; stub others if ("oq_serve" in step or "validate_tasks" in step - or any("mem_consolidate" in a for a in act.get("argv", []))): + or any("mem_consolidate" in a or "lld_repo_pool" in a + for a in act.get("argv", []))): proc = subprocess.run(act["argv"], cwd=self.tmp, capture_output=True, text=True, timeout=30) code, out = proc.returncode, proc.stdout @@ -153,6 +163,7 @@ def test_happy_path(self): "design/collect_references": [("none", None)], "design/prd_approval": [("approve", None)], "design/hld_approval": [("approve", None)], + "design/lld_scope": [("all", None)], "design/lld_approval": [("approve", None)], "contract_approval": [("approve", None)], "release_approval": [("approve", None)], @@ -182,6 +193,7 @@ def test_revise_cascade_from_contract_gate(self): "design/collect_references": [("none", None), ("none", None)], "design/prd_approval": [("approve", None), ("approve", None)], "design/hld_approval": [("approve", None), ("approve", None)], + "design/lld_scope": [("all", None), ("all", None)], "design/lld_approval": [("approve", None), ("approve", None)], "contract_approval": [("revise", "tighten the API"), ("approve", None)], "release_approval": [("approve", None)], @@ -202,6 +214,7 @@ def test_revise_cascade_from_prd_gate(self): "design/collect_references": [("none", None)], "design/prd_approval": [("revise", "sharpen the scope"), ("approve", None)], "design/hld_approval": [("approve", None)], + "design/lld_scope": [("all", None)], "design/lld_approval": [("approve", None)], "contract_approval": [("approve", None)], "release_approval": [("approve", None)], @@ -219,6 +232,7 @@ def test_blocking_arch_review_gate_waive(self): "design/collect_references": [("none", None)], "design/prd_approval": [("approve", None)], "design/hld_approval": [("approve", None)], + "design/lld_scope": [("all", None)], "design/lld_approval": [("approve", None)], "arch_gate": [("waive", None)], "contract_approval": [("approve", None)], @@ -266,6 +280,7 @@ def patched(step, action): "design/collect_references": [("none", None)], "design/prd_approval": [("approve", None)], "design/hld_approval": [("approve", None)], + "design/lld_scope": [("all", None)], "design/lld_approval": [("approve", None)], "contract_approval": [("approve", None)], "release_approval": [("approve", None)], @@ -343,8 +358,15 @@ def test_oq_loop_with_real_scripts(self): resolver.record_gate(run, step, "answer-all", input_text="2") elif step == "prd_approval": resolver.record_gate(run, step, "approve") + elif step == "map_stale_gate": + # this test runs every script for real (unlike drive()'s selective + # stubbing), and setUp's codebase/backend+frontend are freshly `git init`'d + # with no map yet — genuinely stale, so the gate genuinely fires. + resolver.record_gate(run, step, "proceed") elif step == "hld_approval": resolver.record_gate(run, step, "approve") + elif step == "lld_scope": + resolver.record_gate(run, step, "all") elif step == "lld_approval": resolver.record_gate(run, step, "approve") else: @@ -433,6 +455,10 @@ def test_brainstorm_path_when_requirement_empty(self): resolver.record_gate(run, step, "answer-all", input_text="2") elif step == "prd_approval": resolver.record_gate(run, step, "approve") + elif step == "map_stale_gate": + # runs every script for real; setUp's codebase/ repos are freshly + # `git init`'d with no map yet, so this genuinely fires. + resolver.record_gate(run, step, "proceed") else: self.fail(f"unexpected gate {step}") statemod.save("demo", run.state, self.tmp) diff --git a/skills/core/sdlc/repo-design/SKILL.md b/skills/core/sdlc/repo-design/SKILL.md new file mode 100644 index 0000000..1e7f823 --- /dev/null +++ b/skills/core/sdlc/repo-design/SKILL.md @@ -0,0 +1,72 @@ +--- +name: repo-design +description: Author the low-level design (LLD) for a feature IN ONE REPO — read that repo's code to ground the design, then design how the feature slots into it (structure, data/state, the interfaces it exposes and/or consumes, NFRs, tests). Repo-agnostic — works for a backend, frontend, mobile app, or any other repo, whatever it turns out to be. Writes the LLD doc; never edits app code. Front door for /repo-design. +allowed-tools: Read, Grep, Glob, Bash, Write +tags: [sdlc, design, lld] +--- + +# repo-design — low-level design for one repo + +Design how a feature slots into **one specific repo**: read enough of its real code to ground +the design, then write a **buildable LLD for that repo alone**. This is a design artifact, not +code — never edit app code, don't implement, and don't design any other repo. The **cross-repo +contract** is not written here — you describe what this repo exposes and/or consumes; a +separate step reconciles every repo's LLD into the formal contract. + +This skill is deliberately repo-agnostic: it does not assume "backend" or "frontend." The repo +you're given could be either, a mobile app, a CLI, a library, an infra/pipeline repo, or +anything else — figure out what it actually is from its own code and map it to the sections +below, marking a section "n/a" when it genuinely doesn't apply rather than forcing content. + +## Inputs +Your instructions name the **repo** you own (by name/path), the approved HLD, the feature, and +the artifact path to write. Standalone? ask which repo, or infer it from the current directory, +and write to a path you choose (and tell the user where). + +## Steps +1. **Identify what this repo is.** Read its `docs/codebase-map.md` first (umbrella layout: + `codebase//docs/codebase-map.md`) — the standing description of its modules, flows and + execution modes — plus its `CLAUDE.md`/manifest (`package.json`, `pubspec.yaml`, + `pyproject.toml`, a Terraform/CI config, whatever exists). From that, decide its shape: does + it serve requests, render UI, run on a device, ship as a library, define infrastructure? That + shape determines which sections below carry real content. +2. **Ground in the code, cheaply.** Read the actual source only where the feature needs context + the map doesn't cover — the specific flow you're extending and any execution mode it touches. + Cite `file:line` for every constraint you rely on; don't guess. An approach that fits the + happy path but breaks an existing mode (async, batch, offline, multi-tenant…) is a wrong LLD. +3. **Design the structural change** — the modules/components/objects and their responsibilities, + and the sequence for each critical path (happy + main error paths); where new code slots in. +4. **Data & state** (if this repo owns any) — entities, storage/persistence, migration or + versioning plan with rollback. Mark "n/a — this repo holds no persistent state" if true. +5. **Interfaces** — for EACH interface this repo touches, say which direction: what it + **exposes** (an API/event/screen/CLI command others call into) and what it **consumes** (an + API/event/SDK it calls out to). A repo can do both. Be concrete: method/path or + event/topic/screen name, request/response shape, error handling, auth. This is this repo's + *side* of any contract — reconciliation with other repos happens in a later step. +6. **Non-functional requirements**, scaled to what's real here: security & privacy (authz, + secrets, PII), performance, reliability (retries, idempotency, partial failure), and + observability. Skip a sub-area explicitly (state why) rather than padding it. +7. **Edge cases** the design must define, not leave to the implementer (empty/oversized inputs, + concurrent updates, partial failure, auth denial, rate limits — whichever apply to this + repo's shape). +8. **Test plan** — coverage appropriate to this repo's shape (unit/integration/component/E2E). +9. **Write** the LLD; flag anything that constrains or must be reconciled with other repos' + designs (their existence, not their content, is all you may assume). + +## What the LLD must cover (write all; mark a section "n/a" with a one-line reason if it +genuinely doesn't apply to this repo's shape) +Context & constraints (grounded in the code, cited) · structural/component design · data & +state · interfaces exposed and/or consumed · security & privacy · performance · reliability · +observability · edge cases · test plan · rollout/backout note for this repo alone. + +## Output contract +Write your LLD to the given artifact path, with the sections above, each constraint citing +`file:line`. Return `lld_path` and `contract_notes` — a short summary of the +**decisions/constraints that shape reconciliation with other repos** (e.g. "exposes +`GET /favorites` with cursor pagination"; "consumes the backend's existing auth token, no +change needed"). The interfaces section feeds the cross-repo contract step. + +## Definition of done +Every applicable section present and concrete enough to reconcile against other repos' +designs; "n/a" sections justified in one line, not silently dropped; edge cases specified (not +"TBD"); breaking changes flagged. Do not implement — this is a design artifact only. diff --git a/workflows/design.yaml b/workflows/design.yaml index d5e7389..5d6797d 100644 --- a/workflows/design.yaml +++ b/workflows/design.yaml @@ -317,72 +317,200 @@ nodes: Review the document, then choose. options: - - {id: approve, label: Approve — write the detailed designs, to: author_llds} + - {id: approve, label: Approve — write the detailed designs, to: lld_scope_serve} - {id: revise, label: Request revisions (regenerates everything downstream), to: author_hld, input: feedback} - {id: reject, label: Reject — abort the run, to: abort} + # ---- LLD scope: repo-based, not stack-named. lld_scope_serve discovers the ACTUAL repos + # under codebase/ (any count, any names — the same discovery codebase_scan.py uses for the + # freshness gate); the human picks all of them or a specific subset; lld_scope_init + # validates the pick and writes the claim queue every parallel "slot" below drains from. + # A revise cycle from lld_approval re-enters HERE (not straight into author_llds) so scope + # can change on revision too. + - id: lld_scope_serve + type: script + label: Discover repos + run: [python3, .maestro/engine/lld_repo_pool.py, list, --root, .] + next: lld_scope + + - id: lld_scope + type: gate + label: LLD scope + prompt: | + Detected repos: ${steps.lld_scope_serve.outputs.names_csv} + + Write the detailed design for all of them now, or just some? + options: + - {id: all, label: All of them, to: lld_scope_init} + - {id: pick, label: Pick specific repos, to: lld_scope_init, input: repos_text} + + - id: lld_scope_init + type: script + label: Validate scope, build the claim queue + run: + - python3 + - .maestro/engine/lld_repo_pool.py + - init + - --root + - "." + - --slug + - "${inputs.slug}" + - --choice + - "${steps.lld_scope.outputs.choice}" + - --repos-text + - "${steps.lld_scope.outputs.repos_text}" + on_fail: lld_scope + next: author_llds + + # ---- Detailed designs: a FIXED pool of parallel "slots" (parallel branches can't have a + # runtime-variable count), each looping claim -> design -> claim until the queue lld_scope_init + # just built is empty. This is the SAME back-edge-loop shape as the open-questions cycle, + # applied to repos instead of questions — it scales to any repo count without a per-repo + # branch, and still gives real concurrency: run_agents dispatches every slot currently holding + # a claimed repo in one wave, so "all repos at once" and "one at a time" both fall out of the + # same mechanism depending on how many slots have work when you act. - id: author_llds type: parallel - label: Detailed designs + label: Detailed designs (repo pool) join: all on_branch_fail: ask branches: - - id: backend - start: backend_design + - id: slot_1 + start: slot_1_claim + steps: + - id: slot_1_claim + type: script + run: [python3, .maestro/engine/lld_repo_pool.py, claim, --root, ., --slug, "${inputs.slug}"] + max_visits: 40 + routes: + - {when: "${steps.slot_1_claim.outputs.done} == true", to: end} + - {to: slot_1_design} + - id: slot_1_design + type: agent + instruction: | + Write the low-level design for the "${steps.slot_1_claim.outputs.repo}" repo from + the approved HLD at .maestro/runs/${inputs.slug}/hld.md, grounded in that repo's + docs/codebase-map.md and the real code it cites. + If a previous review left feedback, honour it: ${steps.lld_approval.outputs.feedback} + skill: repo-design + inputs: + slug: "${inputs.slug}" + feature: "${inputs.feature}" + repo: "${steps.slot_1_claim.outputs.repo}" + lessons: "${memory.knowledge.repo-design}" + outputs: [lld_path, contract_notes] + artifact: ".maestro/runs/${inputs.slug}/lld/${steps.slot_1_claim.outputs.repo}.md" + max_visits: 40 + next: slot_1_claim + - id: slot_2 + start: slot_2_claim + steps: + - id: slot_2_claim + type: script + run: [python3, .maestro/engine/lld_repo_pool.py, claim, --root, ., --slug, "${inputs.slug}"] + max_visits: 40 + routes: + - {when: "${steps.slot_2_claim.outputs.done} == true", to: end} + - {to: slot_2_design} + - id: slot_2_design + type: agent + instruction: | + Write the low-level design for the "${steps.slot_2_claim.outputs.repo}" repo from + the approved HLD at .maestro/runs/${inputs.slug}/hld.md, grounded in that repo's + docs/codebase-map.md and the real code it cites. + If a previous review left feedback, honour it: ${steps.lld_approval.outputs.feedback} + skill: repo-design + inputs: + slug: "${inputs.slug}" + feature: "${inputs.feature}" + repo: "${steps.slot_2_claim.outputs.repo}" + lessons: "${memory.knowledge.repo-design}" + outputs: [lld_path, contract_notes] + artifact: ".maestro/runs/${inputs.slug}/lld/${steps.slot_2_claim.outputs.repo}.md" + max_visits: 40 + next: slot_2_claim + - id: slot_3 + start: slot_3_claim steps: - - id: backend_design + - id: slot_3_claim + type: script + run: [python3, .maestro/engine/lld_repo_pool.py, claim, --root, ., --slug, "${inputs.slug}"] + max_visits: 40 + routes: + - {when: "${steps.slot_3_claim.outputs.done} == true", to: end} + - {to: slot_3_design} + - id: slot_3_design type: agent instruction: | - Write the backend low-level design from the approved HLD at - .maestro/runs/${inputs.slug}/hld.md, grounded in the maintained codebase map - (docs/codebase-map.md in the backend repo) and the real code it cites. + Write the low-level design for the "${steps.slot_3_claim.outputs.repo}" repo from + the approved HLD at .maestro/runs/${inputs.slug}/hld.md, grounded in that repo's + docs/codebase-map.md and the real code it cites. If a previous review left feedback, honour it: ${steps.lld_approval.outputs.feedback} - skill: backend-design - inputs: {slug: "${inputs.slug}", feature: "${inputs.feature}", lessons: "${memory.knowledge.backend-design}"} + skill: repo-design + inputs: + slug: "${inputs.slug}" + feature: "${inputs.feature}" + repo: "${steps.slot_3_claim.outputs.repo}" + lessons: "${memory.knowledge.repo-design}" outputs: [lld_path, contract_notes] - artifact: ".maestro/runs/${inputs.slug}/lld/backend.md" - next: end - - id: frontend - start: frontend_design + artifact: ".maestro/runs/${inputs.slug}/lld/${steps.slot_3_claim.outputs.repo}.md" + max_visits: 40 + next: slot_3_claim + - id: slot_4 + start: slot_4_claim steps: - - id: frontend_design + - id: slot_4_claim + type: script + run: [python3, .maestro/engine/lld_repo_pool.py, claim, --root, ., --slug, "${inputs.slug}"] + max_visits: 40 + routes: + - {when: "${steps.slot_4_claim.outputs.done} == true", to: end} + - {to: slot_4_design} + - id: slot_4_design type: agent instruction: | - Write the frontend low-level design from the approved HLD at - .maestro/runs/${inputs.slug}/hld.md, grounded in the maintained codebase map - (docs/codebase-map.md in the frontend repo) and the real code it cites. + Write the low-level design for the "${steps.slot_4_claim.outputs.repo}" repo from + the approved HLD at .maestro/runs/${inputs.slug}/hld.md, grounded in that repo's + docs/codebase-map.md and the real code it cites. If a previous review left feedback, honour it: ${steps.lld_approval.outputs.feedback} - skill: frontend-design - inputs: {slug: "${inputs.slug}", feature: "${inputs.feature}", lessons: "${memory.knowledge.frontend-design}"} + skill: repo-design + inputs: + slug: "${inputs.slug}" + feature: "${inputs.feature}" + repo: "${steps.slot_4_claim.outputs.repo}" + lessons: "${memory.knowledge.repo-design}" outputs: [lld_path, contract_notes] - artifact: ".maestro/runs/${inputs.slug}/lld/frontend.md" - next: end + artifact: ".maestro/runs/${inputs.slug}/lld/${steps.slot_4_claim.outputs.repo}.md" + max_visits: 40 + next: slot_4_claim next: lld_approval - # ---- LLD approval: the human reviews BOTH detailed designs before any implementation. - # 'revise' is a back-edge into the parallel node — re-entry cascade-resets both LLDs plus - # everything downstream (contract, test cases). A design change never reaches implementation - # without passing back through this gate. + # ---- LLD approval: the human reviews whichever detailed design(s) were just written. + # 'revise' is a back-edge into lld_scope (not straight back into author_llds) so a revision + # can also change scope; re-entry cascade-resets the whole pool plus everything downstream + # (contract, test cases). A design change never reaches implementation without passing + # back through this gate. - id: lld_approval type: gate label: LLD approval prompt: | - Detailed designs ready for review: - backend: ${steps.author_llds.branches.backend.outputs.lld_path} - frontend: ${steps.author_llds.branches.frontend.outputs.lld_path} + Detailed design(s) ready for review under .maestro/runs/${inputs.slug}/lld/ + (backend.md and/or frontend.md, depending on the scope chosen). - Review both LLDs. Any change to an LLD must be approved here before implementation. + Review whichever were written. Any change to an LLD must be approved here before + implementation. options: - {id: approve, label: Approve — reconcile the contract, to: contract} - - {id: revise, label: Request LLD revisions (regenerates both LLDs + downstream), to: author_llds, input: feedback} + - {id: revise, label: Request LLD revisions (regenerates the LLD(s) + downstream), to: lld_scope, input: feedback} - {id: reject, label: Reject — abort the run, to: abort} - id: contract type: agent label: API contract instruction: | - Reconcile the backend and frontend LLDs under .maestro/runs/${inputs.slug}/lld/ into a - single OpenAPI contract. Flag any mismatch you had to resolve. + Reconcile whichever LLD(s) exist under .maestro/runs/${inputs.slug}/lld/ into an OpenAPI + contract. If only one side was authored this round, describe that side's exposed contract + — do not invent the other side's. Flag any mismatch you had to resolve. skill: api-contract inputs: {slug: "${inputs.slug}", feature: "${inputs.feature}"} outputs: [contract_summary]