@shenlee/devcrew 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/CONTRIBUTING.md +21 -0
- package/LICENSE +186 -0
- package/README.md +142 -0
- package/README.zh-CN.md +156 -0
- package/SECURITY.md +19 -0
- package/dist/packages/adapters/src/index.js +234 -0
- package/dist/packages/cli/src/index.js +76 -0
- package/dist/packages/core/src/active-run.js +24 -0
- package/dist/packages/core/src/artifacts.js +120 -0
- package/dist/packages/core/src/config.js +51 -0
- package/dist/packages/core/src/index.js +11 -0
- package/dist/packages/core/src/paths.js +51 -0
- package/dist/packages/core/src/standards.js +61 -0
- package/dist/packages/core/src/store.js +24 -0
- package/dist/packages/core/src/types.js +48 -0
- package/dist/packages/core/src/validation.js +52 -0
- package/dist/packages/core/src/verification.js +157 -0
- package/dist/packages/core/src/version.js +2 -0
- package/dist/packages/core/src/workflow.js +166 -0
- package/dist/packages/orchestrator/src/index.js +376 -0
- package/dist/packages/plugins/src/index.js +173 -0
- package/dist/packages/service/src/index.js +2 -0
- package/dist/packages/service/src/stdio.js +100 -0
- package/dist/packages/service/src/tools.js +177 -0
- package/docs/claude-code.md +37 -0
- package/docs/codex.md +80 -0
- package/docs/quickstart.md +64 -0
- package/docs/roles.md +43 -0
- package/docs/workflow.md +70 -0
- package/examples/README.md +11 -0
- package/examples/feature-request.md +13 -0
- package/examples/greenfield-request.md +14 -0
- package/package.json +60 -0
- package/packages/adapters/src/index.ts +404 -0
- package/packages/cli/src/index.ts +88 -0
- package/packages/core/src/active-run.ts +31 -0
- package/packages/core/src/artifacts.ts +148 -0
- package/packages/core/src/config.ts +56 -0
- package/packages/core/src/index.ts +11 -0
- package/packages/core/src/paths.ts +66 -0
- package/packages/core/src/standards.ts +70 -0
- package/packages/core/src/store.ts +28 -0
- package/packages/core/src/types.ts +182 -0
- package/packages/core/src/validation.ts +79 -0
- package/packages/core/src/verification.ts +163 -0
- package/packages/core/src/version.ts +2 -0
- package/packages/core/src/workflow.ts +214 -0
- package/packages/orchestrator/src/index.ts +469 -0
- package/packages/plugins/assets/composer-icon.png +0 -0
- package/packages/plugins/assets/logo.png +0 -0
- package/packages/plugins/src/index.ts +211 -0
- package/packages/service/src/index.ts +2 -0
- package/packages/service/src/stdio.ts +121 -0
- package/packages/service/src/tools.ts +215 -0
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +41 -0
- package/plugins/devcrew-codex/.mcp.json +16 -0
- package/plugins/devcrew-codex/agents/architect.toml +6 -0
- package/plugins/devcrew-codex/agents/implementer.toml +6 -0
- package/plugins/devcrew-codex/agents/pm.toml +6 -0
- package/plugins/devcrew-codex/agents/tester.toml +6 -0
- package/plugins/devcrew-codex/assets/composer-icon.png +0 -0
- package/plugins/devcrew-codex/assets/logo.png +0 -0
- package/plugins/devcrew-codex/skills/devcrew/SKILL.md +17 -0
- package/scripts/smoke-codex-plugin.mjs +331 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { configPath, ensureProjectDirectories } from "./paths.js";
|
|
4
|
+
import type { DevCrewConfig } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG: DevCrewConfig = {
|
|
7
|
+
version: 1,
|
|
8
|
+
defaultBackend: "host-preferred",
|
|
9
|
+
executionMode: "plan",
|
|
10
|
+
verifyCommands: [],
|
|
11
|
+
lintCommands: [],
|
|
12
|
+
coverageCommands: [],
|
|
13
|
+
workflow: {
|
|
14
|
+
gates: ["requirements", "architecture", "implementation", "testing"],
|
|
15
|
+
artifactDirectory: "docs/devcrew",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function exists(path: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await access(path);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function ensureConfig(cwd: string): Promise<DevCrewConfig> {
|
|
29
|
+
await ensureProjectDirectories(cwd);
|
|
30
|
+
const path = configPath(cwd);
|
|
31
|
+
if (!(await exists(path))) {
|
|
32
|
+
await writeFile(path, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, "utf8");
|
|
33
|
+
return DEFAULT_CONFIG;
|
|
34
|
+
}
|
|
35
|
+
return readConfig(cwd);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readConfig(cwd: string): Promise<DevCrewConfig> {
|
|
39
|
+
const raw = await readFile(configPath(cwd), "utf8");
|
|
40
|
+
const parsed = JSON.parse(raw) as DevCrewConfig;
|
|
41
|
+
if (parsed.version !== 1) {
|
|
42
|
+
throw new Error("Unsupported .devcrew/config.json version");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...DEFAULT_CONFIG,
|
|
46
|
+
...parsed,
|
|
47
|
+
executionMode: parsed.executionMode ?? "plan",
|
|
48
|
+
verifyCommands: Array.isArray(parsed.verifyCommands) ? parsed.verifyCommands : [],
|
|
49
|
+
lintCommands: Array.isArray(parsed.lintCommands) ? parsed.lintCommands : [],
|
|
50
|
+
coverageCommands: Array.isArray(parsed.coverageCommands) ? parsed.coverageCommands : [],
|
|
51
|
+
workflow: {
|
|
52
|
+
...DEFAULT_CONFIG.workflow,
|
|
53
|
+
...parsed.workflow,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./active-run.js";
|
|
2
|
+
export * from "./artifacts.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./paths.js";
|
|
5
|
+
export * from "./standards.js";
|
|
6
|
+
export * from "./store.js";
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export * from "./validation.js";
|
|
9
|
+
export * from "./version.js";
|
|
10
|
+
export * from "./verification.js";
|
|
11
|
+
export * from "./workflow.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ArtifactName, RunState } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export function devcrewDir(cwd: string): string {
|
|
7
|
+
return join(cwd, ".devcrew");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function runsDir(cwd: string): string {
|
|
11
|
+
return join(devcrewDir(cwd), "runs");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runDir(cwd: string, runId: string): string {
|
|
15
|
+
return join(runsDir(cwd), runId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function statePath(cwd: string, runId: string): string {
|
|
19
|
+
return join(runDir(cwd, runId), "state.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configPath(cwd: string): string {
|
|
23
|
+
return join(devcrewDir(cwd), "config.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function activeRunPath(cwd: string): string {
|
|
27
|
+
return join(devcrewDir(cwd), "active-run.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function standardsPath(cwd: string): string {
|
|
31
|
+
return join(devcrewDir(cwd), "standards.md");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function docsRoot(cwd: string): string {
|
|
35
|
+
return join(cwd, "docs", "devcrew");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function docsRunDir(cwd: string, runId: string): string {
|
|
39
|
+
return join(docsRoot(cwd), runId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function artifactPath(cwd: string, runId: string, artifact: ArtifactName): string {
|
|
43
|
+
const filenameByArtifact: Record<ArtifactName, string> = {
|
|
44
|
+
requirements: "requirements.md",
|
|
45
|
+
architecture: "architecture.md",
|
|
46
|
+
"implementation-plan": "implementation-plan.md",
|
|
47
|
+
"implementation-review": "implementation-review.md",
|
|
48
|
+
"test-report": "test-report.md",
|
|
49
|
+
acceptance: "acceptance.md",
|
|
50
|
+
};
|
|
51
|
+
return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function ensureRunDirectories(cwd: string, runId: string): Promise<void> {
|
|
55
|
+
await mkdir(runDir(cwd, runId), { recursive: true });
|
|
56
|
+
await mkdir(docsRunDir(cwd, runId), { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function ensureProjectDirectories(cwd: string): Promise<void> {
|
|
60
|
+
await mkdir(devcrewDir(cwd), { recursive: true });
|
|
61
|
+
await mkdir(docsRoot(cwd), { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function relativeArtifactMap(state: RunState): Partial<Record<ArtifactName, string>> {
|
|
65
|
+
return state.artifacts;
|
|
66
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { standardsPath } from "./paths.js";
|
|
5
|
+
import type { StandardsDiscovery } from "./types.js";
|
|
6
|
+
import { readPackageJson } from "./verification.js";
|
|
7
|
+
|
|
8
|
+
async function readIfExists(path: string): Promise<string | undefined> {
|
|
9
|
+
try {
|
|
10
|
+
await access(path);
|
|
11
|
+
return readFile(path, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function section(path: string, content: string): string {
|
|
18
|
+
return `## ${path}\n\n${content.trim()}\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function packageJsonSummary(cwd: string): Promise<string | undefined> {
|
|
22
|
+
const parsed = await readPackageJson(cwd);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const scripts = Object.keys(parsed.scripts ?? {});
|
|
27
|
+
if (scripts.length === 0) {
|
|
28
|
+
return "package.json scripts: none";
|
|
29
|
+
}
|
|
30
|
+
return `package.json scripts: ${scripts.join(", ")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function discoverStandards(cwd: string): Promise<StandardsDiscovery> {
|
|
34
|
+
const candidates = [
|
|
35
|
+
standardsPath(cwd),
|
|
36
|
+
join(cwd, "AGENTS.md"),
|
|
37
|
+
join(cwd, "CLAUDE.md"),
|
|
38
|
+
join(cwd, "README.md"),
|
|
39
|
+
join(cwd, "README"),
|
|
40
|
+
join(cwd, "pyproject.toml"),
|
|
41
|
+
join(cwd, "go.mod"),
|
|
42
|
+
join(cwd, "Cargo.toml"),
|
|
43
|
+
];
|
|
44
|
+
const sources: string[] = [];
|
|
45
|
+
const sections: string[] = [];
|
|
46
|
+
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
const content = await readIfExists(candidate);
|
|
49
|
+
if (content?.trim()) {
|
|
50
|
+
sources.push(candidate);
|
|
51
|
+
sections.push(section(candidate, content));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packageSummary = await packageJsonSummary(cwd);
|
|
56
|
+
if (packageSummary) {
|
|
57
|
+
const packagePath = join(cwd, "package.json");
|
|
58
|
+
sources.push(packagePath);
|
|
59
|
+
sections.push(section(packagePath, packageSummary));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (sections.length === 0) {
|
|
63
|
+
sections.push("No project standards were discovered. Follow the current repository style and ask before broad refactors.\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
sources,
|
|
68
|
+
combined: sections.join("\n"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ensureRunDirectories, statePath } from "./paths.js";
|
|
5
|
+
import type { RunState } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export async function saveState(state: RunState): Promise<RunState> {
|
|
8
|
+
state.updatedAt = new Date().toISOString();
|
|
9
|
+
await ensureRunDirectories(state.cwd, state.runId);
|
|
10
|
+
const target = statePath(state.cwd, state.runId);
|
|
11
|
+
const temp = join(dirname(target), `.state.${process.pid}.${Date.now()}.tmp`);
|
|
12
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
13
|
+
await rename(temp, target);
|
|
14
|
+
return state;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadState(cwd: string, runId: string): Promise<RunState> {
|
|
18
|
+
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw) as RunState;
|
|
20
|
+
return {
|
|
21
|
+
...parsed,
|
|
22
|
+
executionMode: parsed.executionMode ?? "plan",
|
|
23
|
+
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
24
|
+
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
25
|
+
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
26
|
+
lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const WORKFLOW_MODES = ["feature", "greenfield"] as const;
|
|
2
|
+
export const EXECUTION_MODES = ["plan", "apply"] as const;
|
|
3
|
+
export const HOSTS = ["codex", "claude"] as const;
|
|
4
|
+
export const BACKENDS = ["codex", "claude", "local"] as const;
|
|
5
|
+
export const PHASES = [
|
|
6
|
+
"requirements",
|
|
7
|
+
"architecture",
|
|
8
|
+
"implementation",
|
|
9
|
+
"testing",
|
|
10
|
+
"acceptance",
|
|
11
|
+
"complete",
|
|
12
|
+
] as const;
|
|
13
|
+
export const GATES = ["requirements", "architecture", "implementation", "testing"] as const;
|
|
14
|
+
export const ARTIFACTS = [
|
|
15
|
+
"requirements",
|
|
16
|
+
"architecture",
|
|
17
|
+
"implementation-plan",
|
|
18
|
+
"implementation-review",
|
|
19
|
+
"test-report",
|
|
20
|
+
"acceptance",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type WorkflowMode = (typeof WORKFLOW_MODES)[number];
|
|
24
|
+
export type ExecutionMode = (typeof EXECUTION_MODES)[number];
|
|
25
|
+
export type Host = (typeof HOSTS)[number];
|
|
26
|
+
export type BackendName = (typeof BACKENDS)[number];
|
|
27
|
+
export type Phase = (typeof PHASES)[number];
|
|
28
|
+
export type GateName = (typeof GATES)[number];
|
|
29
|
+
export type ArtifactName = (typeof ARTIFACTS)[number];
|
|
30
|
+
export type GateStatus = "not_started" | "pending" | "approved" | "rejected";
|
|
31
|
+
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "complete";
|
|
32
|
+
|
|
33
|
+
export interface RoleSection {
|
|
34
|
+
heading: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ROLE_SECTIONS: Record<Exclude<RoleResult["role"], "conductor">, RoleSection[]> = {
|
|
39
|
+
pm: [
|
|
40
|
+
{ heading: "Functional Scope", description: "explicit In Scope and Out of Scope lists" },
|
|
41
|
+
{ heading: "Users and Scenarios", description: "primary users and their key scenarios" },
|
|
42
|
+
{ heading: "Acceptance Criteria", description: "testable criteria written as Given / When / Then" },
|
|
43
|
+
{ heading: "Priorities", description: "classify each requirement as Must / Should / Could / Won't (MoSCoW)" },
|
|
44
|
+
{ heading: "Open Questions", description: "unresolved clarifications for the requester" },
|
|
45
|
+
],
|
|
46
|
+
architect: [
|
|
47
|
+
{ heading: "Technical Decisions", description: "for each key decision record Decision, Options Considered, Choice, Rationale, and Trade-offs" },
|
|
48
|
+
{ heading: "Interface Contracts", description: "for each interface give the signature, request/response schema, error contract, and data model" },
|
|
49
|
+
{ heading: "Data Flow and Deployment", description: "data flow, deployment expectations, and rollback strategy" },
|
|
50
|
+
{ heading: "Architecture Review Checklist", description: "how the design traces back to the approved requirements" },
|
|
51
|
+
],
|
|
52
|
+
implementer: [
|
|
53
|
+
{ heading: "Implementation Summary", description: "the smallest change that satisfies the approved architecture" },
|
|
54
|
+
{ heading: "Standards Compliance", description: "follow discovered standards and lint/format rules; run available lint/format/typecheck and report results" },
|
|
55
|
+
{ heading: "Changed Files", description: "every file you created or modified" },
|
|
56
|
+
{ heading: "Tests Added or Updated", description: "tests covering success, edge, and failure paths" },
|
|
57
|
+
],
|
|
58
|
+
tester: [
|
|
59
|
+
{ heading: "Test Cases", description: "enumerate cases as a table with ID, Scenario, Type (happy/edge/failure/regression), and Expected result" },
|
|
60
|
+
{ heading: "Coverage", description: "run the coverage command and report the coverage summary plus any gaps" },
|
|
61
|
+
{ heading: "Verification Evidence", description: "exact commands, exit codes, and key output" },
|
|
62
|
+
{ heading: "Known Risks", description: "residual risks and follow-ups" },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface DevCrewConfig {
|
|
67
|
+
version: 1;
|
|
68
|
+
defaultBackend: "host-preferred" | BackendName;
|
|
69
|
+
executionMode: ExecutionMode;
|
|
70
|
+
verifyCommands: string[];
|
|
71
|
+
lintCommands: string[];
|
|
72
|
+
coverageCommands: string[];
|
|
73
|
+
workflow: {
|
|
74
|
+
gates: GateName[];
|
|
75
|
+
artifactDirectory: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StandardsDiscovery {
|
|
80
|
+
sources: string[];
|
|
81
|
+
combined: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RoleResult {
|
|
85
|
+
role: "conductor" | "pm" | "architect" | "implementer" | "tester";
|
|
86
|
+
backend: BackendName;
|
|
87
|
+
summary: string;
|
|
88
|
+
markdown: string;
|
|
89
|
+
usedFallback: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface WorkflowFeedback {
|
|
93
|
+
gate: GateName;
|
|
94
|
+
message: string;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface WorkflowApproval {
|
|
99
|
+
gate: GateName;
|
|
100
|
+
note?: string;
|
|
101
|
+
createdAt: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface WorkflowAnswer {
|
|
105
|
+
answer: string;
|
|
106
|
+
createdAt: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Reused for both verification and lint results — the command shape is identical.
|
|
110
|
+
export interface VerificationResult {
|
|
111
|
+
command: string;
|
|
112
|
+
exitCode: number;
|
|
113
|
+
output: string;
|
|
114
|
+
startedAt: string;
|
|
115
|
+
completedAt: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface RunState {
|
|
119
|
+
version: 1;
|
|
120
|
+
runId: string;
|
|
121
|
+
cwd: string;
|
|
122
|
+
host: Host;
|
|
123
|
+
mode: WorkflowMode;
|
|
124
|
+
executionMode: ExecutionMode;
|
|
125
|
+
backend: BackendName;
|
|
126
|
+
request: string;
|
|
127
|
+
phase: Phase;
|
|
128
|
+
status: RunStatus;
|
|
129
|
+
createdAt: string;
|
|
130
|
+
updatedAt: string;
|
|
131
|
+
gates: Record<GateName, GateStatus>;
|
|
132
|
+
artifacts: Partial<Record<ArtifactName, string>>;
|
|
133
|
+
roles: RoleResult[];
|
|
134
|
+
answers: WorkflowAnswer[];
|
|
135
|
+
approvals: WorkflowApproval[];
|
|
136
|
+
feedback: WorkflowFeedback[];
|
|
137
|
+
standards: StandardsDiscovery;
|
|
138
|
+
changedFiles: string[];
|
|
139
|
+
implementationDiff: string;
|
|
140
|
+
verification: VerificationResult[];
|
|
141
|
+
// VerificationResult is reused for lint output — same shape, different semantics.
|
|
142
|
+
lintResults: VerificationResult[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface StartWorkflowInput {
|
|
146
|
+
cwd: string;
|
|
147
|
+
host: Host;
|
|
148
|
+
mode: WorkflowMode;
|
|
149
|
+
request: string;
|
|
150
|
+
backend?: BackendName;
|
|
151
|
+
executionMode?: ExecutionMode;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RunRef {
|
|
155
|
+
cwd: string;
|
|
156
|
+
runId: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ApproveWorkflowInput extends RunRef {
|
|
160
|
+
gate: GateName;
|
|
161
|
+
note?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface RejectWorkflowInput extends RunRef {
|
|
165
|
+
gate: GateName;
|
|
166
|
+
feedback: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface AnswerWorkflowInput extends RunRef {
|
|
170
|
+
answer: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ArtifactRef extends RunRef {
|
|
174
|
+
name: ArtifactName;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ArtifactReadResult {
|
|
178
|
+
name: ArtifactName;
|
|
179
|
+
path: string;
|
|
180
|
+
content: string;
|
|
181
|
+
summary: string;
|
|
182
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARTIFACTS,
|
|
3
|
+
BACKENDS,
|
|
4
|
+
EXECUTION_MODES,
|
|
5
|
+
GATES,
|
|
6
|
+
HOSTS,
|
|
7
|
+
WORKFLOW_MODES,
|
|
8
|
+
type ArtifactName,
|
|
9
|
+
type BackendName,
|
|
10
|
+
type ExecutionMode,
|
|
11
|
+
type GateName,
|
|
12
|
+
type Host,
|
|
13
|
+
type WorkflowMode,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
function assertString(value: unknown, field: string): string {
|
|
17
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
19
|
+
}
|
|
20
|
+
return value.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function oneOf<T extends readonly string[]>(value: unknown, field: string, values: T): T[number] {
|
|
24
|
+
if (typeof value !== "string" || !values.includes(value)) {
|
|
25
|
+
throw new Error(`${field} must be one of: ${values.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
return value as T[number];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseWorkflowMode(value: unknown): WorkflowMode {
|
|
31
|
+
return oneOf(value, "mode", WORKFLOW_MODES);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseExecutionMode(value: unknown): ExecutionMode {
|
|
35
|
+
return oneOf(value, "executionMode", EXECUTION_MODES);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseHost(value: unknown): Host {
|
|
39
|
+
return oneOf(value, "host", HOSTS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseBackend(value: unknown): BackendName {
|
|
43
|
+
return oneOf(value, "backend", BACKENDS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseGate(value: unknown): GateName {
|
|
47
|
+
return oneOf(value, "gate", GATES);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseArtifactName(value: unknown): ArtifactName {
|
|
51
|
+
return oneOf(value, "name", ARTIFACTS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseCwd(value: unknown): string {
|
|
55
|
+
return assertString(value, "cwd");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseRunId(value: unknown): string {
|
|
59
|
+
return assertString(value, "runId");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseRequest(value: unknown): string {
|
|
63
|
+
return assertString(value, "request");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseOptionalNote(value: unknown): string | undefined {
|
|
67
|
+
if (value === undefined || value === null || value === "") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return assertString(value, "note");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseFeedback(value: unknown): string {
|
|
74
|
+
return assertString(value, "feedback");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseAnswer(value: unknown): string {
|
|
78
|
+
return assertString(value, "answer");
|
|
79
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
async function readIfExists(path: string): Promise<string | undefined> {
|
|
5
|
+
try {
|
|
6
|
+
await access(path);
|
|
7
|
+
return readFile(path, "utf8");
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function exists(path: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await access(path);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function packageVerifyCommands(cwd: string): Promise<string[]> {
|
|
23
|
+
const raw = await readIfExists(join(cwd, "package.json"));
|
|
24
|
+
if (!raw) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw) as { scripts?: Record<string, string> };
|
|
29
|
+
const scripts = parsed.scripts ?? {};
|
|
30
|
+
if (scripts.validate) {
|
|
31
|
+
return ["npm run validate"];
|
|
32
|
+
}
|
|
33
|
+
if (scripts.test) {
|
|
34
|
+
return ["npm test"];
|
|
35
|
+
}
|
|
36
|
+
const commands: string[] = [];
|
|
37
|
+
if (scripts.typecheck) {
|
|
38
|
+
commands.push("npm run typecheck");
|
|
39
|
+
}
|
|
40
|
+
if (scripts.lint) {
|
|
41
|
+
commands.push("npm run lint");
|
|
42
|
+
}
|
|
43
|
+
return commands;
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function pythonVerifyCommands(cwd: string): Promise<string[]> {
|
|
50
|
+
const pyproject = await readIfExists(join(cwd, "pyproject.toml"));
|
|
51
|
+
if (!pyproject) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
if (pyproject.includes("[tool.pytest") || pyproject.includes("pytest")) {
|
|
55
|
+
return ["python -m pytest"];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function discoverVerifyCommands(cwd: string): Promise<string[]> {
|
|
61
|
+
const packageCommands = await packageVerifyCommands(cwd);
|
|
62
|
+
if (packageCommands.length > 0) {
|
|
63
|
+
return packageCommands;
|
|
64
|
+
}
|
|
65
|
+
if (await exists(join(cwd, "go.mod"))) {
|
|
66
|
+
return ["go test ./..."];
|
|
67
|
+
}
|
|
68
|
+
if (await exists(join(cwd, "Cargo.toml"))) {
|
|
69
|
+
return ["cargo test"];
|
|
70
|
+
}
|
|
71
|
+
return pythonVerifyCommands(cwd);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function readPackageJson(cwd: string): Promise<{ scripts?: Record<string, string>; dependencies?: Record<string, string>; devDependencies?: Record<string, string> } | undefined> {
|
|
75
|
+
const raw = await readIfExists(join(cwd, "package.json"));
|
|
76
|
+
if (!raw) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(raw) as { scripts?: Record<string, string>; dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function packageLintCommands(cwd: string): Promise<string[]> {
|
|
87
|
+
const parsed = await readPackageJson(cwd);
|
|
88
|
+
if (!parsed) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const scripts = parsed.scripts ?? {};
|
|
92
|
+
const commands: string[] = [];
|
|
93
|
+
if (scripts.typecheck) {
|
|
94
|
+
commands.push("npm run typecheck");
|
|
95
|
+
}
|
|
96
|
+
if (scripts.lint) {
|
|
97
|
+
commands.push("npm run lint");
|
|
98
|
+
}
|
|
99
|
+
if (scripts["format:check"]) {
|
|
100
|
+
commands.push("npm run format:check");
|
|
101
|
+
}
|
|
102
|
+
return commands;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function discoverLintCommands(cwd: string): Promise<string[]> {
|
|
106
|
+
const packageCommands = await packageLintCommands(cwd);
|
|
107
|
+
if (packageCommands.length > 0) {
|
|
108
|
+
return packageCommands;
|
|
109
|
+
}
|
|
110
|
+
const pyproject = await readIfExists(join(cwd, "pyproject.toml"));
|
|
111
|
+
if (pyproject) {
|
|
112
|
+
const commands: string[] = [];
|
|
113
|
+
if (pyproject.includes("ruff")) {
|
|
114
|
+
commands.push("ruff check .");
|
|
115
|
+
}
|
|
116
|
+
if (pyproject.includes("black")) {
|
|
117
|
+
commands.push("black --check .");
|
|
118
|
+
}
|
|
119
|
+
if (commands.length > 0) {
|
|
120
|
+
return commands;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (await exists(join(cwd, "go.mod"))) {
|
|
124
|
+
return ["files=$(gofmt -l .) && test -z \"$files\" || { printf '%s\\n' \"$files\"; exit 1; }", "go vet ./..."];
|
|
125
|
+
}
|
|
126
|
+
if (await exists(join(cwd, "Cargo.toml"))) {
|
|
127
|
+
return ["cargo fmt --check", "cargo clippy"];
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function packageCoverageCommands(cwd: string): Promise<string[]> {
|
|
133
|
+
const parsed = await readPackageJson(cwd);
|
|
134
|
+
if (!parsed) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const scripts = parsed.scripts ?? {};
|
|
138
|
+
if (scripts.coverage) {
|
|
139
|
+
return ["npm run coverage"];
|
|
140
|
+
}
|
|
141
|
+
const deps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
|
|
142
|
+
const testScript = scripts.test ?? "";
|
|
143
|
+
const usesCoverageRunner = "jest" in deps || "vitest" in deps || testScript.includes("jest") || testScript.includes("vitest");
|
|
144
|
+
if (scripts.test && usesCoverageRunner) {
|
|
145
|
+
return ["npm test -- --coverage"];
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function discoverCoverageCommands(cwd: string): Promise<string[]> {
|
|
151
|
+
const packageCommands = await packageCoverageCommands(cwd);
|
|
152
|
+
if (packageCommands.length > 0) {
|
|
153
|
+
return packageCommands;
|
|
154
|
+
}
|
|
155
|
+
const pyproject = await readIfExists(join(cwd, "pyproject.toml"));
|
|
156
|
+
if (pyproject && (pyproject.includes("[tool.pytest") || pyproject.includes("pytest"))) {
|
|
157
|
+
return ["python -m pytest --cov"];
|
|
158
|
+
}
|
|
159
|
+
if (await exists(join(cwd, "go.mod"))) {
|
|
160
|
+
return ["go test -cover ./..."];
|
|
161
|
+
}
|
|
162
|
+
return [];
|
|
163
|
+
}
|