Skip to content

Commit 8972d86

Browse files
blafourcadeclaude
andcommitted
fix(aidd-orchestrator): make audit + finalize self-verifying
Action 06-write-audit now pushes the audit JSON via the Contents API instead of `git checkout` + commit + push. The old flow lost the untracked audit file when switching branches on the runner and caused the action to silently abort, leaving the issue stuck on `claude/working`. Action 04-finalize handles the `no_comments` stop reason (was missing from the input enum, so the skill exited without posting a summary on fresh-PR `@claude /review` triggers) and always posts the summary. Both actions: - emit a `aidd-orchestrator:run-complete` / `:review-complete` completion-marker PR comment as the sole exit signal. - carry a Test section the agent must run before exiting, with one `OK` / `FAIL` assertion per persisted artifact (audit file, check run, labels, summary, marker). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7ad6c53 commit 8972d86

2 files changed

Lines changed: 107 additions & 44 deletions

File tree

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
# 06 -- Write Audit
22

3-
Persists the run record, creates the GitHub Check Run, and transitions the lifecycle labels to `claude/awaiting-review` (or `claude/blocked` on failure).
3+
Persist the run record on the PR branch, finalize the Check Run, transition the issue labels, post the completion marker.
44

55
## Inputs
66

7-
- `run_record` (required) -- merged data from `03-acquire-lock` and `05-delegate-sdlc`
8-
- `config` (required) -- parsed `.claude/aidd-orchestrator.json`
7+
- `run_record` -- merged data from `03-acquire-lock` and `05-delegate-sdlc`
8+
- `config` -- parsed `.claude/aidd-orchestrator.json`
99

1010
## Outputs
1111

1212
```json
1313
{
1414
"audit_path": "aidd_docs/async-runs/2026_05/2026-05-07T10-12-31Z-i42.json",
15-
"check_run_id": 9876543210
15+
"audit_commit_url": "https://github.com/org/repo/commit/<sha>",
16+
"check_run_id": 9876543210,
17+
"labels_after": ["claude/awaiting-review"],
18+
"completion_marker_url": "https://github.com/org/repo/pull/<pr>#issuecomment-..."
1619
}
1720
```
1821

@@ -22,25 +25,43 @@ Persists the run record, creates the GitHub Check Run, and transitions the lifec
2225

2326
## Process
2427

25-
**MANDATORY — this action must run and must persist its artifacts.** Skipping any step (especially the audit write or its commit + push to the PR branch) is a contract violation. The audit JSON is the single source of truth for the run; without it on the PR branch, the run is unverifiable.
26-
27-
1. Compute `audit_dir = config.audit.log_dir + "/" + YYYY_MM` (UTC). Create it if missing.
28-
2. Write `<audit_dir>/<run_id>.json` with the full run record: trigger, issue, dependency check, lock timestamps, SDLC outcome (including `delegated_via_skill` boolean and the skill name actually called), errors if any, plugin version. The `delegated_via_skill` field MUST be `true` when action 05 invoked the SDLC skill via the `Skill` tool; `false` means the orchestrator bypassed delegation and the run is flagged as a contract violation.
29-
3. **Commit and push the audit file to the PR branch** (the SDLC pushed the feature branch in action 05). Run from the runner's working tree:
28+
1. Compose `audit_path = config.audit.log_dir + "/" + YYYY_MM + "/" + run_id + ".json"`. Build the run record JSON: trigger, issue, lock timestamps, SDLC outcome, `delegated_via_skill`, errors, plugin version.
29+
2. Push the audit file to the PR branch via the Contents API (no `git checkout`):
30+
```bash
31+
SHA=$(gh api "repos/$GITHUB_REPOSITORY/contents/$audit_path?ref=$branch" --jq .sha 2>/dev/null || echo "")
32+
PAYLOAD=$(jq -n --arg msg "chore(orchestrator): record async run audit $run_id" \
33+
--arg content "$(printf '%s' "$AUDIT_JSON" | base64 | tr -d '\n')" \
34+
--arg branch "$branch" --arg sha "$SHA" \
35+
'{message:$msg, content:$content, branch:$branch} + (if $sha == "" then {} else {sha:$sha} end)')
36+
audit_commit_url=$(gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$audit_path" --input - <<< "$PAYLOAD" --jq .commit.html_url)
37+
```
38+
3. If `config.audit.github_check_run`: create or update Check Run `aidd-async/<run_id>`. Capture `check_run_id` and set a final `conclusion` (`success` / `failure` / `action_required`).
39+
4. Transition labels: remove `config.labels.working`; on success add `config.labels.awaiting_review`, on failure add `config.labels.blocked` and post the error on the issue. Capture `labels_after` from the API response.
40+
5. Post the completion marker (single PR comment, idempotent via the HTML token):
3041
```
31-
git fetch origin feat/issue-<n>-<slug>
32-
git checkout feat/issue-<n>-<slug>
33-
git add <audit_dir>/<run_id>.json
34-
git commit -m "chore(orchestrator): record async run audit <run_id>" --no-verify
35-
git push origin feat/issue-<n>-<slug>
42+
<!-- aidd-orchestrator:run-complete run_id=<run_id> -->
43+
✅ Async run complete. Audit: <audit_commit_url>.
3644
```
37-
`--no-verify` is used because the audit commit is an orchestrator artefact, not a feature change. If `delegated_via_skill` is `false`, additionally post a PR comment referencing the contract violation so a human can decide whether to keep the PR.
38-
4. If `config.audit.github_check_run` is true, call `gh api repos/{owner}/{repo}/check-runs` to create or update a Check Run named `aidd-async/<run_id>` with `status`, `conclusion`, and `output.summary` reflecting the run (including `delegated_via_skill`).
39-
5. Transition labels on the issue:
40-
- On success (PR was opened): remove `config.labels.working`, add `config.labels.awaiting_review`.
41-
- On failure: remove `config.labels.working`, add `config.labels.blocked`. Post a comment on the issue with the error details.
42-
6. Return the audit path and check run id.
45+
Capture `completion_marker_url`. The agent uses this as its sole exit signal.
46+
6. Run the Test section. Exit only when every assertion prints `OK`.
4347

4448
## Test
4549

46-
After a successful run: `gh api 'repos/{owner}/{repo}/contents/aidd_docs/async-runs/<YYYY_MM>/<run_id>.json?ref=feat/issue-<n>-<slug>'` returns the file (proving it was committed and pushed); `jq '.run_id, .pr_number, .delegated_via_skill' <local-copy>` returns the run id, PR number, and `true`; `gh api repos/{owner}/{repo}/check-runs/<id>` returns `conclusion: "success"`; `gh issue view <n> --repo <owner>/<repo> --json labels --jq '.labels[].name'` includes `claude/awaiting-review` and excludes `claude/working`. If `delegated_via_skill` is `false`, the PR carries a comment naming the contract violation.
50+
```bash
51+
gh api "repos/$GITHUB_REPOSITORY/contents/$audit_path?ref=$branch" --jq .sha >/dev/null \
52+
&& echo "OK audit_file" || echo "FAIL audit_file"
53+
54+
gh api "repos/$GITHUB_REPOSITORY/check-runs/$check_run_id" --jq .conclusion \
55+
| grep -Eq '^(success|neutral|failure|action_required)$' \
56+
&& echo "OK check_run" || echo "FAIL check_run"
57+
58+
gh api "repos/$GITHUB_REPOSITORY/issues/$issue_number" --jq '[.labels[].name]' \
59+
| jq -e 'contains(["claude/awaiting-review"]) and (contains(["claude/working"]) | not)' >/dev/null \
60+
&& echo "OK labels" || echo "FAIL labels"
61+
62+
gh api "repos/$GITHUB_REPOSITORY/issues/$pr_number/comments" \
63+
--jq "[.[] | select(.body | contains(\"aidd-orchestrator:run-complete run_id=$run_id\"))] | length" \
64+
| grep -q '^1$' && echo "OK marker" || echo "FAIL marker"
65+
```
66+
67+
On failure mode (`delegated_via_skill == false` or SDLC errored): assertion 3 expects `claude/blocked` instead of `claude/awaiting-review`.
Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,59 @@
11
# 04 -- Finalize
22

3-
Closes the loop after a stop decision: tags the audit record, posts a structured summary comment, and updates the Check Run.
3+
Tag the audit record, post the structured summary, update the Check Run, transition labels, drop the completion marker.
44

55
## Inputs
66

7-
- `pr_number` (required) -- integer
8-
- `stop_reason` (required) -- one of `max_iterations`, `blocked_label`, `human_reviewer`
9-
- `iteration_log` (required) -- accumulated entries written by `03-fix-iteration`
10-
- `trigger_comment_id` (optional) -- id of the comment that triggered the loop
7+
- `pr_number`
8+
- `stop_reason` -- one of `max_iterations`, `blocked_label`, `human_reviewer`, `no_comments`
9+
- `iteration_log` -- entries from `03-fix-iteration`; may be empty when `stop_reason == "no_comments"`
10+
- `trigger_comment_id` (optional)
11+
- `run_id`
1112

1213
## Outputs
1314

1415
```json
1516
{
1617
"audit_path": "aidd_docs/async-runs/2026_05/2026-05-07T10-12-31Z-i42.json",
18+
"audit_commit_url": "https://github.com/org/repo/commit/<sha>",
1719
"check_run_conclusion": "neutral",
18-
"summary_comment_url": "https://github.com/org/repo/pull/117#issuecomment-..."
20+
"summary_comment_url": "https://github.com/org/repo/pull/<pr>#issuecomment-...",
21+
"labels_after": ["claude/awaiting-review"],
22+
"completion_marker_url": "https://github.com/org/repo/pull/<pr>#issuecomment-..."
1923
}
2024
```
2125

2226
## Depends on
2327

24-
- `02-detect-stop` (only when its `decision == "stop"`)
28+
- `02-detect-stop` (when `decision == "stop"`)
2529

2630
## Process
2731

28-
1. Open the existing audit record. Append `{ "loop_closed_at": "<ISO8601>", "stop_reason": "<reason>", "iterations": iteration_log }`.
29-
2. Update the GitHub Check Run for the run id: `conclusion = "success"` if `stop_reason == "human_reviewer"` and last iteration tests passed, else `"neutral"` for `max_iterations` and `"action_required"` for `blocked_label`.
30-
3. Render the summary body from the template below; substitute every placeholder. Empty sections must be omitted, never left blank.
31-
4. Post the summary as a single PR comment via `gh pr comment <pr> --repo <owner>/<repo> --body-file -`.
32-
5. If `trigger_comment_id` is set, ensure a final reaction is set on the trigger comment: `+1` for success, `confused` for `max_iterations`, `-1` for `blocked_label`. Remove any prior `eyes` reaction first.
33-
6. Transition labels on the linked issue (`gh pr view <pr> --json closingIssuesReferences`):
34-
- On `human_reviewer` (success): remove `config.labels.working`, add `config.labels.awaiting_review`.
35-
- On `max_iterations`: remove `config.labels.working`, add `config.labels.awaiting_review` (PR exists, human must look).
36-
- On `blocked_label`: remove `config.labels.working`, add `config.labels.blocked`.
32+
1. Read the existing audit JSON via the Contents API. Append `{ "loop_closed_at": "<ISO8601>", "stop_reason": "<reason>", "iterations": iteration_log }`. Push the update with `gh api -X PUT` (pass the prior `sha`). Capture `audit_commit_url`.
33+
2. Update Check Run `aidd-async/<run_id>`: `success` for `human_reviewer` (tests passed) or `no_comments`; `neutral` for `max_iterations`; `action_required` for `blocked_label`.
34+
3. Render the summary body from the template below. When `stop_reason == "no_comments"` and `iteration_log` is empty, show only the header row plus `_no fix iterations on this loop_`.
35+
4. Post the summary as a single PR comment: `summary_comment_url=$(gh pr comment $pr_number --body-file -)`. Always post, including on `no_comments`.
36+
5. If `trigger_comment_id` set: remove the prior `eyes` reaction; add the final one (`+1` for `human_reviewer` / `no_comments`, `confused` for `max_iterations`, `-1` for `blocked_label`).
37+
6. Transition labels on the linked issue:
38+
```bash
39+
issue_number=$(gh pr view $pr_number --json closingIssuesReferences --jq '.closingIssuesReferences[0].number')
40+
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$issue_number/labels/$(printf '%s' "$WORKING" | jq -sRr @uri)" || true
41+
case "$stop_reason" in
42+
blocked_label) NEW="$BLOCKED" ;;
43+
*) NEW="$AWAITING" ;;
44+
esac
45+
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$issue_number/labels" -f "labels[]=$NEW"
46+
```
47+
7. Post the completion marker (single PR comment, idempotent):
48+
```
49+
<!-- aidd-orchestrator:review-complete run_id=<run_id> -->
50+
✅ Async review complete (`<stop_reason>`). Summary: <summary_comment_url>.
51+
```
52+
Capture `completion_marker_url`. The agent uses this as its sole exit signal.
53+
8. Run the Test section. Exit only when every assertion prints `OK`.
3754

3855
## Summary template
3956

40-
The body is markdown and must follow this exact structure:
41-
4257
```markdown
4358
## Async review summary
4459

@@ -60,16 +75,43 @@ The body is markdown and must follow this exact structure:
6075

6176
### Next step
6277

63-
- `human_reviewer` -- A reviewer has commented since the last automated push. The loop is paused. To continue, address the comments and re-trigger with `<retry mention>` or `<retry label>`.
64-
- `max_iterations` -- The maximum of `<max_iterations>` automatic iterations was reached without convergence. Human review is required before another retry.
65-
- `blocked_label` -- The `<blocked label>` label is set. Remove it once the blocker is resolved to allow another retry.
78+
- `human_reviewer` -- A reviewer commented since the last automated push. Re-trigger with `<retry mention>` / `<retry label>` once comments are addressed.
79+
- `max_iterations` -- Hit `<max_iterations>` without convergence. Human review required before retry.
80+
- `blocked_label` -- `<blocked label>` is set. Remove it to allow another retry.
81+
- `no_comments` -- Nothing actionable to address. Re-trigger with a concrete inline review comment.
6682

6783
---
6884
*Audit log: `<audit_path>`.*
6985
```
7086

71-
The summary stays under ~30 lines on a typical PR. Truncate quoted comment bodies to 80 characters with `...`. Iterations table has one row per iteration; if no fix iterations ran, show only the header row plus a `_no fix iterations on this loop_` note.
87+
Stay under ~30 lines on a typical PR. Truncate quoted bodies to 80 characters with `...`.
7288

7389
## Test
7490

75-
Given an audit record with two `fix-iteration` entries and `stop_reason = human_reviewer`: after this action runs, the audit JSON contains `loop_closed_at` and `stop_reason`, the Check Run conclusion is `success`, the trigger comment carries a `+1` reaction, and a PR comment exists whose body matches the template (contains the `## Async review summary` heading, an `Iterations` table with two data rows, a `Next step` section with the `human_reviewer` bullet, and an `Audit log` footer).
91+
```bash
92+
gh api "repos/$GITHUB_REPOSITORY/contents/$audit_path?ref=$branch" --jq .content \
93+
| base64 -d | jq -e '.loop_closed_at and .stop_reason' >/dev/null \
94+
&& echo "OK audit_closed" || echo "FAIL audit_closed"
95+
96+
gh api "repos/$GITHUB_REPOSITORY/check-runs/$check_run_id" --jq .conclusion \
97+
| grep -Eq '^(success|neutral|failure|action_required)$' \
98+
&& echo "OK check_run" || echo "FAIL check_run"
99+
100+
gh api "repos/$GITHUB_REPOSITORY/issues/$pr_number/comments" \
101+
--jq "[.[] | select(.body | startswith(\"## Async review summary\"))] | length" \
102+
| grep -q '^[1-9]' && echo "OK summary" || echo "FAIL summary"
103+
104+
gh api "repos/$GITHUB_REPOSITORY/issues/$issue_number" --jq '[.labels[].name]' \
105+
| jq -e --arg sr "$stop_reason" '
106+
if $sr == "blocked_label"
107+
then contains(["claude/blocked"]) and (contains(["claude/working"]) | not)
108+
else contains(["claude/awaiting-review"]) and (contains(["claude/working"]) | not)
109+
end' >/dev/null \
110+
&& echo "OK labels" || echo "FAIL labels"
111+
112+
gh api "repos/$GITHUB_REPOSITORY/issues/$pr_number/comments" \
113+
--jq "[.[] | select(.body | contains(\"aidd-orchestrator:review-complete run_id=$run_id\"))] | length" \
114+
| grep -q '^1$' && echo "OK marker" || echo "FAIL marker"
115+
```
116+
117+
`no_comments` variant: assertion 3 still passes (summary contains the `_no fix iterations on this loop_` note); the `Iterations` table has only its header row.

0 commit comments

Comments
 (0)