@plainconceptsplatform/workflows 0.4.33 → 0.5.1

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 (75) hide show
  1. package/README.md +88 -88
  2. package/loops/actions/agent-output.cjs +17 -17
  3. package/loops/actions/audit-close/action.yml +24 -0
  4. package/loops/actions/classify-route/action.yml +4 -3
  5. package/loops/actions/classify-route/classify-route.sh +69 -29
  6. package/loops/actions/load-issue-context/action.yml +2 -0
  7. package/loops/actions/update-changelog/action.yml +113 -0
  8. package/loops/actions/validate-merge-gate-output/action.yml +40 -0
  9. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +34 -0
  10. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  11. package/loops/actions/validate-review-output/action.yml +35 -0
  12. package/loops/actions/validate-review-output/validate-review-output.sh +30 -0
  13. package/loops/actions/validate-triage-output/action.yml +36 -0
  14. package/loops/actions/validate-triage-output/validate-triage-output.sh +36 -0
  15. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -36
  16. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  17. package/loops/templates/agentics/agentics-checks.yml +86 -86
  18. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  19. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  20. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  21. package/loops/templates/issues/bug_report.yml +109 -109
  22. package/loops/templates/issues/feature_request.yml +75 -75
  23. package/loops/templates/opencode/opencode.ci.json +49 -47
  24. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  25. package/loops/templates/release/github-release.yml +30 -30
  26. package/loops/workflows/agent-apply-review.md +465 -373
  27. package/loops/workflows/agent-audit.md +213 -197
  28. package/loops/workflows/agent-implement.md +538 -374
  29. package/loops/workflows/agent-merge-gate.md +730 -485
  30. package/loops/workflows/agent-refine.md +609 -379
  31. package/loops/workflows/agent-release.md +258 -0
  32. package/loops/workflows/agent-triage.md +447 -0
  33. package/loops/workflows/authorize-bot-work.yml +85 -81
  34. package/loops/workflows/shared/opencode-ci.md +206 -197
  35. package/loops/workflows/shared/platform-defaults.md +19 -16
  36. package/loops/workflows/work-router.yml +862 -551
  37. package/package.json +42 -42
  38. package/dist/catalog-installation.d.ts +0 -30
  39. package/dist/catalog-installation.js +0 -352
  40. package/dist/catalog-installation.test.d.ts +0 -1
  41. package/dist/catalog-installation.test.js +0 -448
  42. package/dist/catalog-listing.d.ts +0 -13
  43. package/dist/catalog-listing.js +0 -70
  44. package/dist/catalog-listing.test.d.ts +0 -1
  45. package/dist/catalog-listing.test.js +0 -150
  46. package/dist/index.d.ts +0 -2
  47. package/dist/index.js +0 -218
  48. package/dist/index.test.d.ts +0 -1
  49. package/dist/index.test.js +0 -273
  50. package/dist/repository-inspection.d.ts +0 -23
  51. package/dist/repository-inspection.js +0 -113
  52. package/dist/repository-inspection.test.d.ts +0 -1
  53. package/dist/repository-inspection.test.js +0 -77
  54. package/dist/repository-state.d.ts +0 -18
  55. package/dist/repository-state.js +0 -77
  56. package/dist/repository-state.test.d.ts +0 -1
  57. package/dist/repository-state.test.js +0 -96
  58. package/dist/route-processing.d.ts +0 -6
  59. package/dist/route-processing.js +0 -150
  60. package/dist/route-processing.test.d.ts +0 -1
  61. package/dist/route-processing.test.js +0 -342
  62. package/dist/stack-defaults.d.ts +0 -11
  63. package/dist/stack-defaults.js +0 -104
  64. package/dist/stack-defaults.test.d.ts +0 -1
  65. package/dist/stack-defaults.test.js +0 -266
  66. package/dist/tui.d.ts +0 -25
  67. package/dist/tui.js +0 -287
  68. package/dist/tui.test.d.ts +0 -1
  69. package/dist/tui.test.js +0 -246
  70. package/dist/workflow-catalog.d.ts +0 -25
  71. package/dist/workflow-catalog.js +0 -57
  72. package/dist/workflow-catalog.test.d.ts +0 -1
  73. package/dist/workflow-catalog.test.js +0 -29
  74. package/loops/workflows/agent-direct.md +0 -363
  75. package/loops/workflows/agent-propose.md +0 -342
package/README.md CHANGED
@@ -1,88 +1,88 @@
1
- # Workflows CLI
2
-
3
- Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
4
-
5
- ## Quick start
6
-
7
- The primary entrypoint is the interactive TUI. Run it with no arguments:
8
-
9
- ```bash
10
- npx @plainconceptsplatform/workflows
11
- ```
12
-
13
- The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
14
-
15
- ## Install
16
-
17
- Before installing workflows, install and configure [`PlainConceptsPlatform/opencode-onboard`](https://github.com/PlainConceptsPlatform/opencode-onboard) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
18
-
19
- For non-interactive use (advanced):
20
-
21
- ```bash
22
- npx @plainconceptsplatform/workflows@latest init
23
- npx @plainconceptsplatform/workflows@latest add
24
- npx @plainconceptsplatform/workflows@latest update
25
- ```
26
-
27
- For a project-local development dependency:
28
-
29
- ```bash
30
- pnpm add -D @plainconceptsplatform/workflows
31
- pnpm exec workflows # launch interactive TUI
32
- pnpm exec workflows init
33
- pnpm exec workflows add
34
- ```
35
-
36
- `init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
37
-
38
- `add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
39
-
40
- ```bash
41
- workflows add # mandatory files only, no worker .md files
42
- workflows add implement refine direct # those route workers plus mandatory files
43
- workflows add --template agentics-checks # named template only (no mandatory files)
44
- workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
45
- ```
46
-
47
- Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
48
-
49
- Use `workflows update --force` to force-overwrite managed files that differ from the package source.
50
-
51
- Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, `app-ci-node-monorepo`, and `github-release`. CI templates are stack-specific copies, not a combined template. `github-release` publishes generated release notes when a `v*` tag is pushed. Edit their top-level `env:` values for repository paths, package names, and commands.
52
-
53
- ## List and search
54
-
55
- List all available workflows, routes, and templates with install status:
56
-
57
- ```bash
58
- npx --yes --package @plainconceptsplatform/workflows@latest workflows list
59
- ```
60
-
61
- Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
62
-
63
- Search by name or description:
64
-
65
- ```bash
66
- npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
67
- ```
68
-
69
- ## Manual installation
70
-
71
- The package includes `loops/`, a copyable equivalent of `.github/`:
72
-
73
- - `loops/actions/` maps to `.github/actions/`
74
- - `loops/workflows/` maps to `.github/workflows/`
75
- - `loops/scripts/` maps to `scripts/`
76
-
77
- Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
78
- top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
79
-
80
- ## Compile
81
-
82
- Consumer repositories generate and commit `*.lock.yml` files:
83
-
84
- ```bash
85
- node scripts/compile-agent-workflows.mjs --force
86
- ```
87
-
88
- Do not commit generated locks to this package source repository.
1
+ # Workflows CLI
2
+
3
+ Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
4
+
5
+ ## Quick start
6
+
7
+ The primary entrypoint is the interactive TUI. Run it with no arguments:
8
+
9
+ ```bash
10
+ npx @plainconceptsplatform/workflows
11
+ ```
12
+
13
+ The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
14
+
15
+ ## Install
16
+
17
+ Before installing workflows, install and configure [`PlainConceptsPlatform/agent-harness`](https://github.com/PlainConceptsPlatform/agent-harness) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
18
+
19
+ For non-interactive use (advanced):
20
+
21
+ ```bash
22
+ npx @plainconceptsplatform/workflows@latest init
23
+ npx @plainconceptsplatform/workflows@latest add
24
+ npx @plainconceptsplatform/workflows@latest update
25
+ ```
26
+
27
+ For a project-local development dependency:
28
+
29
+ ```bash
30
+ pnpm add -D @plainconceptsplatform/workflows
31
+ pnpm exec workflows # launch interactive TUI
32
+ pnpm exec workflows init
33
+ pnpm exec workflows add
34
+ ```
35
+
36
+ `init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
37
+
38
+ `add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
39
+
40
+ ```bash
41
+ workflows add # mandatory files only, no worker .md files
42
+ workflows add implement refine direct # those route workers plus mandatory files
43
+ workflows add --template agentics-checks # named template only (no mandatory files)
44
+ workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
45
+ ```
46
+
47
+ Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
48
+
49
+ Use `workflows update --force` to force-overwrite managed files that differ from the package source.
50
+
51
+ Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, `app-ci-node-monorepo`, and `github-release`. CI templates are stack-specific copies, not a combined template. `github-release` publishes generated release notes when a `v*` tag is pushed. Edit their top-level `env:` values for repository paths, package names, and commands.
52
+
53
+ ## List and search
54
+
55
+ List all available workflows, routes, and templates with install status:
56
+
57
+ ```bash
58
+ npx --yes --package @plainconceptsplatform/workflows@latest workflows list
59
+ ```
60
+
61
+ Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
62
+
63
+ Search by name or description:
64
+
65
+ ```bash
66
+ npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
67
+ ```
68
+
69
+ ## Manual installation
70
+
71
+ The package includes `loops/`, a copyable equivalent of `.github/`:
72
+
73
+ - `loops/actions/` maps to `.github/actions/`
74
+ - `loops/workflows/` maps to `.github/workflows/`
75
+ - `loops/scripts/` maps to `scripts/`
76
+
77
+ Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
78
+ top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
79
+
80
+ ## Compile
81
+
82
+ Consumer repositories generate and commit `*.lock.yml` files:
83
+
84
+ ```bash
85
+ node scripts/compile-agent-workflows.mjs --force
86
+ ```
87
+
88
+ Do not commit generated locks to this package source repository.
@@ -1,17 +1,17 @@
1
- // Managed by @plainconceptsplatform/workflows. Source: loops/actions/agent-output.cjs. Update with `workflows update --force`; consumer edits may be overwritten.
2
- const fs = require('node:fs');
3
-
4
- // Every apply-agent-* action reads the same artifact the same way: a missing file means the
5
- // agent produced nothing, which is a normal outcome rather than a failure.
6
- function readAgentItems(outputFile, type) {
7
- if (!outputFile || !fs.existsSync(outputFile)) {
8
- return [];
9
- }
10
-
11
- const parsed = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
12
- const items = Array.isArray(parsed.items) ? parsed.items : [];
13
-
14
- return type ? items.filter((item) => item.type === type) : items;
15
- }
16
-
17
- module.exports = { readAgentItems };
1
+ // Managed by @plainconceptsplatform/workflows. Source: loops/actions/agent-output.cjs. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ const fs = require('node:fs');
3
+
4
+ // Every apply-agent-* action reads the same artifact the same way: a missing file means the
5
+ // agent produced nothing, which is a normal outcome rather than a failure.
6
+ function readAgentItems(outputFile, type) {
7
+ if (!outputFile || !fs.existsSync(outputFile)) {
8
+ return [];
9
+ }
10
+
11
+ const parsed = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
12
+ const items = Array.isArray(parsed.items) ? parsed.items : [];
13
+
14
+ return type ? items.filter((item) => item.type === type) : items;
15
+ }
16
+
17
+ module.exports = { readAgentItems };
@@ -81,6 +81,30 @@ runs:
81
81
  ? 'This report references no issues.'
82
82
  : `All ${closedRefs} referenced issue(s) are closed.`;
83
83
 
84
+ // Do not close if there are open PRs that close this issue.
85
+ // The audit report may have spawned implement workers whose PRs
86
+ // are still under review — closing the report prematurely is misleading.
87
+ const openPRs = await github.paginate(github.rest.pulls.list, {
88
+ ...context.repo,
89
+ state: 'open',
90
+ per_page: 100,
91
+ });
92
+
93
+ const closesThisReport = openPRs.some((pr) => {
94
+ const body = pr.body ?? '';
95
+ const patterns = [
96
+ new RegExp(`(?:^|[^A-Za-z0-9_/])#(?:closes?|fixes?|resolves?|closed?)\\s+#${report.number}\\b`, 'i'),
97
+ new RegExp(`(?:^|[^A-Za-z0-9_/])#${report.number}\\b`),
98
+ ];
99
+ return patterns.some((p) => p.test(body));
100
+ });
101
+
102
+ if (closesThisReport) {
103
+ core.info(`#${report.number}: kept open, has open PR(s) that close it.`);
104
+ keptOpen += 1;
105
+ continue;
106
+ }
107
+
84
108
  await github.rest.issues.createComment({
85
109
  ...context.repo,
86
110
  issue_number: report.number,
@@ -27,9 +27,9 @@ outputs:
27
27
  refine-mode:
28
28
  description: first or rerefine.
29
29
  value: ${{ steps.classify.outputs.refine-mode }}
30
- direct-mode:
31
- description: first or continue.
32
- value: ${{ steps.classify.outputs.direct-mode }}
30
+ triage-mode:
31
+ description: first or retriage.
32
+ value: ${{ steps.classify.outputs.triage-mode }}
33
33
  trigger-kind:
34
34
  description: scheduled or manual.
35
35
  value: ${{ steps.classify.outputs.trigger-kind }}
@@ -44,6 +44,7 @@ runs:
44
44
  ACTION: ${{ github.event.action }}
45
45
  LABEL: ${{ github.event.label.name }}
46
46
  ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
47
+ ISSUE_STATE: ${{ github.event.issue.state }}
47
48
  EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
48
49
  EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
49
50
  COMMENT_ON_PR: ${{ github.event.issue.pull_request != '' }}
@@ -11,11 +11,7 @@ set -euo pipefail
11
11
  readonly AUDIT_CRON="17 1 * * 1"
12
12
  readonly AUDIT_CLOSE_CRON="43 3 * * *"
13
13
  readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
14
- readonly RECONCILE_BOT_PR_RUNS_CRON="*/30 * * * *"
15
- # Daily, but it proposes far less often than daily: the worker holds one open
16
- # proposal at a time and skips while that slot is filled. The cron is a heartbeat,
17
- # the queue is the pacing.
18
- readonly PROPOSE_CRON="29 7 * * *"
14
+ readonly RECONCILE_BOT_PR_RUNS_CRON="17 * * * *"
19
15
 
20
16
  has_label() {
21
17
  jq -e --arg name "$1" 'index($name)' >/dev/null 2>&1 <<<"${ISSUE_LABELS:-[]}"
@@ -28,11 +24,28 @@ is_issue_number() {
28
24
  classify_route() {
29
25
  local route="none" error=""
30
26
  local issue_number="" pr_number="" ci_conclusion="" ci_run_id=""
31
- local refine_mode="" direct_mode="" trigger_kind=""
27
+ local refine_mode="" triage_mode="" trigger_kind=""
32
28
 
33
29
  case "${EVENT:-}" in
34
30
  issues)
35
- if [ "${ACTION:-}" = "labeled" ]; then
31
+ # A closed issue is finished work, but label edits on one still arrive as events.
32
+ if [ "${ISSUE_STATE:-}" = "closed" ]; then
33
+ error="issue is closed"
34
+ elif [ "${ACTION:-}" = "opened" ]; then
35
+ # Issue opened with a work label → skip triage.
36
+ # The label event will trigger authorize-bot-work → bot-working → the
37
+ # correct worker. Triage would only interfere.
38
+ if has_label refine || has_label implement; then
39
+ error="issue opened with a work label; triage skipped"
40
+ else
41
+ # Issue opened by an outside collaborator → triage. The authorize job
42
+ # gates the caller on is_outside_collaborator; this routes unconditionally
43
+ # so write+ openers classify to triage but the caller job skips them.
44
+ route="triage"
45
+ triage_mode="first"
46
+ issue_number="${EVENT_ISSUE_NUMBER:-}"
47
+ fi
48
+ elif [ "${ACTION:-}" = "labeled" ]; then
36
49
  case "${LABEL:-}" in
37
50
  bot-working)
38
51
  # Bot adds bot-working → route based on which work label is present
@@ -46,15 +59,25 @@ classify_route() {
46
59
  route="refine"
47
60
  refine_mode="first"
48
61
  issue_number="${EVENT_ISSUE_NUMBER:-}"
49
- elif has_label direct; then
50
- route="direct"
51
- direct_mode="first"
52
- issue_number="${EVENT_ISSUE_NUMBER:-}"
53
62
  else
54
- error="bot-working added but no work label (implement/refine/direct) found"
63
+ error="bot-working added but no work label found"
64
+ fi
65
+ ;;
66
+ triage)
67
+ # A maintainer can explicitly re-run triage by adding this label. The
68
+ # triage worker adds it after claiming the issue, so bot label events
69
+ # and an existing claim must not start a second worker.
70
+ if [ "${ACTOR:-}" != "" ] && echo "${ACTOR:-}" | grep -q '\[bot\]$'; then
71
+ error="bot-added triage label does not re-trigger triage"
72
+ elif has_label bot-working; then
73
+ error="issue already has bot-working label; triage already in progress"
74
+ else
75
+ route="triage"
76
+ issue_number="${EVENT_ISSUE_NUMBER:-}"
77
+ triage_mode="first"
55
78
  fi
56
79
  ;;
57
- refine | implement | direct)
80
+ refine | implement)
58
81
  # If the actor is a bot (e.g. refine→implement transition), route directly.
59
82
  # If the actor is a human, authorize-bot-work.yml will add bot-working which triggers the workflow.
60
83
  if [ "${ACTOR:-}" != "" ] && echo "${ACTOR:-}" | grep -q '\[bot\]$'; then
@@ -62,13 +85,11 @@ classify_route() {
62
85
  issue_number="${EVENT_ISSUE_NUMBER:-}"
63
86
  if [ "${LABEL:-}" = "refine" ]; then
64
87
  refine_mode="first"
65
- elif [ "${LABEL:-}" = "direct" ]; then
66
- direct_mode="first"
67
88
  fi
68
89
  elif has_label bot-working; then
69
90
  # Already has bot-working - the workflow is already running or queued.
70
91
  # Don't re-trigger.
71
- error="issue already has bot-working label; implement/refine/direct already in progress"
92
+ error="issue already has bot-working label; implement/refine already in progress"
72
93
  else
73
94
  error="waiting for bot to add bot-working label"
74
95
  fi
@@ -81,16 +102,20 @@ classify_route() {
81
102
  if [ "${COMMENT_ON_PR:-false}" = "true" ]; then
82
103
  route="apply-review"
83
104
  pr_number="${EVENT_ISSUE_NUMBER:-}"
105
+ elif [ "${ISSUE_STATE:-}" = "closed" ]; then
106
+ # A closing comment on a refine-labelled issue used to start a full re-refine, which
107
+ # held a runner for 35 minutes and filed split children under an already-shut parent.
108
+ error="issue is closed"
84
109
  elif [ "${COMMENT_SENDER_TYPE:-}" = "Bot" ]; then
85
110
  error="comment authored by a bot"
111
+ elif has_label triage; then
112
+ route="triage"
113
+ triage_mode="retriage"
114
+ issue_number="${EVENT_ISSUE_NUMBER:-}"
86
115
  elif has_label implement; then
87
116
  error="issue has implement label; comments do not re-trigger implement"
88
- elif has_label direct; then
89
- route="direct"
90
- direct_mode="continue"
91
- issue_number="${EVENT_ISSUE_NUMBER:-}"
92
117
  elif ! has_label refine; then
93
- error="issue does not carry the refine or direct label"
118
+ error="issue does not carry the refine label"
94
119
  else
95
120
  route="refine"
96
121
  refine_mode="rerefine"
@@ -118,10 +143,15 @@ classify_route() {
118
143
  ;;
119
144
 
120
145
  workflow_run)
121
- if is_issue_number "${RUN_PR_NUMBER:-}"; then
146
+ # Only a FAILED CI run on an attached pull request auto-dispatches the gate. A green
147
+ # run reaches merge through app-ci's dispatch-merge-gate and the reconcile belt, so
148
+ # routing success here would double-fire the gate for every passing pull request.
149
+ if [ "${RUN_CONCLUSION:-}" != "failure" ]; then
150
+ error="CI concluded '${RUN_CONCLUSION:-}'; the gate auto-triggers only on failure"
151
+ elif is_issue_number "${RUN_PR_NUMBER:-}"; then
122
152
  route="merge-gate"
123
153
  pr_number="${RUN_PR_NUMBER}"
124
- ci_conclusion="${RUN_CONCLUSION:-}"
154
+ ci_conclusion="failure"
125
155
  ci_run_id="${RUN_ID:-}"
126
156
  else
127
157
  error="CI run has no attached pull request"
@@ -142,19 +172,26 @@ classify_route() {
142
172
  workflow_dispatch)
143
173
  trigger_kind="manual"
144
174
  case "${OPERATION:-}" in
145
- refine | implement | direct)
175
+ refine | implement)
146
176
  if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
147
177
  route="${OPERATION}"
148
178
  issue_number="${INPUT_ISSUE_NUMBER}"
149
179
  if [ "$OPERATION" = "refine" ]; then
150
180
  refine_mode="${INPUT_MODE:-first}"
151
- elif [ "$OPERATION" = "direct" ]; then
152
- direct_mode="${INPUT_MODE:-first}"
153
181
  fi
154
182
  else
155
183
  error="operation '${OPERATION}' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
156
184
  fi
157
185
  ;;
186
+ triage)
187
+ if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
188
+ route="triage"
189
+ issue_number="${INPUT_ISSUE_NUMBER}"
190
+ triage_mode="${INPUT_MODE:-first}"
191
+ else
192
+ error="operation 'triage' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
193
+ fi
194
+ ;;
158
195
  apply-review)
159
196
  if is_issue_number "${INPUT_PR_NUMBER:-}"; then
160
197
  route="apply-review"
@@ -173,13 +210,16 @@ classify_route() {
173
210
  error="operation 'merge-gate' needs a positive pr-number, got '${INPUT_PR_NUMBER:-}'"
174
211
  fi
175
212
  ;;
176
- audit | propose)
213
+ audit)
177
214
  route="${OPERATION}"
178
215
  trigger_kind="${INPUT_TRIGGER_KIND:-manual}"
179
216
  ;;
180
- audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
217
+ audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
181
218
  route="${OPERATION}"
182
219
  ;;
220
+ release)
221
+ route="release"
222
+ ;;
183
223
  *)
184
224
  error="unknown operation '${OPERATION:-}'"
185
225
  ;;
@@ -198,7 +238,7 @@ pr-number=${pr_number}
198
238
  ci-conclusion=${ci_conclusion}
199
239
  ci-run-id=${ci_run_id}
200
240
  refine-mode=${refine_mode}
201
- direct-mode=${direct_mode}
241
+ triage-mode=${triage_mode}
202
242
  trigger-kind=${trigger_kind}
203
243
  error=${error}
204
244
  EOF
@@ -34,6 +34,8 @@ runs:
34
34
  number: issue.data.number,
35
35
  title: issue.data.title,
36
36
  body: issue.data.body,
37
+ author: issue.data.user.login,
38
+ authorAssociation: issue.data.author_association,
37
39
  labels: issue.data.labels.map(({ name }) => name),
38
40
  comments: comments.map(({ user, author_association, body }) => ({
39
41
  author: user.login,
@@ -0,0 +1,113 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-changelog/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: Update changelog
3
+ description: Prepend a changelog entry for a landed issue and push it to the default branch.
4
+
5
+ inputs:
6
+ token:
7
+ description: Token with contents write on the repository.
8
+ required: true
9
+ issue-number:
10
+ description: Issue the landed commit implements.
11
+ required: true
12
+ commit-sha:
13
+ description: Commit the work landed as. Its subject becomes the summary.
14
+ required: true
15
+ changelog-path:
16
+ description: Path to changelog.json.
17
+ required: false
18
+ default: apps/web/src/shared/data/changelog.json
19
+ max-entries:
20
+ description: Newest entries to keep.
21
+ required: false
22
+ default: "20"
23
+
24
+ runs:
25
+ using: composite
26
+ steps:
27
+ - name: Prepend the entry and push
28
+ shell: bash
29
+ env:
30
+ GH_TOKEN: ${{ inputs.token }}
31
+ REPO: ${{ github.repository }}
32
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
33
+ COMMIT_SHA: ${{ inputs.commit-sha }}
34
+ CHANGELOG_PATH: ${{ inputs.changelog-path }}
35
+ MAX_ENTRIES: ${{ inputs.max-entries }}
36
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
37
+ run: |
38
+ set -euo pipefail
39
+
40
+ # This used to be part of the implement prompt. It is deterministic work, and leaving it
41
+ # to the agent made every implement touch the same file: two runs whose branches were cut
42
+ # before the other merged conflicted here, produced correct code, and still failed to open
43
+ # a pull request. Doing it after the merge means exactly one writer, always against a
44
+ # current default branch.
45
+
46
+ host="${GITHUB_SERVER_URL#https://}"
47
+ git config user.name "github-actions[bot]"
48
+ git config user.email "github-actions[bot]@users.noreply.github.com"
49
+ git remote set-url origin "https://x-access-token:${GH_TOKEN}@${host}/${GITHUB_REPOSITORY}.git"
50
+
51
+ git fetch --quiet origin "$DEFAULT_BRANCH"
52
+ git checkout --quiet -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
53
+
54
+ if [ ! -f "$CHANGELOG_PATH" ]; then
55
+ echo "::notice::$CHANGELOG_PATH does not exist; nothing to update."
56
+ exit 0
57
+ fi
58
+
59
+ # Idempotent: a re-run, a retry, or a second call for the same commit must not add a
60
+ # duplicate row.
61
+ short_sha="${COMMIT_SHA:0:7}"
62
+ if jq -e --arg c "$short_sha" '.changes[]? | select(.commit == $c)' "$CHANGELOG_PATH" >/dev/null 2>&1; then
63
+ echo "::notice::$short_sha is already in the changelog."
64
+ exit 0
65
+ fi
66
+
67
+ title=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title --jq '.title')
68
+
69
+ # The commit subject carries what actually changed. Strip the conventional-commit prefix
70
+ # and the trailing issue reference so the summary reads as prose rather than as a commit.
71
+ subject=$(git log -1 --format=%s "$COMMIT_SHA")
72
+ summary=$(printf '%s' "$subject" \
73
+ | sed -E 's/^[a-z]+(\([^)]*\))?!?: *//' \
74
+ | sed -E 's/ *\(#[0-9]+\) *$//')
75
+ [ -n "$summary" ] || summary="$title"
76
+ # Capitalise, because a commit subject conventionally starts lowercase and this is read
77
+ # by people rather than by tooling.
78
+ summary="$(printf '%s' "${summary:0:1}" | tr '[:lower:]' '[:upper:]')${summary:1}"
79
+
80
+ timestamp=$(git log -1 --format=%cI "$COMMIT_SHA")
81
+
82
+ tmp=$(mktemp)
83
+ jq --argjson n "$MAX_ENTRIES" \
84
+ --arg ts "$timestamp" \
85
+ --argjson issue "$ISSUE_NUMBER" \
86
+ --arg title "$title" \
87
+ --arg summary "$summary" \
88
+ --arg commit "$short_sha" \
89
+ '.changes = ([{timestamp: $ts, issue: $issue, title: $title, summary: $summary, commit: $commit}] + (.changes // []))[:$n]' \
90
+ "$CHANGELOG_PATH" > "$tmp"
91
+ mv "$tmp" "$CHANGELOG_PATH"
92
+
93
+ if git diff --quiet -- "$CHANGELOG_PATH"; then
94
+ echo "::notice::changelog unchanged."
95
+ exit 0
96
+ fi
97
+
98
+ git add -- "$CHANGELOG_PATH"
99
+ git commit --quiet -m "docs(changelog): record #${ISSUE_NUMBER}"
100
+
101
+ # The default branch may have moved while this ran. Rebase and retry rather than fail:
102
+ # only this action writes the file, so a rebase here cannot produce a content conflict.
103
+ for attempt in 1 2 3; do
104
+ if git push --quiet origin "$DEFAULT_BRANCH"; then
105
+ echo "Recorded #${ISSUE_NUMBER} at ${short_sha} in the changelog."
106
+ exit 0
107
+ fi
108
+ echo "::notice::push rejected, rebasing (attempt ${attempt})"
109
+ git fetch --quiet origin "$DEFAULT_BRANCH"
110
+ git rebase --quiet "origin/$DEFAULT_BRANCH" || { git rebase --abort || true; break; }
111
+ done
112
+
113
+ echo "::warning::Could not push the changelog entry for #${ISSUE_NUMBER}."
@@ -0,0 +1,40 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Validate merge-gate output
3
+ description: Verify agent_output.json contains a usable merge-gate verdict before any state changes.
4
+ inputs:
5
+ output-file:
6
+ description: Path to agent_output.json.
7
+ required: true
8
+ issue-number:
9
+ description: Issue number that the merge-gate outcome must target.
10
+ required: true
11
+ ci-conclusion:
12
+ description: CI conclusion supplied to the merge gate.
13
+ required: true
14
+ outputs:
15
+ valid:
16
+ description: Whether the output contains a usable merge-gate comment with a verdict.
17
+ value: ${{ steps.validate.outputs.valid }}
18
+ outcome:
19
+ description: "Deterministic outcome: merge, review, remediated, or invalid."
20
+ value: ${{ steps.validate.outputs.outcome }}
21
+ runs:
22
+ using: composite
23
+ steps:
24
+ - name: Validate merge-gate outcome
25
+ id: validate
26
+ shell: bash
27
+ env:
28
+ ISSUE_NUMBER: ${{ inputs.issue-number }}
29
+ CI_CONCLUSION: ${{ inputs.ci-conclusion }}
30
+ OUTPUT_FILE: ${{ inputs.output-file }}
31
+ run: |
32
+ set -euo pipefail
33
+
34
+ outcome="$(bash "${{ github.action_path }}/validate-merge-gate-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER" "$CI_CONCLUSION")"
35
+ echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
36
+ echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
37
+
38
+ if [ "$outcome" = 'invalid' ]; then
39
+ echo "::warning::Agent output has no usable merge-gate outcome. It will not be applied."
40
+ fi
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh. Update with `workflows update --force`; consumer edits may be overwritten.
3
+ # Print the deterministic merge-gate outcome: merge, review, remediated, or invalid.
4
+
5
+ set -euo pipefail
6
+
7
+ output_file="$1"
8
+ issue_number="$2"
9
+ ci_conclusion="$3"
10
+
11
+ if [ ! -f "$output_file" ] || ! jq -e '.items | arrays' "$output_file" >/dev/null 2>&1; then
12
+ echo invalid
13
+ exit 0
14
+ fi
15
+
16
+ # The agent emits exactly one comment on the source issue. Its verdict tells the
17
+ # workflow which App-token state transition to perform.
18
+ jq -r --arg issue "$issue_number" --arg conclusion "$ci_conclusion" '
19
+ .items as $items
20
+ | ($items
21
+ | [.[] | select(.type == "add_comment" and (.item_number | tostring) == $issue and (.body | type == "string"))]
22
+ | map(.body |
23
+ if test("\\*\\*Verdict:\\*\\*\\s*(merge|review|remediated)"; "i") then
24
+ capture("\\*\\*Verdict:\\*\\*\\s*(?<v>merge|review|remediated)"; "i").v | ascii_downcase
25
+ else empty end
26
+ )
27
+ | .[0] // "invalid") as $outcome
28
+ | ([$items[] | select(.type == "push_to_pull_request_branch")] | length) as $pushes
29
+ | if $outcome == "merge" and $conclusion == "success" and $pushes == 0 then "merge"
30
+ elif $outcome == "remediated" and $conclusion == "failure" and $pushes == 1 then "remediated"
31
+ elif $outcome == "review" and $pushes == 0 then "review"
32
+ else "invalid"
33
+ end
34
+ ' "$output_file"