@plainconceptsplatform/workflows 0.1.2 → 0.2.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 +40 -11
- package/dist/catalog-installation.d.ts +11 -2
- package/dist/catalog-installation.js +53 -9
- package/dist/catalog-installation.test.js +198 -16
- package/dist/catalog-listing.d.ts +13 -0
- package/dist/catalog-listing.js +68 -0
- package/dist/catalog-listing.test.d.ts +1 -0
- package/dist/catalog-listing.test.js +137 -0
- package/dist/index.js +76 -12
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +112 -0
- package/dist/repository-inspection.test.d.ts +1 -0
- package/dist/repository-inspection.test.js +77 -0
- package/dist/tui.d.ts +25 -0
- package/dist/tui.js +254 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +246 -0
- package/dist/workflow-catalog.d.ts +15 -8
- package/dist/workflow-catalog.js +25 -15
- package/dist/workflow-catalog.test.js +18 -3
- package/loops/actions/add-issue-labels/action.yml +30 -29
- package/loops/actions/agent-output.cjs +1 -0
- package/loops/actions/apply-agent-bundle/action.yml +24 -23
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +1 -0
- package/loops/actions/apply-agent-comments/action.yml +42 -41
- package/loops/actions/apply-agent-labels/action.yml +55 -54
- package/loops/actions/apply-agent-output/action.yml +108 -107
- package/loops/actions/audit-close/action.yml +104 -103
- package/loops/actions/classify-route/action.yml +91 -90
- package/loops/actions/classify-route/classify-route.sh +1 -0
- package/loops/actions/cleanup-artifacts/action.yml +65 -64
- package/loops/actions/close-agent-issues/action.yml +43 -42
- package/loops/actions/create-agent-issues/action.yml +52 -51
- package/loops/actions/create-issue-comment/action.yml +29 -28
- package/loops/actions/download-agent-output/action.yml +53 -52
- package/loops/actions/identify-gate-subject/action.yml +97 -96
- package/loops/actions/link-pr-to-issue/action.yml +40 -39
- package/loops/actions/list-open-issues/action.yml +33 -32
- package/loops/actions/load-issue-context/action.yml +43 -42
- package/loops/actions/merge-agent-pr/action.yml +36 -35
- package/loops/actions/push-agent-branch/action.yml +45 -44
- package/loops/actions/remove-issue-labels/action.yml +35 -34
- package/loops/actions/stale-recovery/action.yml +288 -287
- package/loops/actions/update-agent-issues/action.yml +58 -57
- package/loops/actions/validate-refine-output/action.yml +44 -43
- package/loops/actions/validate-refine-output/validate-refine-output.sh +1 -0
- package/loops/actions/verify-composite-actions/action.yml +9 -8
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +1 -0
- package/loops/actions/verify-refine-output/action.yml +9 -8
- package/loops/actions/verify-refine-output/verify-refine-output.sh +1 -0
- package/loops/actions/verify-route-matrix/action.yml +9 -8
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +1 -0
- package/loops/scripts/compile-agent-workflows.mjs +29 -13
- package/loops/templates/agentics/agentics-checks.yml +86 -0
- package/loops/templates/agentics/agentics-maintenance.yml +121 -0
- package/loops/templates/ci/app-ci-dotnet-next.yml +167 -0
- package/loops/templates/ci/app-ci-node-monorepo.yml +178 -0
- package/loops/templates/opencode/opencode.ci.json +46 -0
- package/loops/templates/opencode/opencode.ci.json.md +41 -0
- package/loops/workflows/agent-apply-review.md +9 -10
- package/loops/workflows/agent-audit.md +6 -7
- package/loops/workflows/agent-direct.md +7 -8
- package/loops/workflows/agent-implement.md +7 -8
- package/loops/workflows/agent-merge-gate.md +9 -10
- package/loops/workflows/agent-propose.md +7 -8
- package/loops/workflows/agent-refine.md +5 -7
- package/loops/workflows/shared/opencode-ci.md +105 -1
- package/loops/workflows/shared/platform-defaults.md +1 -0
- package/loops/workflows/work-router.yml +1 -0
- package/package.json +2 -2
- package/loops/aw/repo-config.md +0 -12
package/dist/tui.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { formatCatalog, listCatalog } from "./catalog-listing.js";
|
|
3
|
+
import { installCatalog, installMandatoryFiles, installTemplate, isTemplateName } from "./catalog-installation.js";
|
|
4
|
+
const ANSI = {
|
|
5
|
+
clear: "\x1b[2J",
|
|
6
|
+
home: "\x1b[H",
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
cyan: "\x1b[36m",
|
|
11
|
+
green: "\x1b[32m",
|
|
12
|
+
grey: "\x1b[90m",
|
|
13
|
+
white: "\x1b[37m",
|
|
14
|
+
hideCursor: "\x1b[?25l",
|
|
15
|
+
showCursor: "\x1b[?25h",
|
|
16
|
+
};
|
|
17
|
+
export function createSelectionState(entries) {
|
|
18
|
+
const selected = new Set(entries.filter((entry) => entry.installed).map((entry) => entry.name));
|
|
19
|
+
return {
|
|
20
|
+
allItems: entries,
|
|
21
|
+
visibleItems: entries,
|
|
22
|
+
selected,
|
|
23
|
+
cursor: 0,
|
|
24
|
+
filter: "",
|
|
25
|
+
status: "selecting",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function fuzzyMatch(text, query) {
|
|
29
|
+
if (query === "")
|
|
30
|
+
return true;
|
|
31
|
+
const haystack = text.toLowerCase();
|
|
32
|
+
const needle = query.toLowerCase();
|
|
33
|
+
let needleIndex = 0;
|
|
34
|
+
for (let i = 0; i < haystack.length && needleIndex < needle.length; i++) {
|
|
35
|
+
if (haystack[i] === needle[needleIndex])
|
|
36
|
+
needleIndex++;
|
|
37
|
+
}
|
|
38
|
+
return needleIndex === needle.length;
|
|
39
|
+
}
|
|
40
|
+
export function filterItems(items, filter) {
|
|
41
|
+
if (filter === "")
|
|
42
|
+
return items;
|
|
43
|
+
return items.filter((entry) => fuzzyMatch(`${entry.name} ${entry.description}`, filter));
|
|
44
|
+
}
|
|
45
|
+
export function applyFilter(state, filter) {
|
|
46
|
+
const visibleItems = filterItems(state.allItems, filter);
|
|
47
|
+
const cursor = visibleItems.length === 0 ? 0 : Math.min(state.cursor, visibleItems.length - 1);
|
|
48
|
+
return { ...state, visibleItems, filter, cursor };
|
|
49
|
+
}
|
|
50
|
+
export function clearFilter(state) {
|
|
51
|
+
return applyFilter(state, "");
|
|
52
|
+
}
|
|
53
|
+
export function moveCursorUp(state) {
|
|
54
|
+
if (state.visibleItems.length === 0)
|
|
55
|
+
return state;
|
|
56
|
+
const cursor = state.cursor === 0 ? state.visibleItems.length - 1 : state.cursor - 1;
|
|
57
|
+
return { ...state, cursor };
|
|
58
|
+
}
|
|
59
|
+
export function moveCursorDown(state) {
|
|
60
|
+
if (state.visibleItems.length === 0)
|
|
61
|
+
return state;
|
|
62
|
+
const cursor = state.cursor === state.visibleItems.length - 1 ? 0 : state.cursor + 1;
|
|
63
|
+
return { ...state, cursor };
|
|
64
|
+
}
|
|
65
|
+
export function toggleSelection(state) {
|
|
66
|
+
if (state.visibleItems.length === 0)
|
|
67
|
+
return state;
|
|
68
|
+
const entry = state.visibleItems[state.cursor];
|
|
69
|
+
if (entry === undefined)
|
|
70
|
+
return state;
|
|
71
|
+
const selected = new Set(state.selected);
|
|
72
|
+
if (selected.has(entry.name)) {
|
|
73
|
+
selected.delete(entry.name);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
selected.add(entry.name);
|
|
77
|
+
}
|
|
78
|
+
return { ...state, selected };
|
|
79
|
+
}
|
|
80
|
+
export function submitSelection(state) {
|
|
81
|
+
return { ...state, status: "submitting" };
|
|
82
|
+
}
|
|
83
|
+
export function cancelSelection(state) {
|
|
84
|
+
return { ...state, status: "cancelled" };
|
|
85
|
+
}
|
|
86
|
+
export function getItemsToInstall(state, force = false) {
|
|
87
|
+
return state.allItems.filter((entry) => state.selected.has(entry.name) && (force || !entry.installed));
|
|
88
|
+
}
|
|
89
|
+
function renderEntry(entry, highlighted, checked) {
|
|
90
|
+
const indicator = highlighted ? `${ANSI.bold}${ANSI.white}>${ANSI.reset}` : " ";
|
|
91
|
+
if (checked) {
|
|
92
|
+
const checkbox = `${ANSI.green}[x]${ANSI.reset}`;
|
|
93
|
+
const name = highlighted ? `${ANSI.white}${ANSI.bold}${entry.name}${ANSI.reset}` : entry.name;
|
|
94
|
+
const desc = `${ANSI.dim} — ${entry.description}${ANSI.reset}`;
|
|
95
|
+
return `${indicator} ${checkbox} ${name}${desc}`;
|
|
96
|
+
}
|
|
97
|
+
const checkbox = `${ANSI.grey}[ ]${ANSI.reset}`;
|
|
98
|
+
const name = highlighted ? `${ANSI.white}${ANSI.bold}${entry.name}${ANSI.reset}` : entry.name;
|
|
99
|
+
const desc = `${ANSI.dim} — ${entry.description}${ANSI.reset}`;
|
|
100
|
+
return `${indicator} ${checkbox} ${name}${desc}`;
|
|
101
|
+
}
|
|
102
|
+
function render(state) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push(`${ANSI.cyan}${ANSI.bold}Plain Concepts Platform — Agentic Workflows${ANSI.reset}`);
|
|
105
|
+
lines.push(`${ANSI.dim}Select items to install. Already-installed items are pre-checked.${ANSI.reset}`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
if (state.filter) {
|
|
108
|
+
lines.push(`${ANSI.cyan}Filter:${ANSI.reset} ${state.filter}${ANSI.grey}█${ANSI.reset}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
lines.push(`${ANSI.dim}Type to filter, arrows to navigate, space to toggle, Enter to install${ANSI.reset}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push("");
|
|
114
|
+
const routes = state.visibleItems.filter((entry) => entry.kind === "route");
|
|
115
|
+
const templates = state.visibleItems.filter((entry) => entry.kind === "template");
|
|
116
|
+
let index = 0;
|
|
117
|
+
if (routes.length > 0) {
|
|
118
|
+
lines.push(`${ANSI.cyan}Workflows${ANSI.reset}`);
|
|
119
|
+
for (const entry of routes) {
|
|
120
|
+
lines.push(renderEntry(entry, index === state.cursor, state.selected.has(entry.name)));
|
|
121
|
+
index++;
|
|
122
|
+
}
|
|
123
|
+
if (templates.length > 0)
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
if (templates.length > 0) {
|
|
127
|
+
lines.push(`${ANSI.cyan}Templates${ANSI.reset}`);
|
|
128
|
+
for (const entry of templates) {
|
|
129
|
+
lines.push(renderEntry(entry, index === state.cursor, state.selected.has(entry.name)));
|
|
130
|
+
index++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (state.visibleItems.length === 0) {
|
|
134
|
+
lines.push(`${ANSI.grey} No items match the filter.${ANSI.reset}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(`${ANSI.grey}↑↓ navigate space toggle type to filter Enter install Esc clear filter q quit${ANSI.reset}`);
|
|
138
|
+
process.stdout.write(`${ANSI.clear}${ANSI.home}${lines.join("\n")}\n`);
|
|
139
|
+
}
|
|
140
|
+
export async function runInteractive(repositoryPath, options = {}) {
|
|
141
|
+
const force = options.force ?? false;
|
|
142
|
+
if (process.stdin.isTTY !== true) {
|
|
143
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
144
|
+
console.log(formatCatalog(entries));
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log("Run with a TTY for an interactive selection screen, or use: workflows add [--template <name>] [--force]");
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
150
|
+
let state = createSelectionState(entries);
|
|
151
|
+
readline.emitKeypressEvents(process.stdin);
|
|
152
|
+
process.stdin.setRawMode(true);
|
|
153
|
+
process.stdin.resume();
|
|
154
|
+
process.stdout.write(ANSI.hideCursor);
|
|
155
|
+
render(state);
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const onKeypress = (str, key) => {
|
|
158
|
+
if (key?.ctrl && key?.name === "c") {
|
|
159
|
+
cleanup();
|
|
160
|
+
resolve(0);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (str === "q" && state.filter === "" && !key?.ctrl && !key?.meta && !key?.shift) {
|
|
164
|
+
cleanup();
|
|
165
|
+
resolve(0);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key?.name === "escape") {
|
|
169
|
+
state = clearFilter(state);
|
|
170
|
+
render(state);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key?.name === "up") {
|
|
174
|
+
state = moveCursorUp(state);
|
|
175
|
+
render(state);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key?.name === "down") {
|
|
179
|
+
state = moveCursorDown(state);
|
|
180
|
+
render(state);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key?.name === "space" || str === " ") {
|
|
184
|
+
state = toggleSelection(state);
|
|
185
|
+
render(state);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
189
|
+
cleanup();
|
|
190
|
+
void installSelected(state, repositoryPath, force).then((exitCode) => resolve(exitCode));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (key?.name === "backspace") {
|
|
194
|
+
state = applyFilter(state, state.filter.slice(0, -1));
|
|
195
|
+
render(state);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (str !== undefined &&
|
|
199
|
+
str.length === 1 &&
|
|
200
|
+
str >= " " &&
|
|
201
|
+
str <= "~" &&
|
|
202
|
+
!key?.ctrl &&
|
|
203
|
+
!key?.meta) {
|
|
204
|
+
state = applyFilter(state, state.filter + str);
|
|
205
|
+
render(state);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function cleanup() {
|
|
209
|
+
process.stdin.setRawMode(false);
|
|
210
|
+
process.stdin.pause();
|
|
211
|
+
process.stdout.write(`${ANSI.showCursor}${ANSI.reset}`);
|
|
212
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
213
|
+
}
|
|
214
|
+
process.stdin.on("keypress", onKeypress);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function installSelected(state, repositoryPath, force) {
|
|
218
|
+
const items = getItemsToInstall(state, force);
|
|
219
|
+
const routes = items.filter((entry) => entry.kind === "route");
|
|
220
|
+
const templates = items.filter((entry) => entry.kind === "template");
|
|
221
|
+
const allConflicts = [];
|
|
222
|
+
const allInstalled = [];
|
|
223
|
+
if (routes.length > 0) {
|
|
224
|
+
const result = await installCatalog(repositoryPath, { force });
|
|
225
|
+
allConflicts.push(...result.conflicts);
|
|
226
|
+
allInstalled.push(...result.installed);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const result = await installMandatoryFiles(repositoryPath, { force });
|
|
230
|
+
allConflicts.push(...result.conflicts);
|
|
231
|
+
allInstalled.push(...result.installed);
|
|
232
|
+
}
|
|
233
|
+
for (const template of templates) {
|
|
234
|
+
if (!isTemplateName(template.name))
|
|
235
|
+
continue;
|
|
236
|
+
const result = await installTemplate(repositoryPath, template.name, { force });
|
|
237
|
+
allConflicts.push(...result.conflicts);
|
|
238
|
+
allInstalled.push(...result.installed);
|
|
239
|
+
}
|
|
240
|
+
if (allConflicts.length > 0 && !force) {
|
|
241
|
+
console.error(`Conflicts found. Re-run with --force to overwrite:\n${allConflicts.join("\n")}`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
if (allInstalled.length > 0) {
|
|
245
|
+
console.log(`Installed ${allInstalled.length} item(s):`);
|
|
246
|
+
for (const file of allInstalled) {
|
|
247
|
+
console.log(` ${file}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.log("All selected items are already installed.");
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tui.test.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyFilter, cancelSelection, clearFilter, createSelectionState, filterItems, fuzzyMatch, getItemsToInstall, moveCursorDown, moveCursorUp, submitSelection, toggleSelection, } from "./tui.js";
|
|
3
|
+
function makeEntry(name, kind, installed = false) {
|
|
4
|
+
return {
|
|
5
|
+
kind,
|
|
6
|
+
name,
|
|
7
|
+
description: `${name} description`,
|
|
8
|
+
file: `${name}.md`,
|
|
9
|
+
installed,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function makeEntries(installed = []) {
|
|
13
|
+
const routes = ["refine", "implement", "direct", "apply-review", "merge-gate", "audit", "propose"];
|
|
14
|
+
const templates = ["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"];
|
|
15
|
+
return [
|
|
16
|
+
...routes.map((name) => makeEntry(name, "route", installed.includes(name))),
|
|
17
|
+
...templates.map((name) => makeEntry(name, "template", installed.includes(name))),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
describe("createSelectionState", () => {
|
|
21
|
+
it("returns all items as visible initially", () => {
|
|
22
|
+
const entries = makeEntries();
|
|
23
|
+
const state = createSelectionState(entries);
|
|
24
|
+
expect(state.allItems).toHaveLength(12);
|
|
25
|
+
expect(state.visibleItems).toHaveLength(12);
|
|
26
|
+
});
|
|
27
|
+
it("pre-selects installed items", () => {
|
|
28
|
+
const entries = makeEntries(["refine", "agentics-checks"]);
|
|
29
|
+
const state = createSelectionState(entries);
|
|
30
|
+
expect(state.selected.has("refine")).toBe(true);
|
|
31
|
+
expect(state.selected.has("agentics-checks")).toBe(true);
|
|
32
|
+
expect(state.selected.has("implement")).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it("starts with no filter and cursor at 0", () => {
|
|
35
|
+
const state = createSelectionState(makeEntries());
|
|
36
|
+
expect(state.filter).toBe("");
|
|
37
|
+
expect(state.cursor).toBe(0);
|
|
38
|
+
expect(state.status).toBe("selecting");
|
|
39
|
+
});
|
|
40
|
+
it("pre-selects nothing when nothing is installed", () => {
|
|
41
|
+
const state = createSelectionState(makeEntries());
|
|
42
|
+
expect(state.selected.size).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("fuzzyMatch", () => {
|
|
46
|
+
it("returns true for empty query", () => {
|
|
47
|
+
expect(fuzzyMatch("anything", "")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("matches exact string", () => {
|
|
50
|
+
expect(fuzzyMatch("refine", "refine")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("matches subsequence characters", () => {
|
|
53
|
+
expect(fuzzyMatch("refine", "rfe")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("matches non-contiguous characters", () => {
|
|
56
|
+
expect(fuzzyMatch("app-ci-dotnet-next", "acdn")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("is case-insensitive", () => {
|
|
59
|
+
expect(fuzzyMatch("Refine", "REFINE")).toBe(true);
|
|
60
|
+
expect(fuzzyMatch("REFINE", "refine")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it("returns false when characters are not in order", () => {
|
|
63
|
+
expect(fuzzyMatch("refine", "efinr")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("returns false when query has characters not in text", () => {
|
|
66
|
+
expect(fuzzyMatch("refine", "xyz")).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it("matches against full name + description with combined filter", () => {
|
|
69
|
+
const text = "agentics-checks Agentics checks: verifies generated agent lockfiles";
|
|
70
|
+
expect(fuzzyMatch(text, "acch")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("filterItems", () => {
|
|
74
|
+
const entries = makeEntries();
|
|
75
|
+
it("returns all items when filter is empty", () => {
|
|
76
|
+
expect(filterItems(entries, "")).toHaveLength(12);
|
|
77
|
+
});
|
|
78
|
+
it("filters by name", () => {
|
|
79
|
+
const result = filterItems(entries, "refine");
|
|
80
|
+
expect(result).toHaveLength(1);
|
|
81
|
+
expect(result[0].name).toBe("refine");
|
|
82
|
+
});
|
|
83
|
+
it("filters by description", () => {
|
|
84
|
+
const result = filterItems(entries, "description");
|
|
85
|
+
expect(result).toHaveLength(12);
|
|
86
|
+
});
|
|
87
|
+
it("filters by fuzzy subsequence over name and description", () => {
|
|
88
|
+
const result = filterItems(entries, "acdn");
|
|
89
|
+
const names = result.map((e) => e.name);
|
|
90
|
+
expect(names).toContain("app-ci-dotnet-next");
|
|
91
|
+
});
|
|
92
|
+
it("returns empty when nothing matches", () => {
|
|
93
|
+
expect(filterItems(entries, "zzzzz")).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("applyFilter", () => {
|
|
97
|
+
it("updates the filter and visibleItems", () => {
|
|
98
|
+
const state = createSelectionState(makeEntries());
|
|
99
|
+
const next = applyFilter(state, "refine");
|
|
100
|
+
expect(next.filter).toBe("refine");
|
|
101
|
+
expect(next.visibleItems.length).toBe(1);
|
|
102
|
+
expect(next.visibleItems[0].name).toBe("refine");
|
|
103
|
+
});
|
|
104
|
+
it("clamps cursor when result shrinks", () => {
|
|
105
|
+
const entries = makeEntries();
|
|
106
|
+
let state = createSelectionState(entries);
|
|
107
|
+
state = { ...state, cursor: 10 };
|
|
108
|
+
state = applyFilter(state, "ci");
|
|
109
|
+
expect(state.cursor).toBeLessThanOrEqual(Math.max(0, state.visibleItems.length - 1));
|
|
110
|
+
expect(state.cursor).toBeLessThan(state.visibleItems.length);
|
|
111
|
+
});
|
|
112
|
+
it("sets cursor to 0 when no results match", () => {
|
|
113
|
+
const state = applyFilter(createSelectionState(makeEntries()), "zzz");
|
|
114
|
+
expect(state.visibleItems).toHaveLength(0);
|
|
115
|
+
expect(state.cursor).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
it("preserves selected set when filtering", () => {
|
|
118
|
+
const entries = makeEntries(["refine"]);
|
|
119
|
+
const state = applyFilter(createSelectionState(entries), "ref");
|
|
120
|
+
expect(state.selected.has("refine")).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe("clearFilter", () => {
|
|
124
|
+
it("resets filter to empty and restores all items", () => {
|
|
125
|
+
let state = applyFilter(createSelectionState(makeEntries()), "ref");
|
|
126
|
+
state = clearFilter(state);
|
|
127
|
+
expect(state.filter).toBe("");
|
|
128
|
+
expect(state.visibleItems).toHaveLength(12);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("moveCursorUp / moveCursorDown", () => {
|
|
132
|
+
it("moves cursor down by one", () => {
|
|
133
|
+
const state = createSelectionState(makeEntries());
|
|
134
|
+
const next = moveCursorDown(state);
|
|
135
|
+
expect(next.cursor).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
it("wraps cursor to top from bottom", () => {
|
|
138
|
+
const entries = makeEntries();
|
|
139
|
+
let state = createSelectionState(entries);
|
|
140
|
+
state = { ...state, cursor: entries.length - 1 };
|
|
141
|
+
state = moveCursorDown(state);
|
|
142
|
+
expect(state.cursor).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
it("moves cursor up by one", () => {
|
|
145
|
+
const entries = makeEntries();
|
|
146
|
+
let state = createSelectionState(entries);
|
|
147
|
+
state = { ...state, cursor: 3 };
|
|
148
|
+
state = moveCursorUp(state);
|
|
149
|
+
expect(state.cursor).toBe(2);
|
|
150
|
+
});
|
|
151
|
+
it("wraps cursor to bottom from top", () => {
|
|
152
|
+
const state = moveCursorUp(createSelectionState(makeEntries()));
|
|
153
|
+
expect(state.cursor).toBe(11);
|
|
154
|
+
});
|
|
155
|
+
it("does not move when list is empty", () => {
|
|
156
|
+
const entries = [];
|
|
157
|
+
const state = createSelectionState(entries);
|
|
158
|
+
expect(moveCursorUp(state).cursor).toBe(0);
|
|
159
|
+
expect(moveCursorDown(state).cursor).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
it("navigates within filtered results", () => {
|
|
162
|
+
let state = createSelectionState(makeEntries());
|
|
163
|
+
state = applyFilter(state, "ci");
|
|
164
|
+
state = moveCursorDown(state);
|
|
165
|
+
expect(state.cursor).toBe(1);
|
|
166
|
+
expect(state.cursor).toBeLessThan(state.visibleItems.length);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("toggleSelection", () => {
|
|
170
|
+
it("selects an unselected item at cursor", () => {
|
|
171
|
+
const state = createSelectionState(makeEntries());
|
|
172
|
+
expect(state.selected.has("refine")).toBe(false);
|
|
173
|
+
const next = toggleSelection(state);
|
|
174
|
+
expect(next.selected.has("refine")).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
it("deselects a selected item at cursor", () => {
|
|
177
|
+
const entries = makeEntries(["refine"]);
|
|
178
|
+
const state = createSelectionState(entries);
|
|
179
|
+
expect(state.selected.has("refine")).toBe(true);
|
|
180
|
+
const next = toggleSelection(state);
|
|
181
|
+
expect(next.selected.has("refine")).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
it("toggles the item at cursor position within filtered list", () => {
|
|
184
|
+
let state = createSelectionState(makeEntries());
|
|
185
|
+
state = applyFilter(state, "ci");
|
|
186
|
+
state = moveCursorDown(state);
|
|
187
|
+
const entryAtCursor = state.visibleItems[state.cursor];
|
|
188
|
+
const next = toggleSelection(state);
|
|
189
|
+
expect(next.selected.has(entryAtCursor.name)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it("does nothing when list is empty", () => {
|
|
192
|
+
const state = createSelectionState([]);
|
|
193
|
+
const next = toggleSelection(state);
|
|
194
|
+
expect(next.selected.size).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
it("does not change other selections", () => {
|
|
197
|
+
const entries = makeEntries(["refine", "audit"]);
|
|
198
|
+
const state = createSelectionState(entries);
|
|
199
|
+
const next = toggleSelection(state);
|
|
200
|
+
expect(next.selected.has("refine")).toBe(false);
|
|
201
|
+
expect(next.selected.has("audit")).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe("submitSelection / cancelSelection", () => {
|
|
205
|
+
it("sets status to submitting", () => {
|
|
206
|
+
const state = submitSelection(createSelectionState(makeEntries()));
|
|
207
|
+
expect(state.status).toBe("submitting");
|
|
208
|
+
});
|
|
209
|
+
it("sets status to cancelled", () => {
|
|
210
|
+
const state = cancelSelection(createSelectionState(makeEntries()));
|
|
211
|
+
expect(state.status).toBe("cancelled");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe("getItemsToInstall", () => {
|
|
215
|
+
it("returns empty when nothing is selected", () => {
|
|
216
|
+
const state = createSelectionState(makeEntries());
|
|
217
|
+
expect(getItemsToInstall(state)).toHaveLength(0);
|
|
218
|
+
});
|
|
219
|
+
it("returns only selected items that are not already installed", () => {
|
|
220
|
+
const entries = makeEntries(["refine"]);
|
|
221
|
+
let state = createSelectionState(entries);
|
|
222
|
+
state = { ...state, cursor: 1 };
|
|
223
|
+
state = toggleSelection(state);
|
|
224
|
+
const items = getItemsToInstall(state);
|
|
225
|
+
const names = items.map((e) => e.name);
|
|
226
|
+
expect(names).toContain("implement");
|
|
227
|
+
expect(names).not.toContain("refine");
|
|
228
|
+
});
|
|
229
|
+
it("returns already-installed items when force is true", () => {
|
|
230
|
+
const entries = makeEntries(["refine"]);
|
|
231
|
+
let state = createSelectionState(entries);
|
|
232
|
+
const items = getItemsToInstall(state, true);
|
|
233
|
+
const names = items.map((e) => e.name);
|
|
234
|
+
expect(names).toContain("refine");
|
|
235
|
+
});
|
|
236
|
+
it("includes both routes and templates", () => {
|
|
237
|
+
let state = createSelectionState(makeEntries());
|
|
238
|
+
state = toggleSelection(state);
|
|
239
|
+
state = { ...state, cursor: 7 };
|
|
240
|
+
state = toggleSelection(state);
|
|
241
|
+
const items = getItemsToInstall(state);
|
|
242
|
+
const kinds = items.map((e) => e.kind);
|
|
243
|
+
expect(kinds).toContain("route");
|
|
244
|
+
expect(kinds).toContain("template");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -3,15 +3,22 @@ export type RouteName = (typeof routeNames)[number];
|
|
|
3
3
|
export interface WorkflowRoute {
|
|
4
4
|
readonly name: RouteName;
|
|
5
5
|
readonly worker: string;
|
|
6
|
+
readonly description: string;
|
|
6
7
|
readonly defaultEnabled: boolean;
|
|
7
8
|
}
|
|
8
9
|
export declare const workflowRoutes: readonly WorkflowRoute[];
|
|
9
|
-
export declare const packageOwnedTargets: readonly [".github/actions", ".github/workflows/agent-*.md", ".github/workflows/shared/platform-defaults.md", ".github/workflows/shared/opencode-ci.md", ".github/workflows/work-router.yml", "scripts/compile-agent-workflows.mjs"];
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly repositoryVisibility: "private" | "public";
|
|
14
|
-
readonly verificationCommands: readonly string[];
|
|
15
|
-
readonly repositoryRules: readonly string[];
|
|
10
|
+
export declare const packageOwnedTargets: readonly [".github/actions", ".github/workflows/agent-*.md", ".github/workflows/shared/platform-defaults.md", ".github/workflows/shared/opencode-ci.md", ".github/workflows/work-router.yml", "scripts/compile-agent-workflows.mjs", "opencode.ci.json"];
|
|
11
|
+
export interface MandatoryFile {
|
|
12
|
+
readonly source: string;
|
|
13
|
+
readonly target: string;
|
|
16
14
|
}
|
|
17
|
-
export declare const
|
|
15
|
+
export declare const mandatoryFiles: readonly MandatoryFile[];
|
|
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", "opencode.ci.json"];
|
|
18
|
+
export type TemplateName = (typeof templateNames)[number];
|
|
19
|
+
export interface CatalogTemplate {
|
|
20
|
+
readonly name: TemplateName;
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const catalogTemplates: readonly CatalogTemplate[];
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -8,13 +8,13 @@ export const routeNames = [
|
|
|
8
8
|
"propose",
|
|
9
9
|
];
|
|
10
10
|
export const workflowRoutes = [
|
|
11
|
-
{ name: "refine", worker: "agent-refine.md", defaultEnabled: true },
|
|
12
|
-
{ name: "implement", worker: "agent-implement.md", defaultEnabled: true },
|
|
13
|
-
{ name: "direct", worker: "agent-direct.md", defaultEnabled: true },
|
|
14
|
-
{ name: "apply-review", worker: "agent-apply-review.md", defaultEnabled: true },
|
|
15
|
-
{ name: "merge-gate", worker: "agent-merge-gate.md", defaultEnabled: true },
|
|
16
|
-
{ name: "audit", worker: "agent-audit.md", defaultEnabled: true },
|
|
17
|
-
{ name: "propose", worker: "agent-propose.md", defaultEnabled: false },
|
|
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 },
|
|
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 },
|
|
13
|
+
{ 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 },
|
|
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 },
|
|
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 },
|
|
16
|
+
{ name: "audit", worker: "agent-audit.md", description: "Read-only repository audit. Finds 5-7 problems, scores each 1-10, files a single issue with the top 3 refined as actionable user stories.", defaultEnabled: true },
|
|
17
|
+
{ 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 },
|
|
18
18
|
];
|
|
19
19
|
export const packageOwnedTargets = [
|
|
20
20
|
".github/actions",
|
|
@@ -23,17 +23,27 @@ export const packageOwnedTargets = [
|
|
|
23
23
|
".github/workflows/shared/opencode-ci.md",
|
|
24
24
|
".github/workflows/work-router.yml",
|
|
25
25
|
"scripts/compile-agent-workflows.mjs",
|
|
26
|
+
"opencode.ci.json",
|
|
26
27
|
];
|
|
27
|
-
export const
|
|
28
|
-
"
|
|
28
|
+
export const mandatoryFiles = [
|
|
29
|
+
{ source: "templates/opencode/opencode.ci.json", target: "opencode.ci.json" },
|
|
30
|
+
{ source: "scripts/compile-agent-workflows.mjs", target: "scripts/compile-agent-workflows.mjs" },
|
|
29
31
|
];
|
|
30
32
|
export const generatedConsumerTargets = [
|
|
31
33
|
".github/workflows/agent-*.lock.yml",
|
|
32
|
-
".github/workflows/agentics-maintenance.yml",
|
|
33
34
|
".github/aw/actions-lock.json",
|
|
34
35
|
];
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
export const templateNames = [
|
|
37
|
+
"agentics-checks",
|
|
38
|
+
"agentics-maintenance",
|
|
39
|
+
"app-ci-dotnet-next",
|
|
40
|
+
"app-ci-node-monorepo",
|
|
41
|
+
"opencode.ci.json",
|
|
42
|
+
];
|
|
43
|
+
export const catalogTemplates = [
|
|
44
|
+
{ name: "agentics-checks", file: "agentics-checks.yml", description: "Agentics checks: verifies generated agent lockfiles, actionlint, and compile on PRs touching workflow files." },
|
|
45
|
+
{ name: "agentics-maintenance", file: "agentics-maintenance.yml", description: "Agentic maintenance: scheduled daily maintenance workflow for keeping workflows and actions up to date." },
|
|
46
|
+
{ 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." },
|
|
47
|
+
{ 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." },
|
|
48
|
+
{ 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." },
|
|
49
|
+
];
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { catalogTemplates, generatedConsumerTargets, packageOwnedTargets, routeNames, templateNames, workflowRoutes, } from "./workflow-catalog.js";
|
|
3
3
|
describe("workflow catalog", () => {
|
|
4
4
|
it("assigns one worker to each route", () => {
|
|
5
5
|
expect(workflowRoutes.map((route) => route.name)).toEqual(routeNames);
|
|
6
6
|
expect(new Set(workflowRoutes.map((route) => route.worker)).size).toBe(workflowRoutes.length);
|
|
7
7
|
});
|
|
8
|
-
it("
|
|
8
|
+
it("gives every route a non-empty description", () => {
|
|
9
|
+
for (const route of workflowRoutes) {
|
|
10
|
+
expect(route.description.length).toBeGreaterThan(0);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
it("keeps generated files outside package ownership", () => {
|
|
9
14
|
const packageTargets = new Set(packageOwnedTargets);
|
|
10
|
-
for (const target of
|
|
15
|
+
for (const target of generatedConsumerTargets) {
|
|
11
16
|
expect(packageTargets.has(target)).toBe(false);
|
|
12
17
|
}
|
|
13
18
|
});
|
|
19
|
+
it("lists supported optional templates", () => {
|
|
20
|
+
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"]);
|
|
21
|
+
});
|
|
22
|
+
it("gives every catalog template a non-empty description and file", () => {
|
|
23
|
+
expect(catalogTemplates.map((template) => template.name)).toEqual([...templateNames]);
|
|
24
|
+
expect(new Set(catalogTemplates.map((template) => template.file)).size).toBe(catalogTemplates.length);
|
|
25
|
+
for (const template of catalogTemplates) {
|
|
26
|
+
expect(template.description.length).toBeGreaterThan(0);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
14
29
|
});
|