@plainconceptsplatform/workflows 0.6.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -88
- package/dist/catalog-installation.d.ts +39 -2
- package/dist/catalog-installation.js +172 -109
- package/dist/index.js +128 -73
- package/dist/package-baseline.d.ts +25 -0
- package/dist/package-baseline.js +138 -0
- package/dist/route-processing.d.ts +0 -2
- package/dist/route-processing.js +22 -91
- package/dist/stack-defaults.js +18 -12
- package/dist/tui.js +27 -43
- package/dist/worker-env.d.ts +46 -0
- package/dist/worker-env.js +179 -0
- package/dist/workflow-catalog.d.ts +4 -2
- package/dist/workflow-catalog.js +5 -3
- package/loops/actions/add-issue-labels/action.yml +20 -0
- package/loops/actions/audit-close/action.yml +180 -128
- package/loops/actions/classify-route/classify-route.sh +8 -2
- package/loops/actions/housekeeping/action.yml +251 -0
- package/loops/actions/merge-agent-pr/action.yml +13 -0
- package/loops/actions/report-workflow-errors/action.yml +385 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +13 -1
- package/loops/actions/validate-triage-output/action.yml +1 -1
- package/loops/actions/validate-triage-output/validate-triage-output.sh +9 -5
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +53 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +847 -170
- package/loops/templates/agentics/agentics-error-report.yml +97 -0
- package/loops/templates/opencode/opencode.ci.json +1 -1
- package/loops/workflows/agent-apply-review.md +452 -469
- package/loops/workflows/agent-audit.md +201 -213
- package/loops/workflows/agent-implement.md +616 -640
- package/loops/workflows/agent-merge-gate.md +830 -844
- package/loops/workflows/agent-refine.md +599 -633
- package/loops/workflows/agent-release.md +244 -258
- package/loops/workflows/agent-triage.md +476 -447
- package/loops/workflows/authorize-bot-work.yml +26 -6
- package/loops/workflows/work-router.yml +1185 -1038
- package/package.json +9 -8
- package/dist/action-validation.test.d.ts +0 -1
- package/dist/action-validation.test.js +0 -87
- package/dist/catalog-installation.test.d.ts +0 -1
- package/dist/catalog-installation.test.js +0 -485
- package/dist/catalog-listing.test.d.ts +0 -1
- package/dist/catalog-listing.test.js +0 -150
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -273
- package/dist/repository-inspection.test.d.ts +0 -1
- package/dist/repository-inspection.test.js +0 -77
- package/dist/route-processing.test.d.ts +0 -1
- package/dist/route-processing.test.js +0 -283
- package/dist/stack-defaults.test.d.ts +0 -1
- package/dist/stack-defaults.test.js +0 -266
- package/dist/tui.test.d.ts +0 -1
- package/dist/tui.test.js +0 -249
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/actions/stale-recovery/action.yml +0 -288
- package/loops/actions/update-changelog/action.yml +0 -113
package/dist/tui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { formatCatalog, listCatalog } from "./catalog-listing.js";
|
|
3
|
-
import { installCatalog,
|
|
3
|
+
import { installCatalog, installTemplate, isTemplateName, removeRouteFiles } from "./catalog-installation.js";
|
|
4
4
|
import { inspectRepository } from "./repository-inspection.js";
|
|
5
5
|
import { routeNames } from "./workflow-catalog.js";
|
|
6
6
|
const ANSI = {
|
|
@@ -218,7 +218,6 @@ export async function runInteractive(repositoryPath, options = {}) {
|
|
|
218
218
|
}
|
|
219
219
|
async function installSelected(state, repositoryPath, force) {
|
|
220
220
|
const items = getItemsToInstall(state, force);
|
|
221
|
-
const newRoutes = items.filter((entry) => entry.kind === "route");
|
|
222
221
|
const templates = items.filter((entry) => entry.kind === "template");
|
|
223
222
|
const routeEntries = state.allItems.filter((entry) => entry.kind === "route");
|
|
224
223
|
const checkedRoutes = routeEntries
|
|
@@ -232,56 +231,41 @@ async function installSelected(state, repositoryPath, force) {
|
|
|
232
231
|
const removedRoutes = installedRouteNames.filter((name) => !checkedRoutes.includes(name));
|
|
233
232
|
const allConflicts = [];
|
|
234
233
|
const allInstalled = [];
|
|
235
|
-
const allRemoved = [];
|
|
236
234
|
const inspection = await inspectRepository(repositoryPath);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
allRemoved.push(...await removeRouteFiles(repositoryPath, removedRoutes));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
else if (checkedRoutes.length === 0 && removedRoutes.length > 0) {
|
|
246
|
-
const result = await installCatalog(repositoryPath, { force, selectedRoutes: [], inspection });
|
|
247
|
-
allConflicts.push(...result.conflicts);
|
|
248
|
-
allInstalled.push(...result.installed);
|
|
249
|
-
if (result.conflicts.length === 0 || force) {
|
|
250
|
-
allRemoved.push(...await removeRouteFiles(repositoryPath, removedRoutes));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
const result = await installMandatoryFiles(repositoryPath, { force });
|
|
255
|
-
allConflicts.push(...result.conflicts);
|
|
256
|
-
allInstalled.push(...result.installed);
|
|
257
|
-
}
|
|
235
|
+
// Desired state: the checked routes are the target set. Installing it also refreshes every
|
|
236
|
+
// package-managed file that is already there, so Enter on an unchanged selection is an update.
|
|
237
|
+
const result = await installCatalog(repositoryPath, { force, selectedRoutes: checkedRoutes, inspection });
|
|
238
|
+
allInstalled.push(...result.installed);
|
|
239
|
+
const allRemoved = await removeRouteFiles(repositoryPath, removedRoutes);
|
|
258
240
|
for (const template of templates) {
|
|
259
241
|
if (!isTemplateName(template.name))
|
|
260
242
|
continue;
|
|
261
|
-
const
|
|
262
|
-
allConflicts.push(...
|
|
263
|
-
allInstalled.push(...
|
|
243
|
+
const templateResult = await installTemplate(repositoryPath, template.name, { force, inspection });
|
|
244
|
+
allConflicts.push(...templateResult.conflicts);
|
|
245
|
+
allInstalled.push(...templateResult.installed);
|
|
264
246
|
}
|
|
265
247
|
if (allConflicts.length > 0 && !force) {
|
|
266
|
-
console.error(`
|
|
248
|
+
console.error(`Template conflicts found. Re-run with --force to overwrite:\n${allConflicts.join("\n")}`);
|
|
267
249
|
return 1;
|
|
268
250
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (allRemoved.length > 0) {
|
|
277
|
-
console.log(`Removed ${allRemoved.length} item(s):`);
|
|
278
|
-
for (const file of allRemoved) {
|
|
279
|
-
console.log(` ${file}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
251
|
+
const changed = result.changes.filter((change) => change.status === "added" || change.status === "updated" || change.status === "removed");
|
|
252
|
+
const skipped = result.changes.filter((change) => change.status === "skipped");
|
|
253
|
+
console.log(`Package ${result.packageVersion}${result.installedVersions.length > 0 ? ` (was ${result.installedVersions.join(", ")})` : ""}`);
|
|
254
|
+
if (changed.length === 0 && allRemoved.length === 0 && templates.length === 0) {
|
|
255
|
+
console.log("Everything is up to date.");
|
|
282
256
|
}
|
|
283
|
-
|
|
284
|
-
|
|
257
|
+
for (const change of changed) {
|
|
258
|
+
const kept = change.keptEnv?.length ? ` (kept env: ${change.keptEnv.join(", ")})` : "";
|
|
259
|
+
console.log(` ${change.status.padEnd(8)} ${change.target}${kept}`);
|
|
260
|
+
}
|
|
261
|
+
for (const change of skipped) {
|
|
262
|
+
console.log(` skipped ${change.target} (${change.reason ?? "consumer-owned"})`);
|
|
263
|
+
}
|
|
264
|
+
for (const file of allRemoved) {
|
|
265
|
+
console.log(` removed ${file}`);
|
|
266
|
+
}
|
|
267
|
+
for (const template of templates) {
|
|
268
|
+
console.log(` template ${template.name}`);
|
|
285
269
|
}
|
|
286
270
|
return 0;
|
|
287
271
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface EnvEntry {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
/** Comment and blank lines that precede the key. They belong to the key they describe. */
|
|
4
|
+
readonly leading: readonly string[];
|
|
5
|
+
/** The ` KEY: value` line, followed by any continuation lines of a multi-line value. */
|
|
6
|
+
readonly lines: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface WorkerEnv {
|
|
9
|
+
readonly present: boolean;
|
|
10
|
+
readonly before: readonly string[];
|
|
11
|
+
readonly entries: readonly EnvEntry[];
|
|
12
|
+
readonly trailing: readonly string[];
|
|
13
|
+
readonly after: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface WorkerMergeReport {
|
|
16
|
+
/** Consumer values kept because they differ from the package (and from the baseline, when known). */
|
|
17
|
+
readonly keptEnv: readonly string[];
|
|
18
|
+
/** Package defaults applied because the consumer still had the previous default. */
|
|
19
|
+
readonly updatedDefaults: readonly string[];
|
|
20
|
+
/** Keys only the consumer defines, kept at the end of the block. */
|
|
21
|
+
readonly consumerOnlyEnv: readonly string[];
|
|
22
|
+
/** Keys the package removed that the consumer still carried at the old default. */
|
|
23
|
+
readonly droppedEnv: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/** The `env:` block of a worker, which lives inside the markdown frontmatter. */
|
|
26
|
+
export declare function parseWorkerEnv(content: string): WorkerEnv;
|
|
27
|
+
/** The top-level `env:` block of a plain YAML workflow, such as the router. */
|
|
28
|
+
export declare function parseYamlEnv(content: string): WorkerEnv;
|
|
29
|
+
export declare function serializeWorkerEnv(env: WorkerEnv): string;
|
|
30
|
+
export declare function entryValue(entry: EnvEntry): string;
|
|
31
|
+
export declare function mergeWorkerEnv(packageContent: string, consumerContent: string, baselineContent?: string, parse?: (content: string) => WorkerEnv): {
|
|
32
|
+
content: string;
|
|
33
|
+
report: WorkerMergeReport;
|
|
34
|
+
};
|
|
35
|
+
/** Copy the router's own `env:` values into the two literal lines that cannot read them. */
|
|
36
|
+
export declare function mirrorRouterLiterals(router: string): string;
|
|
37
|
+
/** The package's router with the consumer's `env:` values put back and mirrored into place. */
|
|
38
|
+
export declare function mergeRouter(packageContent: string, consumerContent: string, baselineContent?: string): {
|
|
39
|
+
content: string;
|
|
40
|
+
report: WorkerMergeReport;
|
|
41
|
+
};
|
|
42
|
+
/** The package's worker with the consumer's customisable values put back. */
|
|
43
|
+
export declare function mergeWorker(packageContent: string, consumerContent: string, baselineContent?: string): {
|
|
44
|
+
content: string;
|
|
45
|
+
report: WorkerMergeReport;
|
|
46
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The `env:` block at the top of a worker is the only part of it a consumer may customise.
|
|
2
|
+
// Everything else in the file is the package's, so an update takes the package's file and
|
|
3
|
+
// puts the consumer's env values back. This module does that merge.
|
|
4
|
+
//
|
|
5
|
+
// With a baseline (the package version the consumer installed from) the merge is three-way:
|
|
6
|
+
// a value the consumer never touched follows the package when the default changes, and a
|
|
7
|
+
// value the consumer changed is kept. Without one it is two-way: every consumer value is kept.
|
|
8
|
+
const ENTRY = /^ ([A-Za-z_][A-Za-z0-9_.-]*):(.*)$/;
|
|
9
|
+
/** The `env:` block of a worker, which lives inside the markdown frontmatter. */
|
|
10
|
+
export function parseWorkerEnv(content) {
|
|
11
|
+
const lines = content.split("\n");
|
|
12
|
+
if (lines[0] !== "---")
|
|
13
|
+
return absent(lines);
|
|
14
|
+
const end = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
15
|
+
if (end === -1)
|
|
16
|
+
return absent(lines);
|
|
17
|
+
return parseEnvBlock(lines, lines.findIndex((line, index) => index > 0 && index < end && line === "env:"));
|
|
18
|
+
}
|
|
19
|
+
/** The top-level `env:` block of a plain YAML workflow, such as the router. */
|
|
20
|
+
export function parseYamlEnv(content) {
|
|
21
|
+
const lines = content.split("\n");
|
|
22
|
+
return parseEnvBlock(lines, lines.indexOf("env:"));
|
|
23
|
+
}
|
|
24
|
+
function absent(lines) {
|
|
25
|
+
return { present: false, before: lines, entries: [], trailing: [], after: [] };
|
|
26
|
+
}
|
|
27
|
+
function parseEnvBlock(lines, envIndex) {
|
|
28
|
+
if (envIndex === -1)
|
|
29
|
+
return absent(lines);
|
|
30
|
+
// The block runs to the first line that is neither blank nor indented into it. That is the
|
|
31
|
+
// closing `---` of a frontmatter and the next top-level key of a plain YAML file alike.
|
|
32
|
+
let cursor = envIndex + 1;
|
|
33
|
+
while (cursor < lines.length && (lines[cursor] === "" || lines[cursor].startsWith(" ")))
|
|
34
|
+
cursor += 1;
|
|
35
|
+
const entries = [];
|
|
36
|
+
let leading = [];
|
|
37
|
+
let current;
|
|
38
|
+
for (const line of lines.slice(envIndex + 1, cursor)) {
|
|
39
|
+
const match = ENTRY.exec(line);
|
|
40
|
+
if (match !== null) {
|
|
41
|
+
if (current)
|
|
42
|
+
entries.push(current);
|
|
43
|
+
current = { key: match[1], leading, lines: [line] };
|
|
44
|
+
leading = [];
|
|
45
|
+
}
|
|
46
|
+
else if (current && leading.length === 0 && /^ +\S/.test(line)) {
|
|
47
|
+
current.lines.push(line);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
leading.push(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (current)
|
|
54
|
+
entries.push(current);
|
|
55
|
+
return {
|
|
56
|
+
present: true,
|
|
57
|
+
before: lines.slice(0, envIndex + 1),
|
|
58
|
+
entries,
|
|
59
|
+
trailing: leading,
|
|
60
|
+
after: lines.slice(cursor),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function serializeWorkerEnv(env) {
|
|
64
|
+
if (!env.present)
|
|
65
|
+
return env.before.join("\n");
|
|
66
|
+
return [
|
|
67
|
+
...env.before,
|
|
68
|
+
...env.entries.flatMap((entry) => [...entry.leading, ...entry.lines]),
|
|
69
|
+
...env.trailing,
|
|
70
|
+
...env.after,
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
export function entryValue(entry) {
|
|
74
|
+
const first = entry.lines[0].replace(ENTRY, "$2").trim();
|
|
75
|
+
return [first, ...entry.lines.slice(1).map((line) => line.trim())].join("\n").trim();
|
|
76
|
+
}
|
|
77
|
+
export function mergeWorkerEnv(packageContent, consumerContent, baselineContent, parse = parseWorkerEnv) {
|
|
78
|
+
const report = { keptEnv: [], updatedDefaults: [], consumerOnlyEnv: [], droppedEnv: [] };
|
|
79
|
+
const pkg = parse(packageContent);
|
|
80
|
+
const consumer = parse(consumerContent);
|
|
81
|
+
if (!pkg.present || !consumer.present)
|
|
82
|
+
return { content: packageContent, report };
|
|
83
|
+
const baseline = baselineContent === undefined ? undefined : parse(baselineContent);
|
|
84
|
+
const consumerByKey = new Map(consumer.entries.map((entry) => [entry.key, entry]));
|
|
85
|
+
const baselineByKey = new Map((baseline?.entries ?? []).map((entry) => [entry.key, entry]));
|
|
86
|
+
const packageKeys = new Set(pkg.entries.map((entry) => entry.key));
|
|
87
|
+
const merged = [];
|
|
88
|
+
for (const entry of pkg.entries) {
|
|
89
|
+
const mine = consumerByKey.get(entry.key);
|
|
90
|
+
if (mine === undefined || entryValue(mine) === entryValue(entry)) {
|
|
91
|
+
merged.push(entry);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const base = baselineByKey.get(entry.key);
|
|
95
|
+
if (base !== undefined && entryValue(base) === entryValue(mine)) {
|
|
96
|
+
// Still the previous default: the package changed it, the consumer never did.
|
|
97
|
+
merged.push(entry);
|
|
98
|
+
report.updatedDefaults.push(entry.key);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
merged.push({ key: entry.key, leading: entry.leading, lines: mine.lines });
|
|
102
|
+
report.keptEnv.push(entry.key);
|
|
103
|
+
}
|
|
104
|
+
for (const entry of consumer.entries) {
|
|
105
|
+
if (packageKeys.has(entry.key))
|
|
106
|
+
continue;
|
|
107
|
+
const base = baselineByKey.get(entry.key);
|
|
108
|
+
if (base !== undefined && entryValue(base) === entryValue(entry)) {
|
|
109
|
+
report.droppedEnv.push(entry.key);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
merged.push(entry);
|
|
113
|
+
report.consumerOnlyEnv.push(entry.key);
|
|
114
|
+
}
|
|
115
|
+
return { content: serializeWorkerEnv({ ...pkg, entries: merged }), report };
|
|
116
|
+
}
|
|
117
|
+
// Two more per-repository values live outside `env:` because GitHub gives them no other home:
|
|
118
|
+
// `runs-on` cannot read the env context, and the engine's gateway URL is read by the engine
|
|
119
|
+
// before the workflow's env exists.
|
|
120
|
+
function engineEndpoint(content) {
|
|
121
|
+
return /^ OPENAI_BASE_URL: (.+)$/m.exec(content)?.[1];
|
|
122
|
+
}
|
|
123
|
+
// Which self-hosted pool a worker's agent jobs run on. gh-aw cannot share runs-on through an
|
|
124
|
+
// import, so every worker names it. ubuntu-latest is excluded, being GitHub's own runner, used
|
|
125
|
+
// by the deterministic jobs everywhere and chosen by nobody.
|
|
126
|
+
function runnerPools(content) {
|
|
127
|
+
const found = new Set();
|
|
128
|
+
for (const match of content.matchAll(/^\s*runs-on(?:-slim)?: (\S+)\s*$/gm)) {
|
|
129
|
+
if (match[1] !== "ubuntu-latest")
|
|
130
|
+
found.add(match[1]);
|
|
131
|
+
}
|
|
132
|
+
return [...found];
|
|
133
|
+
}
|
|
134
|
+
function preserveRunnerPool(packageContent, consumerContent) {
|
|
135
|
+
const mine = runnerPools(consumerContent);
|
|
136
|
+
const theirs = runnerPools(packageContent);
|
|
137
|
+
// Only an unambiguous swap. A consumer naming several pools has drifted rather than
|
|
138
|
+
// decided, and guessing which it meant is worse than leaving the package's.
|
|
139
|
+
if (mine.length !== 1 || theirs.length !== 1 || mine[0] === theirs[0])
|
|
140
|
+
return packageContent;
|
|
141
|
+
return packageContent.replace(/^(\s*runs-on(?:-slim)?: )(\S+)(\s*)$/gm, (line, prefix, pool, tail) => pool === "ubuntu-latest" ? line : `${prefix}${mine[0]}${tail}`);
|
|
142
|
+
}
|
|
143
|
+
// GitHub evaluates no expression in a `workflow_run.workflows:` list or in a `cron:`, so the two
|
|
144
|
+
// router values that are also needed there cannot be read from `env:` at those two lines. The
|
|
145
|
+
// installer copies them in instead, which is what keeps the pair in step across an update; the
|
|
146
|
+
// route matrix asserts the copies still agree.
|
|
147
|
+
const CI_NAME_TRIGGER = /^(\s*workflows: \[")[^"]*("\]\s*)$/m;
|
|
148
|
+
const AUDIT_CRON_LINE = /^(\s*- cron: ")[^"]*(" # audit slot.*)$/m;
|
|
149
|
+
/** Copy the router's own `env:` values into the two literal lines that cannot read them. */
|
|
150
|
+
export function mirrorRouterLiterals(router) {
|
|
151
|
+
const values = new Map(parseYamlEnv(router).entries.map((entry) => [entry.key, unquote(entryValue(entry))]));
|
|
152
|
+
const ciName = values.get("CI_WORKFLOW_NAME");
|
|
153
|
+
const auditCron = values.get("AUDIT_CRON");
|
|
154
|
+
let result = router;
|
|
155
|
+
if (ciName !== undefined)
|
|
156
|
+
result = result.replace(CI_NAME_TRIGGER, `$1${ciName}$2`);
|
|
157
|
+
if (auditCron !== undefined)
|
|
158
|
+
result = result.replace(AUDIT_CRON_LINE, `$1${auditCron}$2`);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function unquote(value) {
|
|
162
|
+
const match = /^(["'])([\s\S]*)\1$/.exec(value);
|
|
163
|
+
return match === null ? value : match[2];
|
|
164
|
+
}
|
|
165
|
+
/** The package's router with the consumer's `env:` values put back and mirrored into place. */
|
|
166
|
+
export function mergeRouter(packageContent, consumerContent, baselineContent) {
|
|
167
|
+
const { content, report } = mergeWorkerEnv(packageContent, consumerContent, baselineContent, parseYamlEnv);
|
|
168
|
+
return { content: mirrorRouterLiterals(content), report };
|
|
169
|
+
}
|
|
170
|
+
/** The package's worker with the consumer's customisable values put back. */
|
|
171
|
+
export function mergeWorker(packageContent, consumerContent, baselineContent) {
|
|
172
|
+
const { content: envMerged, report } = mergeWorkerEnv(packageContent, consumerContent, baselineContent);
|
|
173
|
+
let result = envMerged;
|
|
174
|
+
const endpoint = engineEndpoint(consumerContent);
|
|
175
|
+
if (endpoint !== undefined) {
|
|
176
|
+
result = result.replace(/^ OPENAI_BASE_URL: .+$/m, ` OPENAI_BASE_URL: ${endpoint}`);
|
|
177
|
+
}
|
|
178
|
+
return { content: preserveRunnerPool(result, consumerContent), report };
|
|
179
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const routeNames: readonly ["refine", "implement", "triage", "apply-review", "merge-gate", "audit"];
|
|
1
|
+
export declare const routeNames: readonly ["refine", "implement", "triage", "apply-review", "merge-gate", "audit", "release"];
|
|
2
2
|
export type RouteName = (typeof routeNames)[number];
|
|
3
3
|
export interface WorkflowRoute {
|
|
4
4
|
readonly name: RouteName;
|
|
@@ -14,12 +14,14 @@ export interface MandatoryFile {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const mandatoryFiles: readonly MandatoryFile[];
|
|
16
16
|
export declare const generatedConsumerTargets: readonly [".github/workflows/agent-*.lock.yml", ".github/aw/actions-lock.json"];
|
|
17
|
-
export declare const templateNames: readonly ["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "feature-request", "github-release", "opencode.ci.json"
|
|
17
|
+
export declare const templateNames: readonly ["agentics-checks", "agentics-error-report", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "feature-request", "github-release", "opencode.ci.json"];
|
|
18
18
|
export type TemplateName = (typeof templateNames)[number];
|
|
19
19
|
export interface CatalogTemplate {
|
|
20
20
|
readonly name: TemplateName;
|
|
21
21
|
readonly file: string;
|
|
22
22
|
readonly description: string;
|
|
23
23
|
readonly target?: string;
|
|
24
|
+
/** Subdirectory of `templates/` holding the file. Defaults to "agentics". */
|
|
25
|
+
readonly directory?: string;
|
|
24
26
|
}
|
|
25
27
|
export declare const catalogTemplates: readonly CatalogTemplate[];
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -5,14 +5,16 @@ export const routeNames = [
|
|
|
5
5
|
"apply-review",
|
|
6
6
|
"merge-gate",
|
|
7
7
|
"audit",
|
|
8
|
+
"release",
|
|
8
9
|
];
|
|
9
10
|
export const workflowRoutes = [
|
|
10
11
|
{ 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 },
|
|
11
12
|
{ 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 },
|
|
12
|
-
{ name: "triage", worker: "agent-triage.md", description: "Triages issues opened
|
|
13
|
+
{ name: "triage", worker: "agent-triage.md", description: "Triages issues opened from outside the organisation: runs 10 checks (template, security, size, danger, duplicates, clarity, reproducibility, acceptance, cross-cutting, product scope). Loops up to 3 rounds. Passes to refine, asks the author for more, parks out-of-scope work open for a maintainer, or closes what is genuinely rejected.", defaultEnabled: true },
|
|
13
14
|
{ 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 },
|
|
14
15
|
{ 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 },
|
|
15
16
|
{ 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 },
|
|
17
|
+
{ name: "release", worker: "agent-release.md", description: "Writes release notes from the commit log, then a deterministic job bumps the version, tags, and publishes a GitHub Release. Manual dispatch only (operation=release).", defaultEnabled: true },
|
|
16
18
|
];
|
|
17
19
|
export const packageOwnedTargets = [
|
|
18
20
|
".github/actions",
|
|
@@ -33,6 +35,7 @@ export const generatedConsumerTargets = [
|
|
|
33
35
|
];
|
|
34
36
|
export const templateNames = [
|
|
35
37
|
"agentics-checks",
|
|
38
|
+
"agentics-error-report",
|
|
36
39
|
"agentics-maintenance",
|
|
37
40
|
"app-ci-dotnet-next",
|
|
38
41
|
"app-ci-node-monorepo",
|
|
@@ -40,10 +43,10 @@ export const templateNames = [
|
|
|
40
43
|
"feature-request",
|
|
41
44
|
"github-release",
|
|
42
45
|
"opencode.ci.json",
|
|
43
|
-
"visual-evidence",
|
|
44
46
|
];
|
|
45
47
|
export const catalogTemplates = [
|
|
46
48
|
{ name: "agentics-checks", file: "agentics-checks.yml", description: "Agentics checks: verifies generated agent lockfiles, actionlint, and compile on PRs touching workflow files." },
|
|
49
|
+
{ name: "agentics-error-report", file: "agentics-error-report.yml", description: "Daily error report: classifies the last day of failures in the workflows this package ships and files them upstream so the package gets fixed. Deterministic, no model; sends only package-owned names, conclusions and counts, and refuses to file anything a leak scanner flags." },
|
|
47
50
|
{ name: "agentics-maintenance", file: "agentics-maintenance.yml", description: "Agentic maintenance: scheduled daily maintenance workflow for keeping workflows and actions up to date." },
|
|
48
51
|
{ name: "app-ci-dotnet-next", file: "app-ci-dotnet-next.yml", description: "App CI pipeline for a .NET + Next.js monorepo: build, test, and lint on PRs and schedule." },
|
|
49
52
|
{ name: "app-ci-node-monorepo", file: "app-ci-node-monorepo.yml", description: "App CI pipeline for a Node monorepo: build, test, and lint on PRs and schedule." },
|
|
@@ -51,5 +54,4 @@ export const catalogTemplates = [
|
|
|
51
54
|
{ name: "feature-request", file: "feature_request.yml", description: "Feature request issue template scoped to small, well-scoped improvements (Small/Medium only; large work belongs in a planning issue).", target: ".github/ISSUE_TEMPLATE/feature_request.yml" },
|
|
52
55
|
{ name: "github-release", file: "github-release.yml", description: "Publishes or updates a GitHub Release with generated notes whenever a v* tag is pushed." },
|
|
53
56
|
{ name: "opencode.ci.json", file: "opencode.ci.json", description: "Standalone OpenCode CI config: plainconcepts provider, GLM model registration, ci-workflow-agent, and LSP defaults for consumer repositories." },
|
|
54
|
-
{ name: "visual-evidence", file: "visual-evidence.yml", description: "Visual evidence: captures screenshots of UI changes on bot-authored PRs by reading the capturePlan left by the agent in evidence.json and executing it on a runner with Docker and Chrome access." },
|
|
55
57
|
];
|
|
@@ -23,6 +23,26 @@ runs:
|
|
|
23
23
|
github-token: ${{ inputs.token }}
|
|
24
24
|
script: |
|
|
25
25
|
const labels = process.env.LABELS.split(/\r?\n|,/).map((label) => label.trim()).filter(Boolean);
|
|
26
|
+
if (labels.length === 0) {
|
|
27
|
+
core.notice('No labels to add.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create what does not exist yet, so shipping a new label in the package does not
|
|
32
|
+
// require a person to go and make it in every repository first. Its sibling
|
|
33
|
+
// remove-issue-labels has swallowed 404 for a long time; this one had no guard at all,
|
|
34
|
+
// so a label the package introduced would fail the step in every consumer until
|
|
35
|
+
// somebody created it by hand. 422 is "already exists", which is the happy path on
|
|
36
|
+
// every run after the first.
|
|
37
|
+
for (const name of labels) {
|
|
38
|
+
try {
|
|
39
|
+
await github.rest.issues.createLabel({ ...context.repo, name, color: 'ededed' });
|
|
40
|
+
core.notice(`Created the ${name} label.`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.status !== 422) throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
await github.rest.issues.addLabels({
|
|
27
47
|
...context.repo,
|
|
28
48
|
issue_number: Number(process.env.ISSUE_NUMBER),
|