A GitHub Project card that says “make nav better” leaves the scope and acceptance criteria to its reviewers.

Someone still has to define acceptance criteria (Specify), inspect the repository (Research), write an implementable proposal (Plan), and perhaps open a pull request (PR) (Implement).

I tested whether Aiden could take a vague GitHub issue through those stages and open a review PR. Issue comments provide a review trail; they do not replace human review of the proposed code.

We dogfooded this on this blog’s repo and a personal GitHub Project. This post is Aiden the Hard Way: a flat root of sg_* resources with the StackGen Terraform/OpenTofu provider — every layer you would otherwise hide inside a module. Prefer module "…" instead? Read the follow-up, Aiden the Easy Way.

Note: Snippets below are teaching shapes (shortened personas/runbooks). Swap URLs, tokens, and column names for your board.


Best practices for AI kanban automation

GitHub event drives Specify, Research, Plan, and optional review PR, while merge remains a human decision

The workflow leaves issue receipts and a review PR; people decide whether to merge.

Before writing code, apply bring-up discipline to the board.

Do Don’t
Process one issue per run Process the whole board at once
Comment evidence directly on the issue Keep findings hidden only in chat
Hop Status one column at a time Jump from Specify to Done in silence
Open a PR for humans to review and merge Allow the AI to auto-merge

Step 0: Set up the StackGen OpenTofu provider

Prerequisite: you need an active StackGen / Aiden tenant (URL + token + organization/project identifier). Without that, tofu apply has nowhere to create resources — this is not a local-only sandbox.

The StackGen provider "sg" lets this configuration be reviewed with plan and applied to a tenant. This example shows resource wiring rather than a local-only agent setup.

terraform {
  required_version = ">= 1.5"
  required_providers {
    sg = {
      source  = "releases.stackgen.com/stackgen/stackgen"
      version = ">= 0.1.33, < 0.2.0"
    }
  }
}

provider "sg" {
  stackgen_url   = var.stackgen_url   # e.g. https://ai.dev.stackgen.com
  stackgen_token = var.stackgen_token # never commit
  project_id     = var.stackgen_project_id
}

variable "stackgen_url" { type = string }
variable "stackgen_token" {
  type      = string
  sensitive = true
}
variable "stackgen_project_id" { type = string }

variable "openai_api_key" {
  type      = string
  sensitive = true
}
variable "github_token" {
  type        = string
  sensitive   = true
  description = "PAT with repo + Projects v2 (read:project, project)"
}

variable "default_project_url" {
  type    = string
  default = "https://github.com/users/YOU/projects/1"
}

This example uses OpenTofu (tofu) for fmt, init, plan, and apply; Terraform uses comparable commands.


Step 1: Configure the LLM provider and models

You only need one working model for this demo. We use OpenAI here; attach Anthropic the same way if you prefer Claude.

resource "sg_secret" "openai" {
  name        = "demo-openai-vault"
  description = "OpenAI API key"
  category    = "LLM"
  subcategory = "openai"
  metadata = {
    OPENAI_API_KEY = var.openai_api_key
  }
}

resource "sg_guild_model_provider" "openai" {
  name            = "openai"
  provider_type   = "openai"
  token_reference = sg_secret.openai.name
}

resource "sg_guild_model" "primary" {
  name          = "gpt-5.4-2026-03-05" # use a model your tenant already knows
  provider_name = sg_guild_model_provider.openai.name
  # …other model fields per your tenant / provider docs
}

Step 2: Integrate GitHub Issues and Projects

A GitHub personal access token (PAT) needs Projects scopes for GraphQL Status reads; without them, those reads fail in opaque ways. Budget time for token scopes before blaming the agent.

resource "sg_secret" "github" {
  name        = "demo-github-vault"
  description = "GitHub PAT for Projects + issues + PRs"
  category    = "SCM"
  subcategory = "github"
  metadata = {
    provider = "github"
    token    = var.github_token
  }
}

resource "sg_guild_integration" "github" {
  name           = "github-integration"
  description    = "GitHub for Project Status, comments, optional PR"
  type           = "github"
  enabled        = true
  secret_ref_ids = [sg_secret.github.id]

  image = {
    name = "ghcr.io/stackgenhq/github-mcp:latest" # pin what your tenant documents
  }
}

Step 3: Define guardrails and human-in-the-loop (HITL) policies

We want the agent to have autonomy to open a PR (gh pr create), but accountability to land it. Keep destructive actions and merges behind HITL approval. The sample string-matching policy covers the listed command text, not every possible mutation path; review tool permissions separately.

resource "sg_policy" "guardrails" {
  name        = "github-project-assistant-guardrails"
  type        = "intervention"
  description = "HITL for destructive shell / merge / CI dispatch"
  rego_source = <<-REGO
    package policy
    import rego.v1

    default approval_required := false

    approval_required if {
      contains(input.tool.name, "_execute_command")
      cmd := lower(input.tool.arguments.command)
      some p in {"rm -rf", "git push --force", "gh pr merge", "gh workflow run"}
      contains(cmd, p)
    }
  REGO
}

Step 4: Deploy the AI agent and budget policy

Put your long persona in a persona.md file next to your root (mission, comment template, “do not merge”). Keep absolute filesystem paths out of spawn goals — they trip execution-surface guards when GitHub tools expect relative paths.

resource "sg_agent" "project_assistant" {
  name        = "github-project-assistant"
  persona     = file("${path.cwd}/persona.md")
  model_names = [sg_guild_model.primary.name]

  integrations = [sg_guild_integration.github.name]

  auto_approve_tools = [
    { tool = "${sg_guild_integration.github.name}_*" },
    { tool = "note" },
    { tool = "read_notes" },
  ]
}

resource "sg_agent_budget" "project_assistant" {
  agent_name  = sg_agent.project_assistant.name
  limit_usd   = 20
  period_type = "daily"
}

resource "sg_agent_policy_attachment" "guardrails" {
  agent_name = sg_agent.project_assistant.name
  policy_id  = sg_policy.guardrails.id
  enabled    = true
}

Step 5: Bind the agent runbook to a workflow

The runbook markdown is the playbook: how to read Status, what Specify/Research/Plan must post, and when to open a PR. Bind it to a single workflow stage so chat and webhooks share one entrypoint.

resource "sg_runbook_sop" "item_assist" {
  name        = "github-project-item-assist"
  approve     = true
  description = file("${path.cwd}/runbook-item-assist.md")
}

resource "sg_workflow" "item_assist" {
  name        = "github-project-item-assist"
  domain      = "software-engineering"
  description = "One GitHub Project item: Specify → Research → Plan (+ optional PR)"
  approve     = true

  required_inputs = ["project_url", "issue_number"]
  optional_inputs = ["repository", "auto_advance", "sdlc_chain"]

  stages = [
    {
      stage_id    = "assist_item"
      description = "Board SDLC for one issue; comments; Status hops; optional PR"
      required    = true
    },
  ]

  stage_bindings = [
    {
      stage_id     = "assist_item"
      agent_ref    = sg_agent.project_assistant.name
      runbook_refs = [sg_runbook_sop.item_assist.name]
    },
  ]
}

Start the workflow, not a naked agent chat — inputs stay structured.


Step 6: Trigger workflows with GitHub webhooks

Register the repo webhook for Issues (opened), content type JSON, and secret = webhook_token.

resource "sg_webhook" "issues" {
  name           = "github-project-item-assist-issues"
  target_type    = "workflow"
  target_name    = sg_workflow.item_assist.name
  enabled        = true
  token_rotation = "v1"

  action = <<-EOT
    GitHub issue opened. Extract repository.full_name and issue.number.
    project_url = ${var.default_project_url}
    auto_advance = true
    sdlc_chain = true
    If the issue is missing from the Project, add it under Specify, then run
    Specify → Research → Plan. When implement is enabled in the runbook, open a
    review PR after Plan. Do not merge. Ignore non-open noise.
  EOT
}

output "webhook_token" {
  value     = sg_webhook.issues.token
  sensitive = true
}

output "webhook_ingress_payload_url" {
  description = "Paste into GitHub → Webhooks → Payload URL"
  sensitive   = true
  value = format(
    "%s/guild/api/v1/webhooks/trigger?apiKey=%s&orgId=%s",
    trimsuffix(var.stackgen_url, "/"),
    urlencode(sg_webhook.issues.token),
    urlencode(var.stackgen_project_id),
  )
}

Step 7: Schedule Status polls for Projects

Dragging a Projects v2 card is not an issues event. If you want the agent to react when a human drags a card from Research to Plan, you need a schedule. Webhooks and schedules share the same target_type and target_name pairing.

resource "sg_agent_schedule" "status_poll" {
  target_type = "workflow"
  target_name = sg_workflow.item_assist.name
  name        = "github-project-status-poll"
  expression  = "*/5 * * * *" # five-field cron, UTC
  enabled     = true

  action = <<-EOT
    Poll Project ${var.default_project_url}.
    Pick at most ONE item in Research or Plan that still needs a matching
    "### Aiden project assist — <Stage>" comment (or Plan ready for implement
    with no PR yet). Prefer Plan. Then run github-project-item-assist for that
    issue with project_url, issue_number, repository, auto_advance=true,
    sdlc_chain=true. If idle, reply "status poll: idle" and stop.
  EOT
}

Troubleshooting common pitfalls

If you hit a wall during your tofu apply loop, check these:

  • Dragging a card did nothing: Repo Issues deliveries only show opened and ping. Dragging a Projects v2 card is not an issues event. The schedule block in Step 7 is the fix for personal boards — eventual, not instant. Match the trigger to the UI gesture.
  • The agent posted @comment.md as text: For gh api, a capital -F expands the @file, while a lowercase -f sends the literal path string. Make sure your runbook explicitly uses -F:
gh api "repos/$OWNER/$REPO/issues/$N/comments" -F body=@assist_comment.md

This is the same class of bug as putting #42 on a shell command line — everything after # is treated as a comment, so the rest of the command disappears.


Wrapping up: see it in action

Apply the flat root, then open a vague issue (or drag a card and wait for the poll):

tofu init
tofu plan
tofu apply

tofu output -raw webhook_ingress_payload_url
tofu output -raw webhook_token

Once applied and triggered, the workflow should leave structured comments on GitHub issues; inspect the actual comments and PR before treating a stage as complete. A successful stage comment should look like this:

### Aiden project assist — Specify

**Issue:** #29
**Status (before):** Specify
**Status (after):** Research
**auto_advance:** true

#### Output
Goal, in/out of scope, acceptance criteria, open questions…

#### Next human step
Review, answer questions, or let the chain continue.

Our dogfooding closed with navigation polish PR #31 — the agent opened it, and a human merged it.

Key takeaways

  • sg_* resources are the product surface — understand them before wrapping modules.
  • Order of operations matters — secrets → providers/models → integrations → agent → workflow → webhook/schedule.
  • Receipts on GitHub — issue comments beat chat transcripts.
  • Issue opened ≠ card dragged ≠ PR merged — wire the triggers you care about.
  • Merge stays human in this workflow; also review other mutation paths beyond the example policy.

Where to look next:


Building AI agents that leave receipts on the systems you already use? Find me on GitHub or LinkedIn.


StackGen develops AI tools for site reliability engineering (SRE), including incident triage and diagnostic workflows. Product details are at ai.stackgen.com.

FAQ

Can an AI agent run a GitHub Project board SDLC?

Yes, if you keep the board as the source of truth, write evidence as issue comments, and treat Status hops as one step at a time — with humans merging any PR.

How do you configure Aiden agents with Terraform or OpenTofu?

Aiden ships a Terraform/OpenTofu provider. This Hard Way post declares agents, workflows, webhooks, schedules, and policies as flat sg_* resources. For a packaged module, see Aiden the Easy Way.

Why did dragging a Project card not trigger my agent webhook?

Repo Issues webhooks fire on issue events, not on Projects v2 Status changes. You need a different signal — for example, a short poll of the board — if humans drag cards.

Should the agent merge the PR?

No. Opening a reviewable PR is enough for a first loop. Merge stays a human decision.