@plainconceptsplatform/workflows 1.3.1 → 1.4.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 (41) hide show
  1. package/dist/worker-env.js +2 -3
  2. package/loops/actions/add-issue-labels/action.yml +50 -50
  3. package/loops/actions/agent-output.cjs +17 -17
  4. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  5. package/loops/actions/apply-agent-comments/action.yml +42 -42
  6. package/loops/actions/apply-agent-labels/action.yml +55 -55
  7. package/loops/actions/apply-agent-output/action.yml +5 -0
  8. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  9. package/loops/actions/close-agent-issues/action.yml +43 -43
  10. package/loops/actions/create-agent-issues/action.yml +64 -3
  11. package/loops/actions/create-issue-comment/action.yml +29 -29
  12. package/loops/actions/download-agent-output/action.yml +53 -53
  13. package/loops/actions/housekeeping/action.yml +95 -1
  14. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  15. package/loops/actions/list-open-issues/action.yml +33 -33
  16. package/loops/actions/load-issue-context/action.yml +45 -45
  17. package/loops/actions/merge-agent-pr/action.yml +49 -49
  18. package/loops/actions/push-agent-branch/action.yml +45 -45
  19. package/loops/actions/remove-issue-labels/action.yml +37 -37
  20. package/loops/actions/update-agent-issues/action.yml +58 -58
  21. package/loops/actions/validate-refine-output/validate-refine-output.sh +30 -1
  22. package/loops/actions/validate-review-output/action.yml +35 -35
  23. package/loops/actions/validate-triage-output/action.yml +36 -36
  24. package/loops/actions/verify-composite-actions/action.yml +9 -9
  25. package/loops/actions/verify-refine-output/action.yml +9 -9
  26. package/loops/actions/verify-refine-output/verify-refine-output.sh +17 -0
  27. package/loops/actions/verify-route-matrix/action.yml +9 -9
  28. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +214 -7
  29. package/loops/docs/diagrams.md +90 -12
  30. package/loops/scripts/compile-agent-workflows.mjs +76 -2
  31. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  32. package/loops/templates/issues/bug_report.yml +109 -109
  33. package/loops/templates/issues/feature_request.yml +75 -75
  34. package/loops/templates/release/github-release.yml +30 -30
  35. package/loops/workflows/agent-implement.md +37 -7
  36. package/loops/workflows/agent-merge-gate.md +74 -6
  37. package/loops/workflows/agent-refine.md +152 -5
  38. package/loops/workflows/authorize-bot-work.yml +105 -105
  39. package/loops/workflows/shared/platform-defaults.md +23 -23
  40. package/loops/workflows/work-router.yml +330 -2
  41. package/package.json +1 -1
@@ -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).`);
@@ -8,6 +8,12 @@ inputs:
8
8
  token:
9
9
  description: GitHub token with issues:write.
10
10
  required: true
11
+ parent-issue:
12
+ description: >-
13
+ The split parent, set only when the run is a split. Each created issue is stamped with the
14
+ split-parent marker and linked as a sub-issue of it, and temporary ids are resolved.
15
+ required: false
16
+ default: ''
11
17
  outputs:
12
18
  created-count:
13
19
  description: Number of issues created.
@@ -15,6 +21,9 @@ outputs:
15
21
  first-issue-number:
16
22
  description: Issue number of the first issue created, or empty.
17
23
  value: ${{ steps.create.outputs.first-issue-number }}
24
+ created-numbers:
25
+ description: Space-separated numbers of every issue created, in order.
26
+ value: ${{ steps.create.outputs.created-numbers }}
18
27
  runs:
19
28
  using: composite
20
29
  steps:
@@ -24,13 +33,23 @@ runs:
24
33
  env:
25
34
  AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
26
35
  OUTPUT_FILE: ${{ inputs.output-file }}
36
+ PARENT_ISSUE: ${{ inputs.parent-issue }}
27
37
  with:
28
38
  github-token: ${{ inputs.token }}
29
39
  script: |
30
40
  const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
31
41
  const items = readAgentItems(process.env.OUTPUT_FILE, 'create_issue');
42
+ // Set only for a split. The agent writes `<!-- split-parent: N -->` in every child, but
43
+ // gh-aw's sanitizer strips HTML comments before agent_output.json is written, so the
44
+ // marker never reaches this file (Lyceum #45, run 38747143251: three markers in
45
+ // safeoutputs.jsonl, none in agent_output.json). gh-aw used to link the children as
46
+ // sub-issues itself; staged, nothing does, and the hand-over found no children. So the
47
+ // workflow stamps the marker and links the sub-issue here, from a value it owns.
48
+ const parent = /^[0-9]+$/.test(process.env.PARENT_ISSUE || '') ? Number(process.env.PARENT_ISSUE) : null;
49
+ const marker = parent ? `<!-- split-parent: ${parent} -->` : '';
32
50
 
33
51
  const created = [];
52
+ const temporaryIds = {};
34
53
 
35
54
  for (const item of items) {
36
55
  if (!item.title) {
@@ -38,15 +57,57 @@ runs:
38
57
  continue;
39
58
  }
40
59
 
60
+ let body = item.body ?? '';
61
+ if (parent && !body.includes(marker)) body = `${body}\n\n${marker}`;
62
+
41
63
  const { data } = await github.rest.issues.create({
42
64
  ...context.repo,
43
65
  title: item.title,
44
- body: item.body ?? '',
66
+ body,
45
67
  });
46
68
 
47
- created.push(data.number);
69
+ created.push({ number: data.number, body });
70
+ const tid = String(item.temporary_id ?? '').replace(/^#/, '');
71
+ if (tid) temporaryIds[tid] = data.number;
48
72
  core.info(`Created #${data.number}: ${item.title}`);
73
+
74
+ if (parent) {
75
+ try {
76
+ await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', {
77
+ ...context.repo,
78
+ issue_number: parent,
79
+ sub_issue_id: data.id,
80
+ });
81
+ core.info(`Linked #${data.number} under #${parent}`);
82
+ } catch (error) {
83
+ // The marker above is the fallback the hand-over searches, so a failed link is
84
+ // degraded, not lost.
85
+ core.warning(`Could not link #${data.number} under #${parent}: ${error.message}`);
86
+ }
87
+ }
88
+ }
89
+
90
+ // The agent references siblings by the temporary id it gave them, because their numbers
91
+ // do not exist until now: "Blocked by #aw_struct_modules". gh-aw resolves these when it
92
+ // creates the issues; staged, they reached the issues unresolved. Rewrite them in the
93
+ // children and in the parent's checklist, which was updated before this step ran.
94
+ const resolve = text => text.replace(/#(aw_[A-Za-z0-9_]+)/g, (whole, id) => (temporaryIds[id] ? `#${temporaryIds[id]}` : whole));
95
+ if (Object.keys(temporaryIds).length > 0) {
96
+ for (const issue of created) {
97
+ const next = resolve(issue.body);
98
+ if (next !== issue.body) {
99
+ await github.rest.issues.update({ ...context.repo, issue_number: issue.number, body: next });
100
+ }
101
+ }
102
+ if (parent) {
103
+ const { data: current } = await github.rest.issues.get({ ...context.repo, issue_number: parent });
104
+ const next = resolve(current.body ?? '');
105
+ if (next !== (current.body ?? '')) {
106
+ await github.rest.issues.update({ ...context.repo, issue_number: parent, body: next });
107
+ }
108
+ }
49
109
  }
50
110
 
51
111
  core.setOutput('created-count', String(created.length));
52
- core.setOutput('first-issue-number', created.length > 0 ? String(created[0]) : '');
112
+ core.setOutput('first-issue-number', created.length > 0 ? String(created[0].number) : '');
113
+ core.setOutput('created-numbers', created.map(issue => issue.number).join(' '));
@@ -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"
@@ -220,6 +220,14 @@ runs:
220
220
  for (const [parent, children] of childrenOf) {
221
221
  const open = children.filter((c) => c.state === 'open');
222
222
  if (open.length > 0 || !issues.some((i) => i.number === parent)) continue;
223
+ // A stack feature issue. Its root pull request closing the issue is what ends it, not
224
+ // this sweep: closing the tracker here would close a feature whose root pull request
225
+ // is still open and holding a branch with real work on it. Refine stamps the root
226
+ // markers on the parent before the first child is ever handed over, so the marker
227
+ // is there from birth; finding none means flat mode, and the original rule applies.
228
+ // Everything stack-specific about these parents is decided in section 3b below,
229
+ // which sees the whole stack, not just the child count.
230
+ if (issues.some((i) => i.number === parent && /<!-- stack-root-branch: \S+ -->/.test(i.body ?? ''))) continue;
223
231
  await act(`#${parent}: closing split parent, all ${children.length} children are done`, async () => {
224
232
  await github.rest.issues.createComment({
225
233
  ...context.repo, issue_number: parent,
@@ -229,6 +237,87 @@ runs:
229
237
  });
230
238
  }
231
239
 
240
+ // The stack and branch sections both need every pull request the repository has ever
241
+ // seen, closed included, so it is paginated once here rather than once per section.
242
+ const allPulls = await github.paginate(github.rest.pulls.list, { ...context.repo, state: 'all', per_page: 100 });
243
+
244
+ // 3b. Stacks. Section 3 refused to close stack feature issues; this is what happens to
245
+ // them instead. The whole stack state is on the feature issue (root pull request) plus
246
+ // the children of the split, and both are already in memory, so a repository without
247
+ // stacks pays nothing: the marker test below fails first and the loop body never runs.
248
+ // No polling is added -- this runs on the same cadence the janitor already has, and it
249
+ // is the backstop for the one event the router can miss: a child issue that closes
250
+ // seconds after a router run already counted it as open.
251
+ for (const issue of issues) {
252
+ if (issue.pull_request) continue;
253
+ const marker = /<!-- stack-root-pr: (\d+) -->/.exec(issue.body ?? '');
254
+ if (!marker) continue;
255
+ const rootPr = Number(marker[1]);
256
+ const root = allPulls.find((p) => p.number === rootPr);
257
+ const children = childrenOf.get(issue.number) ?? [];
258
+ // A child only means work in progress while its pull request is still open.
259
+ // merge-gate closes the child issue the moment its pull request lands on the
260
+ // feature branch; if that close was lost -- an API blip, a run that died between
261
+ // the merge and the close -- the child stays open here for ever and the stack
262
+ // never reads as finished, which is the stall this sweep exists to prevent. The
263
+ // child's own `implement-pr` marker names its pull request, so the test is exact:
264
+ // an open child whose named pull request has merged has landed, whatever its issue
265
+ // state still claims. Closing it here is the same idempotent backstop section 3 is
266
+ // for flat splits, and the App token this sweep runs on is what raises the event.
267
+ let openChildren = 0;
268
+ for (const child of children) {
269
+ if (child.state !== 'open') continue;
270
+ const prMarker = /<!-- implement-pr: (\d+) -->/.exec(child.body ?? '');
271
+ const landed = prMarker && allPulls.some((p) => p.number === Number(prMarker[1]) && p.merged_at);
272
+ if (!landed) { openChildren += 1; continue; }
273
+ await act(`#${child.number}: closing child, its pull request #${prMarker[1]} merged into the stack`, async () => {
274
+ await github.rest.issues.createComment({
275
+ ...context.repo, issue_number: child.number,
276
+ body: `${DIGEST_MARKER}\nClosed automatically: its pull request #${prMarker[1]} merged into the feature branch, so this child is done.`,
277
+ });
278
+ await github.rest.issues.update({ ...context.repo, issue_number: child.number, state: 'closed', state_reason: 'completed' });
279
+ });
280
+ }
281
+ if (!root) {
282
+ needsHuman.push({ kind: 'stack-root-missing', issue, rootPr, waited: age(issue.updated_at) });
283
+ } else if (root.state === 'open') {
284
+ if (openChildren > 0) continue; // mid-stack: the normal state, not worth a line
285
+ // Every child is done, so the completion announcement should exist on the root
286
+ // pull request. Its absence is the lost race above; a person is told here rather
287
+ // than the stack sitting finished and silent. When the announcement exists but
288
+ // nobody has merged for days, that is staleness worth one line too.
289
+ const comments = await github.paginate(github.rest.issues.listComments, {
290
+ ...context.repo, issue_number: rootPr, per_page: 100,
291
+ });
292
+ const told = comments.find((c) => (c.body ?? '').includes('<!-- stack-complete -->'));
293
+ if (!told) {
294
+ needsHuman.push({ kind: 'stack-ready', issue, pull: root, waited: age(issue.updated_at) });
295
+ } else if (age(told.created_at) > stalePrMs) {
296
+ needsHuman.push({ kind: 'stack-held', issue, pull: root, waited: age(told.created_at) });
297
+ }
298
+ } else if (root.merged_at) {
299
+ // The root merged to the default branch and its closing keyword should have
300
+ // closed this issue with it. If something raced that, the same rule section 3
301
+ // applies to flat splits closes it here: every child done, root landed, done.
302
+ if (openChildren === 0) {
303
+ await act(`#${issue.number}: closing stack feature, its root PR #${rootPr} merged`, async () => {
304
+ await github.rest.issues.createComment({
305
+ ...context.repo, issue_number: issue.number,
306
+ body: `${DIGEST_MARKER}\nClosed automatically: the stack's root pull request #${rootPr} merged with every child landed (${children.map((c) => `#${c.number}`).join(', ')}).`,
307
+ });
308
+ await github.rest.issues.update({ ...context.repo, issue_number: issue.number, state: 'closed', state_reason: 'completed' });
309
+ });
310
+ } else {
311
+ needsHuman.push({ kind: 'stack-orphaned', issue, pull: root, waited: age(root.merged_at), openChildren });
312
+ }
313
+ } else {
314
+ // Closed without merging. That is a person's decision or an accident, and the
315
+ // landed child work only exists on the feature branch, which section 4 may have
316
+ // already deleted -- restorable through refs/pull/<n>/head, but a person decides.
317
+ needsHuman.push({ kind: 'stack-root-closed', issue, pull: root, waited: age(root.closed_at), openChildren });
318
+ }
319
+ }
320
+
232
321
  // 4. Branches. Nothing in the fleet has ever deleted one, so they accumulate for ever
233
322
  // after a merge and after a close alike. Only ever touch a branch whose pull request is
234
323
  // finished; never the default branch, and never one with a pull request still open.
@@ -241,7 +330,6 @@ runs:
241
330
  // back, and the pull request page still shows the diff.
242
331
  const branches = await github.paginate(github.rest.repos.listBranches, { ...context.repo, per_page: 100 });
243
332
  const defaultBranch = context.payload.repository.default_branch;
244
- const allPulls = await github.paginate(github.rest.pulls.list, { ...context.repo, state: 'all', per_page: 100 });
245
333
  const byHead = new Map();
246
334
  for (const pull of allPulls) {
247
335
  if (!byHead.has(pull.head.ref)) byHead.set(pull.head.ref, []);
@@ -319,6 +407,11 @@ runs:
319
407
  const line = (entry) => {
320
408
  if (entry.kind === 'pr-parked') return `- PR #${entry.pull.number} · parked ${days(entry.waited)}d · gate verdict \`${entry.verdict}\` · ${entry.pull.title}`;
321
409
  if (entry.kind === 'pr-stale') return `- PR #${entry.pull.number} · ${days(entry.waited)}d old · no gate verdict yet · ${entry.pull.title}`;
410
+ if (entry.kind === 'stack-ready') return `- PR #${entry.pull.number} · stack finished ${days(entry.waited)}d ago, never announced · human merge needed · ${entry.pull.title}`;
411
+ if (entry.kind === 'stack-held') return `- PR #${entry.pull.number} · stack ready and waiting on a human merge for ${days(entry.waited)}d · ${entry.pull.title}`;
412
+ if (entry.kind === 'stack-root-missing') return `- #${entry.issue.number} · names root PR #${entry.rootPr}, which does not exist · ${entry.issue.title}`;
413
+ if (entry.kind === 'stack-root-closed') return `- PR #${entry.pull.number} · stack root closed unmerged, ${entry.openChildren} child(ren) still open · ${entry.issue.title} · restore the branch from refs/pull/${entry.pull.number}/head if accidental`;
414
+ if (entry.kind === 'stack-orphaned') return `- #${entry.issue.number} · root merged but ${entry.openChildren} child(ren) still open · ${entry.issue.title}`;
322
415
  if (entry.kind === 'retries-exhausted') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · retried ${entry.attempts}× and still stalling · ${entry.issue.title}`;
323
416
  if (entry.kind === 'no-work-label') return `- #${entry.issue.number} · stalled ${days(entry.waited)}d · no work label to retry with · ${entry.issue.title}`;
324
417
  if (entry.kind === 'stale-audit') return `- #${entry.issue.number} · open ${days(entry.waited)}d · audit report nobody actioned · ${entry.issue.title}`;
@@ -356,6 +449,7 @@ runs:
356
449
  DIGEST_MARKER,
357
450
  `_Updated ${new Date().toISOString().slice(0, 16).replace('T', ' ')} UTC. This issue is rewritten by the housekeeping job; edits to it are lost._`,
358
451
  section('Waiting on a decision', ['decided']),
452
+ section('Stacked features waiting on a human', ['stack-ready', 'stack-held', 'stack-root-missing', 'stack-root-closed', 'stack-orphaned']),
359
453
  section('Pull requests parked by the gate', ['pr-parked', 'pr-stale']),
360
454
  section('Stalled beyond the retry budget', ['retries-exhausted', 'no-work-label']),
361
455
  section('Audit reports nobody actioned', ['stale-audit']),
@@ -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"