@plainconceptsplatform/workflows 0.4.34 → 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 (73) 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 +1 -3
  5. package/loops/actions/classify-route/classify-route.sh +40 -35
  6. package/loops/actions/update-changelog/action.yml +113 -0
  7. package/loops/actions/validate-merge-gate-output/action.yml +40 -40
  8. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  9. package/loops/actions/validate-review-output/action.yml +35 -35
  10. package/loops/actions/validate-triage-output/action.yml +36 -36
  11. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +91 -36
  12. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  13. package/loops/templates/agentics/agentics-checks.yml +86 -86
  14. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  15. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  16. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  17. package/loops/templates/issues/bug_report.yml +109 -109
  18. package/loops/templates/issues/feature_request.yml +75 -75
  19. package/loops/templates/opencode/opencode.ci.json +49 -47
  20. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  21. package/loops/templates/release/github-release.yml +30 -30
  22. package/loops/workflows/agent-apply-review.md +465 -443
  23. package/loops/workflows/agent-audit.md +213 -197
  24. package/loops/workflows/agent-implement.md +538 -414
  25. package/loops/workflows/agent-merge-gate.md +730 -584
  26. package/loops/workflows/agent-refine.md +609 -418
  27. package/loops/workflows/agent-release.md +258 -0
  28. package/loops/workflows/agent-triage.md +447 -439
  29. package/loops/workflows/authorize-bot-work.yml +85 -82
  30. package/loops/workflows/shared/opencode-ci.md +206 -197
  31. package/loops/workflows/shared/platform-defaults.md +19 -16
  32. package/loops/workflows/work-router.yml +862 -632
  33. package/package.json +7 -8
  34. package/dist/action-validation.test.d.ts +0 -1
  35. package/dist/action-validation.test.js +0 -84
  36. package/dist/catalog-installation.d.ts +0 -30
  37. package/dist/catalog-installation.js +0 -379
  38. package/dist/catalog-installation.test.d.ts +0 -1
  39. package/dist/catalog-installation.test.js +0 -448
  40. package/dist/catalog-listing.d.ts +0 -13
  41. package/dist/catalog-listing.js +0 -70
  42. package/dist/catalog-listing.test.d.ts +0 -1
  43. package/dist/catalog-listing.test.js +0 -150
  44. package/dist/index.d.ts +0 -2
  45. package/dist/index.js +0 -218
  46. package/dist/index.test.d.ts +0 -1
  47. package/dist/index.test.js +0 -273
  48. package/dist/repository-inspection.d.ts +0 -23
  49. package/dist/repository-inspection.js +0 -113
  50. package/dist/repository-inspection.test.d.ts +0 -1
  51. package/dist/repository-inspection.test.js +0 -77
  52. package/dist/repository-state.d.ts +0 -18
  53. package/dist/repository-state.js +0 -77
  54. package/dist/repository-state.test.d.ts +0 -1
  55. package/dist/repository-state.test.js +0 -96
  56. package/dist/route-processing.d.ts +0 -6
  57. package/dist/route-processing.js +0 -150
  58. package/dist/route-processing.test.d.ts +0 -1
  59. package/dist/route-processing.test.js +0 -350
  60. package/dist/stack-defaults.d.ts +0 -11
  61. package/dist/stack-defaults.js +0 -104
  62. package/dist/stack-defaults.test.d.ts +0 -1
  63. package/dist/stack-defaults.test.js +0 -266
  64. package/dist/tui.d.ts +0 -25
  65. package/dist/tui.js +0 -287
  66. package/dist/tui.test.d.ts +0 -1
  67. package/dist/tui.test.js +0 -246
  68. package/dist/workflow-catalog.d.ts +0 -25
  69. package/dist/workflow-catalog.js +0 -59
  70. package/dist/workflow-catalog.test.d.ts +0 -1
  71. package/dist/workflow-catalog.test.js +0 -29
  72. package/loops/workflows/agent-direct.md +0 -375
  73. 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/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
+ # 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,6 @@ 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 }}
33
30
  triage-mode:
34
31
  description: first or retriage.
35
32
  value: ${{ steps.classify.outputs.triage-mode }}
@@ -47,6 +44,7 @@ runs:
47
44
  ACTION: ${{ github.event.action }}
48
45
  LABEL: ${{ github.event.label.name }}
49
46
  ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
47
+ ISSUE_STATE: ${{ github.event.issue.state }}
50
48
  EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
51
49
  EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
52
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="17 */2 * * *"
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,17 +24,27 @@ 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="" triage_mode="" trigger_kind=""
27
+ local refine_mode="" triage_mode="" trigger_kind=""
32
28
 
33
29
  case "${EVENT:-}" in
34
30
  issues)
35
- if [ "${ACTION:-}" = "opened" ]; then
36
- # Issue opened by an outside collaborator → triage. The authorize job
37
- # gates the caller on is_outside_collaborator; this routes unconditionally
38
- # so write+ openers classify to triage but the caller job skips them.
39
- route="triage"
40
- triage_mode="first"
41
- issue_number="${EVENT_ISSUE_NUMBER:-}"
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
42
48
  elif [ "${ACTION:-}" = "labeled" ]; then
43
49
  case "${LABEL:-}" in
44
50
  bot-working)
@@ -53,12 +59,8 @@ classify_route() {
53
59
  route="refine"
54
60
  refine_mode="first"
55
61
  issue_number="${EVENT_ISSUE_NUMBER:-}"
56
- elif has_label direct; then
57
- route="direct"
58
- direct_mode="first"
59
- issue_number="${EVENT_ISSUE_NUMBER:-}"
60
62
  else
61
- error="bot-working added but no work label (implement/refine/direct) found"
63
+ error="bot-working added but no work label found"
62
64
  fi
63
65
  ;;
64
66
  triage)
@@ -75,7 +77,7 @@ classify_route() {
75
77
  triage_mode="first"
76
78
  fi
77
79
  ;;
78
- refine | implement | direct)
80
+ refine | implement)
79
81
  # If the actor is a bot (e.g. refine→implement transition), route directly.
80
82
  # If the actor is a human, authorize-bot-work.yml will add bot-working which triggers the workflow.
81
83
  if [ "${ACTOR:-}" != "" ] && echo "${ACTOR:-}" | grep -q '\[bot\]$'; then
@@ -83,13 +85,11 @@ classify_route() {
83
85
  issue_number="${EVENT_ISSUE_NUMBER:-}"
84
86
  if [ "${LABEL:-}" = "refine" ]; then
85
87
  refine_mode="first"
86
- elif [ "${LABEL:-}" = "direct" ]; then
87
- direct_mode="first"
88
88
  fi
89
89
  elif has_label bot-working; then
90
90
  # Already has bot-working - the workflow is already running or queued.
91
91
  # Don't re-trigger.
92
- 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"
93
93
  else
94
94
  error="waiting for bot to add bot-working label"
95
95
  fi
@@ -102,6 +102,10 @@ classify_route() {
102
102
  if [ "${COMMENT_ON_PR:-false}" = "true" ]; then
103
103
  route="apply-review"
104
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"
105
109
  elif [ "${COMMENT_SENDER_TYPE:-}" = "Bot" ]; then
106
110
  error="comment authored by a bot"
107
111
  elif has_label triage; then
@@ -110,12 +114,8 @@ classify_route() {
110
114
  issue_number="${EVENT_ISSUE_NUMBER:-}"
111
115
  elif has_label implement; then
112
116
  error="issue has implement label; comments do not re-trigger implement"
113
- elif has_label direct; then
114
- route="direct"
115
- direct_mode="continue"
116
- issue_number="${EVENT_ISSUE_NUMBER:-}"
117
117
  elif ! has_label refine; then
118
- error="issue does not carry the refine or direct label"
118
+ error="issue does not carry the refine label"
119
119
  else
120
120
  route="refine"
121
121
  refine_mode="rerefine"
@@ -143,10 +143,15 @@ classify_route() {
143
143
  ;;
144
144
 
145
145
  workflow_run)
146
- 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
147
152
  route="merge-gate"
148
153
  pr_number="${RUN_PR_NUMBER}"
149
- ci_conclusion="${RUN_CONCLUSION:-}"
154
+ ci_conclusion="failure"
150
155
  ci_run_id="${RUN_ID:-}"
151
156
  else
152
157
  error="CI run has no attached pull request"
@@ -167,14 +172,12 @@ classify_route() {
167
172
  workflow_dispatch)
168
173
  trigger_kind="manual"
169
174
  case "${OPERATION:-}" in
170
- refine | implement | direct)
175
+ refine | implement)
171
176
  if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
172
177
  route="${OPERATION}"
173
178
  issue_number="${INPUT_ISSUE_NUMBER}"
174
179
  if [ "$OPERATION" = "refine" ]; then
175
180
  refine_mode="${INPUT_MODE:-first}"
176
- elif [ "$OPERATION" = "direct" ]; then
177
- direct_mode="${INPUT_MODE:-first}"
178
181
  fi
179
182
  else
180
183
  error="operation '${OPERATION}' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
@@ -207,13 +210,16 @@ classify_route() {
207
210
  error="operation 'merge-gate' needs a positive pr-number, got '${INPUT_PR_NUMBER:-}'"
208
211
  fi
209
212
  ;;
210
- audit | propose)
213
+ audit)
211
214
  route="${OPERATION}"
212
215
  trigger_kind="${INPUT_TRIGGER_KIND:-manual}"
213
216
  ;;
214
- audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
217
+ audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
215
218
  route="${OPERATION}"
216
219
  ;;
220
+ release)
221
+ route="release"
222
+ ;;
217
223
  *)
218
224
  error="unknown operation '${OPERATION:-}'"
219
225
  ;;
@@ -232,7 +238,6 @@ pr-number=${pr_number}
232
238
  ci-conclusion=${ci_conclusion}
233
239
  ci-run-id=${ci_run_id}
234
240
  refine-mode=${refine_mode}
235
- direct-mode=${direct_mode}
236
241
  triage-mode=${triage_mode}
237
242
  trigger-kind=${trigger_kind}
238
243
  error=${error}
@@ -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}."
@@ -1,40 +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
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