@plainconceptsplatform/workflows 0.4.34 → 0.6.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.
- package/README.md +88 -88
- package/dist/action-validation.test.js +5 -2
- package/dist/catalog-installation.js +50 -7
- package/dist/catalog-installation.test.js +59 -22
- package/dist/catalog-listing.test.js +4 -4
- package/dist/index.js +48 -48
- package/dist/index.test.js +9 -9
- package/dist/repository-inspection.test.js +2 -2
- package/dist/route-processing.js +24 -25
- package/dist/route-processing.test.js +166 -233
- package/dist/stack-defaults.js +14 -14
- package/dist/stack-defaults.test.js +79 -79
- package/dist/tui.test.js +10 -7
- package/dist/workflow-catalog.d.ts +1 -1
- package/dist/workflow-catalog.js +1 -5
- package/loops/actions/add-issue-labels/action.yml +2 -2
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +8 -3
- package/loops/actions/classify-route/classify-route.sh +58 -38
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -40
- package/loops/actions/validate-refine-output/validate-refine-output.sh +34 -5
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +375 -36
- package/loops/scripts/compile-agent-workflows.mjs +290 -6
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +125 -8
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -178
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +49 -47
- package/loops/templates/opencode/opencode.ci.json.md +49 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +469 -443
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +640 -414
- package/loops/workflows/agent-merge-gate.md +844 -584
- package/loops/workflows/agent-refine.md +633 -418
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -439
- package/loops/workflows/authorize-bot-work.yml +85 -82
- package/loops/workflows/shared/opencode-ci.md +206 -197
- package/loops/workflows/shared/platform-defaults.md +19 -16
- package/loops/workflows/work-router.yml +1038 -632
- package/package.json +7 -8
- package/dist/repository-state.d.ts +0 -18
- package/dist/repository-state.js +0 -77
- package/dist/repository-state.test.d.ts +0 -1
- package/dist/repository-state.test.js +0 -96
- package/loops/workflows/agent-direct.md +0 -375
- package/loops/workflows/agent-propose.md +0 -342
|
@@ -65,10 +65,10 @@ describe("generateStackDefaults", () => {
|
|
|
65
65
|
});
|
|
66
66
|
describe("injectStackEnv", () => {
|
|
67
67
|
it("injects VERIFY_COMMANDS into a worker that has an env block", () => {
|
|
68
|
-
const content = `---
|
|
69
|
-
env:
|
|
70
|
-
REPO_RULES: "some rules"
|
|
71
|
-
description: test
|
|
68
|
+
const content = `---
|
|
69
|
+
env:
|
|
70
|
+
REPO_RULES: "some rules"
|
|
71
|
+
description: test
|
|
72
72
|
---`;
|
|
73
73
|
const defaults = generateStackDefaults(makeInspection({
|
|
74
74
|
solutionFiles: ["app.slnx"],
|
|
@@ -77,9 +77,9 @@ description: test
|
|
|
77
77
|
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
78
|
});
|
|
79
79
|
it("injects pnpm verification when no .slnx is present", () => {
|
|
80
|
-
const content = `---
|
|
81
|
-
env:
|
|
82
|
-
REPO_RULES: "some rules"
|
|
80
|
+
const content = `---
|
|
81
|
+
env:
|
|
82
|
+
REPO_RULES: "some rules"
|
|
83
83
|
---`;
|
|
84
84
|
const defaults = generateStackDefaults(makeInspection({
|
|
85
85
|
pnpmLockfile: true,
|
|
@@ -88,47 +88,47 @@ env:
|
|
|
88
88
|
expect(result).toContain('VERIFY_COMMANDS: "pnpm verify"');
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
|
-
const OPENCODE_CI_MD = `---
|
|
92
|
-
env:
|
|
93
|
-
AGENTMEMORY_VERSION: "0.9.28"
|
|
94
|
-
CODEGRAPH_VERSION: "1.5.0"
|
|
95
|
-
description: Shared CI setup.
|
|
96
|
-
|
|
97
|
-
pre-agent-steps:
|
|
98
|
-
- name: Install RTK
|
|
99
|
-
run: |
|
|
100
|
-
rtk --version
|
|
101
|
-
rtk init -g --opencode --auto-patch
|
|
102
|
-
- name: Install opencode plugin dependencies
|
|
103
|
-
run: |
|
|
104
|
-
set -euo pipefail
|
|
105
|
-
if [ ! -f .opencode/package.json ]; then
|
|
106
|
-
echo "No .opencode/package.json, nothing to install"
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
- name: Install workspace dependencies
|
|
110
|
-
run: pnpm install --frozen-lockfile
|
|
111
|
-
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
112
|
-
run: |
|
|
113
|
-
jq -e . "$FRAGMENT" > /dev/null
|
|
91
|
+
const OPENCODE_CI_MD = `---
|
|
92
|
+
env:
|
|
93
|
+
AGENTMEMORY_VERSION: "0.9.28"
|
|
94
|
+
CODEGRAPH_VERSION: "1.5.0"
|
|
95
|
+
description: Shared CI setup.
|
|
96
|
+
|
|
97
|
+
pre-agent-steps:
|
|
98
|
+
- name: Install RTK
|
|
99
|
+
run: |
|
|
100
|
+
rtk --version
|
|
101
|
+
rtk init -g --opencode --auto-patch
|
|
102
|
+
- name: Install opencode plugin dependencies
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
if [ ! -f .opencode/package.json ]; then
|
|
106
|
+
echo "No .opencode/package.json, nothing to install"
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
- name: Install workspace dependencies
|
|
110
|
+
run: pnpm install --frozen-lockfile
|
|
111
|
+
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
112
|
+
run: |
|
|
113
|
+
jq -e . "$FRAGMENT" > /dev/null
|
|
114
114
|
---`;
|
|
115
115
|
describe("generateOpencodeCi", () => {
|
|
116
116
|
it("adds NuGet cache and restores detected solution when .slnx is found", () => {
|
|
117
117
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
-
solutionFiles: ["apps/api/
|
|
118
|
+
solutionFiles: ["apps/api/Contoso.slnx"],
|
|
119
119
|
}));
|
|
120
120
|
expect(result).toContain("Cache NuGet packages");
|
|
121
121
|
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
-
expect(result).toContain("dotnet restore apps/api/
|
|
122
|
+
expect(result).toContain("dotnet restore apps/api/Contoso.slnx");
|
|
123
123
|
expect(result).toContain("\n - name: Restore .NET dependencies\n");
|
|
124
124
|
expect(result).not.toContain("\n - name: Restore .NET dependencies\n");
|
|
125
125
|
});
|
|
126
126
|
it("uses POSIX separators for .NET restore commands", () => {
|
|
127
127
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
128
|
-
solutionFiles: ["apps\\api\\
|
|
128
|
+
solutionFiles: ["apps\\api\\Contoso.slnx"],
|
|
129
129
|
}));
|
|
130
|
-
expect(result).toContain("dotnet restore apps/api/
|
|
131
|
-
expect(result).not.toContain("dotnet restore apps\\api\\
|
|
130
|
+
expect(result).toContain("dotnet restore apps/api/Contoso.slnx");
|
|
131
|
+
expect(result).not.toContain("dotnet restore apps\\api\\Contoso.slnx");
|
|
132
132
|
});
|
|
133
133
|
it("adds the pinned OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
134
134
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
@@ -160,49 +160,49 @@ describe("generateOpencodeCi", () => {
|
|
|
160
160
|
expect(result).toContain("Merge the CI-only OpenCode provider");
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
|
-
const OPENCODE_CI_JSON = `{
|
|
164
|
-
"$schema": "https://opencode.ai/config.json",
|
|
165
|
-
"model": "plainconcepts/glm-5-
|
|
166
|
-
"plugin": [],
|
|
167
|
-
"default_agent": "ci-workflow-agent",
|
|
168
|
-
"agent": {
|
|
169
|
-
"ci-workflow-agent": {
|
|
170
|
-
"description": "Executes GitHub Agentic Workflow tasks in CI.",
|
|
171
|
-
"mode": "primary",
|
|
172
|
-
"prompt": "You execute the GitHub Agentic Workflow task in the user prompt."
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
"permission": {
|
|
176
|
-
"read": "allow"
|
|
177
|
-
},
|
|
178
|
-
"lsp": {
|
|
179
|
-
"csharp": {
|
|
180
|
-
"disabled": true
|
|
181
|
-
},
|
|
182
|
-
"fsharp": {
|
|
183
|
-
"disabled": true
|
|
184
|
-
},
|
|
185
|
-
"razor": {
|
|
186
|
-
"disabled": true
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"provider": {
|
|
190
|
-
"plainconcepts": {
|
|
191
|
-
"api": "http://172.30.0.30:10000",
|
|
192
|
-
"options": {
|
|
193
|
-
"apiKey": "awf-openai-proxy"
|
|
194
|
-
},
|
|
195
|
-
"models": {
|
|
196
|
-
"glm-5-
|
|
197
|
-
"name": "GLM 5.
|
|
198
|
-
},
|
|
199
|
-
"glm-5-
|
|
200
|
-
"name": "GLM 5.
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
163
|
+
const OPENCODE_CI_JSON = `{
|
|
164
|
+
"$schema": "https://opencode.ai/config.json",
|
|
165
|
+
"model": "plainconcepts/glm-5-3",
|
|
166
|
+
"plugin": [],
|
|
167
|
+
"default_agent": "ci-workflow-agent",
|
|
168
|
+
"agent": {
|
|
169
|
+
"ci-workflow-agent": {
|
|
170
|
+
"description": "Executes GitHub Agentic Workflow tasks in CI.",
|
|
171
|
+
"mode": "primary",
|
|
172
|
+
"prompt": "You execute the GitHub Agentic Workflow task in the user prompt."
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"permission": {
|
|
176
|
+
"read": "allow"
|
|
177
|
+
},
|
|
178
|
+
"lsp": {
|
|
179
|
+
"csharp": {
|
|
180
|
+
"disabled": true
|
|
181
|
+
},
|
|
182
|
+
"fsharp": {
|
|
183
|
+
"disabled": true
|
|
184
|
+
},
|
|
185
|
+
"razor": {
|
|
186
|
+
"disabled": true
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"provider": {
|
|
190
|
+
"plainconcepts": {
|
|
191
|
+
"api": "http://172.30.0.30:10000",
|
|
192
|
+
"options": {
|
|
193
|
+
"apiKey": "awf-openai-proxy"
|
|
194
|
+
},
|
|
195
|
+
"models": {
|
|
196
|
+
"glm-5-3": {
|
|
197
|
+
"name": "GLM 5.3"
|
|
198
|
+
},
|
|
199
|
+
"glm-5-2": {
|
|
200
|
+
"name": "GLM 5.2"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
206
|
`;
|
|
207
207
|
describe("generateOpencodeConfig", () => {
|
|
208
208
|
it("keeps LSP section when .slnx is present", () => {
|
|
@@ -243,8 +243,8 @@ describe("generateOpencodeConfig", () => {
|
|
|
243
243
|
solutionFiles: ["app.slnx"],
|
|
244
244
|
}));
|
|
245
245
|
const parsed = JSON.parse(result);
|
|
246
|
+
expect(parsed.provider.plainconcepts.models["glm-5-3"]).toBeDefined();
|
|
246
247
|
expect(parsed.provider.plainconcepts.models["glm-5-2"]).toBeDefined();
|
|
247
|
-
expect(parsed.provider.plainconcepts.models["glm-5-1"]).toBeDefined();
|
|
248
248
|
});
|
|
249
249
|
it("preserves the plainconcepts provider and its API URL", () => {
|
|
250
250
|
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
package/dist/tui.test.js
CHANGED
|
@@ -10,19 +10,22 @@ function makeEntry(name, kind, installed = false) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
function makeEntries(installed = []) {
|
|
13
|
-
const routes = ["refine", "implement", "
|
|
13
|
+
const routes = ["refine", "implement", "triage", "apply-review", "merge-gate", "audit"];
|
|
14
14
|
const templates = ["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"];
|
|
15
15
|
return [
|
|
16
16
|
...routes.map((name) => makeEntry(name, "route", installed.includes(name))),
|
|
17
17
|
...templates.map((name) => makeEntry(name, "template", installed.includes(name))),
|
|
18
18
|
];
|
|
19
19
|
}
|
|
20
|
+
// Counts derive from the fixture, so adding or removing a route above cannot silently
|
|
21
|
+
// desynchronise five assertions again.
|
|
22
|
+
const FIXTURE_COUNT = makeEntries().length;
|
|
20
23
|
describe("createSelectionState", () => {
|
|
21
24
|
it("returns all items as visible initially", () => {
|
|
22
25
|
const entries = makeEntries();
|
|
23
26
|
const state = createSelectionState(entries);
|
|
24
|
-
expect(state.allItems).toHaveLength(
|
|
25
|
-
expect(state.visibleItems).toHaveLength(
|
|
27
|
+
expect(state.allItems).toHaveLength(FIXTURE_COUNT);
|
|
28
|
+
expect(state.visibleItems).toHaveLength(FIXTURE_COUNT);
|
|
26
29
|
});
|
|
27
30
|
it("pre-selects installed items", () => {
|
|
28
31
|
const entries = makeEntries(["refine", "agentics-checks"]);
|
|
@@ -73,7 +76,7 @@ describe("fuzzyMatch", () => {
|
|
|
73
76
|
describe("filterItems", () => {
|
|
74
77
|
const entries = makeEntries();
|
|
75
78
|
it("returns all items when filter is empty", () => {
|
|
76
|
-
expect(filterItems(entries, "")).toHaveLength(
|
|
79
|
+
expect(filterItems(entries, "")).toHaveLength(FIXTURE_COUNT);
|
|
77
80
|
});
|
|
78
81
|
it("filters by name", () => {
|
|
79
82
|
const result = filterItems(entries, "refine");
|
|
@@ -82,7 +85,7 @@ describe("filterItems", () => {
|
|
|
82
85
|
});
|
|
83
86
|
it("filters by description", () => {
|
|
84
87
|
const result = filterItems(entries, "description");
|
|
85
|
-
expect(result).toHaveLength(
|
|
88
|
+
expect(result).toHaveLength(FIXTURE_COUNT);
|
|
86
89
|
});
|
|
87
90
|
it("filters by fuzzy subsequence over name and description", () => {
|
|
88
91
|
const result = filterItems(entries, "acdn");
|
|
@@ -125,7 +128,7 @@ describe("clearFilter", () => {
|
|
|
125
128
|
let state = applyFilter(createSelectionState(makeEntries()), "ref");
|
|
126
129
|
state = clearFilter(state);
|
|
127
130
|
expect(state.filter).toBe("");
|
|
128
|
-
expect(state.visibleItems).toHaveLength(
|
|
131
|
+
expect(state.visibleItems).toHaveLength(FIXTURE_COUNT);
|
|
129
132
|
});
|
|
130
133
|
});
|
|
131
134
|
describe("moveCursorUp / moveCursorDown", () => {
|
|
@@ -150,7 +153,7 @@ describe("moveCursorUp / moveCursorDown", () => {
|
|
|
150
153
|
});
|
|
151
154
|
it("wraps cursor to bottom from top", () => {
|
|
152
155
|
const state = moveCursorUp(createSelectionState(makeEntries()));
|
|
153
|
-
expect(state.cursor).toBe(
|
|
156
|
+
expect(state.cursor).toBe(FIXTURE_COUNT - 1);
|
|
154
157
|
});
|
|
155
158
|
it("does not move when list is empty", () => {
|
|
156
159
|
const entries = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const routeNames: readonly ["refine", "implement", "
|
|
1
|
+
export declare const routeNames: readonly ["refine", "implement", "triage", "apply-review", "merge-gate", "audit"];
|
|
2
2
|
export type RouteName = (typeof routeNames)[number];
|
|
3
3
|
export interface WorkflowRoute {
|
|
4
4
|
readonly name: RouteName;
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
export const routeNames = [
|
|
2
2
|
"refine",
|
|
3
3
|
"implement",
|
|
4
|
-
"direct",
|
|
5
4
|
"triage",
|
|
6
5
|
"apply-review",
|
|
7
6
|
"merge-gate",
|
|
8
7
|
"audit",
|
|
9
|
-
"propose",
|
|
10
8
|
];
|
|
11
9
|
export const workflowRoutes = [
|
|
12
10
|
{ name: "refine", worker: "agent-refine.md", description: "Refines an issue into a user story, on a first pass or after the author has answered the bot's questions.", defaultEnabled: true },
|
|
13
11
|
{ name: "implement", worker: "agent-implement.md", description: "Implements an issue and opens a pull request. Stops there: the merge decision belongs to the merge gate.", defaultEnabled: true },
|
|
14
|
-
{ name: "direct", worker: "agent-direct.md", description: "Executes a free-form instruction from an issue body and posts the results back on the same issue.", defaultEnabled: true },
|
|
15
12
|
{ name: "triage", worker: "agent-triage.md", description: "Triages issues opened by outside collaborators: runs 10 checks (template, security, size, danger, duplicates, clarity, reproducibility, acceptance, cross-cutting). Loops up to 3 rounds. Passes to refine or blocks.", defaultEnabled: true },
|
|
16
13
|
{ name: "apply-review", worker: "agent-apply-review.md", description: "Applies reviewer feedback to an open pull request the bot authored, then pushes the fixes to the same branch.", defaultEnabled: true },
|
|
17
14
|
{ name: "merge-gate", worker: "agent-merge-gate.md", description: "Decides what happens to a bot-authored pull request once CI has reported: merge, hand to a human, or fix CI.", defaultEnabled: true },
|
|
18
|
-
{ name: "audit", worker: "agent-audit.md", description: "Read-only repository audit. Finds 5-7 problems, scores each 1-10, files a single issue
|
|
19
|
-
{ name: "propose", worker: "agent-propose.md", description: "Proposes the next feature. Reads the manifesto, recent history, and comparable tools, scores candidates, and files the winner as a single issue.", defaultEnabled: false },
|
|
15
|
+
{ name: "audit", worker: "agent-audit.md", description: "Read-only repository audit. Finds 5-7 problems, scores each 1-10, and files a single issue that Refine then sizes and splits.", defaultEnabled: true },
|
|
20
16
|
];
|
|
21
17
|
export const packageOwnedTargets = [
|
|
22
18
|
".github/actions",
|
|
@@ -9,7 +9,7 @@ inputs:
|
|
|
9
9
|
description: Issue or pull request number.
|
|
10
10
|
required: true
|
|
11
11
|
labels:
|
|
12
|
-
description:
|
|
12
|
+
description: Labels to add, one per line. A comma-separated list is accepted as well.
|
|
13
13
|
required: true
|
|
14
14
|
runs:
|
|
15
15
|
using: composite
|
|
@@ -22,7 +22,7 @@ runs:
|
|
|
22
22
|
with:
|
|
23
23
|
github-token: ${{ inputs.token }}
|
|
24
24
|
script: |
|
|
25
|
-
const labels = process.env.LABELS.split(/\r?\n
|
|
25
|
+
const labels = process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean);
|
|
26
26
|
await github.rest.issues.addLabels({
|
|
27
27
|
...context.repo,
|
|
28
28
|
issue_number: Number(process.env.ISSUE_NUMBER),
|
|
@@ -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 };
|
|
@@ -40,7 +40,20 @@ git fetch --no-tags "$BUNDLE_FILE" "$BUNDLE_REF"
|
|
|
40
40
|
[ "$(git cat-file -t "$BUNDLE_TIP")" = commit ] || fail "Git bundle ref must point directly to a commit"
|
|
41
41
|
[ "$(git rev-parse FETCH_HEAD)" = "$BUNDLE_TIP" ] || fail "Fetched bundle commit did not match listed bundle ref"
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# Fast-forward only, deliberately: a push that is not one would discard somebody's commits.
|
|
44
|
+
#
|
|
45
|
+
# "Cannot fast-forward" said nothing about why, and there are two reasons: the agent
|
|
46
|
+
# rewrote history (a rebase, an amend, a reset), or somebody pushed to the branch while it
|
|
47
|
+
# was working. Both mean the same thing to git, that the branch carries commits the bundle
|
|
48
|
+
# does not, so the applier cannot tell them apart and does not try. It prints the commits
|
|
49
|
+
# that are missing instead: their authors and dates answer the question immediately.
|
|
50
|
+
if ! git merge-base --is-ancestor "$TARGET_TIP" "$BUNDLE_TIP"; then
|
|
51
|
+
echo "::error::Nothing was applied. $TARGET_BRANCH is at $TARGET_TIP and the bundle is at $BUNDLE_TIP, which does not contain it, so pushing would drop the commits below. Either the agent rewrote history, which this push refuses, or the branch moved while it was working, in which case nothing is lost and the gate only needs running again on the current head."
|
|
52
|
+
echo "On $TARGET_BRANCH but missing from the bundle:"
|
|
53
|
+
git --no-pager log --format=" %h %ad %an %s" --date=short "$BUNDLE_TIP..$TARGET_TIP" |
|
|
54
|
+
head -20 || true
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
44
57
|
git switch --detach "$TARGET_TIP"
|
|
45
58
|
git merge --ff-only "$BUNDLE_TIP"
|
|
46
59
|
git push origin "HEAD:refs/heads/$TARGET_BRANCH"
|
|
@@ -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,
|
|
@@ -24,12 +24,15 @@ outputs:
|
|
|
24
24
|
ci-run-id:
|
|
25
25
|
description: Workflow run ID of that CI run.
|
|
26
26
|
value: ${{ steps.classify.outputs.ci-run-id }}
|
|
27
|
+
merge-gate-attempts:
|
|
28
|
+
description: Failed merge-gate attempts already made against this CI verdict.
|
|
29
|
+
value: ${{ steps.classify.outputs.merge-gate-attempts }}
|
|
30
|
+
implement-attempts:
|
|
31
|
+
description: Implement runs already made for this issue that died before producing an answer.
|
|
32
|
+
value: ${{ steps.classify.outputs.implement-attempts }}
|
|
27
33
|
refine-mode:
|
|
28
34
|
description: first or rerefine.
|
|
29
35
|
value: ${{ steps.classify.outputs.refine-mode }}
|
|
30
|
-
direct-mode:
|
|
31
|
-
description: first or continue.
|
|
32
|
-
value: ${{ steps.classify.outputs.direct-mode }}
|
|
33
36
|
triage-mode:
|
|
34
37
|
description: first or retriage.
|
|
35
38
|
value: ${{ steps.classify.outputs.triage-mode }}
|
|
@@ -47,6 +50,7 @@ runs:
|
|
|
47
50
|
ACTION: ${{ github.event.action }}
|
|
48
51
|
LABEL: ${{ github.event.label.name }}
|
|
49
52
|
ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
|
|
53
|
+
ISSUE_STATE: ${{ github.event.issue.state }}
|
|
50
54
|
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
51
55
|
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
52
56
|
COMMENT_ON_PR: ${{ github.event.issue.pull_request != '' }}
|
|
@@ -62,6 +66,7 @@ runs:
|
|
|
62
66
|
INPUT_MODE: ${{ github.event.inputs.mode }}
|
|
63
67
|
INPUT_CI_CONCLUSION: ${{ github.event.inputs.ci-conclusion }}
|
|
64
68
|
INPUT_CI_RUN_ID: ${{ github.event.inputs.ci-run-id }}
|
|
69
|
+
INPUT_ATTEMPTS_SO_FAR: ${{ github.event.inputs.attempts_so_far }}
|
|
65
70
|
INPUT_TRIGGER_KIND: ${{ github.event.inputs.trigger-kind }}
|
|
66
71
|
run: |
|
|
67
72
|
set -euo pipefail
|
|
@@ -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
|
|
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:-[]}"
|
|
@@ -27,18 +23,28 @@ is_issue_number() {
|
|
|
27
23
|
|
|
28
24
|
classify_route() {
|
|
29
25
|
local route="none" error=""
|
|
30
|
-
local issue_number="" pr_number="" ci_conclusion="" ci_run_id=""
|
|
31
|
-
local refine_mode=""
|
|
26
|
+
local issue_number="" pr_number="" ci_conclusion="" ci_run_id="" merge_gate_attempts="0" implement_attempts="0"
|
|
27
|
+
local refine_mode="" triage_mode="" trigger_kind=""
|
|
32
28
|
|
|
33
29
|
case "${EVENT:-}" in
|
|
34
30
|
issues)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -100,8 +100,18 @@ classify_route() {
|
|
|
100
100
|
|
|
101
101
|
issue_comment)
|
|
102
102
|
if [ "${COMMENT_ON_PR:-false}" = "true" ]; then
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
if [ "${COMMENT_SENDER_TYPE:-}" = "Bot" ]; then
|
|
104
|
+
# Every App-token comment on a pull request is an issue_comment event, and the
|
|
105
|
+
# workers comment on pull requests they own. None of that is reviewer feedback.
|
|
106
|
+
error="comment authored by a bot"
|
|
107
|
+
else
|
|
108
|
+
route="apply-review"
|
|
109
|
+
pr_number="${EVENT_ISSUE_NUMBER:-}"
|
|
110
|
+
fi
|
|
111
|
+
elif [ "${ISSUE_STATE:-}" = "closed" ]; then
|
|
112
|
+
# A closing comment on a refine-labelled issue used to start a full re-refine, which
|
|
113
|
+
# held a runner for 35 minutes and filed split children under an already-shut parent.
|
|
114
|
+
error="issue is closed"
|
|
105
115
|
elif [ "${COMMENT_SENDER_TYPE:-}" = "Bot" ]; then
|
|
106
116
|
error="comment authored by a bot"
|
|
107
117
|
elif has_label triage; then
|
|
@@ -110,12 +120,8 @@ classify_route() {
|
|
|
110
120
|
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
111
121
|
elif has_label implement; then
|
|
112
122
|
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
123
|
elif ! has_label refine; then
|
|
118
|
-
error="issue does not carry the refine
|
|
124
|
+
error="issue does not carry the refine label"
|
|
119
125
|
else
|
|
120
126
|
route="refine"
|
|
121
127
|
refine_mode="rerefine"
|
|
@@ -143,10 +149,17 @@ classify_route() {
|
|
|
143
149
|
;;
|
|
144
150
|
|
|
145
151
|
workflow_run)
|
|
146
|
-
|
|
152
|
+
# Only a FAILED CI run on an attached pull request auto-dispatches the gate. A green
|
|
153
|
+
# run reaches the gate through the consumer CI's dispatch-merge-gate job and the
|
|
154
|
+
# reconcile belt, so routing success here would double-fire the gate for every passing
|
|
155
|
+
# pull request. GitHub delivers workflow_run only for CI runs whose actor is a human;
|
|
156
|
+
# a bot pull request's CI never arrives here at all, and the same two paths cover it.
|
|
157
|
+
if [ "${RUN_CONCLUSION:-}" != "failure" ]; then
|
|
158
|
+
error="CI concluded '${RUN_CONCLUSION:-}'; the gate auto-triggers only on failure"
|
|
159
|
+
elif is_issue_number "${RUN_PR_NUMBER:-}"; then
|
|
147
160
|
route="merge-gate"
|
|
148
161
|
pr_number="${RUN_PR_NUMBER}"
|
|
149
|
-
ci_conclusion="
|
|
162
|
+
ci_conclusion="failure"
|
|
150
163
|
ci_run_id="${RUN_ID:-}"
|
|
151
164
|
else
|
|
152
165
|
error="CI run has no attached pull request"
|
|
@@ -167,14 +180,16 @@ classify_route() {
|
|
|
167
180
|
workflow_dispatch)
|
|
168
181
|
trigger_kind="manual"
|
|
169
182
|
case "${OPERATION:-}" in
|
|
170
|
-
refine | implement
|
|
183
|
+
refine | implement)
|
|
171
184
|
if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
|
|
172
185
|
route="${OPERATION}"
|
|
173
186
|
issue_number="${INPUT_ISSUE_NUMBER}"
|
|
174
187
|
if [ "$OPERATION" = "refine" ]; then
|
|
175
188
|
refine_mode="${INPUT_MODE:-first}"
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
else
|
|
190
|
+
# The implement worker re-dispatches itself when a run dies before doing any work,
|
|
191
|
+
# and carries the count so the budget is bounded.
|
|
192
|
+
implement_attempts="${INPUT_ATTEMPTS_SO_FAR:-0}"
|
|
178
193
|
fi
|
|
179
194
|
else
|
|
180
195
|
error="operation '${OPERATION}' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
|
|
@@ -203,17 +218,21 @@ classify_route() {
|
|
|
203
218
|
pr_number="${INPUT_PR_NUMBER}"
|
|
204
219
|
ci_conclusion="${INPUT_CI_CONCLUSION:-}"
|
|
205
220
|
ci_run_id="${INPUT_CI_RUN_ID:-}"
|
|
221
|
+
merge_gate_attempts="${INPUT_ATTEMPTS_SO_FAR:-0}"
|
|
206
222
|
else
|
|
207
223
|
error="operation 'merge-gate' needs a positive pr-number, got '${INPUT_PR_NUMBER:-}'"
|
|
208
224
|
fi
|
|
209
225
|
;;
|
|
210
|
-
audit
|
|
226
|
+
audit)
|
|
211
227
|
route="${OPERATION}"
|
|
212
228
|
trigger_kind="${INPUT_TRIGGER_KIND:-manual}"
|
|
213
229
|
;;
|
|
214
|
-
|
|
230
|
+
audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
|
|
215
231
|
route="${OPERATION}"
|
|
216
232
|
;;
|
|
233
|
+
release)
|
|
234
|
+
route="release"
|
|
235
|
+
;;
|
|
217
236
|
*)
|
|
218
237
|
error="unknown operation '${OPERATION:-}'"
|
|
219
238
|
;;
|
|
@@ -231,8 +250,9 @@ issue-number=${issue_number}
|
|
|
231
250
|
pr-number=${pr_number}
|
|
232
251
|
ci-conclusion=${ci_conclusion}
|
|
233
252
|
ci-run-id=${ci_run_id}
|
|
253
|
+
merge-gate-attempts=${merge_gate_attempts}
|
|
254
|
+
implement-attempts=${implement_attempts}
|
|
234
255
|
refine-mode=${refine_mode}
|
|
235
|
-
direct-mode=${direct_mode}
|
|
236
256
|
triage-mode=${triage_mode}
|
|
237
257
|
trigger-kind=${trigger_kind}
|
|
238
258
|
error=${error}
|