Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,39 @@ paths:
$ref: '#/components/responses/TypedError'
'404':
$ref: '#/components/responses/TypedError'
/api/settings/github:
get:
tags: [Settings]
summary: Read GitHub commit and PR instructions
responses:
'200':
description: GitHub workflow instructions.
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsGithubResponse'
'404':
$ref: '#/components/responses/TypedError'
patch:
tags: [Settings]
summary: Update GitHub commit and PR instructions
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsGithubUpdateRequest'
responses:
'200':
description: Updated GitHub workflow instructions.
content:
application/json:
schema:
$ref: '#/components/schemas/SettingsGithubResponse'
'400':
$ref: '#/components/responses/TypedError'
'404':
$ref: '#/components/responses/TypedError'
/api/workspaces/{workspaceId}/projects:
parameters:
- $ref: '#/components/parameters/WorkspaceId'
Expand Down Expand Up @@ -846,6 +879,15 @@ components:
type: array
items:
$ref: '#/components/schemas/SettingsReasoningEffort'
SettingsGithubResponse:
type: object
additionalProperties: false
required: [commitInstruction, prInstruction]
properties:
commitInstruction:
type: string
prInstruction:
type: string
SettingsModelStage:
type: object
additionalProperties: false
Expand Down Expand Up @@ -884,6 +926,14 @@ components:
type: array
items:
$ref: '#/components/schemas/SettingsModelStageUpdate'
SettingsGithubUpdateRequest:
type: object
additionalProperties: false
properties:
commitInstruction:
type: string
prInstruction:
type: string
SettingsModelStageUpdate:
type: object
additionalProperties: false
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/features/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
parseOptionalPositiveInt,
parseRecipientsFromEnv,
} from "./env-normalizers";
import {
DEFAULT_GITHUB_COMMIT_INSTRUCTION,
DEFAULT_GITHUB_PR_INSTRUCTION,
} from "./github-instructions";
import type { InstanceServerDatabaseConfig } from "./instance-database-path";
import { normalizeOptionalPath } from "./path-resolution";
import { loadSqliteEnv } from "./sqlite-env";
Expand Down Expand Up @@ -63,6 +67,8 @@ export function buildEnvBase(
github: {
useGhCli: true,
defaultBugLabel: env.GITHUB_BUG_LABEL ?? "bug",
commitInstruction: DEFAULT_GITHUB_COMMIT_INSTRUCTION,
prInstruction: DEFAULT_GITHUB_PR_INSTRUCTION,
},
server: {
database: {
Expand Down
51 changes: 51 additions & 0 deletions packages/cli/src/features/config/github-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {
GithubInstructionContext,
GithubInstructionsConfig,
} from "./types/github-instructions.types";

export const DEFAULT_GITHUB_COMMIT_INSTRUCTION =
"[devos] {issueKey}: {issueTitle}";

export const DEFAULT_GITHUB_PR_INSTRUCTION = [
"Workflow task: {issueKey}",
"",
"This PR was created by the devos.ing ADHD (Agentic Development Hub & Daemon) workflow.",
"",
"Includes:",
"- plan + implement session output",
"- separate review/testing session",
].join("\n");

export function normalizeGithubInstructionsConfig(
value: Record<string, unknown>,
): GithubInstructionsConfig | undefined {
const commitInstruction = normalizeGithubInstruction(value.commitInstruction);
const prInstruction = normalizeGithubInstruction(value.prInstruction);
if (!commitInstruction && !prInstruction) {
return undefined;
}
return {
...(commitInstruction ? { commitInstruction } : {}),
...(prInstruction ? { prInstruction } : {}),
};
}

export function normalizeGithubInstruction(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}

export function renderGithubInstruction(
template: string | undefined,
context: GithubInstructionContext,
defaultTemplate: string,
): string {
const source = normalizeGithubInstruction(template) ?? defaultTemplate;
return source.replace(
/\{(baseBranch|branch|issueKey|issueTitle)\}/g,
(_match, key: keyof GithubInstructionContext) => context[key],
);
}
23 changes: 19 additions & 4 deletions packages/cli/src/features/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { normalizeOptionalValue } from "./env-normalizers";
import { loadInstanceCodexConfig } from "./instance-codex";
import { loadInstanceServerDatabaseConfig } from "./instance-database-path";
import { loadInstanceGithubConfig } from "./instance-github";
import { loadInstanceWorkspaceConfig } from "./instance-workspace";
import { resolveNotifications } from "./notification-resolution";
import { applyInstancePlugins } from "./plugin-resolution";
Expand Down Expand Up @@ -57,8 +58,12 @@ async function loadConfigWithOptions(
const envNotifications = buildEnvNotifications(env);
const workspace = await loadInstanceWorkspaceConfig();
const instanceCodex = await loadInstanceCodexConfig();
const root = createRuntimeRootConfig(instanceCodex);
const defaultProjectRoot = createDefaultProjectRootConfig(instanceCodex);
const instanceGithub = await loadInstanceGithubConfig();
const root = createRuntimeRootConfig(instanceCodex, instanceGithub);
const defaultProjectRoot = createDefaultProjectRootConfig(
instanceCodex,
instanceGithub,
);

const resolvedProjects = resolveProjects(cwd, envBase, root);
const metadataProjects = options.applyServerProjectMetadata
Expand Down Expand Up @@ -89,14 +94,24 @@ async function loadConfigWithOptions(

function createRuntimeRootConfig(
codex: DevosRootConfig["codex"],
github: DevosRootConfig["github"],
): DevosRootConfig {
return { ...(codex ? { codex } : {}), projects: [] };
return {
...(codex ? { codex } : {}),
...(github ? { github } : {}),
projects: [],
};
}

function createDefaultProjectRootConfig(
codex: DevosRootConfig["codex"],
github: DevosRootConfig["github"],
): DevosRootConfig {
return { ...(codex ? { codex } : {}), projects: [{ id: "default" }] };
return {
...(codex ? { codex } : {}),
...(github ? { github } : {}),
projects: [{ id: "default" }],
};
}

export function getProjectById(
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/features/config/instance-github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { readFile } from "node:fs/promises";
import { normalizeGithubInstructionsConfig } from "./github-instructions";
import { instanceConfigPath } from "./home-paths";
import type { GithubInstructionsConfig } from "./types/github-instructions.types";

export async function loadInstanceGithubConfig(
readText: (
targetPath: string,
encoding: BufferEncoding,
) => Promise<string> = readFile,
): Promise<GithubInstructionsConfig | undefined> {
let content: string;
try {
content = await readText(instanceConfigPath(), "utf8");
} catch {
return undefined;
}

try {
const parsed = JSON.parse(content) as unknown;
if (!isRecord(parsed) || !isRecord(parsed.github)) return undefined;
return normalizeGithubInstructionsConfig(parsed.github);
} catch {
return undefined;
}
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface GithubInstructionsConfig {
commitInstruction?: string;
prInstruction?: string;
}

export interface GithubInstructionContext {
baseBranch: string;
branch: string;
issueKey: string;
issueTitle: string;
}
3 changes: 2 additions & 1 deletion packages/cli/src/features/config/types/runtime.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { GithubInstructionsConfig } from "./github-instructions.types";
import type { ServerRuntimeConfig } from "./server.types";

export type CodexReasoningEffort = "low" | "medium" | "high" | "xhigh";
Expand Down Expand Up @@ -38,7 +39,7 @@ export interface ProjectRuntimeConfig {
name: string;
baseBranch: string;
};
github: {
github: GithubInstructionsConfig & {
useGhCli: boolean;
defaultBugLabel: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export interface OnboardCodexConfig {
reasoningEfforts?: Partial<Record<OnboardModelStage, OnboardReasoningEffort>>;
}

export interface OnboardGithubConfig {
commitInstruction?: string;
prInstruction?: string;
}

export interface OnboardInstanceConfig {
$meta: {
version: 1;
Expand Down Expand Up @@ -100,6 +105,7 @@ export interface OnboardInstanceConfig {
};
};
codex?: OnboardCodexConfig;
github?: OnboardGithubConfig;
plugins?: InstancePluginsConfig;
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/features/onboard/types/onboard.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export interface OnboardDraftPromptDeps {

export interface OnboardWizardDeps extends Partial<OnboardDraftPromptDeps> {
runCommand?: OnboardCheckDeps["runCommand"];
write?: (chunk: string) => void;
writeOnboardFiles?: (cwd: string, draft: OnboardDraft) => Promise<void>;
collectOnboardChecks?: (cwd: string) => Promise<OnboardCheck[]>;
configurePluginCredentials?: (
Expand Down
19 changes: 9 additions & 10 deletions packages/cli/src/features/onboard/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,32 +35,31 @@ export async function runOnboardWizard(
const collectChecks = deps.collectOnboardChecks ?? collectOnboardChecks;
const configurePluginCredentials =
deps.configurePluginCredentials ?? configureInstalledPluginCredentials;
process.stdout.write(renderOnboardCustomizationIntro());
const write = deps.write ?? ((chunk: string) => process.stdout.write(chunk));
write(renderOnboardCustomizationIntro());
const rtk = await safeRun(commandRunner, "rtk", ["--version"], cwd);
if (rtk.code !== 0) await promptForRtkInstall(cwd, commandRunner, prompts);
const gh = await safeRun(commandRunner, "gh", ["auth", "status"], cwd);
if (gh.code !== 0) process.stdout.write(renderOnboardGitHubInstallPrompt());
if (gh.code !== 0) write(renderOnboardGitHubInstallPrompt());

const draft = await collectOnboardDraft(cwd, {
prompts,
inferGitHubDefaults: deps.inferGitHubDefaults,
});
await writeFiles(cwd, draft);
await configurePluginCredentials(cwd, prompts);
process.stdout.write(
write(
`${renderCliHeading("Onboarding files written:")}\n${ENV_FILE}\nInstance config: ${instanceConfigPath()}\nSecrets saved to ${sqliteEnvDbPath(cwd)}\n\n`,
);
process.stdout.write(`${renderDevosBanner()}\n`);
process.stdout.write(`devos v${getCliVersion()}\n`);
process.stdout.write(`\n${renderCliHeading("Running doctor checks...")}\n`);
write(`${renderDevosBanner()}\n`);
write(`devos v${getCliVersion()}\n`);
write(`\n${renderCliHeading("Running doctor checks...")}\n`);
const checks = await collectChecks(cwd);
process.stdout.write(formatOnboardChecks(checks));
write(formatOnboardChecks(checks));
if (checks.some((check) => check.status === "fail")) {
throw new Error("Onboard check failed");
}
process.stdout.write(
`${renderCliOutlineBox("Next command", "devos daemon")}\n`,
);
write(`${renderCliOutlineBox("Next command", "devos daemon")}\n`);
}

async function promptForRtkInstall(
Expand Down
42 changes: 31 additions & 11 deletions packages/cli/src/integrations/github/github.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { access } from "node:fs/promises";
import {
DEFAULT_GITHUB_COMMIT_INSTRUCTION,
DEFAULT_GITHUB_PR_INSTRUCTION,
renderGithubInstruction,
} from "../../features/config/github-instructions";
import type {
BugRecord,
PullRequestRef,
Expand Down Expand Up @@ -160,7 +165,17 @@ export async function createDraftPrFromWorktree(
);
}

const commitTitle = `[devos] ${issueKey}: ${issueTitle}`;
const instructionContext = {
baseBranch: config.repo.baseBranch,
branch,
issueKey,
issueTitle,
};
const commitTitle = renderGithubInstruction(
config.github.commitInstruction,
instructionContext,
DEFAULT_GITHUB_COMMIT_INSTRUCTION,
);
await commitChanges(config, commitTitle, {
runCommand: commandRunner,
assertCommandOk: assertOk,
Expand All @@ -175,15 +190,11 @@ export async function createDraftPrFromWorktree(
assertCommandOk: assertOk,
});
const prTitle = `[codex] ${issueKey}: ${issueTitle}`;
const prBody = [
`Workflow task: ${issueKey}`,
"",
"This PR was created by the devos.ing ADHD (Agentic Development Hub & Daemon) workflow.",
"",
"Includes:",
"- plan + implement session output",
"- separate review/testing session",
].join("\n");
const prBody = renderGithubInstruction(
config.github.prInstruction,
instructionContext,
DEFAULT_GITHUB_PR_INSTRUCTION,
);

const create = await withRetries("gh pr create", async () => {
const result = await commandRunner(
Expand Down Expand Up @@ -245,7 +256,16 @@ export async function updateDraftPrFromWorktree(
return false;
}

const commitTitle = `[devos] ${issueKey}: address review feedback`;
const commitTitle = renderGithubInstruction(
config.github.commitInstruction,
{
baseBranch: config.repo.baseBranch,
branch: prBranch,
issueKey,
issueTitle: "address review feedback",
},
DEFAULT_GITHUB_COMMIT_INSTRUCTION,
);
await commitChanges(config, commitTitle, {
runCommand: commandRunner,
assertCommandOk: assertOk,
Expand Down
Loading
Loading