@plainconceptsplatform/workflows 0.17.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/catalog-installation.js +25 -13
  2. package/dist/stack-defaults.js +16 -16
  3. package/dist/worker-env.js +24 -21
  4. package/loops/actions/add-issue-labels/action.yml +50 -50
  5. package/loops/actions/agent-output.cjs +17 -17
  6. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  7. package/loops/actions/apply-agent-comments/action.yml +42 -42
  8. package/loops/actions/apply-agent-labels/action.yml +55 -55
  9. package/loops/actions/apply-agent-output/action.yml +108 -108
  10. package/loops/actions/assess-blast-radius/action.yml +148 -0
  11. package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
  12. package/loops/actions/classify-route/action.yml +100 -100
  13. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  14. package/loops/actions/close-agent-issues/action.yml +43 -43
  15. package/loops/actions/create-agent-issues/action.yml +52 -52
  16. package/loops/actions/create-issue-comment/action.yml +29 -29
  17. package/loops/actions/download-agent-output/action.yml +53 -53
  18. package/loops/actions/housekeeping/action.yml +55 -2
  19. package/loops/actions/identify-gate-subject/action.yml +134 -122
  20. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  21. package/loops/actions/list-open-issues/action.yml +33 -33
  22. package/loops/actions/load-issue-context/action.yml +45 -45
  23. package/loops/actions/merge-agent-pr/action.yml +49 -49
  24. package/loops/actions/push-agent-branch/action.yml +45 -45
  25. package/loops/actions/remove-issue-labels/action.yml +37 -37
  26. package/loops/actions/require-open-issue/action.yml +59 -0
  27. package/loops/actions/update-agent-issues/action.yml +58 -58
  28. package/loops/actions/validate-merge-gate-output/action.yml +62 -40
  29. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
  30. package/loops/actions/validate-refine-output/action.yml +48 -44
  31. package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
  32. package/loops/actions/validate-review-output/action.yml +35 -35
  33. package/loops/actions/validate-triage-output/action.yml +36 -36
  34. package/loops/actions/verify-composite-actions/action.yml +9 -9
  35. package/loops/actions/verify-refine-output/action.yml +9 -9
  36. package/loops/actions/verify-refine-output/verify-refine-output.sh +6 -1
  37. package/loops/actions/verify-route-matrix/action.yml +9 -9
  38. package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
  39. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +630 -8
  40. package/loops/scripts/compile-agent-workflows.mjs +331 -331
  41. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  42. package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
  43. package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
  44. package/loops/templates/issues/bug_report.yml +109 -109
  45. package/loops/templates/issues/feature_request.yml +75 -75
  46. package/loops/templates/opencode/opencode.ci.json +55 -49
  47. package/loops/templates/opencode/opencode.ci.json.md +59 -49
  48. package/loops/templates/release/github-release.yml +30 -30
  49. package/loops/workflows/agent-apply-review.md +33 -3
  50. package/loops/workflows/agent-audit.md +36 -0
  51. package/loops/workflows/agent-implement.md +77 -5
  52. package/loops/workflows/agent-merge-gate.md +368 -148
  53. package/loops/workflows/agent-refine.md +93 -17
  54. package/loops/workflows/agent-release.md +4 -4
  55. package/loops/workflows/agent-triage.md +33 -1
  56. package/loops/workflows/authorize-bot-work.yml +105 -105
  57. package/loops/workflows/shared/opencode-ci.md +206 -206
  58. package/loops/workflows/shared/platform-defaults.md +19 -19
  59. package/loops/workflows/work-router.yml +25 -11
  60. package/package.json +2 -2
@@ -1,53 +1,53 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/download-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Download agent output
3
- description: Download the agent artifact and expose the output JSON and bundle paths.
4
- inputs:
5
- artifact-name:
6
- description: >-
7
- Artifact name to download. gh-aw prefixes it with the value it derives from the
8
- workflow's inputs, so a workflow_call worker must pass the activation job's
9
- artifact_prefix output followed by "agent", not the bare name.
10
- required: true
11
- outputs:
12
- output-file:
13
- description: Path to agent_output.json, or empty if not found.
14
- value: ${{ steps.download.outputs.output-file }}
15
- bundle-file:
16
- description: Path to the git bundle, or empty if not found.
17
- value: ${{ steps.download.outputs.bundle-file }}
18
- item-count:
19
- description: Number of items in the agent output.
20
- value: ${{ steps.download.outputs.item-count }}
21
- runs:
22
- using: composite
23
- steps:
24
- - name: Download agent artifact
25
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
26
- with:
27
- name: ${{ inputs.artifact-name }}
28
- path: agent-artifact
29
- - name: Locate output and bundle
30
- id: download
31
- shell: bash
32
- env:
33
- ARTIFACT_NAME: ${{ inputs.artifact-name }}
34
- run: |
35
- set -euo pipefail
36
- OUTPUT_FILE=""
37
- BUNDLE_FILE=""
38
- ITEM_COUNT="0"
39
-
40
- if [ -f "agent-artifact/agent_output.json" ]; then
41
- OUTPUT_FILE="agent-artifact/agent_output.json"
42
- ITEM_COUNT=$(jq '.items | length' "$OUTPUT_FILE")
43
- BUNDLE_FILE=$(ls agent-artifact/aw-*.bundle 2>/dev/null | head -1 || true)
44
- else
45
- echo "::error::Artifact '$ARTIFACT_NAME' contained no agent_output.json. Nothing the agent proposed will be applied."
46
- exit 1
47
- fi
48
-
49
- echo "Artifact '$ARTIFACT_NAME' carries $ITEM_COUNT item(s)."
50
-
51
- echo "output-file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
52
- echo "bundle-file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT"
53
- echo "item-count=$ITEM_COUNT" >> "$GITHUB_OUTPUT"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/download-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Download agent output
3
+ description: Download the agent artifact and expose the output JSON and bundle paths.
4
+ inputs:
5
+ artifact-name:
6
+ description: >-
7
+ Artifact name to download. gh-aw prefixes it with the value it derives from the
8
+ workflow's inputs, so a workflow_call worker must pass the activation job's
9
+ artifact_prefix output followed by "agent", not the bare name.
10
+ required: true
11
+ outputs:
12
+ output-file:
13
+ description: Path to agent_output.json, or empty if not found.
14
+ value: ${{ steps.download.outputs.output-file }}
15
+ bundle-file:
16
+ description: Path to the git bundle, or empty if not found.
17
+ value: ${{ steps.download.outputs.bundle-file }}
18
+ item-count:
19
+ description: Number of items in the agent output.
20
+ value: ${{ steps.download.outputs.item-count }}
21
+ runs:
22
+ using: composite
23
+ steps:
24
+ - name: Download agent artifact
25
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
26
+ with:
27
+ name: ${{ inputs.artifact-name }}
28
+ path: agent-artifact
29
+ - name: Locate output and bundle
30
+ id: download
31
+ shell: bash
32
+ env:
33
+ ARTIFACT_NAME: ${{ inputs.artifact-name }}
34
+ run: |
35
+ set -euo pipefail
36
+ OUTPUT_FILE=""
37
+ BUNDLE_FILE=""
38
+ ITEM_COUNT="0"
39
+
40
+ if [ -f "agent-artifact/agent_output.json" ]; then
41
+ OUTPUT_FILE="agent-artifact/agent_output.json"
42
+ ITEM_COUNT=$(jq '.items | length' "$OUTPUT_FILE")
43
+ BUNDLE_FILE=$(ls agent-artifact/aw-*.bundle 2>/dev/null | head -1 || true)
44
+ else
45
+ echo "::error::Artifact '$ARTIFACT_NAME' contained no agent_output.json. Nothing the agent proposed will be applied."
46
+ exit 1
47
+ fi
48
+
49
+ echo "Artifact '$ARTIFACT_NAME' carries $ITEM_COUNT item(s)."
50
+
51
+ echo "output-file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
52
+ echo "bundle-file=$BUNDLE_FILE" >> "$GITHUB_OUTPUT"
53
+ echo "item-count=$ITEM_COUNT" >> "$GITHUB_OUTPUT"
@@ -75,6 +75,12 @@ runs:
75
75
  const names = (issue) => issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
76
76
 
77
77
  const needsHuman = [];
78
+ // Gate telemetry. Every disposition the gate has ever posted is already on a pull
79
+ // request, marked, so the rate costs one extra listing rather than a store.
80
+ const dispositions = {};
81
+ const autoMerged = [];
82
+ const reverts = [];
83
+ const metricsWindowMs = 14 * 24 * 3600000;
78
84
  // Stalled work still inside its retry window. Separate from needsHuman because nobody
79
85
  // needs to act on it yet, and reported anyway because the alternative is silence: the
80
86
  // window is 6 hours and the budget is 3 attempts, so an issue that keeps stalling was
@@ -203,14 +209,45 @@ runs:
203
209
  ...context.repo, issue_number: pull.number, per_page: 100,
204
210
  });
205
211
  const verdict = [...comments].reverse().find((c) => (c.body ?? '').includes('<!-- agent-merge-gate -->') && (c.body ?? '').includes('**Verdict:**'));
206
- const parsed = verdict ? /\*\*Verdict:\*\*\s*(\w+)/.exec(verdict.body)?.[1] : undefined;
207
- if (parsed && parsed !== 'merge') {
212
+ const parsed = verdict ? /\*\*Verdict:\*\*\s*([\w-]+)/.exec(verdict.body)?.[1] : undefined;
213
+ // The gate's dispositions are auto-merge, human-review, owner-review and blocked.
214
+ // Only the first needs nobody. The character class has to allow the hyphen or every
215
+ // disposition reads as the bare word "auto" or "human" and nothing matches.
216
+ if (parsed) dispositions[parsed] = (dispositions[parsed] ?? 0) + 1;
217
+ if (parsed && parsed !== 'auto-merge') {
208
218
  needsHuman.push({ kind: 'pr-parked', pull, verdict: parsed, waited: age(verdict.created_at) });
209
219
  } else if (!verdict && age(pull.created_at) > stalePrMs) {
210
220
  needsHuman.push({ kind: 'pr-stale', pull, waited: age(pull.created_at) });
211
221
  }
212
222
  }
213
223
 
224
+ // 5b. The one number this whole gate exists to move. Open pull requests alone would
225
+ // only ever count the parked ones, because an auto-merged pull request closes within
226
+ // seconds of its verdict, so the rate would read as zero forever. Closed pull requests
227
+ // from the digest window are where the merges are.
228
+ for (const pull of await github.paginate(github.rest.pulls.list, {
229
+ ...context.repo, state: 'closed', sort: 'updated', direction: 'desc', per_page: 100,
230
+ })) {
231
+ if (age(pull.updated_at) > metricsWindowMs) break;
232
+ // A revert names the pull request it undoes, which is how an escaped defect is
233
+ // attributed back to the disposition that let it through. Reverts are frequently
234
+ // opened by a person, so this one line sits above the bot filter.
235
+ if (pull.merged_at) {
236
+ const undone = /revert .*#(\d+)/i.exec(pull.title ?? '');
237
+ if (undone) reverts.push(Number(undone[1]));
238
+ }
239
+ if (!isBot(pull.user?.login ?? '')) continue;
240
+ const comments = await github.paginate(github.rest.issues.listComments, {
241
+ ...context.repo, issue_number: pull.number, per_page: 100,
242
+ });
243
+ const verdict = [...comments].reverse().find((c) => (c.body ?? '').includes('<!-- agent-merge-gate -->') && (c.body ?? '').includes('**Verdict:**'));
244
+ const parsed = verdict ? /\*\*Verdict:\*\*\s*([\w-]+)/.exec(verdict.body)?.[1] : undefined;
245
+ if (parsed) dispositions[parsed] = (dispositions[parsed] ?? 0) + 1;
246
+ // An auto-merge followed by a revert is the number that has to stay at zero while
247
+ // the rate climbs. Anything else is a rate bought with escaped defects.
248
+ if (parsed === 'auto-merge' && pull.merged_at) autoMerged.push(pull.number);
249
+ }
250
+
214
251
  // 6. One digest. Everything above either fixed itself or is listed here, so there is a
215
252
  // single place to look instead of four repositories and three label queries.
216
253
  // Audit reports nobody actioned. audit-close labels these `stale-audit` once they are
@@ -243,6 +280,21 @@ runs:
243
280
  `- #${entry.issue.number} · stalled ${hours(entry.waited)}h ago · attempt ${entry.attempts + 1} of ${maxRetries} due in ~${hours(entry.due)}h · ${entry.issue.title}`);
244
281
  return `\n### Retrying by itself, no action needed (${retrying.length})\n\n${rows.join('\n')}\n`;
245
282
  };
283
+ // The gate's own scoreboard. A digest that lists only what went wrong tells you
284
+ // nothing about whether the thing is getting better.
285
+ const gateSection = () => {
286
+ const total = Object.values(dispositions).reduce((a, b) => a + b, 0);
287
+ if (total === 0) return '';
288
+ const auto = dispositions['auto-merge'] ?? 0;
289
+ const rate = Math.round((auto / total) * 100);
290
+ const rows = Object.entries(dispositions)
291
+ .sort((a, b) => b[1] - a[1])
292
+ .map(([name, count]) => `- \`${name}\` · ${count}`);
293
+ const reverted = autoMerged.filter((number) => reverts.includes(number)).length;
294
+ return `\n### Merge gate, last ${metricsWindowMs / (24 * 3600000)} days\n\n` +
295
+ `**${rate}% auto-merged** (${auto} of ${total} dispositions).\n\n${rows.join('\n')}\n\n` +
296
+ `Auto-merged pull requests later reverted: ${reverted}.\n`;
297
+ };
246
298
  const body = [
247
299
  DIGEST_MARKER,
248
300
  `_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
@@ -251,6 +303,7 @@ runs:
251
303
  section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
252
304
  section('Audit reports nobody actioned', ['stale-audit']),
253
305
  retryingSection(),
306
+ gateSection(),
254
307
  needsHuman.length === 0 ? '\nNothing is waiting on a person right now.\n' : '',
255
308
  `\n---\n\nThis run retried ${retried} stalled issue(s) and made ${planned.length} change(s) in total.`,
256
309
  ].filter(Boolean).join('\n');
@@ -1,122 +1,134 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Identify gate subject
3
- description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
4
- inputs:
5
- token:
6
- description: GitHub token with issues and pull-requests read access.
7
- required: true
8
- pr-number:
9
- description: Pull request number to gate.
10
- required: true
11
- ci-conclusion:
12
- description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
13
- required: false
14
- default: ''
15
- linked-issue:
16
- description: Issue the pull request closes, when the router already resolved it.
17
- required: false
18
- default: ''
19
- require-label:
20
- description: Label the closing issue must carry. Empty disables the check.
21
- required: false
22
- default: ''
23
- outputs:
24
- found:
25
- description: Whether a valid gate subject was found.
26
- value: ${{ steps.identify.outputs.found }}
27
- pr:
28
- description: Pull request number.
29
- value: ${{ steps.identify.outputs.pr }}
30
- issue:
31
- description: Issue number the pull request closes.
32
- value: ${{ steps.identify.outputs.issue }}
33
- conclusion:
34
- description: CI conclusion, passed through unchanged or fetched from latest CI run.
35
- value: ${{ steps.identify.outputs.conclusion }}
36
- run-id:
37
- description: CI run ID when fetched.
38
- value: ${{ steps.identify.outputs.run-id }}
39
- runs:
40
- using: composite
41
- steps:
42
- - name: Identify pull request and closing issue
43
- id: identify
44
- shell: bash
45
- env:
46
- GH_TOKEN: ${{ inputs.token }}
47
- REPO: ${{ github.repository }}
48
- PR_NUMBER: ${{ inputs.pr-number }}
49
- CONCLUSION: ${{ inputs.ci-conclusion }}
50
- LINKED_ISSUE: ${{ inputs.linked-issue }}
51
- REQUIRE_LABEL: ${{ inputs.require-label }}
52
- run: |
53
- set -euo pipefail
54
-
55
- none() {
56
- echo "found=false" >> "$GITHUB_OUTPUT"
57
- echo "$1"
58
- exit 0
59
- }
60
-
61
- pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" \
62
- --json state,author,closingIssuesReferences,body)"
63
-
64
- [ "$(jq -r '.state' <<<"$pr")" = "OPEN" ] || none "PR #$PR_NUMBER is not open"
65
-
66
- # Use GitHub's bot flag because App login formats differ across APIs.
67
- if [ "$(jq -r '.author.is_bot' <<<"$pr")" != "true" ]; then
68
- none "PR #$PR_NUMBER was authored by $(jq -r '.author.login' <<<"$pr"), a human."
69
- fi
70
-
71
- issue="$LINKED_ISSUE"
72
-
73
- if [ -z "$issue" ]; then
74
- issue="$(jq -r '.closingIssuesReferences[0].number // empty' <<<"$pr")"
75
- fi
76
-
77
- if [ -z "$issue" ]; then
78
- issue="$(jq -r '.body // ""' <<<"$pr" |
79
- grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' |
80
- grep -oE '[0-9]+' | head -n 1 || true)"
81
- fi
82
-
83
- [ -n "$issue" ] || none "PR #$PR_NUMBER does not close a numbered issue"
84
-
85
- if [ -n "$REQUIRE_LABEL" ]; then
86
- labels="$(gh issue view "$issue" --repo "$REPO" --json labels \
87
- --jq '[.labels[].name]')"
88
-
89
- if ! jq -e --arg name "$REQUIRE_LABEL" 'index($name)' >/dev/null <<<"$labels"; then
90
- none "issue #$issue does not carry $REQUIRE_LABEL (has: $(jq -r 'join(", ")' <<<"$labels"))"
91
- fi
92
- fi
93
-
94
- # Fetch CI conclusion if not provided (label-triggered runs)
95
- ci_conclusion="$CONCLUSION"
96
- ci_run_id=""
97
- if [ -z "$ci_conclusion" ]; then
98
- head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
99
- # A run keeps the name it had when it ran, so a repository whose CI was renamed still
100
- # carries runs under the old casing. Fold the name before comparing.
101
- ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" \
102
- --jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
103
-
104
- if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
105
- ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
106
- ci_run_id="$(jq -r '.id' <<<"$ci_run")"
107
- echo "::notice::Fetched CI status: $ci_conclusion (run $ci_run_id)"
108
- else
109
- ci_conclusion="unknown"
110
- echo "::warning::No CI run found for PR #$PR_NUMBER"
111
- fi
112
- fi
113
-
114
- {
115
- echo "found=true"
116
- echo "pr=$PR_NUMBER"
117
- echo "issue=$issue"
118
- echo "conclusion=$ci_conclusion"
119
- echo "run-id=$ci_run_id"
120
- } >> "$GITHUB_OUTPUT"
121
-
122
- echo "PR #$PR_NUMBER closes #$issue; CI concluded $ci_conclusion"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Identify gate subject
3
+ description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
4
+ inputs:
5
+ token:
6
+ description: GitHub token with issues and pull-requests read access.
7
+ required: true
8
+ pr-number:
9
+ description: Pull request number to gate.
10
+ required: true
11
+ ci-conclusion:
12
+ description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
13
+ required: false
14
+ default: ''
15
+ ci-run-id:
16
+ description: CI run ID reported by the run that triggered this gate. If empty, resolved from the PR's latest CI run.
17
+ required: false
18
+ default: ''
19
+ linked-issue:
20
+ description: Issue the pull request closes, when the router already resolved it.
21
+ required: false
22
+ default: ''
23
+ require-label:
24
+ description: Label the closing issue must carry. Empty disables the check.
25
+ required: false
26
+ default: ''
27
+ outputs:
28
+ found:
29
+ description: Whether a valid gate subject was found.
30
+ value: ${{ steps.identify.outputs.found }}
31
+ pr:
32
+ description: Pull request number.
33
+ value: ${{ steps.identify.outputs.pr }}
34
+ issue:
35
+ description: Issue number the pull request closes.
36
+ value: ${{ steps.identify.outputs.issue }}
37
+ conclusion:
38
+ description: CI conclusion, passed through unchanged or fetched from latest CI run.
39
+ value: ${{ steps.identify.outputs.conclusion }}
40
+ run-id:
41
+ description: CI run ID when fetched.
42
+ value: ${{ steps.identify.outputs.run-id }}
43
+ runs:
44
+ using: composite
45
+ steps:
46
+ - name: Identify pull request and closing issue
47
+ id: identify
48
+ shell: bash
49
+ env:
50
+ GH_TOKEN: ${{ inputs.token }}
51
+ REPO: ${{ github.repository }}
52
+ PR_NUMBER: ${{ inputs.pr-number }}
53
+ CONCLUSION: ${{ inputs.ci-conclusion }}
54
+ CI_RUN_ID: ${{ inputs.ci-run-id }}
55
+ LINKED_ISSUE: ${{ inputs.linked-issue }}
56
+ REQUIRE_LABEL: ${{ inputs.require-label }}
57
+ run: |
58
+ set -euo pipefail
59
+
60
+ none() {
61
+ echo "found=false" >> "$GITHUB_OUTPUT"
62
+ echo "$1"
63
+ exit 0
64
+ }
65
+
66
+ pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" \
67
+ --json state,author,closingIssuesReferences,body)"
68
+
69
+ [ "$(jq -r '.state' <<<"$pr")" = "OPEN" ] || none "PR #$PR_NUMBER is not open"
70
+
71
+ # Use GitHub's bot flag because App login formats differ across APIs.
72
+ if [ "$(jq -r '.author.is_bot' <<<"$pr")" != "true" ]; then
73
+ none "PR #$PR_NUMBER was authored by $(jq -r '.author.login' <<<"$pr"), a human."
74
+ fi
75
+
76
+ issue="$LINKED_ISSUE"
77
+
78
+ if [ -z "$issue" ]; then
79
+ issue="$(jq -r '.closingIssuesReferences[0].number // empty' <<<"$pr")"
80
+ fi
81
+
82
+ if [ -z "$issue" ]; then
83
+ issue="$(jq -r '.body // ""' <<<"$pr" |
84
+ grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' |
85
+ grep -oE '[0-9]+' | head -n 1 || true)"
86
+ fi
87
+
88
+ [ -n "$issue" ] || none "PR #$PR_NUMBER does not close a numbered issue"
89
+
90
+ if [ -n "$REQUIRE_LABEL" ]; then
91
+ labels="$(gh issue view "$issue" --repo "$REPO" --json labels \
92
+ --jq '[.labels[].name]')"
93
+
94
+ if ! jq -e --arg name "$REQUIRE_LABEL" 'index($name)' >/dev/null <<<"$labels"; then
95
+ none "issue #$issue does not carry $REQUIRE_LABEL (has: $(jq -r 'join(", ")' <<<"$labels"))"
96
+ fi
97
+ fi
98
+
99
+ # The run ID is what lets the gate read the failing job and its logs, so it has to be
100
+ # resolved on every path -- not only the label-triggered one. It used to be set inside
101
+ # `if [ -z "$ci_conclusion" ]`, which meant the caller that *knew* the ID got an empty
102
+ # one: the router passes ci-conclusion and ci-run-id together, so the branch was skipped
103
+ # and the value discarded. The gate then wrote `failed-jobs.json` as `[]`, and the agent,
104
+ # with no failing job name and no logs, escalated to `review`. Pliny-Bot #129 and #131
105
+ # both stalled on a human that way while CI was legitimately red and remediable.
106
+ ci_conclusion="$CONCLUSION"
107
+ ci_run_id="$CI_RUN_ID"
108
+ if [ -z "$ci_conclusion" ] || [ -z "$ci_run_id" ]; then
109
+ head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
110
+ # A run keeps the name it had when it ran, so a repository whose CI was renamed still
111
+ # carries runs under the old casing. Fold the name before comparing.
112
+ ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" --jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
113
+
114
+ if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
115
+ [ -n "$ci_conclusion" ] || ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
116
+ [ -n "$ci_run_id" ] || ci_run_id="$(jq -r '.id' <<<"$ci_run")"
117
+ echo "::notice::Resolved CI status: $ci_conclusion (run $ci_run_id)"
118
+ elif [ -z "$ci_conclusion" ]; then
119
+ ci_conclusion="unknown"
120
+ echo "::warning::No CI run found for PR #$PR_NUMBER"
121
+ else
122
+ echo "::warning::CI concluded $ci_conclusion but no run was found for PR #$PR_NUMBER; the gate has no failure evidence"
123
+ fi
124
+ fi
125
+
126
+ {
127
+ echo "found=true"
128
+ echo "pr=$PR_NUMBER"
129
+ echo "issue=$issue"
130
+ echo "conclusion=$ci_conclusion"
131
+ echo "run-id=$ci_run_id"
132
+ } >> "$GITHUB_OUTPUT"
133
+
134
+ echo "PR #$PR_NUMBER closes #$issue; CI concluded $ci_conclusion"
@@ -1,40 +1,40 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/link-pr-to-issue/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Link pull request to issue
3
- description: Ensure the pull request body carries a closing reference to its source issue.
4
- inputs:
5
- token:
6
- description: GitHub token with pull-requests write access.
7
- required: true
8
- pr-number:
9
- description: Pull request number.
10
- required: true
11
- issue-number:
12
- description: Issue number the pull request should close.
13
- required: true
14
- runs:
15
- using: composite
16
- steps:
17
- - name: Ensure a closing reference
18
- shell: bash
19
- env:
20
- GH_TOKEN: ${{ inputs.token }}
21
- REPO: ${{ github.repository }}
22
- PR_NUMBER: ${{ inputs.pr-number }}
23
- ISSUE_NUMBER: ${{ inputs.issue-number }}
24
- run: |
25
- set -euo pipefail
26
-
27
- pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences,body)"
28
-
29
- if [ "$(jq '.closingIssuesReferences | length' <<<"$pr")" -gt 0 ]; then
30
- echo "PR #$PR_NUMBER already closes an issue."
31
- exit 0
32
- fi
33
-
34
- # A closing reference can only be created from the pull request body; there is no
35
- # REST or GraphQL mutation for it.
36
- body="$(jq -r '.body // ""' <<<"$pr")"
37
- printf '%s\n\nCloses #%s\n' "$body" "$ISSUE_NUMBER" > /tmp/pr-body.md
38
-
39
- gh pr edit "$PR_NUMBER" --repo "$REPO" --body-file /tmp/pr-body.md
40
- echo "Added 'Closes #$ISSUE_NUMBER' to PR #$PR_NUMBER."
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/link-pr-to-issue/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Link pull request to issue
3
+ description: Ensure the pull request body carries a closing reference to its source issue.
4
+ inputs:
5
+ token:
6
+ description: GitHub token with pull-requests write access.
7
+ required: true
8
+ pr-number:
9
+ description: Pull request number.
10
+ required: true
11
+ issue-number:
12
+ description: Issue number the pull request should close.
13
+ required: true
14
+ runs:
15
+ using: composite
16
+ steps:
17
+ - name: Ensure a closing reference
18
+ shell: bash
19
+ env:
20
+ GH_TOKEN: ${{ inputs.token }}
21
+ REPO: ${{ github.repository }}
22
+ PR_NUMBER: ${{ inputs.pr-number }}
23
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
24
+ run: |
25
+ set -euo pipefail
26
+
27
+ pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json closingIssuesReferences,body)"
28
+
29
+ if [ "$(jq '.closingIssuesReferences | length' <<<"$pr")" -gt 0 ]; then
30
+ echo "PR #$PR_NUMBER already closes an issue."
31
+ exit 0
32
+ fi
33
+
34
+ # A closing reference can only be created from the pull request body; there is no
35
+ # REST or GraphQL mutation for it.
36
+ body="$(jq -r '.body // ""' <<<"$pr")"
37
+ printf '%s\n\nCloses #%s\n' "$body" "$ISSUE_NUMBER" > /tmp/pr-body.md
38
+
39
+ gh pr edit "$PR_NUMBER" --repo "$REPO" --body-file /tmp/pr-body.md
40
+ echo "Added 'Closes #$ISSUE_NUMBER' to PR #$PR_NUMBER."
@@ -1,33 +1,33 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/list-open-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: List open issues
3
- description: |
4
- Writes all open issues (number, title, labels) to `/tmp/gh-aw/agent/open-issues.json`
5
- so the agent can check for semantic duplicates without spending turns discovering them.
6
- inputs:
7
- token:
8
- description: GitHub token with issues:read permission.
9
- required: true
10
- repo:
11
- description: Repository in `owner/name` format.
12
- required: true
13
- output-path:
14
- description: Path to write the JSON output.
15
- required: false
16
- default: /tmp/gh-aw/agent/open-issues.json
17
- runs:
18
- using: composite
19
- steps:
20
- - name: List open issues
21
- shell: bash
22
- env:
23
- GH_TOKEN: ${{ inputs.token }}
24
- REPO: ${{ inputs.repo }}
25
- OUTPUT_PATH: ${{ inputs.output-path }}
26
- run: |
27
- set -euo pipefail
28
- mkdir -p "$(dirname "$OUTPUT_PATH")"
29
- gh issue list --repo "$REPO" --state open --limit 500 \
30
- --json number,title,labels \
31
- --jq '[.[] | {number, title, labels: [.labels[].name]}]' \
32
- > "$OUTPUT_PATH"
33
- echo "Wrote $(jq 'length' "$OUTPUT_PATH") open issues to $OUTPUT_PATH"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/list-open-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: List open issues
3
+ description: |
4
+ Writes all open issues (number, title, labels) to `/tmp/gh-aw/agent/open-issues.json`
5
+ so the agent can check for semantic duplicates without spending turns discovering them.
6
+ inputs:
7
+ token:
8
+ description: GitHub token with issues:read permission.
9
+ required: true
10
+ repo:
11
+ description: Repository in `owner/name` format.
12
+ required: true
13
+ output-path:
14
+ description: Path to write the JSON output.
15
+ required: false
16
+ default: /tmp/gh-aw/agent/open-issues.json
17
+ runs:
18
+ using: composite
19
+ steps:
20
+ - name: List open issues
21
+ shell: bash
22
+ env:
23
+ GH_TOKEN: ${{ inputs.token }}
24
+ REPO: ${{ inputs.repo }}
25
+ OUTPUT_PATH: ${{ inputs.output-path }}
26
+ run: |
27
+ set -euo pipefail
28
+ mkdir -p "$(dirname "$OUTPUT_PATH")"
29
+ gh issue list --repo "$REPO" --state open --limit 500 \
30
+ --json number,title,labels \
31
+ --jq '[.[] | {number, title, labels: [.labels[].name]}]' \
32
+ > "$OUTPUT_PATH"
33
+ echo "Wrote $(jq 'length' "$OUTPUT_PATH") open issues to $OUTPUT_PATH"