@plainconceptsplatform/workflows 0.1.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/dist/catalog-installation.d.ts +12 -0
- package/dist/catalog-installation.js +81 -0
- package/dist/catalog-installation.test.d.ts +1 -0
- package/dist/catalog-installation.test.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +57 -0
- package/dist/repository-inspection.d.ts +23 -0
- package/dist/repository-inspection.js +91 -0
- package/dist/repository-state.d.ts +18 -0
- package/dist/repository-state.js +77 -0
- package/dist/repository-state.test.d.ts +1 -0
- package/dist/repository-state.test.js +96 -0
- package/dist/workflow-catalog.d.ts +17 -0
- package/dist/workflow-catalog.js +39 -0
- package/dist/workflow-catalog.test.d.ts +1 -0
- package/dist/workflow-catalog.test.js +14 -0
- package/loops/actions/add-issue-labels/action.yml +29 -0
- package/loops/actions/agent-output.cjs +16 -0
- package/loops/actions/apply-agent-bundle/action.yml +23 -0
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +45 -0
- package/loops/actions/apply-agent-comments/action.yml +41 -0
- package/loops/actions/apply-agent-labels/action.yml +54 -0
- package/loops/actions/apply-agent-output/action.yml +107 -0
- package/loops/actions/audit-close/action.yml +103 -0
- package/loops/actions/classify-route/action.yml +90 -0
- package/loops/actions/classify-route/classify-route.sh +172 -0
- package/loops/actions/cleanup-artifacts/action.yml +64 -0
- package/loops/actions/close-agent-issues/action.yml +42 -0
- package/loops/actions/create-agent-issues/action.yml +51 -0
- package/loops/actions/create-issue-comment/action.yml +28 -0
- package/loops/actions/download-agent-output/action.yml +52 -0
- package/loops/actions/identify-gate-subject/action.yml +96 -0
- package/loops/actions/link-pr-to-issue/action.yml +39 -0
- package/loops/actions/list-open-issues/action.yml +32 -0
- package/loops/actions/load-issue-context/action.yml +42 -0
- package/loops/actions/merge-agent-pr/action.yml +35 -0
- package/loops/actions/push-agent-branch/action.yml +44 -0
- package/loops/actions/remove-issue-labels/action.yml +34 -0
- package/loops/actions/stale-recovery/action.yml +287 -0
- package/loops/actions/update-agent-issues/action.yml +57 -0
- package/loops/actions/validate-refine-output/action.yml +43 -0
- package/loops/actions/validate-refine-output/validate-refine-output.sh +46 -0
- package/loops/actions/verify-composite-actions/action.yml +8 -0
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +50 -0
- package/loops/actions/verify-refine-output/action.yml +8 -0
- package/loops/actions/verify-refine-output/verify-refine-output.sh +65 -0
- package/loops/actions/verify-route-matrix/action.yml +8 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +212 -0
- package/loops/aw/repo-config.md +12 -0
- package/loops/scripts/compile-agent-workflows.mjs +13 -0
- package/loops/workflows/agent-apply-review.md +372 -0
- package/loops/workflows/agent-audit.md +197 -0
- package/loops/workflows/agent-direct.md +362 -0
- package/loops/workflows/agent-implement.md +329 -0
- package/loops/workflows/agent-merge-gate.md +470 -0
- package/loops/workflows/agent-propose.md +342 -0
- package/loops/workflows/agent-refine.md +338 -0
- package/loops/workflows/shared/opencode-ci.md +26 -0
- package/loops/workflows/shared/platform-defaults.md +14 -0
- package/loops/workflows/work-router.yml +391 -0
- package/package.json +28 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { repositoryConfigRelativePath } from "./repository-state.js";
|
|
2
|
+
export interface CatalogInstallResult {
|
|
3
|
+
readonly installed: readonly string[];
|
|
4
|
+
readonly conflicts: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface CatalogInstallOptions {
|
|
7
|
+
readonly force?: boolean;
|
|
8
|
+
readonly sourcePath?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function catalogSourcePath(): string;
|
|
11
|
+
export declare function installCatalog(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
12
|
+
export { repositoryConfigRelativePath };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { repositoryConfigRelativePath } from "./repository-state.js";
|
|
6
|
+
const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
|
+
const sourceMappings = [
|
|
8
|
+
["actions", ".github/actions"],
|
|
9
|
+
["workflows", ".github/workflows"],
|
|
10
|
+
["scripts", "scripts"],
|
|
11
|
+
];
|
|
12
|
+
export function catalogSourcePath() {
|
|
13
|
+
return join(packageDirectory, "loops");
|
|
14
|
+
}
|
|
15
|
+
export async function installCatalog(repositoryPath, options = {}) {
|
|
16
|
+
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
17
|
+
const files = await catalogFiles(sourcePath);
|
|
18
|
+
const managedFiles = files.filter((file) => file.managed);
|
|
19
|
+
const conflicts = (await Promise.all(managedFiles.map(async (file) => {
|
|
20
|
+
const destination = join(repositoryPath, file.target);
|
|
21
|
+
return await exists(destination) && !(await filesMatch(file.source, destination)) ? file.target : undefined;
|
|
22
|
+
}))).filter((file) => file !== undefined);
|
|
23
|
+
if (conflicts.length > 0 && !options.force)
|
|
24
|
+
return { installed: [], conflicts };
|
|
25
|
+
await Promise.all(files.map(async (file) => {
|
|
26
|
+
const destination = join(repositoryPath, file.target);
|
|
27
|
+
if (!file.managed && await exists(destination))
|
|
28
|
+
return;
|
|
29
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
30
|
+
await copyFile(file.source, destination);
|
|
31
|
+
}));
|
|
32
|
+
return { installed: files.map((file) => file.target), conflicts };
|
|
33
|
+
}
|
|
34
|
+
async function catalogFiles(sourcePath) {
|
|
35
|
+
const files = [];
|
|
36
|
+
for (const [sourceDirectory, targetDirectory] of sourceMappings) {
|
|
37
|
+
for (const file of await filesIn(join(sourcePath, sourceDirectory))) {
|
|
38
|
+
if (isGeneratedFile(file))
|
|
39
|
+
continue;
|
|
40
|
+
files.push({
|
|
41
|
+
source: join(sourcePath, sourceDirectory, file),
|
|
42
|
+
target: `${targetDirectory}/${file.replaceAll("\\", "/")}`,
|
|
43
|
+
managed: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const repositoryConfigSource = join(sourcePath, "aw", "repo-config.md");
|
|
48
|
+
if (await exists(repositoryConfigSource)) {
|
|
49
|
+
files.push({ source: repositoryConfigSource, target: repositoryConfigRelativePath, managed: false });
|
|
50
|
+
}
|
|
51
|
+
return files.sort((left, right) => left.target.localeCompare(right.target));
|
|
52
|
+
}
|
|
53
|
+
function isGeneratedFile(file) {
|
|
54
|
+
const normalized = file.replaceAll("\\", "/");
|
|
55
|
+
return normalized.endsWith(".lock.yml") || normalized.endsWith("actions-lock.json");
|
|
56
|
+
}
|
|
57
|
+
async function filesIn(path) {
|
|
58
|
+
const entries = await readdir(path, { recursive: true, withFileTypes: true });
|
|
59
|
+
return entries
|
|
60
|
+
.filter((entry) => entry.isFile())
|
|
61
|
+
.map((entry) => entry.parentPath === undefined ? entry.name : relative(path, join(entry.parentPath, entry.name)));
|
|
62
|
+
}
|
|
63
|
+
async function filesMatch(source, destination) {
|
|
64
|
+
try {
|
|
65
|
+
const [sourceContent, destinationContent] = await Promise.all([readFile(source), readFile(destination)]);
|
|
66
|
+
return sourceContent.equals(destinationContent);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function exists(path) {
|
|
73
|
+
try {
|
|
74
|
+
await access(path, constants.F_OK);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export { repositoryConfigRelativePath };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { installCatalog, repositoryConfigRelativePath } from "./catalog-installation.js";
|
|
6
|
+
const temporaryDirectories = [];
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
9
|
+
});
|
|
10
|
+
describe("catalog installation", () => {
|
|
11
|
+
it("installs package-owned loops files and initializes repository config", async () => {
|
|
12
|
+
const sourcePath = await createDirectory({
|
|
13
|
+
"actions/check/action.yml": "name: Check\n",
|
|
14
|
+
"workflows/agent-check.md": "# Check\n",
|
|
15
|
+
"workflows/shared/defaults.md": "defaults\n",
|
|
16
|
+
"scripts/compile.mjs": "console.log('compile');\n",
|
|
17
|
+
"aw/repo-config.md": "# Repository configuration\n",
|
|
18
|
+
"workflows/agent-check.lock.yml": "generated\n",
|
|
19
|
+
"actions/actions-lock.json": "generated\n",
|
|
20
|
+
});
|
|
21
|
+
const repositoryPath = await createDirectory({});
|
|
22
|
+
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
|
|
23
|
+
installed: [
|
|
24
|
+
".github/actions/check/action.yml",
|
|
25
|
+
".github/workflows/agent-check.md",
|
|
26
|
+
".github/workflows/shared/defaults.md",
|
|
27
|
+
repositoryConfigRelativePath,
|
|
28
|
+
"scripts/compile.mjs",
|
|
29
|
+
],
|
|
30
|
+
conflicts: [],
|
|
31
|
+
});
|
|
32
|
+
await expect(readFile(join(repositoryPath, repositoryConfigRelativePath), "utf8")).resolves.toBe("# Repository configuration\n");
|
|
33
|
+
});
|
|
34
|
+
it("does not report identical managed files as conflicts", async () => {
|
|
35
|
+
const sourcePath = await createDirectory({
|
|
36
|
+
"actions/check/action.yml": "name: Check\n",
|
|
37
|
+
"workflows/agent-check.md": "# Check\n",
|
|
38
|
+
"scripts/compile.mjs": "compile\n",
|
|
39
|
+
});
|
|
40
|
+
const repositoryPath = await createDirectory({ ".github/workflows/agent-check.md": "# Check\n" });
|
|
41
|
+
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({ conflicts: [] });
|
|
42
|
+
});
|
|
43
|
+
it("requires force for different managed files and preserves repository config", async () => {
|
|
44
|
+
const sourcePath = await createDirectory({
|
|
45
|
+
"actions/check/action.yml": "package action\n",
|
|
46
|
+
"workflows/agent-check.md": "package workflow\n",
|
|
47
|
+
"scripts/compile.mjs": "package script\n",
|
|
48
|
+
"aw/repo-config.md": "package config\n",
|
|
49
|
+
});
|
|
50
|
+
const repositoryPath = await createDirectory({
|
|
51
|
+
".github/actions/check/action.yml": "consumer action\n",
|
|
52
|
+
".github/workflows/agent-check.md": "consumer workflow\n",
|
|
53
|
+
"scripts/compile.mjs": "consumer script\n",
|
|
54
|
+
[repositoryConfigRelativePath]: "consumer config\n",
|
|
55
|
+
});
|
|
56
|
+
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
|
|
57
|
+
installed: [],
|
|
58
|
+
conflicts: [
|
|
59
|
+
".github/actions/check/action.yml",
|
|
60
|
+
".github/workflows/agent-check.md",
|
|
61
|
+
"scripts/compile.mjs",
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
await expect(installCatalog(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
|
|
65
|
+
conflicts: [
|
|
66
|
+
".github/actions/check/action.yml",
|
|
67
|
+
".github/workflows/agent-check.md",
|
|
68
|
+
"scripts/compile.mjs",
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
await expect(readFile(join(repositoryPath, ".github/actions/check/action.yml"), "utf8")).resolves.toBe("package action\n");
|
|
72
|
+
await expect(readFile(join(repositoryPath, repositoryConfigRelativePath), "utf8")).resolves.toBe("consumer config\n");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
async function createDirectory(files) {
|
|
76
|
+
const directory = await mkdtemp(join(tmpdir(), "platform-workflows-"));
|
|
77
|
+
temporaryDirectories.push(directory);
|
|
78
|
+
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
79
|
+
const path = join(directory, relativePath);
|
|
80
|
+
const { mkdir } = await import("node:fs/promises");
|
|
81
|
+
await mkdir(dirname(path), { recursive: true });
|
|
82
|
+
await writeFile(path, content, "utf8");
|
|
83
|
+
}));
|
|
84
|
+
return directory;
|
|
85
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { inspectRepository, parseVisibility, resolveVisibility } from "./repository-inspection.js";
|
|
3
|
+
import { installCatalog } from "./catalog-installation.js";
|
|
4
|
+
import { initializeRepository, readManifest, repositoryConfigRelativePath } from "./repository-state.js";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
export async function run(arguments_, repositoryPath = process.cwd()) {
|
|
8
|
+
const [command, ...options] = arguments_;
|
|
9
|
+
if (command === "--help" || command === "-h" || command === undefined) {
|
|
10
|
+
console.log("Usage: platform-workflows <init|add|update|status> [--visibility public|private] [--force]");
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
if (command === "init") {
|
|
14
|
+
const visibility = readVisibilityOption(options);
|
|
15
|
+
if (visibility === "invalid")
|
|
16
|
+
return fail("--visibility must be public or private.");
|
|
17
|
+
const inspection = await inspectRepository(repositoryPath);
|
|
18
|
+
const resolvedVisibility = await resolveVisibility(repositoryPath, visibility);
|
|
19
|
+
const result = await initializeRepository(inspection, resolvedVisibility);
|
|
20
|
+
console.log(JSON.stringify({ command, ...result }, null, 2));
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (command === "status") {
|
|
24
|
+
const inspection = await inspectRepository(repositoryPath);
|
|
25
|
+
const manifest = await readManifest(repositoryPath);
|
|
26
|
+
console.log(JSON.stringify({ command, inspection, manifest, repositoryConfigPath: repositoryConfigRelativePath }, null, 2));
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (command === "add" || command === "update") {
|
|
30
|
+
if (options.some((option) => option !== "--force"))
|
|
31
|
+
return fail(`${command} accepts only --force.`);
|
|
32
|
+
const result = await installCatalog(repositoryPath, { force: options.includes("--force") });
|
|
33
|
+
if (result.conflicts.length > 0 && !options.includes("--force")) {
|
|
34
|
+
console.error(`Catalog conflicts found. Re-run with --force to overwrite package-managed files:\n${result.conflicts.join("\n")}`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
console.log(JSON.stringify({ command, ...result }, null, 2));
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
return fail(`Unknown command: ${command}`);
|
|
41
|
+
}
|
|
42
|
+
function readVisibilityOption(options) {
|
|
43
|
+
if (options.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (options.length !== 2 || options[0] !== "--visibility")
|
|
46
|
+
return "invalid";
|
|
47
|
+
return parseVisibility(options[1]) ?? "invalid";
|
|
48
|
+
}
|
|
49
|
+
function fail(message) {
|
|
50
|
+
console.error(message);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
54
|
+
void run(process.argv.slice(2)).then((exitCode) => {
|
|
55
|
+
process.exitCode = exitCode;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RepositoryVisibility = "public" | "private";
|
|
2
|
+
export type VisibilitySource = "argument" | "environment" | "github" | "fallback";
|
|
3
|
+
export interface StackHints {
|
|
4
|
+
readonly packageJson: boolean;
|
|
5
|
+
readonly pnpmLockfile: boolean;
|
|
6
|
+
readonly solutionFiles: readonly string[];
|
|
7
|
+
readonly openSpec: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RepositoryInspection {
|
|
10
|
+
readonly repositoryPath: string;
|
|
11
|
+
readonly existingAgentWorkflows: readonly string[];
|
|
12
|
+
readonly stackHints: StackHints;
|
|
13
|
+
}
|
|
14
|
+
export interface VisibilityResolution {
|
|
15
|
+
readonly value: RepositoryVisibility;
|
|
16
|
+
readonly source: VisibilitySource;
|
|
17
|
+
}
|
|
18
|
+
export interface CommandRunner {
|
|
19
|
+
readonly run: (command: string, arguments_: readonly string[], cwd: string) => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare function inspectRepository(repositoryPath: string): Promise<RepositoryInspection>;
|
|
22
|
+
export declare function resolveVisibility(repositoryPath: string, override: RepositoryVisibility | undefined, environment?: NodeJS.ProcessEnv, commandRunner?: CommandRunner): Promise<VisibilityResolution>;
|
|
23
|
+
export declare function parseVisibility(value: string | undefined): RepositoryVisibility | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { access, readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const defaultCommandRunner = {
|
|
8
|
+
async run(command, arguments_, cwd) {
|
|
9
|
+
const { stdout } = await execFileAsync(command, arguments_, { cwd, windowsHide: true });
|
|
10
|
+
return stdout;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export async function inspectRepository(repositoryPath) {
|
|
14
|
+
const workflowsPath = join(repositoryPath, ".github", "workflows");
|
|
15
|
+
const workflowEntries = await readDirectoryNames(workflowsPath);
|
|
16
|
+
const solutionFiles = await findSolutionFiles(repositoryPath);
|
|
17
|
+
return {
|
|
18
|
+
repositoryPath,
|
|
19
|
+
existingAgentWorkflows: workflowEntries.filter((entry) => /^agent-.*\.md$/i.test(entry)).sort(),
|
|
20
|
+
stackHints: {
|
|
21
|
+
packageJson: await pathExists(join(repositoryPath, "package.json")),
|
|
22
|
+
pnpmLockfile: await pathExists(join(repositoryPath, "pnpm-lock.yaml")),
|
|
23
|
+
solutionFiles,
|
|
24
|
+
openSpec: await isDirectory(join(repositoryPath, "openspec")),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function resolveVisibility(repositoryPath, override, environment = process.env, commandRunner = defaultCommandRunner) {
|
|
29
|
+
if (override !== undefined)
|
|
30
|
+
return { value: override, source: "argument" };
|
|
31
|
+
const environmentOverride = parseVisibility(environment.PLATFORM_WORKFLOWS_VISIBILITY);
|
|
32
|
+
if (environmentOverride !== undefined)
|
|
33
|
+
return { value: environmentOverride, source: "environment" };
|
|
34
|
+
try {
|
|
35
|
+
const output = await commandRunner.run("gh", ["repo", "view", "--json", "visibility"], repositoryPath);
|
|
36
|
+
const visibility = parseVisibilityFromGitHub(output);
|
|
37
|
+
if (visibility !== undefined)
|
|
38
|
+
return { value: visibility, source: "github" };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// GitHub CLI is optional. Initializing a local repository must still work offline.
|
|
42
|
+
}
|
|
43
|
+
return { value: "private", source: "fallback" };
|
|
44
|
+
}
|
|
45
|
+
export function parseVisibility(value) {
|
|
46
|
+
return value === "public" || value === "private" ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
async function findSolutionFiles(repositoryPath) {
|
|
49
|
+
const entries = await readdir(repositoryPath, { recursive: true, withFileTypes: true });
|
|
50
|
+
return entries
|
|
51
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".slnx"))
|
|
52
|
+
.map((entry) => entry.parentPath === undefined ? entry.name : join(entry.parentPath, entry.name))
|
|
53
|
+
.sort();
|
|
54
|
+
}
|
|
55
|
+
async function pathExists(path) {
|
|
56
|
+
try {
|
|
57
|
+
await access(path, constants.F_OK);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function isDirectory(path) {
|
|
65
|
+
try {
|
|
66
|
+
return (await stat(path)).isDirectory();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function readDirectoryNames(path) {
|
|
73
|
+
try {
|
|
74
|
+
return await readdir(path);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseVisibilityFromGitHub(output) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(output);
|
|
83
|
+
if (typeof parsed !== "object" || parsed === null || !("visibility" in parsed))
|
|
84
|
+
return undefined;
|
|
85
|
+
const { visibility } = parsed;
|
|
86
|
+
return typeof visibility === "string" ? parseVisibility(visibility.toLowerCase()) : undefined;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RepositoryInspection, RepositoryVisibility, VisibilityResolution } from "./repository-inspection.js";
|
|
2
|
+
export declare const manifestRelativePath = ".github/workflows/shared/platform-workflows.json";
|
|
3
|
+
export declare const repositoryConfigRelativePath = ".github/workflows/shared/repo-config.md";
|
|
4
|
+
export interface PlatformWorkflowsManifest {
|
|
5
|
+
readonly schemaVersion: 1;
|
|
6
|
+
readonly repositoryPath: string;
|
|
7
|
+
readonly initializedAt: string;
|
|
8
|
+
readonly visibility: RepositoryVisibility;
|
|
9
|
+
readonly visibilitySource: VisibilityResolution["source"];
|
|
10
|
+
readonly existingAgentWorkflows: readonly string[];
|
|
11
|
+
readonly stackHints: RepositoryInspection["stackHints"];
|
|
12
|
+
}
|
|
13
|
+
export interface InitializationResult {
|
|
14
|
+
readonly manifest: PlatformWorkflowsManifest;
|
|
15
|
+
readonly repositoryConfigCreated: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function initializeRepository(inspection: RepositoryInspection, visibility: VisibilityResolution, now?: Date): Promise<InitializationResult>;
|
|
18
|
+
export declare function readManifest(repositoryPath: string): Promise<PlatformWorkflowsManifest | undefined>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export const manifestRelativePath = ".github/workflows/shared/platform-workflows.json";
|
|
4
|
+
export const repositoryConfigRelativePath = ".github/workflows/shared/repo-config.md";
|
|
5
|
+
export async function initializeRepository(inspection, visibility, now = new Date()) {
|
|
6
|
+
const configPath = join(inspection.repositoryPath, repositoryConfigRelativePath);
|
|
7
|
+
const repositoryConfigCreated = await writeRepositoryConfigIfMissing(configPath, inspection, visibility.value);
|
|
8
|
+
const manifest = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
repositoryPath: inspection.repositoryPath,
|
|
11
|
+
initializedAt: now.toISOString(),
|
|
12
|
+
visibility: visibility.value,
|
|
13
|
+
visibilitySource: visibility.source,
|
|
14
|
+
existingAgentWorkflows: inspection.existingAgentWorkflows,
|
|
15
|
+
stackHints: inspection.stackHints,
|
|
16
|
+
};
|
|
17
|
+
await writeFile(join(inspection.repositoryPath, manifestRelativePath), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
18
|
+
return { manifest, repositoryConfigCreated };
|
|
19
|
+
}
|
|
20
|
+
export async function readManifest(repositoryPath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = await readFile(join(repositoryPath, manifestRelativePath), "utf8");
|
|
23
|
+
return parseManifest(JSON.parse(content));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeRepositoryConfigIfMissing(configPath, inspection, visibility) {
|
|
30
|
+
try {
|
|
31
|
+
await readFile(configPath, "utf8");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
36
|
+
await writeFile(configPath, createRepositoryConfig(inspection, visibility), { encoding: "utf8", flag: "wx" });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createRepositoryConfig(inspection, visibility) {
|
|
41
|
+
const stack = [
|
|
42
|
+
inspection.stackHints.packageJson ? "- Node.js (`package.json`)" : undefined,
|
|
43
|
+
inspection.stackHints.pnpmLockfile ? "- pnpm (`pnpm-lock.yaml`)" : undefined,
|
|
44
|
+
...inspection.stackHints.solutionFiles.map((file) => `- .NET solution (${file})`),
|
|
45
|
+
inspection.stackHints.openSpec ? "- OpenSpec (`openspec/`)" : undefined,
|
|
46
|
+
].filter((hint) => hint !== undefined);
|
|
47
|
+
return [
|
|
48
|
+
"# Repository workflow configuration",
|
|
49
|
+
"",
|
|
50
|
+
`- Visibility: ${visibility}`,
|
|
51
|
+
"",
|
|
52
|
+
"## Detected stack",
|
|
53
|
+
"",
|
|
54
|
+
...(stack.length > 0 ? stack : ["- No supported stack markers detected"]),
|
|
55
|
+
"",
|
|
56
|
+
"## Verification commands",
|
|
57
|
+
"",
|
|
58
|
+
"Add repository-specific verification commands here.",
|
|
59
|
+
"",
|
|
60
|
+
"## Repository rules",
|
|
61
|
+
"",
|
|
62
|
+
"Add repository-specific workflow rules here.",
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function parseManifest(value) {
|
|
67
|
+
if (typeof value !== "object" || value === null)
|
|
68
|
+
return undefined;
|
|
69
|
+
const manifest = value;
|
|
70
|
+
if (manifest.schemaVersion !== 1 || typeof manifest.repositoryPath !== "string" || typeof manifest.initializedAt !== "string")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (manifest.visibility !== "public" && manifest.visibility !== "private")
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!Array.isArray(manifest.existingAgentWorkflows) || typeof manifest.stackHints !== "object" || manifest.stackHints === null)
|
|
75
|
+
return undefined;
|
|
76
|
+
return manifest;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { inspectRepository, resolveVisibility } from "./repository-inspection.js";
|
|
6
|
+
import { initializeRepository, manifestRelativePath, readManifest, repositoryConfigRelativePath } from "./repository-state.js";
|
|
7
|
+
import { run } from "./index.js";
|
|
8
|
+
const temporaryDirectories = [];
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
11
|
+
});
|
|
12
|
+
describe("repository inspection", () => {
|
|
13
|
+
it("finds agent workflows and supported stack markers", async () => {
|
|
14
|
+
const repositoryPath = await createRepository({
|
|
15
|
+
".github/workflows/agent-refine.md": "# Refine",
|
|
16
|
+
".github/workflows/agent-custom.md": "# Custom",
|
|
17
|
+
".github/workflows/other.md": "# Other",
|
|
18
|
+
"package.json": "{}",
|
|
19
|
+
"pnpm-lock.yaml": "lockfileVersion: '9.0'",
|
|
20
|
+
"apps/api/Numa.slnx": "<Solution />",
|
|
21
|
+
"openspec/changes/.gitkeep": "",
|
|
22
|
+
});
|
|
23
|
+
await expect(inspectRepository(repositoryPath)).resolves.toMatchObject({
|
|
24
|
+
existingAgentWorkflows: ["agent-custom.md", "agent-refine.md"],
|
|
25
|
+
stackHints: {
|
|
26
|
+
packageJson: true,
|
|
27
|
+
pnpmLockfile: true,
|
|
28
|
+
solutionFiles: [join(repositoryPath, "apps", "api", "Numa.slnx")],
|
|
29
|
+
openSpec: true,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it("uses an explicit visibility before environment or GitHub", async () => {
|
|
34
|
+
const runner = { run: async () => '{"visibility":"public"}' };
|
|
35
|
+
await expect(resolveVisibility("repo", "private", { PLATFORM_WORKFLOWS_VISIBILITY: "public" }, runner)).resolves.toEqual({
|
|
36
|
+
value: "private",
|
|
37
|
+
source: "argument",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("falls back to private when GitHub CLI is unavailable", async () => {
|
|
41
|
+
const runner = { run: async () => Promise.reject(new Error("missing gh")) };
|
|
42
|
+
await expect(resolveVisibility("repo", undefined, {}, runner)).resolves.toEqual({ value: "private", source: "fallback" });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("repository initialization", () => {
|
|
46
|
+
it("creates config once and updates the managed manifest", async () => {
|
|
47
|
+
const repositoryPath = await createRepository({ "package.json": "{}" });
|
|
48
|
+
const inspection = await inspectRepository(repositoryPath);
|
|
49
|
+
const first = await initializeRepository(inspection, { value: "public", source: "argument" }, new Date("2026-08-12T00:00:00.000Z"));
|
|
50
|
+
const configPath = join(repositoryPath, repositoryConfigRelativePath);
|
|
51
|
+
await writeFile(configPath, "consumer settings\n", "utf8");
|
|
52
|
+
const second = await initializeRepository(inspection, { value: "private", source: "fallback" }, new Date("2026-08-13T00:00:00.000Z"));
|
|
53
|
+
expect(first.repositoryConfigCreated).toBe(true);
|
|
54
|
+
expect(second.repositoryConfigCreated).toBe(false);
|
|
55
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe("consumer settings\n");
|
|
56
|
+
await expect(readManifest(repositoryPath)).resolves.toMatchObject({
|
|
57
|
+
initializedAt: "2026-08-13T00:00:00.000Z",
|
|
58
|
+
visibility: "private",
|
|
59
|
+
visibilitySource: "fallback",
|
|
60
|
+
});
|
|
61
|
+
await expect(readFile(join(repositoryPath, manifestRelativePath), "utf8")).resolves.toContain('"schemaVersion": 1');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("CLI commands", () => {
|
|
65
|
+
it("keeps init available", async () => {
|
|
66
|
+
const repositoryPath = await createRepository({ "package.json": "{}" });
|
|
67
|
+
const output = captureConsole("log");
|
|
68
|
+
await expect(run(["init", "--visibility", "public"], repositoryPath)).resolves.toBe(0);
|
|
69
|
+
expect(output.calls).toHaveLength(1);
|
|
70
|
+
output.restore();
|
|
71
|
+
});
|
|
72
|
+
it("accepts update as an add alias", async () => {
|
|
73
|
+
const repositoryPath = await createRepository({});
|
|
74
|
+
const error = captureConsole("error");
|
|
75
|
+
await expect(run(["update", "--invalid"], repositoryPath)).resolves.toBe(1);
|
|
76
|
+
expect(error.calls).toEqual(["update accepts only --force."]);
|
|
77
|
+
error.restore();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
function captureConsole(method) {
|
|
81
|
+
const original = console[method];
|
|
82
|
+
const calls = [];
|
|
83
|
+
console[method] = (message) => calls.push(message);
|
|
84
|
+
return { calls, restore: () => { console[method] = original; } };
|
|
85
|
+
}
|
|
86
|
+
async function createRepository(files) {
|
|
87
|
+
const repositoryPath = await mkdtemp(join(tmpdir(), "platform-workflows-"));
|
|
88
|
+
temporaryDirectories.push(repositoryPath);
|
|
89
|
+
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
90
|
+
const path = join(repositoryPath, relativePath);
|
|
91
|
+
const { mkdir } = await import("node:fs/promises");
|
|
92
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
93
|
+
await writeFile(path, content, "utf8");
|
|
94
|
+
}));
|
|
95
|
+
return repositoryPath;
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const routeNames: readonly ["refine", "implement", "direct", "apply-review", "merge-gate", "audit", "propose"];
|
|
2
|
+
export type RouteName = (typeof routeNames)[number];
|
|
3
|
+
export interface WorkflowRoute {
|
|
4
|
+
readonly name: RouteName;
|
|
5
|
+
readonly worker: string;
|
|
6
|
+
readonly defaultEnabled: boolean;
|
|
7
|
+
}
|
|
8
|
+
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 declare const consumerOwnedTargets: readonly [".github/workflows/shared/repo-config.md"];
|
|
11
|
+
export declare const generatedConsumerTargets: readonly [".github/workflows/agent-*.lock.yml", ".github/workflows/agentics-maintenance.yml", ".github/aw/actions-lock.json"];
|
|
12
|
+
export interface RepositoryConfigTemplate {
|
|
13
|
+
readonly repositoryVisibility: "private" | "public";
|
|
14
|
+
readonly verificationCommands: readonly string[];
|
|
15
|
+
readonly repositoryRules: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const defaultRepositoryConfig: RepositoryConfigTemplate;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const routeNames = [
|
|
2
|
+
"refine",
|
|
3
|
+
"implement",
|
|
4
|
+
"direct",
|
|
5
|
+
"apply-review",
|
|
6
|
+
"merge-gate",
|
|
7
|
+
"audit",
|
|
8
|
+
"propose",
|
|
9
|
+
];
|
|
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 },
|
|
18
|
+
];
|
|
19
|
+
export const packageOwnedTargets = [
|
|
20
|
+
".github/actions",
|
|
21
|
+
".github/workflows/agent-*.md",
|
|
22
|
+
".github/workflows/shared/platform-defaults.md",
|
|
23
|
+
".github/workflows/shared/opencode-ci.md",
|
|
24
|
+
".github/workflows/work-router.yml",
|
|
25
|
+
"scripts/compile-agent-workflows.mjs",
|
|
26
|
+
];
|
|
27
|
+
export const consumerOwnedTargets = [
|
|
28
|
+
".github/workflows/shared/repo-config.md",
|
|
29
|
+
];
|
|
30
|
+
export const generatedConsumerTargets = [
|
|
31
|
+
".github/workflows/agent-*.lock.yml",
|
|
32
|
+
".github/workflows/agentics-maintenance.yml",
|
|
33
|
+
".github/aw/actions-lock.json",
|
|
34
|
+
];
|
|
35
|
+
export const defaultRepositoryConfig = {
|
|
36
|
+
repositoryVisibility: "private",
|
|
37
|
+
verificationCommands: [],
|
|
38
|
+
repositoryRules: [],
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { consumerOwnedTargets, generatedConsumerTargets, packageOwnedTargets, routeNames, workflowRoutes, } from "./workflow-catalog.js";
|
|
3
|
+
describe("workflow catalog", () => {
|
|
4
|
+
it("assigns one worker to each route", () => {
|
|
5
|
+
expect(workflowRoutes.map((route) => route.name)).toEqual(routeNames);
|
|
6
|
+
expect(new Set(workflowRoutes.map((route) => route.worker)).size).toBe(workflowRoutes.length);
|
|
7
|
+
});
|
|
8
|
+
it("keeps generated and consumer-owned files outside package ownership", () => {
|
|
9
|
+
const packageTargets = new Set(packageOwnedTargets);
|
|
10
|
+
for (const target of [...consumerOwnedTargets, ...generatedConsumerTargets]) {
|
|
11
|
+
expect(packageTargets.has(target)).toBe(false);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
});
|