From 40677a3b86621afc3c7622a710b77d6e83463331 Mon Sep 17 00:00:00 2001 From: Sami Date: Thu, 26 Mar 2026 01:30:22 +0100 Subject: [PATCH] feat(daggie): Enhance Daggie agent with more accurate instructions and new arguments --- daggie/src/daggie/main.py | 96 ++++++++++++++++-- daggie/src/daggie/prompts/assist_prompt.md | 22 +++- daggie/src/daggie/prompts/system_prompt.md | 112 ++++++++++++++++++++- 3 files changed, 217 insertions(+), 13 deletions(-) diff --git a/daggie/src/daggie/main.py b/daggie/src/daggie/main.py index ef3dfb9..df600ce 100644 --- a/daggie/src/daggie/main.py +++ b/daggie/src/daggie/main.py @@ -8,6 +8,7 @@ from agent_base import constants, github_tools, llm_helpers, routing, workspace _DAGGER_CONFIG_FILE = "DAGGER.md" +_DAGGER_ACTION_FALLBACK_VERSION = "v8.4.1" # File patterns to read from cloned Dagger modules _MODULE_SOURCE_GLOBS = [ @@ -39,6 +40,26 @@ def _parse_module_url(url: str) -> tuple[str, str, str]: return repo_url, branch, path +async def _fetch_repo_tags(repo_url: str) -> list[str]: + """Fetch all Git tags from a remote repository via git ls-remote.""" + try: + output = await ( + dag.container() + .from_("alpine/git:latest") + .with_exec(["git", "ls-remote", "--tags", "--refs", repo_url]) + .stdout() + ) + # Parse "sha\trefs/tags/tag-name" lines → ["tag-name", ...] + tags = [] + for line in output.strip().splitlines(): + if "\trefs/tags/" in line: + tag = line.split("refs/tags/", 1)[1] + tags.append(tag) + return sorted(tags) + except Exception: + return [] + + async def _read_module_tree(tree: dagger.Directory) -> str: """Read key files from a cloned Dagger module and return formatted docs.""" sections = [] @@ -65,8 +86,9 @@ class Daggie: Specializes in creating, explaining, and debugging Dagger CI modules and pipelines across all SDKs (Python, TypeScript, Go, Java). - Accepts Git URLs of Dagger modules at runtime, clones them via dag.git(), - reads their source code, and uses that knowledge to propose implementations. + Accepts Git URLs of Dagger module repositories at runtime, clones them via + dag.git(), and auto-discovers all modules within (by finding dagger.json files). + You can also point to a specific module with the url#branch:path format. Pass your project as --source and optionally reference Dagger modules via --module-urls for the agent to learn from. @@ -79,7 +101,9 @@ class Daggie: ] = field() module_urls: Annotated[ list[str], - Doc("Git URLs of Dagger modules to clone and read for reference (e.g. 'https://github.com/org/repo.git#main:path/to/module')"), + Doc("Git URLs of Dagger module repositories to clone and read for reference. " + "Points to a repo root to auto-discover all modules (e.g. 'https://github.com/org/modules.git'), " + "or to a specific module with 'url#branch:path' (e.g. 'https://github.com/org/repo.git#main:my-module')"), ] = field(default=list) self_improve: Annotated[ str, @@ -126,7 +150,15 @@ def _load_prompt(self, filename: str) -> dagger.File: return dag.current_module().source().file(f"src/daggie/prompts/{filename}") async def _load_module_sources(self) -> str: - """Clone and read all referenced Dagger modules.""" + """Clone and read all referenced Dagger modules. + + When a URL points to a repository root (no path), auto-discovers + all Dagger modules by finding dagger.json files in the tree. + When a URL includes a path, reads only that specific module. + + Also fetches Git tags from each repository so the agent knows + available versions for toolchain/dependency pinning. + """ if not self.module_urls: return "" @@ -135,15 +167,49 @@ async def _load_module_sources(self) -> str: try: repo_url, branch, path = _parse_module_url(url) tree = dag.git(repo_url).branch(branch).tree() - if path: - tree = tree.directory(path) - module_docs = await _read_module_tree(tree) - if module_docs: - sections.append(f"## Reference Module: {url}\n\n{module_docs}") + # Fetch available version tags from the repository + tags = await _fetch_repo_tags(repo_url) + tags_section = "" + if tags: + tags_section = "\n\n### Available version tags\n```\n" + "\n".join(tags) + "\n```" + + if path: + # Explicit path — read that single module + module_tree = tree.directory(path) + module_docs = await _read_module_tree(module_tree) + if module_docs: + sections.append(f"## Reference Module: {url} ({path})\n\n{module_docs}{tags_section}") + else: + # No path — auto-discover all modules in the repo + dagger_jsons = await tree.glob("**/dagger.json") + # Add tags once at the repo level, before individual modules + if tags_section: + sections.append(f"## Repository: {repo_url}{tags_section}") + for dj_path in dagger_jsons: + module_dir = "/".join(dj_path.split("/")[:-1]) if "/" in dj_path else "" + module_tree = tree.directory(module_dir) if module_dir else tree + module_docs = await _read_module_tree(module_tree) + if module_docs: + label = module_dir or repo_url + sections.append(f"## Reference Module: {label}\n\n{module_docs}") except Exception as exc: sections.append(f"## Reference Module: {url}\n\n*Failed to clone: {exc}*") + # Fetch latest dagger-for-github action version + action_tags = await _fetch_repo_tags("https://github.com/dagger/dagger-for-github.git") + action_version = _DAGGER_ACTION_FALLBACK_VERSION + if action_tags: + # Filter to vN.N.N tags (ignore pre-releases, rc, etc.) + version_tags = [t for t in action_tags if t.startswith("v") and t.count(".") == 2 and all(p.isdigit() for p in t[1:].split("."))] + if version_tags: + action_version = version_tags[-1] # sorted, last = latest + sections.append( + f"## GitHub Action: dagger/dagger-for-github\n\n" + f"Latest stable version tag: **{action_version}**\n" + f"Use `dagger/dagger-for-github@{action_version}` in all workflow files." + ) + if not sections: return "" @@ -211,14 +277,24 @@ async def _build_suggest_fix_llm(self, env, source=None): @function async def assist( self, - assignment: Annotated[str, Doc("What you want the agent to do (e.g. 'Create a Dagger pipeline for building and testing a Python project')")], + assignment: Annotated[str, Doc("What you want the agent to do (e.g. 'Create a Dagger pipeline for building and testing a Python project')")] = "", + assignment_file: Annotated[dagger.File | None, Doc("File containing the assignment instructions (used if --assignment is not set)")] = None, source: Annotated[dagger.Directory | None, Doc("Override source directory (uses constructor source if omitted)")] = None, ) -> dagger.Directory: """Create Dagger pipelines and modules, implement features. Reads reference modules, understands your project, and implements Dagger CI pipelines. Returns the modified workspace directory. + + Provide instructions via --assignment (takes precedence) or + --assignment-file for longer/structured prompts. """ + if not assignment and assignment_file: + assignment = await assignment_file.contents() + if not assignment: + msg = "Provide either --assignment or --assignment-file" + raise ValueError(msg) + ws = source or self.source env = ( dag.env() diff --git a/daggie/src/daggie/prompts/assist_prompt.md b/daggie/src/daggie/prompts/assist_prompt.md index 45eda5c..2b8cc0c 100644 --- a/daggie/src/daggie/prompts/assist_prompt.md +++ b/daggie/src/daggie/prompts/assist_prompt.md @@ -19,7 +19,27 @@ Accomplish the coding task described in the **assignment** input. - Any helper modules or utilities 5. **Verify**: Read back modified files to confirm correctness -## Dagger Module Checklist +## Choosing the Right Approach + +Determine whether the assignment calls for: + +**A) Toolchain-only setup** (keywords: "toolchains", "dagger check", "zero code", "no pipeline code"): +- Generate `dagger.json` with `toolchains` array only — NO `sdk`, `include`, or `dependencies` fields +- Set `engineVersion` from the reference modules' `dagger.json` (use the latest version seen) +- For monorepos: add `customizations` with `defaultPath` for EVERY `@check` function's `source` param on EVERY toolchain — including deploy modules with `scan` checks. Do NOT skip any toolchain. +- Generate `.github/workflows/ci.yml` using the `dagger/dagger-for-github` action at the version discovered in the "Pre-loaded Module References" section +- The action only accepts `version`, `verb`, and `args` fields — no other fields exist +- Use the `engineVersion` from `dagger.json` (without `v` prefix) as the `version` field +- Do NOT create `.dagger/`, `src/`, or any pipeline code +- Call external agents via `-m` in CI workflow `args` field, not as local functions + +**B) SDK pipeline module** (keywords: "pipeline", "orchestrate", "custom logic"): +- Generate `.dagger/` module with `dagger.json` containing `sdk` and `dependencies` +- Write pipeline code with `@object_type`, `@function` decorators + +If the assignment mentions toolchains, always use approach A. + +## Dagger Module Checklist (approach B only) When creating a new module, ensure: - [ ] `dagger.json` has correct `name`, `sdk`, `engineVersion` diff --git a/daggie/src/daggie/prompts/system_prompt.md b/daggie/src/daggie/prompts/system_prompt.md index fee1b47..d17115e 100644 --- a/daggie/src/daggie/prompts/system_prompt.md +++ b/daggie/src/daggie/prompts/system_prompt.md @@ -87,8 +87,8 @@ dagger toolchain install github.com/example/toolchain --name mytool "name": "my-app", "toolchains": [ { - "name": "acme-backend", - "source": "github.com/org/modules/acme-backend@v1.0.0", + "name": "my-backend", + "source": "github.com/org/modules/my-backend@v1.0.0", "customizations": [ { "function": ["test"], @@ -114,6 +114,8 @@ dagger toolchain install github.com/example/toolchain --name mytool **IMPORTANT — `function` field in customizations:** The `function` field scopes a customization to a specific function. Without it, the override targets the module's **constructor** arguments only. If the argument you're overriding lives on a function (e.g., `source` on `test()` or `lint()`), you **must** include `"function": [""]` — otherwise the override is silently ignored. Each function needs its own customization entry. +**IMPORTANT — Monorepo `source` customizations:** In a monorepo, EVERY `@check` function that has a `source` parameter needs a `customizations` entry with `defaultPath` pointing to the correct subdirectory. This applies to ALL toolchains, not just backend/frontend — deploy toolchains with `@check` functions (e.g., `scan`) also need their `source` customized. Inspect each module's functions and ensure none are missed. + **Making modules toolchain-ready:** Add `@check` decorator to validation functions (test, lint, audit) and `DefaultPath(".")` to their source parameter. This makes them discoverable via `dagger check` and auto-injects the project source: ```python @function @@ -199,8 +201,114 @@ dagger check pytest:* # Run toolchain-namespaced checks **When to use `@check`:** Any function that validates code quality, security, or standards — tests, linting, audits, vulnerability scans, type checking, formatting checks. Do NOT mark build/deploy/assist functions as checks. +### Toolchain-Only Projects (No SDK) +When the user asks for a toolchain-based setup, the `dagger.json` should contain **only** `name`, `engineVersion`, and `toolchains`. Do NOT include `sdk`, `include`, or `dependencies` — those are for SDK-based pipeline modules. A toolchain-only project has zero pipeline code: + +```json +{ + "name": "my-project", + "engineVersion": "v0.20.3", + "toolchains": [...] +} +``` + +Do NOT create a `.dagger/` directory, `src/` module, or any pipeline Python/Go/TS code for toolchain-only setups. The entire CI configuration lives in `dagger.json` and the CI workflow file. + +### GitHub Actions Integration +Always use the official `dagger/dagger-for-github` action for CI — never install Dagger manually via `curl`. + +**CRITICAL — Action version and fields:** +- The action version MUST come from the "Pre-loaded Module References" section below, which contains the latest discovered version tag for `dagger/dagger-for-github`. Use that exact tag (e.g., `@v8.4.1`). If no version was discovered, the fallback version will be provided automatically. +- The action accepts **exactly three** `with:` fields: `version`, `verb`, and `args`. No other fields exist — do NOT invent fields like `module:`, `tool:`, `command:`, etc. +- The `version` field is the **Dagger engine version** and MUST match the `engineVersion` value from the project's `dagger.json` (strip the `v` prefix). For example, if `dagger.json` has `"engineVersion": "v0.20.3"`, use `version: "0.20.3"`. Never hardcode an arbitrary version — always read it from `dagger.json`. + +**Running checks:** +```yaml +- uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: check +``` + +**Calling toolchain functions:** +```yaml +- uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: call + args: my-toolchain my-function --source=./app --flag=value ... +``` + +**Calling external modules (agents, tools):** Use `-m` in the `args` field: +```yaml +- uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: call + args: >- + -m github.com/org/agent-module@v1.0.0 + --source=./app + suggest-github-fix + --github-token=env:GITHUB_TOKEN + --pr-number=${{ github.event.pull_request.number }} + --repo=${{ github.repository }} + --commit-sha=${{ github.event.pull_request.head.sha }} + --error-output="${{ steps.checks.outputs.stderr }}" +``` + +**Workflow structure for toolchain projects:** +```yaml +permissions: + contents: read + pull-requests: write # For agent PR comments + id-token: write # For GCP OIDC auth + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Run all checks + id: checks + uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: check + - name: Suggest fix on failure + if: failure() && github.event_name == 'pull_request' + uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: call + args: -m github.com/org/coding-agent@v1.0.0 --source=./app suggest-github-fix ... + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + deploy: + runs-on: ubuntu-latest + needs: check + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v6 + - uses: dagger/dagger-for-github@v8.4.1 + with: + version: "0.20.3" # Must match dagger.json engineVersion + verb: call + args: my-deploy-toolchain cloud-run --source=./backend ... +``` + +**OIDC tokens for GCP auth:** GitHub Actions auto-exposes `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and `ACTIONS_ID_TOKEN_REQUEST_URL` when the workflow has `id-token: write` permission. Pass them via `env:`: +```yaml +args: >- + my-deploy-toolchain cloud-run + --oidc-request-token=env:ACTIONS_ID_TOKEN_REQUEST_TOKEN + --oidc-request-url=env:ACTIONS_ID_TOKEN_REQUEST_URL +``` + ### CLI Commands - `dagger call [args]` — call a module function +- `dagger call -m [args]` — call an external module's function - `dagger functions` — list available functions - `dagger develop` — generate SDK bindings - `dagger init --sdk=python` — initialize a new module