fix: address all 9 review comments from @illegalcall
1. prompt-builder: gate PR/CI instructions on project.repo — use
trimmed BASE_AGENT_PROMPT_NO_REPO when no remote configured
2. orchestrator-prompt: gate Quick Start and Available Commands
sections on project.repo — omit issue/PR commands when absent
3. types.ts: add changeset (minor for ao-core) with migration note
4. config.ts: consistent includes("/") guard for tracker inference
5. start.ts: fail fast with clear error when run in non-git directory
6. start.ts: stricter repo validation regex (requires non-empty
segments on both sides of slash)
7. start.ts: addProjectToConfig now prompts for repo like
autoCreateConfig does, with matching warning message
8. scm-webhooks.ts: pre-filter projects without repo in
findWebhookProjects to skip unnecessary SCM plugin lookups
9. lifecycle-manager.ts: fix GitLab subgroup split — use lastIndexOf
to correctly handle group/subgroup/repo paths
Closes #1154
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ae7c00cef
commit
c8af50fac2
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
"@aoagents/ao-core": minor
|
||||
"@aoagents/ao-cli": patch
|
||||
"@aoagents/ao-web": patch
|
||||
"@aoagents/ao-plugin-scm-github": patch
|
||||
"@aoagents/ao-plugin-scm-gitlab": patch
|
||||
"@aoagents/ao-plugin-tracker-github": patch
|
||||
"@aoagents/ao-plugin-tracker-gitlab": patch
|
||||
---
|
||||
|
||||
Make `ProjectConfig.repo` optional to support projects without a configured remote.
|
||||
|
||||
**Migration:** `ProjectConfig.repo` is now `string | undefined` instead of `string`.
|
||||
External plugins that access `project.repo` directly (e.g. `project.repo.split("/")`) must
|
||||
add a null check first. Use a guard like `if (!project.repo) return null;` or a helper that
|
||||
throws with a descriptive error.
|
||||
|
|
@ -535,6 +535,15 @@ async function autoCreateConfig(workingDir: string): Promise<OrchestratorConfig>
|
|||
console.log(chalk.dim(" Detecting project and generating config...\n"));
|
||||
|
||||
const env = await detectEnvironment(workingDir);
|
||||
|
||||
if (!env.isGitRepo) {
|
||||
throw new Error(
|
||||
`"${workingDir}" is not a git repository.\n` +
|
||||
` ao requires a git repo to manage worktrees and branches.\n` +
|
||||
` Run \`git init\` first, then try again.`,
|
||||
);
|
||||
}
|
||||
|
||||
const projectType = detectProjectType(workingDir);
|
||||
|
||||
// Show detection results
|
||||
|
|
@ -561,7 +570,7 @@ async function autoCreateConfig(workingDir: string): Promise<OrchestratorConfig>
|
|||
const agentRules = generateRulesFromTemplates(projectType);
|
||||
|
||||
// Build config with smart defaults
|
||||
const projectId = env.isGitRepo ? basename(workingDir) : "my-project";
|
||||
const projectId = basename(workingDir);
|
||||
let repo: string | undefined = env.ownerRepo ?? undefined;
|
||||
const path = workingDir;
|
||||
const defaultBranch = env.defaultBranch || "main";
|
||||
|
|
@ -575,7 +584,7 @@ async function autoCreateConfig(workingDir: string): Promise<OrchestratorConfig>
|
|||
"owner/repo",
|
||||
);
|
||||
const trimmed = entered.trim();
|
||||
if (trimmed && trimmed.includes("/")) {
|
||||
if (trimmed && /^[^\s/]+\/[^\s/]+/.test(trimmed)) {
|
||||
repo = trimmed;
|
||||
console.log(chalk.green(` ✓ Repo: ${repo}`));
|
||||
} else if (trimmed) {
|
||||
|
|
@ -696,6 +705,24 @@ async function addProjectToConfig(
|
|||
if (match) ownerRepo = match[1];
|
||||
}
|
||||
|
||||
// If no repo detected, prompt the user (same as autoCreateConfig)
|
||||
/* c8 ignore start -- interactive prompt */
|
||||
if (!ownerRepo && isHumanCaller()) {
|
||||
console.log(chalk.yellow(" ⚠ Could not auto-detect a GitHub/GitLab remote."));
|
||||
const entered = await promptText(
|
||||
" Enter repo (owner/repo) or leave empty to skip:",
|
||||
"owner/repo",
|
||||
);
|
||||
const trimmed = entered.trim();
|
||||
if (trimmed && /^[^\s/]+\/[^\s/]+/.test(trimmed)) {
|
||||
ownerRepo = trimmed;
|
||||
console.log(chalk.green(` ✓ Repo: ${ownerRepo}`));
|
||||
} else if (trimmed) {
|
||||
console.log(chalk.yellow(` ⚠ "${trimmed}" doesn't look like owner/repo — skipping.`));
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
const defaultBranch = await detectDefaultBranch(resolvedPath, ownerRepo);
|
||||
|
||||
// Generate unique session prefix
|
||||
|
|
@ -749,8 +776,8 @@ async function addProjectToConfig(
|
|||
console.log(chalk.green(`\n✓ Added "${projectId}" to ${config.configPath}\n`));
|
||||
|
||||
if (!ownerRepo) {
|
||||
console.log(chalk.yellow("⚠ Could not detect GitHub remote."));
|
||||
console.log(chalk.dim(" Update the 'repo' field in the config before spawning agents.\n"));
|
||||
console.log(chalk.yellow("⚠ No repo configured — issue tracking and PR features will be unavailable."));
|
||||
console.log(chalk.dim(" Add a 'repo' field (owner/repo) to the config to enable them.\n"));
|
||||
}
|
||||
|
||||
return projectId;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { buildPrompt, BASE_AGENT_PROMPT } from "../prompt-builder.js";
|
||||
import { buildPrompt, BASE_AGENT_PROMPT, BASE_AGENT_PROMPT_NO_REPO } from "../prompt-builder.js";
|
||||
import type { ProjectConfig } from "../types.js";
|
||||
|
||||
let tmpDir: string;
|
||||
|
|
@ -55,6 +55,16 @@ describe("buildPrompt", () => {
|
|||
expect(result).toContain("main");
|
||||
});
|
||||
|
||||
it("uses trimmed base prompt when repo is not configured", () => {
|
||||
const noRepoProject = { ...project, repo: undefined };
|
||||
const result = buildPrompt({ project: noRepoProject, projectId: "test-app" });
|
||||
expect(result).toContain(BASE_AGENT_PROMPT_NO_REPO);
|
||||
expect(result).not.toContain(BASE_AGENT_PROMPT);
|
||||
expect(result).not.toContain("create a PR");
|
||||
expect(result).not.toContain("PR Best Practices");
|
||||
expect(result).not.toContain("Repository:");
|
||||
});
|
||||
|
||||
it("includes issue ID in task section", () => {
|
||||
const result = buildPrompt({
|
||||
project,
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ function applyProjectDefaults(config: OrchestratorConfig): OrchestratorConfig {
|
|||
}
|
||||
|
||||
// Infer tracker from repo if not set (default to github issues)
|
||||
if (!project.tracker && project.repo) {
|
||||
if (!project.tracker && project.repo?.includes("/")) {
|
||||
project.tracker = { plugin: inferredPlugin };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export { createLifecycleManager } from "./lifecycle-manager.js";
|
|||
export type { LifecycleManagerDeps } from "./lifecycle-manager.js";
|
||||
|
||||
// Prompt builder — layered prompt composition
|
||||
export { buildPrompt, BASE_AGENT_PROMPT } from "./prompt-builder.js";
|
||||
export { buildPrompt, BASE_AGENT_PROMPT, BASE_AGENT_PROMPT_NO_REPO } from "./prompt-builder.js";
|
||||
export type { PromptBuildConfig } from "./prompt-builder.js";
|
||||
|
||||
// Orchestrator prompt — generates orchestrator context for `ao start`
|
||||
|
|
|
|||
|
|
@ -245,7 +245,11 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
// Find the project for this PR
|
||||
const project = Object.values(config.projects).find((p) => {
|
||||
if (!p.repo) return false;
|
||||
const [owner, repo] = p.repo.split("/");
|
||||
// Use lastIndexOf to correctly handle GitLab subgroup paths (group/subgroup/repo)
|
||||
const slashIdx = p.repo.lastIndexOf("/");
|
||||
if (slashIdx < 0) return false;
|
||||
const owner = p.repo.slice(0, slashIdx);
|
||||
const repo = p.repo.slice(slashIdx + 1);
|
||||
return owner === pr.owner && repo === pr.repo;
|
||||
});
|
||||
if (!project?.scm?.plugin) continue;
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ Your role is to coordinate and manage worker agent sessions. You do NOT write co
|
|||
- **Local Path**: ${project.path}
|
||||
- **Dashboard Port**: ${config.port ?? 3000}`);
|
||||
|
||||
// Quick Start
|
||||
sections.push(`## Quick Start
|
||||
// Quick Start — include issue/PR commands only when repo is configured
|
||||
if (project.repo) {
|
||||
sections.push(`## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# See all sessions at a glance
|
||||
|
|
@ -78,24 +79,51 @@ ao session kill ${project.sessionPrefix}-1
|
|||
# Open all sessions in terminal tabs
|
||||
ao open ${projectId}
|
||||
\`\`\``);
|
||||
} else {
|
||||
sections.push(`## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# See all sessions at a glance
|
||||
ao status
|
||||
|
||||
# Spawn a session (prompt-driven, no issue tracker configured)
|
||||
ao spawn --prompt "Refactor the auth module to use JWT"
|
||||
|
||||
# List sessions
|
||||
ao session ls -p ${projectId}
|
||||
|
||||
# Send message to a session
|
||||
ao send ${project.sessionPrefix}-1 "Your message here"
|
||||
|
||||
# Kill a session
|
||||
ao session kill ${project.sessionPrefix}-1
|
||||
\`\`\`
|
||||
|
||||
> **Note:** No repository remote is configured. Issue tracking, PR, and CI features are unavailable.
|
||||
> Add a \`repo\` field (owner/repo) to \`agent-orchestrator.yaml\` to enable them.`);
|
||||
}
|
||||
|
||||
// Available Commands — omit PR/issue commands when no repo configured
|
||||
const cmdRows = [
|
||||
`| \`ao status\` | Show all sessions${project.repo ? " with PR/CI/review status" : ""} |`,
|
||||
`| \`ao spawn [issue] [--prompt <text>]${project.repo ? " [--claim-pr <pr>]" : ""}\` | Spawn a worker session${project.repo ? "; use issue ID or --prompt for freeform tasks" : " with --prompt for freeform tasks"} |`,
|
||||
...(project.repo ? [`| \`ao batch-spawn <issues...>\` | Spawn multiple sessions in parallel (project auto-detected) |`] : []),
|
||||
`| \`ao session ls [-p project]\` | List all sessions (optionally filter by project) |`,
|
||||
...(project.repo ? [`| \`ao session claim-pr <pr> [session]\` | Attach an existing PR to a worker session |`] : []),
|
||||
`| \`ao session attach <session>\` | Attach to a session's tmux window |`,
|
||||
`| \`ao session kill <session>\` | Kill a specific session |`,
|
||||
`| \`ao session cleanup [-p project]\` | Kill completed/merged sessions |`,
|
||||
`| \`ao send <session> <message>\` | Send a message to a running session |`,
|
||||
`| \`ao send --no-wait <session> <message>\` | Send without waiting for session to become idle |`,
|
||||
`| \`ao dashboard\` | Start the web dashboard (http://localhost:${config.port ?? 3000}) |`,
|
||||
`| \`ao open <project>\` | Open all project sessions in terminal tabs |`,
|
||||
];
|
||||
|
||||
// Available Commands
|
||||
sections.push(`## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| \`ao status\` | Show all sessions with PR/CI/review status |
|
||||
| \`ao spawn [issue] [--prompt <text>] [--claim-pr <pr>]\` | Spawn a worker session; use issue ID or --prompt for freeform tasks |
|
||||
| \`ao batch-spawn <issues...>\` | Spawn multiple sessions in parallel (project auto-detected) |
|
||||
| \`ao session ls [-p project]\` | List all sessions (optionally filter by project) |
|
||||
| \`ao session claim-pr <pr> [session]\` | Attach an existing PR to a worker session |
|
||||
| \`ao session attach <session>\` | Attach to a session's tmux window |
|
||||
| \`ao session kill <session>\` | Kill a specific session |
|
||||
| \`ao session cleanup [-p project]\` | Kill completed/merged sessions |
|
||||
| \`ao send <session> <message>\` | Send a message to a running session |
|
||||
| \`ao send --no-wait <session> <message>\` | Send without waiting for session to become idle |
|
||||
| \`ao dashboard\` | Start the web dashboard (http://localhost:${config.port ?? 3000}) |
|
||||
| \`ao open <project>\` | Open all project sessions in terminal tabs |`);
|
||||
${cmdRows.join("\n")}`);
|
||||
|
||||
// Session Management
|
||||
sections.push(`## Session Management
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@ export const BASE_AGENT_PROMPT = `You are an AI coding agent managed by the Agen
|
|||
- If the repo has CI checks, make sure they pass before requesting review.
|
||||
- Respond to every review comment, even if just to acknowledge it.`;
|
||||
|
||||
/** Trimmed base prompt for projects without a configured repo/remote. */
|
||||
export const BASE_AGENT_PROMPT_NO_REPO = `You are an AI coding agent managed by the Agent Orchestrator (ao).
|
||||
|
||||
## Session Lifecycle
|
||||
- You are running inside a managed session. Focus on the assigned task.
|
||||
- No remote repository is configured — work locally. PR, CI, and review features are unavailable.
|
||||
|
||||
## Git Workflow
|
||||
- Always create a feature branch from the default branch (never commit directly to it).
|
||||
- Use conventional commit messages (feat:, fix:, chore:, etc.).`;
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
|
@ -151,7 +162,8 @@ export function buildPrompt(config: PromptBuildConfig): string {
|
|||
const sections: string[] = [];
|
||||
|
||||
// Layer 1: Base prompt is always included for every managed session.
|
||||
sections.push(BASE_AGENT_PROMPT);
|
||||
// Use trimmed prompt when no repo is configured (PR/CI instructions don't apply).
|
||||
sections.push(config.project.repo ? BASE_AGENT_PROMPT : BASE_AGENT_PROMPT_NO_REPO);
|
||||
|
||||
// Layer 2: Config-derived context
|
||||
sections.push(buildConfigLayer(config));
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function findWebhookProjects(
|
|||
pathname: string,
|
||||
): WebhookProjectMatch[] {
|
||||
return Object.entries(config.projects).flatMap(([projectId, project]) => {
|
||||
if (!project.scm?.plugin) return [];
|
||||
if (!project.repo || !project.scm?.plugin) return [];
|
||||
const webhookPath = getProjectWebhookPath(project);
|
||||
if (!webhookPath || webhookPath !== pathname) return [];
|
||||
const scm = registry.get<SCM>("scm", project.scm.plugin);
|
||||
|
|
|
|||
Loading…
Reference in New Issue