Aiden the Hard Way: Vague GitHub Issues to Review PRs
GitHub Projects are great until the board fills with cards that say “make nav better” and absolutely nothing else.
Humans then have to do unpaid product work in the comments: turning the wish into acceptance criteria (Specify), looking at the repo to see what already exists (Research), writing a plan someone could actually implement (Plan), and maybe opening a PR (Implement).
I wanted a boring demo that still felt magical: open a vague GitHub issue, watch it land on a Project board, and let Aiden walk it through that entire lifecycle — ending with a review PR. No second orchestration product. No “trust me, the agent did homework” without a comment on the issue as evidence.
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
Before writing code, apply bring-up discipline to the board.
| Do | Don’t |
|---|---|
| Process one issue per run | Boil the ocean across the whole board |
| 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 + org/project id). Without that, tofu apply has nowhere to create resources — this is not a local-only sandbox.
Aiden is not a platform where you just “paste a system prompt into a dashboard.” StackGen ships a native provider "sg". Point it at your tenant, and every agent object becomes plan, apply, and drift — using the same muscle memory as the rest of your infrastructure.
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"
}
We use OpenTofu (tofu) interchangeably with Terraform for fmt, init, plan, and apply.
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
Without Projects scopes, GraphQL Status 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 strictly behind HITL approval.
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
openedandping. Dragging a Projects v2 card is not anissuesevent. 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.mdas text: Forgh api, a capital-Fexpands the@file, while a lowercase-fsends 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, your agent leaves structured receipts on GitHub issues. 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 — always require human-in-the-loop for destructive actions.
Where to look next:
- Aiden the Easy Way — same outcome via a public OpenTofu module
- Terraform for Agent Configuration
- How to Debug Multi-Step AI Agent Workflows
- AI Agent Runtime vs Platform
Building AI agents that leave receipts on the systems you already use? Find me on GitHub or LinkedIn.
🚀 We’re building AI-powered SRE at StackGen. If you’re tired of 3 AM pages and want AI agents that triage incidents, run diagnostics, and draft RCA reports — check out ai.stackgen.com and try our new SRE offering.
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.
Stay in the loop — production notes on AI agents, workflows, and SRE.
Low volume — new posts and curated reading lists. Unsubscribe anytime.