@plainconceptsplatform/workflows 1.13.0 → 1.14.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 (32) hide show
  1. package/loops/actions/add-issue-labels/action.yml +50 -50
  2. package/loops/actions/agent-output.cjs +17 -17
  3. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  4. package/loops/actions/apply-agent-comments/action.yml +42 -42
  5. package/loops/actions/apply-agent-labels/action.yml +55 -55
  6. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  7. package/loops/actions/close-agent-issues/action.yml +43 -43
  8. package/loops/actions/create-issue-comment/action.yml +29 -29
  9. package/loops/actions/download-agent-output/action.yml +53 -53
  10. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  11. package/loops/actions/list-open-issues/action.yml +33 -33
  12. package/loops/actions/load-issue-context/action.yml +45 -45
  13. package/loops/actions/merge-agent-pr/action.yml +49 -49
  14. package/loops/actions/merge-dependabot/action.yml +10 -2
  15. package/loops/actions/merge-dependabot/merge-dependabot.mjs +47 -17
  16. package/loops/actions/remove-issue-labels/action.yml +37 -37
  17. package/loops/actions/update-agent-issues/action.yml +58 -58
  18. package/loops/actions/validate-review-output/action.yml +35 -35
  19. package/loops/actions/validate-triage-output/action.yml +36 -36
  20. package/loops/actions/verify-composite-actions/action.yml +9 -9
  21. package/loops/actions/verify-refine-output/action.yml +9 -9
  22. package/loops/actions/verify-route-matrix/action.yml +9 -9
  23. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +7 -2
  24. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  25. package/loops/templates/dependabot-merge/.github/workflows/dependabot-merge.yml +3 -1
  26. package/loops/templates/issues/bug_report.yml +109 -109
  27. package/loops/templates/issues/feature_request.yml +75 -75
  28. package/loops/workflows/agent-implement.md +13 -12
  29. package/loops/workflows/shared/opencode-ci.md +17 -5
  30. package/loops/workflows/shared/platform-defaults.md +53 -53
  31. package/loops/workflows/work-router.yml +4 -0
  32. package/package.json +2 -2
@@ -19,6 +19,13 @@ export const NO_CHECKS_GRACE_MINUTES = 60;
19
19
 
20
20
  export const isDependabot = (login) => DEPENDABOT_LOGINS.has(login ?? "");
21
21
 
22
+ // A read the token may not make. The pull request is skipped and the run goes on.
23
+ export class ReadDenied extends Error {
24
+ constructor(what, permission) {
25
+ super(`cannot read ${what}: grant ${permission}: read to the App or the workflow token`);
26
+ }
27
+ }
28
+
22
29
  export function authorshipProblem(pr, commits, repository) {
23
30
  if (!isDependabot(pr.user?.login)) return `opened by ${pr.user?.login ?? "unknown"}`;
24
31
  if (String(pr.head?.repo?.full_name).toLowerCase() !== repository.toLowerCase()) return "its branch is in another repository";
@@ -196,7 +203,7 @@ export function createClient({ token, apiUrl = "https://api.github.com", fetchIm
196
203
  let next = path;
197
204
  while (next && items.length < limit) {
198
205
  const page = await request("GET", next);
199
- if (page.status >= 400) throw new Error(`GET ${next} answered ${page.status}: ${JSON.stringify(page.data)}`);
206
+ if (page.status >= 400) throw Object.assign(new Error(`GET ${next} answered ${page.status}: ${JSON.stringify(page.data)}`), { status: page.status });
200
207
  items.push(...(Array.isArray(page.data) ? page.data : page.data?.check_runs ?? []));
201
208
  next = /<([^>]+)>;\s*rel="next"/.exec(page.link)?.[1];
202
209
  }
@@ -207,9 +214,16 @@ export function createClient({ token, apiUrl = "https://api.github.com", fetchIm
207
214
 
208
215
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
209
216
 
217
+ const denied = async (what, permission, read) => {
218
+ try { return await read(); } catch (error) {
219
+ throw error.status === 403 ? new ReadDenied(what, permission) : error;
220
+ }
221
+ };
222
+
210
223
  async function readPullRequest(client, repository, number, wait) {
211
224
  for (let attempt = 0; ; attempt++) {
212
- const { data } = await client.request("GET", `/repos/${repository}/pulls/${number}`);
225
+ const { status, data } = await client.request("GET", `/repos/${repository}/pulls/${number}`);
226
+ if (status === 403) throw new ReadDenied("pull requests", "Pull requests");
213
227
  if (data?.mergeable !== null || attempt >= 3) return data;
214
228
  await wait(2000);
215
229
  }
@@ -228,8 +242,9 @@ async function requiredChecks(client, repository, branch) {
228
242
  }
229
243
 
230
244
  async function readChecks(client, repository, sha, required) {
231
- const checkRuns = await client.paginate(`/repos/${repository}/commits/${sha}/check-runs?per_page=100`);
245
+ const checkRuns = await denied("checks", "Checks", () => client.paginate(`/repos/${repository}/commits/${sha}/check-runs?per_page=100`));
232
246
  const combined = await client.request("GET", `/repos/${repository}/commits/${sha}/status?per_page=100`);
247
+ if (combined.status === 403) throw new ReadDenied("commit statuses", "Commit statuses");
233
248
  return summarizeChecks({ checkRuns, statuses: combined.data?.statuses ?? [], required });
234
249
  }
235
250
 
@@ -267,27 +282,39 @@ async function land(client, repository, pr, config) {
267
282
  return { action: "error", reason: `merge answered ${merged.status}: ${message}` };
268
283
  }
269
284
 
270
- export async function run({ client, repository, config, now = Date.now(), log = console.log, wait = sleep }) {
285
+ // `client` writes (approve, merge, comment) and `reader` reads. The App token writes, so its merge
286
+ // starts the push workflows; the workflow token reads, as the App may hold no Checks permission.
287
+ export async function run({ client, reader = client, repository, config, now = Date.now(), log = console.log, wait = sleep }) {
271
288
  const rows = [];
289
+ const deniedReads = new Set();
272
290
  if (config.scope === "off") return { rows, off: true };
273
291
 
274
- const { data: repo } = await client.request("GET", `/repos/${repository}`);
292
+ const { data: repo } = await reader.request("GET", `/repos/${repository}`);
275
293
  const defaultBranch = repo.default_branch;
276
- const open = await client.paginate(`/repos/${repository}/pulls?state=open&sort=created&direction=asc&per_page=100`);
294
+ const open = await reader.paginate(`/repos/${repository}/pulls?state=open&sort=created&direction=asc&per_page=100`);
277
295
  const candidates = open.filter((pr) => isDependabot(pr.user?.login)).slice(0, MAX_EXAMINED);
278
- const required = candidates.length > 0 ? await requiredChecks(client, repository, defaultBranch) : [];
296
+ const required = candidates.length > 0 ? await requiredChecks(reader, repository, defaultBranch) : [];
279
297
  let merges = 0;
280
298
 
281
299
  for (const listed of candidates) {
282
- if (client.rateRemaining() < MIN_RATE_REMAINING) {
300
+ if (reader.rateRemaining() < MIN_RATE_REMAINING) {
283
301
  rows.push({ number: listed.number, title: listed.title, bump: "", action: "wait", reason: "API rate limit nearly spent" });
284
302
  continue;
285
303
  }
286
- const pr = await readPullRequest(client, repository, listed.number, wait);
287
- const commits = await client.paginate(`/repos/${repository}/pulls/${pr.number}/commits?per_page=100`, 250);
288
- const comments = await client.paginate(`/repos/${repository}/issues/${pr.number}/comments?per_page=100`);
289
- const mergeableNow = pr.mergeable === true && !["dirty", "behind"].includes(pr.mergeable_state);
290
- const checks = mergeableNow ? await readChecks(client, repository, pr.head.sha, required) : { state: "none", failing: [], pending: [] };
304
+ let pr, commits, comments, checks;
305
+ try {
306
+ pr = await readPullRequest(reader, repository, listed.number, wait);
307
+ commits = await denied("pull request commits", "Pull requests", () => reader.paginate(`/repos/${repository}/pulls/${pr.number}/commits?per_page=100`, 250));
308
+ comments = await denied("issue comments", "Issues", () => reader.paginate(`/repos/${repository}/issues/${pr.number}/comments?per_page=100`));
309
+ const mergeableNow = pr.mergeable === true && !["dirty", "behind"].includes(pr.mergeable_state);
310
+ checks = mergeableNow ? await readChecks(reader, repository, pr.head.sha, required) : { state: "none", failing: [], pending: [] };
311
+ } catch (error) {
312
+ if (!(error instanceof ReadDenied)) throw error;
313
+ deniedReads.add(error.message);
314
+ log(`::warning::#${listed.number} skipped: ${error.message}`);
315
+ rows.push({ number: listed.number, title: listed.title, bump: "", action: "skip", reason: error.message });
316
+ continue;
317
+ }
291
318
  let outcome = decide({
292
319
  pr, commits, defaultBranch, repository, comments, checks, now,
293
320
  skipLabels: config.skipLabels, scope: config.scope, mergeWithoutChecks: config.mergeWithoutChecks,
@@ -305,12 +332,13 @@ export async function run({ client, repository, config, now = Date.now(), log =
305
332
  log(`#${pr.number} ${outcome.action}: ${outcome.reason}`);
306
333
  rows.push({ number: pr.number, title: pr.title, bump: outcome.bump, action: outcome.action, reason: outcome.reason });
307
334
  }
308
- return { rows, off: false };
335
+ return { rows, off: false, denied: [...deniedReads] };
309
336
  }
310
337
 
311
- export function renderSummary({ rows, off }, config) {
338
+ export function renderSummary({ rows, off, denied: deniedReads = [] }, config) {
312
339
  const lines = [`## Dependabot merge (${config.dryRun ? "dry run, " : ""}scope: ${config.scope})`, ""];
313
340
  if (off) return [...lines, "DEPENDABOT_MERGE is off: nothing was examined.", ""].join("\n");
341
+ for (const message of deniedReads) lines.push("> [!WARNING]", `> ${message}.`, "");
314
342
  if (rows.length === 0) return [...lines, "No open Dependabot pull requests.", ""].join("\n");
315
343
  lines.push("| PR | Update | Bump | Result | Why |", "|---|---|---|---|---|");
316
344
  const cell = (value) => String(value ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
@@ -323,8 +351,10 @@ async function main() {
323
351
  SCOPE: process.env.SCOPE, MAX_MERGES: process.env.MAX_MERGES, MERGE_WITHOUT_CHECKS: process.env.MERGE_WITHOUT_CHECKS,
324
352
  SKIP_LABELS: process.env.SKIP_LABELS, DRY_RUN: process.env.DRY_RUN, POST_MERGE_EVENT: process.env.POST_MERGE_EVENT,
325
353
  });
326
- const client = createClient({ token: process.env.GH_TOKEN, apiUrl: process.env.GITHUB_API_URL || "https://api.github.com" });
327
- const result = await run({ client, repository: process.env.REPO, config });
354
+ const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
355
+ const client = createClient({ token: process.env.GH_TOKEN, apiUrl });
356
+ const reader = process.env.READ_TOKEN ? createClient({ token: process.env.READ_TOKEN, apiUrl }) : client;
357
+ const result = await run({ client, reader, repository: process.env.REPO, config });
328
358
  const summary = renderSummary(result, config);
329
359
  if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`);
330
360
  else console.log(summary);
@@ -1,37 +1,37 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Remove issue labels
3
- description: Remove one or more labels from an issue or pull request.
4
- inputs:
5
- token:
6
- description: GitHub token used by github-script.
7
- required: true
8
- issue-number:
9
- description: Issue or pull request number.
10
- required: true
11
- labels:
12
- description: Labels to remove, one per line. A comma-separated list is accepted as well.
13
- required: true
14
- runs:
15
- using: composite
16
- steps:
17
- - name: Remove labels
18
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
19
- env:
20
- ISSUE_NUMBER: ${{ inputs.issue-number }}
21
- LABELS: ${{ inputs.labels }}
22
- with:
23
- github-token: ${{ inputs.token }}
24
- script: |
25
- // Callers wrote `labels: a,b` while this split on newlines, so one label named "a,b"
26
- // was removed: a 404, swallowed below, and neither label ever came off.
27
- for (const label of process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean)) {
28
- try {
29
- await github.rest.issues.removeLabel({
30
- ...context.repo,
31
- issue_number: Number(process.env.ISSUE_NUMBER),
32
- name: label,
33
- });
34
- } catch (error) {
35
- if (error.status !== 404) throw error;
36
- }
37
- }
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/remove-issue-labels/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Remove issue labels
3
+ description: Remove one or more labels from an issue or pull request.
4
+ inputs:
5
+ token:
6
+ description: GitHub token used by github-script.
7
+ required: true
8
+ issue-number:
9
+ description: Issue or pull request number.
10
+ required: true
11
+ labels:
12
+ description: Labels to remove, one per line. A comma-separated list is accepted as well.
13
+ required: true
14
+ runs:
15
+ using: composite
16
+ steps:
17
+ - name: Remove labels
18
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
19
+ env:
20
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
21
+ LABELS: ${{ inputs.labels }}
22
+ with:
23
+ github-token: ${{ inputs.token }}
24
+ script: |
25
+ // Callers wrote `labels: a,b` while this split on newlines, so one label named "a,b"
26
+ // was removed: a 404, swallowed below, and neither label ever came off.
27
+ for (const label of process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean)) {
28
+ try {
29
+ await github.rest.issues.removeLabel({
30
+ ...context.repo,
31
+ issue_number: Number(process.env.ISSUE_NUMBER),
32
+ name: label,
33
+ });
34
+ } catch (error) {
35
+ if (error.status !== 404) throw error;
36
+ }
37
+ }
@@ -1,58 +1,58 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Update agent issues
3
- description: Apply every update_issue item from agent_output.json.
4
- inputs:
5
- output-file:
6
- description: Path to agent_output.json.
7
- required: true
8
- token:
9
- description: GitHub token with issues:write.
10
- required: true
11
- fallback-issue-number:
12
- description: Issue an item targets when it names none.
13
- required: false
14
- default: ''
15
- runs:
16
- using: composite
17
- steps:
18
- - name: Update issues
19
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
20
- env:
21
- AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
22
- OUTPUT_FILE: ${{ inputs.output-file }}
23
- FALLBACK_ISSUE_NUMBER: ${{ inputs.fallback-issue-number }}
24
- with:
25
- github-token: ${{ inputs.token }}
26
- script: |
27
- const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
28
- const items = readAgentItems(process.env.OUTPUT_FILE, 'update_issue');
29
-
30
- let updated = 0;
31
-
32
- for (const item of items) {
33
- const target = item.item_number ?? process.env.FALLBACK_ISSUE_NUMBER;
34
-
35
- if (!target) {
36
- core.warning(`update_issue item has no item_number and no fallback, skipping: ${JSON.stringify(item)}`);
37
- continue;
38
- }
39
-
40
- const changes = {};
41
- if (item.title) changes.title = item.title;
42
- if (item.body) changes.body = item.body;
43
-
44
- if (Object.keys(changes).length === 0) {
45
- core.warning(`update_issue item changes nothing, skipping: ${JSON.stringify(item)}`);
46
- continue;
47
- }
48
-
49
- await github.rest.issues.update({
50
- ...context.repo,
51
- issue_number: Number(target),
52
- ...changes,
53
- });
54
-
55
- updated += 1;
56
- }
57
-
58
- core.info(`Updated ${updated} issue(s).`);
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Update agent issues
3
+ description: Apply every update_issue item from agent_output.json.
4
+ inputs:
5
+ output-file:
6
+ description: Path to agent_output.json.
7
+ required: true
8
+ token:
9
+ description: GitHub token with issues:write.
10
+ required: true
11
+ fallback-issue-number:
12
+ description: Issue an item targets when it names none.
13
+ required: false
14
+ default: ''
15
+ runs:
16
+ using: composite
17
+ steps:
18
+ - name: Update issues
19
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
20
+ env:
21
+ AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
22
+ OUTPUT_FILE: ${{ inputs.output-file }}
23
+ FALLBACK_ISSUE_NUMBER: ${{ inputs.fallback-issue-number }}
24
+ with:
25
+ github-token: ${{ inputs.token }}
26
+ script: |
27
+ const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
28
+ const items = readAgentItems(process.env.OUTPUT_FILE, 'update_issue');
29
+
30
+ let updated = 0;
31
+
32
+ for (const item of items) {
33
+ const target = item.item_number ?? process.env.FALLBACK_ISSUE_NUMBER;
34
+
35
+ if (!target) {
36
+ core.warning(`update_issue item has no item_number and no fallback, skipping: ${JSON.stringify(item)}`);
37
+ continue;
38
+ }
39
+
40
+ const changes = {};
41
+ if (item.title) changes.title = item.title;
42
+ if (item.body) changes.body = item.body;
43
+
44
+ if (Object.keys(changes).length === 0) {
45
+ core.warning(`update_issue item changes nothing, skipping: ${JSON.stringify(item)}`);
46
+ continue;
47
+ }
48
+
49
+ await github.rest.issues.update({
50
+ ...context.repo,
51
+ issue_number: Number(target),
52
+ ...changes,
53
+ });
54
+
55
+ updated += 1;
56
+ }
57
+
58
+ core.info(`Updated ${updated} issue(s).`);
@@ -1,35 +1,35 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-review-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Validate review output
3
- description: Verify an apply-review outcome accounts for every unresolved review thread.
4
- inputs:
5
- output-file:
6
- description: Path to agent_output.json.
7
- required: true
8
- pr-number:
9
- description: Pull request receiving the review feedback.
10
- required: true
11
- review-threads-file:
12
- description: JSON file containing unresolved review threads.
13
- required: true
14
- outputs:
15
- valid:
16
- description: Whether the agent output is a valid review outcome.
17
- value: ${{ steps.validate.outputs.valid }}
18
- outcome:
19
- description: "Deterministic review outcome: implemented, already-satisfied, needs-human, or invalid."
20
- value: ${{ steps.validate.outputs.outcome }}
21
- runs:
22
- using: composite
23
- steps:
24
- - name: Validate review outcome
25
- id: validate
26
- shell: bash
27
- env:
28
- OUTPUT_FILE: ${{ inputs.output-file }}
29
- PR_NUMBER: ${{ inputs.pr-number }}
30
- REVIEW_THREADS_FILE: ${{ inputs.review-threads-file }}
31
- run: |
32
- set -euo pipefail
33
- outcome="$(bash "${{ github.action_path }}/validate-review-output.sh" "$OUTPUT_FILE" "$PR_NUMBER" "$REVIEW_THREADS_FILE")"
34
- echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
35
- echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-review-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Validate review output
3
+ description: Verify an apply-review outcome accounts for every unresolved review thread.
4
+ inputs:
5
+ output-file:
6
+ description: Path to agent_output.json.
7
+ required: true
8
+ pr-number:
9
+ description: Pull request receiving the review feedback.
10
+ required: true
11
+ review-threads-file:
12
+ description: JSON file containing unresolved review threads.
13
+ required: true
14
+ outputs:
15
+ valid:
16
+ description: Whether the agent output is a valid review outcome.
17
+ value: ${{ steps.validate.outputs.valid }}
18
+ outcome:
19
+ description: "Deterministic review outcome: implemented, already-satisfied, needs-human, or invalid."
20
+ value: ${{ steps.validate.outputs.outcome }}
21
+ runs:
22
+ using: composite
23
+ steps:
24
+ - name: Validate review outcome
25
+ id: validate
26
+ shell: bash
27
+ env:
28
+ OUTPUT_FILE: ${{ inputs.output-file }}
29
+ PR_NUMBER: ${{ inputs.pr-number }}
30
+ REVIEW_THREADS_FILE: ${{ inputs.review-threads-file }}
31
+ run: |
32
+ set -euo pipefail
33
+ outcome="$(bash "${{ github.action_path }}/validate-review-output.sh" "$OUTPUT_FILE" "$PR_NUMBER" "$REVIEW_THREADS_FILE")"
34
+ echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
35
+ echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
@@ -1,36 +1,36 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-triage-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Validate triage output
3
- description: Verify agent_output.json contains a usable triage verdict before any GitHub writes.
4
- inputs:
5
- output-file:
6
- description: Path to agent_output.json.
7
- required: true
8
- issue-number:
9
- description: Issue number that every triage outcome must target.
10
- required: true
11
- outputs:
12
- valid:
13
- description: Whether the output contains a usable triage comment with a verdict.
14
- value: ${{ steps.validate.outputs.valid }}
15
- outcome:
16
- description: "Deterministic triage outcome: pass, needs-info, needs-maintainer, block, or invalid."
17
- value: ${{ steps.validate.outputs.outcome }}
18
- runs:
19
- using: composite
20
- steps:
21
- - name: Validate triage outcome
22
- id: validate
23
- shell: bash
24
- env:
25
- ISSUE_NUMBER: ${{ inputs.issue-number }}
26
- OUTPUT_FILE: ${{ inputs.output-file }}
27
- run: |
28
- set -euo pipefail
29
-
30
- outcome="$(bash "${{ github.action_path }}/validate-triage-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER")"
31
- echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
32
- echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
33
-
34
- if [ "$outcome" = 'invalid' ]; then
35
- echo "::warning::Agent output has no usable triage outcome. It will not be applied."
36
- fi
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-triage-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Validate triage output
3
+ description: Verify agent_output.json contains a usable triage verdict before any GitHub writes.
4
+ inputs:
5
+ output-file:
6
+ description: Path to agent_output.json.
7
+ required: true
8
+ issue-number:
9
+ description: Issue number that every triage outcome must target.
10
+ required: true
11
+ outputs:
12
+ valid:
13
+ description: Whether the output contains a usable triage comment with a verdict.
14
+ value: ${{ steps.validate.outputs.valid }}
15
+ outcome:
16
+ description: "Deterministic triage outcome: pass, needs-info, needs-maintainer, block, or invalid."
17
+ value: ${{ steps.validate.outputs.outcome }}
18
+ runs:
19
+ using: composite
20
+ steps:
21
+ - name: Validate triage outcome
22
+ id: validate
23
+ shell: bash
24
+ env:
25
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
26
+ OUTPUT_FILE: ${{ inputs.output-file }}
27
+ run: |
28
+ set -euo pipefail
29
+
30
+ outcome="$(bash "${{ github.action_path }}/validate-triage-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER")"
31
+ echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
32
+ echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
33
+
34
+ if [ "$outcome" = 'invalid' ]; then
35
+ echo "::warning::Agent output has no usable triage outcome. It will not be applied."
36
+ fi
@@ -1,9 +1,9 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-composite-actions/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Verify composite actions
3
- description: Check every local composite action manifest parses and uses only contexts a composite action actually has.
4
- runs:
5
- using: composite
6
- steps:
7
- - name: Validate composite action manifests
8
- shell: bash
9
- run: bash "${GITHUB_ACTION_PATH}/verify-composite-actions.sh"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-composite-actions/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Verify composite actions
3
+ description: Check every local composite action manifest parses and uses only contexts a composite action actually has.
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Validate composite action manifests
8
+ shell: bash
9
+ run: bash "${GITHUB_ACTION_PATH}/verify-composite-actions.sh"
@@ -1,9 +1,9 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Verify refine output validation
3
- description: Exercise deterministic Refine output validation regressions.
4
- runs:
5
- using: composite
6
- steps:
7
- - name: Verify refinement outcomes
8
- shell: bash
9
- run: bash "${{ github.action_path }}/verify-refine-output.sh"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-refine-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Verify refine output validation
3
+ description: Exercise deterministic Refine output validation regressions.
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Verify refinement outcomes
8
+ shell: bash
9
+ run: bash "${{ github.action_path }}/verify-refine-output.sh"
@@ -1,9 +1,9 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-route-matrix/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Verify route matrix
3
- description: Exercise the router's classifier against every supported event, and check that each route and dispatch operation has a job in work-router.yml.
4
- runs:
5
- using: composite
6
- steps:
7
- - name: Run route matrix verification
8
- shell: bash
9
- run: bash "${GITHUB_ACTION_PATH}/verify-route-matrix.sh"
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/verify-route-matrix/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Verify route matrix
3
+ description: Exercise the router's classifier against every supported event, and check that each route and dispatch operation has a job in work-router.yml.
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Run route matrix verification
8
+ shell: bash
9
+ run: bash "${GITHUB_ACTION_PATH}/verify-route-matrix.sh"
@@ -1303,6 +1303,11 @@ if [ -f "${DEPENDABOT_DIR}/action.yml" ] && grep -q '^ housekeeping:$' "$ROUTER
1303
1303
  db_step=$(printf '%s\n' "$hk_job" | sed -n '/uses: \.\/\.github\/actions\/merge-dependabot/,/^ - /p')
1304
1304
  printf '%s' "$db_step" | grep -q 'token: ${{ steps.app-token.outputs.token }}' ||
1305
1305
  { DB_OK=0; echo "FAIL: the maintenance run does not merge Dependabot pull requests with the App token" >&2; }
1306
+ # The App installation may hold no Checks permission: the reads run on the job token.
1307
+ if ! printf '%s' "$db_step" | grep -q 'read-token: ${{ github.token }}' ||
1308
+ ! printf '%s\n' "$hk_job" | grep -q '^ checks: read$'; then
1309
+ DB_OK=0; echo "FAIL: the Dependabot merge must read checks with GITHUB_TOKEN granted checks: read" >&2
1310
+ fi
1306
1311
  for knob in DEPENDABOT_MERGE DEPENDABOT_MERGE_WITHOUT_CHECKS; do
1307
1312
  if [ -z "$(router_env "$knob")" ] || ! printf '%s' "$db_step" | grep -q "env.${knob}"; then
1308
1313
  DB_OK=0
@@ -1911,8 +1916,8 @@ fi
1911
1916
  if ! grep -qF 'contents/${FRAGMENT}?ref=${DEFAULT_BRANCH}' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
1912
1917
  LEAD_OK=0; echo "FAIL: the CI config merge must read opencode.ci.json from the default branch" >&2
1913
1918
  fi
1914
- if worker_installed implement && ! grep -qF 'subagent_type: planner' "$IMPLEMENT_WORKER_MD"; then
1915
- LEAD_OK=0; echo "FAIL: the implement prompt must run propose and archive in the planner agent" >&2
1919
+ if worker_installed implement && ! grep -qF 'subagent_type: planner-sub' "$IMPLEMENT_WORKER_MD"; then
1920
+ LEAD_OK=0; echo "FAIL: the implement prompt must run propose and archive in the planner-sub agent" >&2
1916
1921
  fi
1917
1922
  if [ "$LEAD_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1918
1923
  # The harness plugins only load with .opencode/node_modules installed; without it every CI run