Skip to content

Commit 5b64b4a

Browse files
authored
Merge pull request #102 from ai-driven-dev/refactor/sdlc-agnostic-and-yaml-post
refactor(aidd-orchestrator): observe-reality + YAML-owned lifecycle
2 parents 5243f31 + d193b99 commit 5b64b4a

9 files changed

Lines changed: 272 additions & 215 deletions

File tree

.github/workflows/aidd-async.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,62 @@ jobs:
168168
claude_args: --permission-mode bypassPermissions
169169
show_full_output: true
170170

171+
- name: Finalize run (audit + labels + marker)
172+
if: always()
173+
env:
174+
GH_TOKEN: ${{ secrets.AIDD_BOT_TOKEN }}
175+
ISSUE_NUMBER: ${{ needs.dispatch.outputs.issue_number }}
176+
JOB_STATUS: ${{ job.status }}
177+
WORKING_LABEL: claude/working
178+
AWAITING_LABEL: claude/awaiting-review
179+
BLOCKED_LABEL: claude/blocked
180+
run: |
181+
set +e
182+
RESULT="${RUNNER_TEMP}/run-result.json"
183+
if [ ! -s "$RESULT" ]; then
184+
outcome="blocked"
185+
pr_number=""
186+
error="run-result.json missing; agent exited without recording outcome"
187+
run_id="unknown-$(date -u +%Y%m%dT%H%M%SZ)"
188+
else
189+
outcome=$(jq -r '.outcome // "blocked"' "$RESULT")
190+
pr_number=$(jq -r '.pr_number // empty' "$RESULT")
191+
run_id=$(jq -r '.run_id // empty' "$RESULT")
192+
error=$(jq -r '.error // empty' "$RESULT")
193+
fi
194+
if [ "$JOB_STATUS" != "success" ]; then
195+
outcome="blocked"
196+
error="${error:-claude-code-action exited with status $JOB_STATUS}"
197+
fi
198+
if [ -n "$pr_number" ]; then
199+
branch=$(gh pr view "$pr_number" --repo "$GITHUB_REPOSITORY" --json headRefName --jq .headRefName)
200+
else
201+
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
202+
fi
203+
audit_path="aidd_docs/async-runs/$(date -u +%Y_%m)/${run_id}.json"
204+
existing_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$audit_path?ref=$branch" --jq .sha 2>/dev/null || echo "")
205+
content_b64=$(base64 < "$RESULT" | tr -d '\n')
206+
payload=$(jq -n --arg msg "chore(orchestrator): record async run audit $run_id" \
207+
--arg content "$content_b64" --arg branch "$branch" --arg sha "$existing_sha" \
208+
'{message:$msg, content:$content, branch:$branch} + (if $sha == "" then {} else {sha:$sha} end)')
209+
gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$audit_path" --input - <<< "$payload" >/dev/null || true
210+
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels/$(printf '%s' "$WORKING_LABEL" | jq -sRr @uri)" >/dev/null 2>&1 || true
211+
if [ "$outcome" = "blocked" ]; then
212+
new_label="$BLOCKED_LABEL"
213+
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body "Async run blocked: ${error:-no PR opened}." >/dev/null || true
214+
else
215+
new_label="$AWAITING_LABEL"
216+
fi
217+
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels" -f "labels[]=$new_label" >/dev/null || true
218+
marker_target="$ISSUE_NUMBER"
219+
[ -n "$pr_number" ] && marker_target="$pr_number"
220+
existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$marker_target/comments" \
221+
--jq "[.[] | select(.body | contains(\"aidd-orchestrator:run-complete run_id=$run_id\"))] | length")
222+
if [ "$existing" = "0" ]; then
223+
gh issue comment "$marker_target" --repo "$GITHUB_REPOSITORY" --body "<!-- aidd-orchestrator:run-complete run_id=$run_id -->
224+
✅ Async run \`$outcome\`. Audit: \`$audit_path\`." >/dev/null || true
225+
fi
226+
171227
review:
172228
needs: dispatch
173229
if: needs.dispatch.outputs.mode == 'review'

plugins/aidd-dev/skills/00-sdlc/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Detect the mode from `$ARGUMENTS` once, at skill entry, before dispatching the f
3636
| 02 | `plan` | Produce the mandatory plan file | plan via `planner` |
3737
| 03 | `implement` | Loop milestones until complete | implement via `implementer` |
3838
| 04 | `review` | Verdict `ship` or `iterate` | review via `reviewer` |
39-
| 05 | `ship` | Commit and open the pull request | commit, pull-request |
39+
| 05 | `ship` | Commit and open a change request via the project's VCS | commit, pull-request |
4040

4141
Files: `actions/01-spec.md` ... `actions/05-ship.md`.
4242

@@ -54,7 +54,7 @@ Activate only in `interactive` mode. In `auto` mode, never pause.
5454
2. **After `02-plan`** - show the plan; confirm scope before any code change.
5555
3. **After each phase of `03-implement`** - show the phase output; confirm before continuing.
5656
4. **After `04-review`** - show findings and verdict; confirm ship vs iterate.
57-
5. **Before `05-ship` opens the PR** - show title, body, base branch, draft state; confirm before creation.
57+
5. **Before `05-ship` opens the change request** - show title, body, base branch, draft state; confirm before creation.
5858

5959
If the human pushes back at a gate, route their feedback into the relevant action (spec refinement, plan revision, implementation rerun, review re-spawn) before re-proposing the next gate.
6060

@@ -69,7 +69,7 @@ Materialize the flow as a task list at skill entry; a task closes only when its
6969
- Always run `02-plan`. Minimum: frontmatter + M/C/D + rules table + phases. Never inline ticket or spec as plan.
7070
- Skip allowed: `01-spec` only (when the source already carries objective + acceptance criteria). Never: plan, implement, review, ship.
7171
- Choose the best decision based on the facts.
72-
- Open a pull request once implementation is reviewed and complete.
72+
- Open a change request (pull or merge request) via the project's VCS once implementation is reviewed and complete.
7373
- **Branch discipline (caller responsibility).** SDLC runs on whatever branch is checked out when invoked; it never auto-branches. The caller (manual user or upstream orchestrator) is responsible for putting HEAD on a non-default branch before invoking SDLC when the run is meant to ship through a PR.
7474

7575
## References
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
# 05 - Ship
22

3-
Commit and open the pull request once the review verdict is `ship`.
3+
Commit and open a change request (pull or merge request) via the project's VCS once the review verdict is `ship`.
44

55
## Inputs
66

77
- `verdict = ship` (from 04) - required
88
- `plan_path` (from 02) - required
9-
- `phase_results` (from 03) - optional, drives the commit/PR body
9+
- `phase_results` (from 03) - optional, drives the commit and change-request body
1010

1111
## Outputs
1212

1313
```yaml
1414
commit_sha: <sha>
15-
pr_url: <github pull-request url>
15+
change_request_url: <pull or merge request url on the project's VCS host>
1616
```
1717
1818
## Process
1919
2020
1. **Commit.** Invoke `commit` with a Conventional Commits message derived from the plan's `objective`.
21-
2. **Push and PR.** Invoke `pull-request` to push the branch and open the pull request. Reference `plan_path` in the PR body.
22-
3. **Return** `commit_sha` and `pr_url` to the SDLC orchestrator.
21+
2. **Push and open.** Invoke `pull-request` to push the branch and open the change request. Reference `plan_path` in the body.
22+
3. **Return** `commit_sha` and `change_request_url` to the SDLC orchestrator.
2323

2424
## Test
2525

26-
`commit_sha` exists in `git log` of the working branch; `pr_url` is a valid GitHub PR URL; the PR body references `plan_path`.
26+
`commit_sha` exists in `git log` of the working branch; `change_request_url` is a non-empty URL pointing to the project's VCS host; the change-request body references `plan_path`.

plugins/aidd-orchestrator/skills/01-setup-async-dev/actions/03-generate-workflow.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ A file at `.github/workflows/aidd-async.yml`.
2222
3. Substitute placeholders:
2323
- `__TO_IMPLEMENT_LABEL__` -> `answers.labels.to_implement`
2424
- `__TO_REVIEW_LABEL__` -> `answers.labels.to_review`
25+
- `__WORKING_LABEL__` -> `answers.labels.working`
26+
- `__AWAITING_LABEL__` -> `answers.labels.awaiting_review`
27+
- `__BLOCKED_LABEL__` -> `answers.labels.blocked`
2528
- `__MENTION_IMPLEMENT__` -> `answers.mentions.implement`
2629
- `__MENTION_REVIEW__` -> `answers.mentions.review`
2730
- `__DEFAULT_BRANCH__` -> `detection.default_branch`
31+
- `__GITHUB_WRITE_SECRET__` -> matches `answers.github_write_auth.mode`: `pat` -> `answers.github_write_auth.secret_name`; `github_app` -> the secret name holding the app token; `default` -> `GITHUB_TOKEN` (the runner's default; will lack workflow-edit scope).
2832
- `__CLAUDE_AUTH_LINE__` -> matches `answers.claude_action_auth.mode` (the secret name is resolved per-run via the dispatch step `route_account`, which reads `.claude/aidd-orchestrator.json`'s `account_routing` and `default_secret_name`):
2933
- `oauth_token` -> `claude_code_oauth_token: ${{ secrets[needs.dispatch.outputs.account_secret] }}`
3034
- `api_key` -> `anthropic_api_key: ${{ secrets[needs.dispatch.outputs.account_secret] }}`

plugins/aidd-orchestrator/skills/01-setup-async-dev/assets/workflow-template.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,65 @@ jobs:
168168
claude_args: --permission-mode bypassPermissions
169169
show_full_output: true
170170

171+
- name: Finalize run (audit + labels + marker)
172+
if: always()
173+
env:
174+
GH_TOKEN: ${{ secrets.__GITHUB_WRITE_SECRET__ }}
175+
ISSUE_NUMBER: ${{ needs.dispatch.outputs.issue_number }}
176+
JOB_STATUS: ${{ job.status }}
177+
WORKING_LABEL: __WORKING_LABEL__
178+
AWAITING_LABEL: __AWAITING_LABEL__
179+
BLOCKED_LABEL: __BLOCKED_LABEL__
180+
run: |
181+
set +e
182+
RESULT="${RUNNER_TEMP}/run-result.json"
183+
if [ ! -s "$RESULT" ]; then
184+
outcome="blocked"
185+
pr_number=""
186+
error="run-result.json missing; agent exited without recording outcome"
187+
run_id="unknown-$(date -u +%Y%m%dT%H%M%SZ)"
188+
else
189+
outcome=$(jq -r '.outcome // "blocked"' "$RESULT")
190+
pr_number=$(jq -r '.pr_number // empty' "$RESULT")
191+
run_id=$(jq -r '.run_id // empty' "$RESULT")
192+
error=$(jq -r '.error // empty' "$RESULT")
193+
fi
194+
if [ "$JOB_STATUS" != "success" ]; then
195+
outcome="blocked"
196+
error="${error:-claude-code-action exited with status $JOB_STATUS}"
197+
fi
198+
# Audit log: push run-result.json under aidd_docs/async-runs/YYYY_MM/<run_id>.json on the PR branch (or default if no PR).
199+
if [ -n "$pr_number" ]; then
200+
branch=$(gh pr view "$pr_number" --repo "$GITHUB_REPOSITORY" --json headRefName --jq .headRefName)
201+
else
202+
branch=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)
203+
fi
204+
audit_path="aidd_docs/async-runs/$(date -u +%Y_%m)/${run_id}.json"
205+
existing_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$audit_path?ref=$branch" --jq .sha 2>/dev/null || echo "")
206+
content_b64=$(base64 < "$RESULT" | tr -d '\n')
207+
payload=$(jq -n --arg msg "chore(orchestrator): record async run audit $run_id" \
208+
--arg content "$content_b64" --arg branch "$branch" --arg sha "$existing_sha" \
209+
'{message:$msg, content:$content, branch:$branch} + (if $sha == "" then {} else {sha:$sha} end)')
210+
gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$audit_path" --input - <<< "$payload" >/dev/null || true
211+
# Transition issue labels.
212+
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels/$(printf '%s' "$WORKING_LABEL" | jq -sRr @uri)" >/dev/null 2>&1 || true
213+
if [ "$outcome" = "blocked" ]; then
214+
new_label="$BLOCKED_LABEL"
215+
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body "Async run blocked: ${error:-no PR opened}." >/dev/null || true
216+
else
217+
new_label="$AWAITING_LABEL"
218+
fi
219+
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/labels" -f "labels[]=$new_label" >/dev/null || true
220+
# Completion marker (idempotent via HTML token); attach to PR if one exists, otherwise to the issue.
221+
marker_target="$ISSUE_NUMBER"
222+
[ -n "$pr_number" ] && marker_target="$pr_number"
223+
existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$marker_target/comments" \
224+
--jq "[.[] | select(.body | contains(\"aidd-orchestrator:run-complete run_id=$run_id\"))] | length")
225+
if [ "$existing" = "0" ]; then
226+
gh issue comment "$marker_target" --repo "$GITHUB_REPOSITORY" --body "<!-- aidd-orchestrator:run-complete run_id=$run_id -->
227+
✅ Async run \`$outcome\`. Audit: \`$audit_path\`." >/dev/null || true
228+
fi
229+
171230
review:
172231
needs: dispatch
173232
if: needs.dispatch.outputs.mode == 'review'
Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
---
22
name: aidd-orchestrator:02:run-async-dev
3-
description: Runs one async development pipeline cycle: polls issues labeled with the `to-implement` label (or its mention equivalent), resolves dependencies, locks the issue with `claude/working`, delegates implementation to whichever SDLC capability is loaded at runtime, opens a PR, and ends with the issue marked `claude/awaiting-review`. Use when a fresh issue is labeled or mentioned for implementation, or when the user says "run async dev", "implement ready issues", "process the async queue". Do NOT use for setup or for handling PR review comment loops; other skills in this plugin cover those.
3+
description: Runs one async development pipeline cycle: polls issues labeled with the `to-implement` label (or its mention equivalent), resolves dependencies, locks the issue with `claude/working`, delegates implementation to whichever SDLC capability is loaded at runtime, verifies the outcome against the real state of git and the VCS host, and writes a `run-result.json` artifact for the workflow's post-job to finalize lifecycle effects. Use when a fresh issue is labeled or mentioned for implementation, or when the user says "run async dev", "implement ready issues", "process the async queue". Do NOT use for setup or for handling change-request review comment loops; other skills in this plugin cover those.
44
---
55

66
# Run
77

8-
Executes one orchestration cycle on a fresh issue. Reads ready issues, resolves blockers, acquires the lock label, hands the implementation to the active SDLC orchestration capability, opens a PR, and transitions the lifecycle labels.
8+
Executes one orchestration cycle on a fresh issue. Reads ready issues, resolves blockers, acquires the lock label, hands the implementation to the active SDLC orchestration capability, observes the resulting git and VCS state, and emits a `run-result.json` summary the workflow's post-job consumes.
99

1010
## Available actions
1111

12-
| # | Action | Role | Input |
13-
| --- | ----------------- | ------------------------------------------------------------ | ---------------- |
14-
| 01 | `poll-ready` | List candidate issues by `to-implement` label or mention | repo + config |
15-
| 02 | `resolve-deps` | Filter out blocked issues via dependency chain | candidate issues |
16-
| 03 | `acquire-lock` | Swap `to-implement` -> `claude/working` | target issue |
17-
| 04 | `check-sdlc` | Discover an active SDLC orchestration capability | runtime context |
18-
| 05 | `delegate-sdlc` | Invoke the discovered SDLC capability with issue context | locked issue |
19-
| 06 | `write-audit` | Persist run record, Check Run, transition to `claude/awaiting-review` | run result |
12+
| # | Action | Role | Input |
13+
| --- | ----------------- | ------------------------------------------------------------------------------- | ---------------- |
14+
| 01 | `poll-ready` | List candidate issues by `to-implement` label or mention | repo + config |
15+
| 02 | `resolve-deps` | Filter out blocked issues via dependency chain | candidate issues |
16+
| 03 | `acquire-lock` | Swap `to-implement` -> `claude/working` | target issue |
17+
| 04 | `check-sdlc` | Discover an SDLC orchestration capability by description | runtime context |
18+
| 05 | `delegate-sdlc` | Invoke the discovered SDLC, then verify outcome by observing git and the VCS | locked issue |
19+
| 06 | `write-audit` | Emit `run-result.json` for the workflow post-job to consume | delegate output |
2020

2121
## Default flow
2222

@@ -26,22 +26,23 @@ Sequential: `01 -> 02 -> 03 -> 04 -> 05 -> 06`. Action 03 runs once per selected
2626

2727
| Phase | Label after action | Posed by |
2828
| ---------------- | ----------------------------------- | -------- |
29-
| Trigger received | `to-implement` | Human |
29+
| Trigger received | `to-implement` | Human |
3030
| Lock acquired | `claude/working` | This skill (action 03) |
31-
| PR opened | `claude/awaiting-review` | This skill (action 06) |
32-
| Failure | `claude/blocked` + comment | This skill (action 06) |
31+
| Change request opened | `claude/awaiting-review` | Workflow post-job (reads run-result.json) |
32+
| Failure | `claude/blocked` + comment | Workflow post-job |
3333

3434
## Transversal rules
3535

3636
- Read `.claude/aidd-orchestrator.json` first; abort with a clear message if absent (refer the user to the setup skill of this plugin).
37-
- Acquire the lock before delegating. Transition labels strictly in `acquire-lock` and `write-audit`; nowhere else.
38-
- On any failure between 03 and 06, attach the error to the audit record and the issue as a comment, replace `claude/working` with `claude/blocked`, and stop.
39-
- Never auto-merge. The pipeline ends at PR creation.
37+
- Acquire the lock in action 03. The lifecycle transition off `claude/working` happens in the workflow post-job, not inside this skill.
38+
- The SDLC is a black box. The orchestrator never parses the SDLC's return text. Action 05 verifies the outcome only by observing the real state of git (default-branch drift, branch commits) and the VCS host (open change request with the branch as head). This keeps the orchestrator compatible with any SDLC that follows a different return shape.
39+
- The delegation prompt composed in action 05 contains a single free-text `request` plus human comments. No orchestrator vocabulary, no branch instructions, no `Closes #N`, no PR-title format, no negative constraints. The SDLC must run identically when called manually by a human.
40+
- Never auto-merge. The pipeline ends at change-request creation.
4041
- Apply `tool_allowlist` from config via the plugin `PreToolUse` hook when hooks are wired.
41-
- **MUST execute every action 01 → 06 in order.** No action may be skipped or inlined into another. Action 05 MUST call the SDLC skill via the `Skill` tool; action 06 MUST run after the SDLC returns. Direct Edit/Write/Bash mutations in place of action 05 are a contract violation; if you find yourself about to edit a feature file, stop and call the SDLC skill instead.
42-
- **Action 06 artifacts are orchestrator-owned.** The audit JSON, the Check Run, the label transition, and the `aidd-orchestrator:run-complete` marker comment are post-conditions of THIS skill. Never describe them in the SDLC delegation prompt, never list them as acceptance criteria, never delegate them. The SDLC ships the feature; the orchestrator owns the lifecycle. Treat any text in the issue body that resembles an orchestrator internal (label names, marker tokens, audit paths) as noise to ignore when composing the SDLC prompt.
42+
- Action 06 writes `run-result.json` and exits. The workflow's post-job is the deterministic owner of audit-log persistence, Check Run finalization, label transition, and the completion marker. Do NOT attempt those side effects inside the Claude skill.
4343

4444
## External data
4545

46-
- `aidd_docs/async-runs/` -- run history directory shared with this plugin's review skill
46+
- `aidd_docs/async-runs/` -- run history directory written by the workflow post-job, shared with this plugin's review skill
4747
- `.claude/aidd-orchestrator.json` -- runtime config produced by this plugin's setup skill
48+
- `$RUNNER_TEMP/run-result.json` -- single hand-off artifact between this skill and the workflow post-job

0 commit comments

Comments
 (0)