@plainconceptsplatform/workflows 1.9.0 → 1.10.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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/catalog-installation.js +2 -2
  3. package/dist/package-baseline.js +4 -1
  4. package/dist/route-processing.js +4 -1
  5. package/dist/worker-env.d.ts +1 -1
  6. package/dist/worker-env.js +6 -2
  7. package/dist/workflow-catalog.d.ts +3 -1
  8. package/dist/workflow-catalog.js +6 -0
  9. package/loops/actions/add-issue-labels/action.yml +50 -50
  10. package/loops/actions/agent-output.cjs +17 -17
  11. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  12. package/loops/actions/apply-agent-comments/action.yml +42 -42
  13. package/loops/actions/apply-agent-labels/action.yml +55 -55
  14. package/loops/actions/audit-close/action.yml +13 -6
  15. package/loops/actions/classify-route/classify-route.sh +3 -1
  16. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  17. package/loops/actions/close-agent-issues/action.yml +43 -43
  18. package/loops/actions/collect-doc-drift/action.yml +29 -0
  19. package/loops/actions/collect-doc-drift/collect-doc-drift.mjs +133 -0
  20. package/loops/actions/create-issue-comment/action.yml +29 -29
  21. package/loops/actions/download-agent-output/action.yml +53 -53
  22. package/loops/actions/identify-gate-subject/action.yml +4 -0
  23. package/loops/actions/land-pull-request/action.yml +10 -0
  24. package/loops/actions/land-pull-request/land-pull-request.sh +14 -0
  25. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  26. package/loops/actions/list-open-issues/action.yml +33 -33
  27. package/loops/actions/load-issue-context/action.yml +45 -45
  28. package/loops/actions/merge-agent-pr/action.yml +49 -49
  29. package/loops/actions/remove-issue-labels/action.yml +37 -37
  30. package/loops/actions/report-workflow-errors/action.yml +1 -1
  31. package/loops/actions/update-agent-issues/action.yml +58 -58
  32. package/loops/actions/validate-refine-output/action.yml +6 -1
  33. package/loops/actions/validate-refine-output/validate-refine-output.sh +20 -2
  34. package/loops/actions/validate-review-output/action.yml +35 -35
  35. package/loops/actions/validate-triage-output/action.yml +36 -36
  36. package/loops/actions/verify-composite-actions/action.yml +9 -9
  37. package/loops/actions/verify-refine-output/action.yml +9 -9
  38. package/loops/actions/verify-route-matrix/action.yml +9 -9
  39. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +38 -5
  40. package/loops/docs/diagrams.md +45 -2
  41. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  42. package/loops/templates/issues/bug_report.yml +109 -109
  43. package/loops/templates/issues/feature_request.yml +75 -75
  44. package/loops/workflows/agent-docs-audit.md +266 -0
  45. package/loops/workflows/agent-merge-gate.md +11 -7
  46. package/loops/workflows/agent-refine.md +61 -11
  47. package/loops/workflows/shared/platform-defaults.md +53 -53
  48. package/loops/workflows/work-router.yml +120 -3
  49. package/package.json +1 -1
@@ -1,91 +1,91 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/cleanup-artifacts/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Clean up artifacts
3
- description: Delete workflow artifacts older than the configured retention period.
4
- inputs:
5
- token:
6
- description: GitHub token with actions write access.
7
- required: true
8
- artifact-retention-days:
9
- description: Retention period in days. Invalid or empty values use 3.
10
- required: false
11
- default: ''
12
- runs:
13
- using: composite
14
- steps:
15
- - name: Delete expired artifacts
16
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
17
- env:
18
- ARTIFACT_RETENTION_DAYS: ${{ inputs.artifact-retention-days }}
19
- with:
20
- github-token: ${{ inputs.token }}
21
- script: |
22
- const parsedDays = Number.parseInt(process.env.ARTIFACT_RETENTION_DAYS, 10);
23
- const days = Number.isInteger(parsedDays) && parsedDays > 0 ? parsedDays : 3;
24
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
25
-
26
- core.info(`Retention: ${days} days (cutoff ${cutoff.toISOString()})`);
27
-
28
- const artifacts = await github.paginate(github.rest.actions.listArtifactsForRepo, {
29
- ...context.repo,
30
- per_page: 100,
31
- });
32
-
33
- const expired = artifacts.filter((artifact) => new Date(artifact.created_at) < cutoff);
34
-
35
- let deleted = 0;
36
- const failed = [];
37
-
38
- for (const artifact of expired) {
39
- // GitHub answers a delete with a 5xx now and then. Three tries, because the
40
- // alternative is a red daily run over one artifact that tomorrow's run would
41
- // have removed anyway.
42
- let lastError;
43
- for (let attempt = 1; attempt <= 3; attempt += 1) {
44
- try {
45
- await github.rest.actions.deleteArtifact({
46
- ...context.repo,
47
- artifact_id: artifact.id,
48
- });
49
-
50
- core.info(`Deleted ${artifact.name} (created ${artifact.created_at})`);
51
- deleted += 1;
52
- lastError = undefined;
53
- break;
54
- } catch (error) {
55
- lastError = error;
56
- if (attempt < 3) await new Promise((wait) => setTimeout(wait, attempt * 2000));
57
- }
58
- }
59
-
60
- if (lastError) {
61
- core.warning(
62
- `Could not delete ${artifact.name} (${artifact.id}) after 3 tries: ${lastError.message}`,
63
- );
64
- failed.push({ name: artifact.name, status: lastError.status });
65
- }
66
- }
67
-
68
- await core.summary
69
- .addHeading('Artifact cleanup', 2)
70
- .addList([
71
- `Retention: ${days} days (cutoff ${cutoff.toISOString()})`,
72
- `Deleted: ${deleted}`,
73
- `Failed to delete: ${failed.length}`,
74
- `Within retention: ${artifacts.length - expired.length}`,
75
- ])
76
- .write();
77
-
78
- // Deleting nothing at all while expired artifacts exist is a permission or
79
- // configuration problem, and has to be loud. One stubborn artifact among many is
80
- // the API having a bad minute: tomorrow's run picks it up, and failing here only
81
- // teaches people to ignore a red cleanup. A 4xx is never transient, so it fails
82
- // whatever else succeeded.
83
- const names = failed.map((entry) => entry.name).join(', ');
84
- const structural = failed.some((entry) => entry.status && entry.status < 500);
85
- if (failed.length > 0 && (deleted === 0 || structural)) {
86
- core.setFailed(`${failed.length} artifact(s) could not be deleted: ${names}`);
87
- } else if (failed.length > 0) {
88
- core.warning(
89
- `${failed.length} artifact(s) survived this run and will be retried tomorrow: ${names}`,
90
- );
91
- }
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/cleanup-artifacts/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Clean up artifacts
3
+ description: Delete workflow artifacts older than the configured retention period.
4
+ inputs:
5
+ token:
6
+ description: GitHub token with actions write access.
7
+ required: true
8
+ artifact-retention-days:
9
+ description: Retention period in days. Invalid or empty values use 3.
10
+ required: false
11
+ default: ''
12
+ runs:
13
+ using: composite
14
+ steps:
15
+ - name: Delete expired artifacts
16
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
17
+ env:
18
+ ARTIFACT_RETENTION_DAYS: ${{ inputs.artifact-retention-days }}
19
+ with:
20
+ github-token: ${{ inputs.token }}
21
+ script: |
22
+ const parsedDays = Number.parseInt(process.env.ARTIFACT_RETENTION_DAYS, 10);
23
+ const days = Number.isInteger(parsedDays) && parsedDays > 0 ? parsedDays : 3;
24
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
25
+
26
+ core.info(`Retention: ${days} days (cutoff ${cutoff.toISOString()})`);
27
+
28
+ const artifacts = await github.paginate(github.rest.actions.listArtifactsForRepo, {
29
+ ...context.repo,
30
+ per_page: 100,
31
+ });
32
+
33
+ const expired = artifacts.filter((artifact) => new Date(artifact.created_at) < cutoff);
34
+
35
+ let deleted = 0;
36
+ const failed = [];
37
+
38
+ for (const artifact of expired) {
39
+ // GitHub answers a delete with a 5xx now and then. Three tries, because the
40
+ // alternative is a red daily run over one artifact that tomorrow's run would
41
+ // have removed anyway.
42
+ let lastError;
43
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
44
+ try {
45
+ await github.rest.actions.deleteArtifact({
46
+ ...context.repo,
47
+ artifact_id: artifact.id,
48
+ });
49
+
50
+ core.info(`Deleted ${artifact.name} (created ${artifact.created_at})`);
51
+ deleted += 1;
52
+ lastError = undefined;
53
+ break;
54
+ } catch (error) {
55
+ lastError = error;
56
+ if (attempt < 3) await new Promise((wait) => setTimeout(wait, attempt * 2000));
57
+ }
58
+ }
59
+
60
+ if (lastError) {
61
+ core.warning(
62
+ `Could not delete ${artifact.name} (${artifact.id}) after 3 tries: ${lastError.message}`,
63
+ );
64
+ failed.push({ name: artifact.name, status: lastError.status });
65
+ }
66
+ }
67
+
68
+ await core.summary
69
+ .addHeading('Artifact cleanup', 2)
70
+ .addList([
71
+ `Retention: ${days} days (cutoff ${cutoff.toISOString()})`,
72
+ `Deleted: ${deleted}`,
73
+ `Failed to delete: ${failed.length}`,
74
+ `Within retention: ${artifacts.length - expired.length}`,
75
+ ])
76
+ .write();
77
+
78
+ // Deleting nothing at all while expired artifacts exist is a permission or
79
+ // configuration problem, and has to be loud. One stubborn artifact among many is
80
+ // the API having a bad minute: tomorrow's run picks it up, and failing here only
81
+ // teaches people to ignore a red cleanup. A 4xx is never transient, so it fails
82
+ // whatever else succeeded.
83
+ const names = failed.map((entry) => entry.name).join(', ');
84
+ const structural = failed.some((entry) => entry.status && entry.status < 500);
85
+ if (failed.length > 0 && (deleted === 0 || structural)) {
86
+ core.setFailed(`${failed.length} artifact(s) could not be deleted: ${names}`);
87
+ } else if (failed.length > 0) {
88
+ core.warning(
89
+ `${failed.length} artifact(s) survived this run and will be retried tomorrow: ${names}`,
90
+ );
91
+ }
@@ -1,43 +1,43 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/close-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Close agent issues
3
- description: Close every close_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
- runs:
12
- using: composite
13
- steps:
14
- - name: Close issues
15
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
16
- env:
17
- AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
18
- OUTPUT_FILE: ${{ inputs.output-file }}
19
- with:
20
- github-token: ${{ inputs.token }}
21
- script: |
22
- const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
23
- const items = readAgentItems(process.env.OUTPUT_FILE, 'close_issue');
24
-
25
- let closed = 0;
26
-
27
- for (const item of items) {
28
- if (!item.item_number) {
29
- core.warning(`close_issue item has no item_number, skipping: ${JSON.stringify(item)}`);
30
- continue;
31
- }
32
-
33
- await github.rest.issues.update({
34
- ...context.repo,
35
- issue_number: Number(item.item_number),
36
- state: 'closed',
37
- state_reason: item.state_reason === 'not_planned' ? 'not_planned' : 'completed',
38
- });
39
-
40
- closed += 1;
41
- }
42
-
43
- core.info(`Closed ${closed} issue(s).`);
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/close-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Close agent issues
3
+ description: Close every close_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
+ runs:
12
+ using: composite
13
+ steps:
14
+ - name: Close issues
15
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
16
+ env:
17
+ AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
18
+ OUTPUT_FILE: ${{ inputs.output-file }}
19
+ with:
20
+ github-token: ${{ inputs.token }}
21
+ script: |
22
+ const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
23
+ const items = readAgentItems(process.env.OUTPUT_FILE, 'close_issue');
24
+
25
+ let closed = 0;
26
+
27
+ for (const item of items) {
28
+ if (!item.item_number) {
29
+ core.warning(`close_issue item has no item_number, skipping: ${JSON.stringify(item)}`);
30
+ continue;
31
+ }
32
+
33
+ await github.rest.issues.update({
34
+ ...context.repo,
35
+ issue_number: Number(item.item_number),
36
+ state: 'closed',
37
+ state_reason: item.state_reason === 'not_planned' ? 'not_planned' : 'completed',
38
+ });
39
+
40
+ closed += 1;
41
+ }
42
+
43
+ core.info(`Closed ${closed} issue(s).`);
@@ -0,0 +1,29 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/collect-doc-drift/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Collect documentation drift
3
+ description: |
4
+ Writes the documentation facts a script can establish on its own to
5
+ `/tmp/gh-aw/agent/doc-drift.json`: relative Markdown links that lead nowhere, and paths the
6
+ entry documents cite that no longer exist. Free, deterministic, no model.
7
+ inputs:
8
+ entry-docs:
9
+ description: Comma-separated entry documents, whose cited paths are checked.
10
+ required: false
11
+ default: README.md,ARCHITECTURE.md,AGENTS.md,CLAUDE.md,CONTRIBUTING.md,docs/README.md
12
+ exclude:
13
+ description: Comma-separated path prefixes left out of the scan, such as archives.
14
+ required: false
15
+ default: .github/workflows/,.agents/,.claude/,.opencode/,node_modules/
16
+ output-path:
17
+ description: Path to write the JSON output.
18
+ required: false
19
+ default: /tmp/gh-aw/agent/doc-drift.json
20
+ runs:
21
+ using: composite
22
+ steps:
23
+ - name: Collect documentation drift
24
+ shell: bash
25
+ env:
26
+ ENTRY_DOCS: ${{ inputs.entry-docs }}
27
+ EXCLUDE: ${{ inputs.exclude }}
28
+ OUTPUT_PATH: ${{ inputs.output-path }}
29
+ run: node "${GITHUB_ACTION_PATH}/collect-doc-drift.mjs"
@@ -0,0 +1,133 @@
1
+ // Managed by @plainconceptsplatform/workflows. Source: loops/actions/collect-doc-drift/collect-doc-drift.mjs. Update with workflows update --force; consumer edits may be overwritten.
2
+ //
3
+ // Everything here is something a model would otherwise spend turns establishing, and could get
4
+ // wrong. Existence only: whether a link's anchor resolves, or whether a sentence is still true,
5
+ // is left to the agent.
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
+ import { dirname, join, normalize } from "node:path";
10
+ import { pathToFileURL } from "node:url";
11
+
12
+ // A report nobody can finish reading is a report nobody acts on, and it costs input tokens.
13
+ const MAX_LISTED = 200;
14
+
15
+ const FENCE = /^\s*(```|~~~)/;
16
+ const INLINE_LINK = /(?<!!)\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
17
+ const IMAGE_LINK = /!\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
18
+ const CODE_SPAN = /`([^`\n]+)`/g;
19
+ const CITED_PATH = /^\.?\/?[\w.-]+(\/[\w.-]+)*\/?$/;
20
+ const EXTERNAL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
21
+
22
+ const splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
23
+
24
+ export const isExcluded = (path, prefixes) => prefixes.some((prefix) => path.startsWith(prefix));
25
+
26
+ /** The file a link points at, or null when it does not point at a file in this repository. */
27
+ export function linkTarget(fromFile, href) {
28
+ if (href.startsWith("#") || EXTERNAL.test(href)) return null;
29
+ const path = href.split(/[#?]/)[0];
30
+ if (path === "") return null;
31
+ const decoded = decodePath(path);
32
+ return normalize(decoded.startsWith("/") ? decoded.slice(1) : join(dirname(fromFile), decoded));
33
+ }
34
+
35
+ function decodePath(path) {
36
+ try {
37
+ return decodeURIComponent(path);
38
+ } catch {
39
+ // A literal % that is not an escape: the undecoded path is the one to check.
40
+ return path;
41
+ }
42
+ }
43
+
44
+ // A code span in prose often is a path, but `owner/repo` and `application/json` have the same
45
+ // shape. Requiring an existing first segment keeps paths into this repository and drops those.
46
+ export function citedPath(span, topLevelEntries) {
47
+ if (!span.includes("/") || !CITED_PATH.test(span)) return null;
48
+ const path = span.replace(/^\.?\//, "");
49
+ return topLevelEntries.has(path.split("/")[0]) ? path : null;
50
+ }
51
+
52
+ /** Walk a Markdown document line by line, skipping fenced code, where a link is an example. */
53
+ export function* proseLines(markdown) {
54
+ let fenced = false;
55
+ let number = 0;
56
+ for (const line of markdown.split(/\r?\n/)) {
57
+ number += 1;
58
+ if (FENCE.test(line)) {
59
+ fenced = !fenced;
60
+ continue;
61
+ }
62
+ if (!fenced) yield { number, line };
63
+ }
64
+ }
65
+
66
+ export function scan({ files, entryDocs, read, exists, topLevelEntries }) {
67
+ const brokenLinks = [];
68
+ const missingPaths = [];
69
+ const seenCitations = new Set();
70
+
71
+ for (const file of files) {
72
+ const isEntry = entryDocs.includes(file);
73
+ for (const { number, line } of proseLines(read(file))) {
74
+ const withoutCode = line.replace(CODE_SPAN, "");
75
+ for (const pattern of [INLINE_LINK, IMAGE_LINK]) {
76
+ for (const [, href] of withoutCode.matchAll(pattern)) {
77
+ const target = linkTarget(file, href);
78
+ if (target !== null && !exists(target)) brokenLinks.push({ file, line: number, target: href });
79
+ }
80
+ }
81
+ if (!isEntry) continue;
82
+ for (const [, span] of line.matchAll(CODE_SPAN)) {
83
+ const path = citedPath(span, topLevelEntries);
84
+ const key = `${file}:${path}`;
85
+ if (path === null || seenCitations.has(key) || exists(path)) continue;
86
+ seenCitations.add(key);
87
+ missingPaths.push({ file, line: number, path });
88
+ }
89
+ }
90
+ }
91
+
92
+ return { brokenLinks, missingPaths };
93
+ }
94
+
95
+ function main() {
96
+ const entryDocs = splitList(process.env.ENTRY_DOCS);
97
+ const exclude = splitList(process.env.EXCLUDE);
98
+ const outputPath = process.env.OUTPUT_PATH ?? "/tmp/gh-aw/agent/doc-drift.json";
99
+
100
+ const tracked = execFileSync("git", ["ls-files", "-z"], { encoding: "utf8", maxBuffer: 1 << 28 })
101
+ .split("\0").filter(Boolean);
102
+ const files = tracked.filter((path) => path.endsWith(".md") && !isExcluded(path, exclude));
103
+ const trackedSet = new Set(tracked);
104
+ const exists = (path) => {
105
+ if (trackedSet.has(path)) return true;
106
+ // A directory is tracked only through its files, and a generated file not at all.
107
+ return existsSync(path) && (statSync(path).isDirectory() || statSync(path).isFile());
108
+ };
109
+
110
+ const { brokenLinks, missingPaths } = scan({
111
+ files,
112
+ entryDocs,
113
+ read: (path) => readFileSync(path, "utf8"),
114
+ exists,
115
+ topLevelEntries: new Set(tracked.map((path) => path.split("/")[0])),
116
+ });
117
+
118
+ const report = {
119
+ scannedDocuments: files.length,
120
+ entryDocuments: entryDocs.filter((path) => trackedSet.has(path)),
121
+ missingEntryDocuments: entryDocs.filter((path) => !trackedSet.has(path)),
122
+ brokenLinkCount: brokenLinks.length,
123
+ missingPathCount: missingPaths.length,
124
+ brokenLinks: brokenLinks.slice(0, MAX_LISTED),
125
+ missingPaths: missingPaths.slice(0, MAX_LISTED),
126
+ };
127
+
128
+ mkdirSync(dirname(outputPath), { recursive: true });
129
+ writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`);
130
+ console.log(`Scanned ${files.length} documents: ${brokenLinks.length} broken links, ${missingPaths.length} missing cited paths.`);
131
+ }
132
+
133
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
@@ -1,29 +1,29 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/create-issue-comment/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
- name: Create issue comment
3
- description: Create a comment on 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
- body:
12
- description: Complete Markdown comment body.
13
- required: true
14
- runs:
15
- using: composite
16
- steps:
17
- - name: Create comment
18
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
19
- env:
20
- ISSUE_NUMBER: ${{ inputs.issue-number }}
21
- BODY: ${{ inputs.body }}
22
- with:
23
- github-token: ${{ inputs.token }}
24
- script: |
25
- await github.rest.issues.createComment({
26
- ...context.repo,
27
- issue_number: Number(process.env.ISSUE_NUMBER),
28
- body: process.env.BODY,
29
- });
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/create-issue-comment/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Create issue comment
3
+ description: Create a comment on 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
+ body:
12
+ description: Complete Markdown comment body.
13
+ required: true
14
+ runs:
15
+ using: composite
16
+ steps:
17
+ - name: Create comment
18
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
19
+ env:
20
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
21
+ BODY: ${{ inputs.body }}
22
+ with:
23
+ github-token: ${{ inputs.token }}
24
+ script: |
25
+ await github.rest.issues.createComment({
26
+ ...context.repo,
27
+ issue_number: Number(process.env.ISSUE_NUMBER),
28
+ body: process.env.BODY,
29
+ });
@@ -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"
@@ -49,6 +49,9 @@ outputs:
49
49
  run-id:
50
50
  description: CI run ID when fetched.
51
51
  value: ${{ steps.identify.outputs.run-id }}
52
+ human-author:
53
+ description: Whether a person with write access opened the pull request, rather than a trusted bot.
54
+ value: ${{ steps.identify.outputs.human-author }}
52
55
  runs:
53
56
  using: composite
54
57
  steps:
@@ -194,6 +197,7 @@ runs:
194
197
  if [ -n "$issue" ]; then echo "closes=closes issue #$issue"; else echo "closes=closes no issue"; fi
195
198
  echo "conclusion=$ci_conclusion"
196
199
  echo "run-id=$ci_run_id"
200
+ if [ "$bot" = true ]; then echo "human-author=false"; else echo "human-author=true"; fi
197
201
  } >> "$GITHUB_OUTPUT"
198
202
 
199
203
  echo "PR #$PR_NUMBER by $author closes #${issue:-nothing}; CI concluded $ci_conclusion"
@@ -23,6 +23,14 @@ inputs:
23
23
  required: false
24
24
  default: Approved by the merge gate.
25
25
 
26
+ post-merge-event:
27
+ description: >-
28
+ repository_dispatch event type posted after a merge lands on the default branch. GitHub does
29
+ not always create a push run for a merge the App performs, so a consumer that deploys on push
30
+ opts into this event instead of relying on that run.
31
+ required: false
32
+ default: agentic-post-merge
33
+
26
34
  outputs:
27
35
  landed:
28
36
  description: "'merged', 'enqueued', or empty when nothing happened."
@@ -40,4 +48,6 @@ runs:
40
48
  HEAD_SHA: ${{ inputs.head-sha }}
41
49
  APPROVE: ${{ inputs.approve }}
42
50
  APPROVAL_BODY: ${{ inputs.approval-body }}
51
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
52
+ POST_MERGE_EVENT: ${{ inputs.post-merge-event }}
43
53
  run: bash "${GITHUB_ACTION_PATH}/land-pull-request.sh"