From 2022a0751b477147ec192c0675fe4a4e9aa34edc Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Tue, 26 May 2026 22:51:56 +0530 Subject: [PATCH 1/5] fix: handle unknown update versions and flat interactive config --- packages/cli/__tests__/commands/start.test.ts | 57 +++++++++++++++ .../cli/__tests__/lib/update-check.test.ts | 67 ++++++++++++++++++ packages/cli/src/commands/start.ts | 70 +++++++++++++++++-- packages/cli/src/lib/update-check.ts | 12 ++-- packages/core/src/update-cache.ts | 14 ++-- 5 files changed, 204 insertions(+), 16 deletions(-) diff --git a/packages/cli/__tests__/commands/start.test.ts b/packages/cli/__tests__/commands/start.test.ts index 1b7a3f5935..53f444ef7c 100644 --- a/packages/cli/__tests__/commands/start.test.ts +++ b/packages/cli/__tests__/commands/start.test.ts @@ -20,6 +20,7 @@ import { tmpdir } from "node:os"; import { parse as parseYaml } from "yaml"; import { EventEmitter } from "node:events"; import { + generateExternalId, getDefaultRuntime, recordActivityEvent, type SessionManager, @@ -3208,4 +3209,60 @@ describe("start command — global registry mutations", () => { else process.env["AO_GLOBAL_CONFIG"] = origGlobalEnv; } }); + + it("writes interactive agent overrides to a flat repo-local config", async () => { + const repoDir = join(tmpDir, "current"); + createFakeRepo(repoDir, "https://github.com/org/current.git"); + + const localConfigPath = join(repoDir, "agent-orchestrator.yaml"); + writeFileSync(localConfigPath, "agent: claude-code\n"); + + const projectId = generateExternalId(repoDir, "https://github.com/org/current.git"); + mockConfigRef.current = makeConfig({ + [projectId]: makeProject({ + name: "Current", + path: repoDir, + sessionPrefix: "current", + }), + }); + (mockConfigRef.current as Record).configPath = localConfigPath; + + const detectAgent = await import("../../src/lib/detect-agent.js"); + vi.mocked(detectAgent.detectAvailableAgents).mockResolvedValue([ + { name: "claude-code", displayName: "Claude Code" }, + { name: "codex", displayName: "OpenAI Codex" }, + ]); + mockPromptSelect.mockResolvedValueOnce("claude-code").mockResolvedValueOnce("codex"); + const originalStdinTty = process.stdin.isTTY; + const originalStdoutTty = process.stdout.isTTY; + Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true }); + Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true }); + + try { + await program.parseAsync([ + "node", + "test", + "start", + "--interactive", + "--no-dashboard", + "--no-orchestrator", + ]); + + const localConfig = readFileSync(localConfigPath, "utf-8"); + expect(localConfig).toContain("agent: claude-code"); + expect(localConfig).toContain("orchestrator:"); + expect(localConfig).toContain("worker:"); + expect(localConfig).toContain("agent: codex"); + expect(localConfig).not.toContain("projects:"); + } finally { + Object.defineProperty(process.stdin, "isTTY", { + value: originalStdinTty, + configurable: true, + }); + Object.defineProperty(process.stdout, "isTTY", { + value: originalStdoutTty, + configurable: true, + }); + } + }); }); diff --git a/packages/cli/__tests__/lib/update-check.test.ts b/packages/cli/__tests__/lib/update-check.test.ts index 8897123725..6b8de79267 100644 --- a/packages/cli/__tests__/lib/update-check.test.ts +++ b/packages/cli/__tests__/lib/update-check.test.ts @@ -55,6 +55,10 @@ const { mockGlobalConfig } = vi.hoisted(() => ({ mockGlobalConfig: { value: null as null | { updateChannel?: string; installMethod?: string } }, })); +const { mockGetInstalledAoVersion } = vi.hoisted(() => ({ + mockGetInstalledAoVersion: vi.fn(() => "0.0.0"), +})); + import type * as AoCoreType from "@aoagents/ao-core"; vi.mock("@aoagents/ao-core", async () => { @@ -62,6 +66,7 @@ vi.mock("@aoagents/ao-core", async () => { return { ...actual, loadGlobalConfig: () => mockGlobalConfig.value, + getInstalledAoVersion: () => mockGetInstalledAoVersion(), }; }); @@ -109,6 +114,8 @@ describe("update-check", () => { // Default to nightly so checkForUpdate exercises the registry path. // Individual tests reset this when they need different channel behavior. mockGlobalConfig.value = { updateChannel: "nightly" }; + mockGetInstalledAoVersion.mockReturnValue("0.0.0"); + mockGetCliVersion.mockReturnValue("0.2.2"); }); afterEach(() => { @@ -334,6 +341,20 @@ describe("update-check", () => { const version = getCurrentVersion(); expect(version).toMatch(/^\d+\.\d+\.\d+/); }); + + it("uses the core-installed AO version when it is available", () => { + mockGetInstalledAoVersion.mockReturnValue("0.9.3"); + mockGetCliVersion.mockReturnValue("0.0.0"); + + expect(getCurrentVersion()).toBe("0.9.3"); + }); + + it("falls back to the CLI package version when core only has the placeholder", () => { + mockGetInstalledAoVersion.mockReturnValue("0.0.0"); + mockGetCliVersion.mockReturnValue("0.9.3"); + + expect(getCurrentVersion()).toBe("0.9.3"); + }); }); // ----------------------------------------------------------------------- @@ -1036,6 +1057,52 @@ describe("update-check", () => { expect(output).toContain("npm install -g @aoagents/ao@latest"); }); + it("does not print placeholder 0.0.0 when the current version is unknown", () => { + mockGlobalConfig.value = { updateChannel: "nightly" }; + mockGetInstalledAoVersion.mockReturnValue("0.0.0"); + mockGetCliVersion.mockReturnValue("0.0.0"); + mockReadFileSync.mockReturnValue( + JSON.stringify({ + latestVersion: "0.9.3-nightly-abc", + checkedAt: new Date().toISOString(), + currentVersionAtCheck: "0.0.0", + installMethod: "unknown", + channel: "nightly", + }), + ); + + maybeShowUpdateNotice(); + + expect(stderrSpy).toHaveBeenCalledTimes(1); + const output = stderrSpy.mock.calls[0]![0] as string; + expect(output).toContain("Update available (nightly): update to latest version"); + expect(output).toContain("npm install -g @aoagents/ao@nightly"); + expect(output).not.toContain("0.0.0"); + }); + + it("does not print placeholder 0.0.0 on the stable channel", () => { + mockGlobalConfig.value = { updateChannel: "stable" }; + mockGetInstalledAoVersion.mockReturnValue("0.0.0"); + mockGetCliVersion.mockReturnValue("0.0.0"); + mockReadFileSync.mockReturnValue( + JSON.stringify({ + latestVersion: "0.9.3", + checkedAt: new Date().toISOString(), + currentVersionAtCheck: "0.0.0", + installMethod: "unknown", + channel: "stable", + }), + ); + + maybeShowUpdateNotice(); + + expect(stderrSpy).toHaveBeenCalledTimes(1); + const output = stderrSpy.mock.calls[0]![0] as string; + expect(output).toContain("Update available: update to latest version"); + expect(output).toContain("npm install -g @aoagents/ao@latest"); + expect(output).not.toContain("0.0.0"); + }); + it("prints git update notice from cached git state", () => { mockGlobalConfig.value = { updateChannel: "stable" }; const currentVersion = getCurrentVersion(); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 4f5d86eb01..4d5fd5ead4 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1698,6 +1698,7 @@ export function registerStart(program: Command): void { const agentOverride = opts?.interactive ? await promptAgentSelection() : null; if (agentOverride) { const { orchestratorAgent, workerAgent } = agentOverride; + let updatedProject: ProjectConfig | null = null; if (isCanonicalGlobalConfigPath(config.configPath)) { const nextLocalConfig = readProjectBehaviorConfig(project.path); @@ -1713,15 +1714,70 @@ export function registerStart(program: Command): void { console.log(chalk.dim(` ✓ Saved to ${project.path}/agent-orchestrator.yaml\n`)); } else { const rawYaml = readFileSync(config.configPath, "utf-8"); - const rawConfig = yamlParse(rawYaml); - const proj = rawConfig.projects[projectId]; - proj.orchestrator = { ...(proj.orchestrator ?? {}), agent: orchestratorAgent }; - proj.worker = { ...(proj.worker ?? {}), agent: workerAgent }; - writeFileSync(config.configPath, configToYaml(rawConfig as Record)); + const rawConfig = yamlParse(rawYaml) as Record | null; + const projects = + rawConfig && + typeof rawConfig === "object" && + rawConfig["projects"] && + typeof rawConfig["projects"] === "object" + ? (rawConfig["projects"] as Record | undefined>) + : null; + + if (projects) { + const proj = projects[projectId]; + if (!proj) { + throw new Error(`Project "${projectId}" not found in ${config.configPath}`); + } + proj.orchestrator = { + ...((proj.orchestrator as Record | undefined) ?? {}), + agent: orchestratorAgent, + }; + proj.worker = { + ...((proj.worker as Record | undefined) ?? {}), + agent: workerAgent, + }; + writeFileSync( + config.configPath, + configToYaml(rawConfig as Record), + ); + } else { + const nextLocalConfig = readProjectBehaviorConfig(project.path); + nextLocalConfig.orchestrator = { + ...(nextLocalConfig.orchestrator ?? {}), + agent: orchestratorAgent, + }; + nextLocalConfig.worker = { + ...(nextLocalConfig.worker ?? {}), + agent: workerAgent, + }; + writeProjectBehaviorConfig(project.path, nextLocalConfig); + updatedProject = { + ...project, + orchestrator: { + ...(project.orchestrator ?? {}), + agent: orchestratorAgent, + }, + worker: { + ...(project.worker ?? {}), + agent: workerAgent, + }, + }; + } console.log(chalk.dim(` ✓ Saved to ${config.configPath}\n`)); } - config = loadConfig(config.configPath); - project = config.projects[projectId]; + if (updatedProject) { + project = updatedProject; + config = { + ...config, + projects: { + ...config.projects, + [projectId]: updatedProject, + }, + }; + } else { + config = loadConfig(config.configPath); + project = config.projects[projectId]; + } } const actualPort = await runStartup(config, projectId, project, opts); diff --git a/packages/cli/src/lib/update-check.ts b/packages/cli/src/lib/update-check.ts index 7dc72019f5..fa0bd2df4e 100644 --- a/packages/cli/src/lib/update-check.ts +++ b/packages/cli/src/lib/update-check.ts @@ -597,10 +597,14 @@ export function maybeShowUpdateNotice(): void { const channelSuffix = channel === "nightly" ? " (nightly)" : ""; const command = getUpdateCommand(installMethod, channel); - const message = - installMethod === "git" - ? `\nUpdate available${channelSuffix} from ${cached.latestVersion} — Run: ${command}\n\n` - : `\nUpdate available${channelSuffix}: ${currentVersion} → ${cached.latestVersion} — Run: ${command}\n\n`; + let message: string; + if (installMethod === "git") { + message = `\nUpdate available${channelSuffix} from ${cached.latestVersion} — Run: ${command}\n\n`; + } else if (currentVersion === "0.0.0") { + message = `\nUpdate available${channelSuffix}: update to latest version — Run: ${command}\n\n`; + } else { + message = `\nUpdate available${channelSuffix}: ${currentVersion} → ${cached.latestVersion} — Run: ${command}\n\n`; + } process.stderr.write(message); } diff --git a/packages/core/src/update-cache.ts b/packages/core/src/update-cache.ts index 591b7e301a..0c3dbc7c61 100644 --- a/packages/core/src/update-cache.ts +++ b/packages/core/src/update-cache.ts @@ -71,20 +71,24 @@ export function readUpdateCheckCacheRaw(): UpdateCheckCacheRaw | null { * The currently-installed `@aoagents/ao` version. * * Tries the wrapper package first (the canonical version users see). Falls - * back to `@aoagents/ao-web` for dev mode where the wrapper isn't always in - * `node_modules` — the dashboard ships in lockstep with `@aoagents/ao` (the - * changeset linked group), so the web version is a safe proxy. + * back to the CLI package, then `@aoagents/ao-web` for dev mode where the + * wrapper isn't always in `node_modules` — these packages ship in lockstep + * with `@aoagents/ao` (the changeset linked group), so either is a safe proxy. * * Final fallback returns `"0.0.0"` so callers always have a string to * `isVersionOutdated` against. */ export function getInstalledAoVersion(): string { const require = createRequire(fileURLToPath(import.meta.url)); - const candidates = ["@aoagents/ao/package.json", "@aoagents/ao-web/package.json"]; + const candidates = [ + "@aoagents/ao/package.json", + "@aoagents/ao-cli/package.json", + "@aoagents/ao-web/package.json", + ]; for (const candidate of candidates) { try { const pkg = require(candidate) as { version?: unknown }; - if (typeof pkg.version === "string") return pkg.version; + if (typeof pkg.version === "string" && pkg.version !== "0.0.0") return pkg.version; } catch { // try next candidate } From 0c3156399523f4b5b90fdc30feaf27c4370a1bfe Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Tue, 26 May 2026 23:05:50 +0530 Subject: [PATCH 2/5] ci: pin GitHub Actions to SHAs --- .github/workflows/canary.yml | 6 +++--- .github/workflows/ci.yml | 24 ++++++++++++------------ .github/workflows/coverage.yml | 6 +++--- .github/workflows/deploy-vps.yml | 2 +- .github/workflows/integration-tests.yml | 6 +++--- .github/workflows/onboarding-test.yml | 6 +++--- .github/workflows/release.yml | 8 ++++---- .github/workflows/security.yml | 12 ++++++------ 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 9cdfe5dce7..5399239d97 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -33,7 +33,7 @@ jobs: name: Publish canary runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: main fetch-depth: 0 @@ -79,9 +79,9 @@ jobs: echo "skip=false" >> "$GITHUB_OUTPUT" fi - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d if: steps.check.outputs.skip != 'true' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 if: steps.check.outputs.skip != 'true' with: node-version: 20 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2ab5c11ce..e7d2d2030f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,9 +20,9 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm @@ -33,9 +33,9 @@ jobs: name: Typecheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm @@ -55,9 +55,9 @@ jobs: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm @@ -80,9 +80,9 @@ jobs: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 12ea5ac75e..f9898a7fb1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,12 +19,12 @@ jobs: # Non-blocking: never prevent merging even if this job fails continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: fetch-depth: 0 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm diff --git a/.github/workflows/deploy-vps.yml b/.github/workflows/deploy-vps.yml index 756066eb7a..7773266249 100644 --- a/.github/workflows/deploy-vps.yml +++ b/.github/workflows/deploy-vps.yml @@ -78,7 +78,7 @@ jobs: env: DEPLOY_SHA: ${{ steps.resolve_deploy.outputs.deploy_sha }} FETCH_REF: ${{ steps.resolve_deploy.outputs.fetch_ref }} - uses: appleboy/ssh-action@v1.2.2 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f with: host: ${{ secrets.VPS_HOST }} username: aoagent diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index cb21ad3012..4c11a164d6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -18,9 +18,9 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm diff --git a/.github/workflows/onboarding-test.yml b/.github/workflows/onboarding-test.yml index 704514a8ba..ba6561b426 100644 --- a/.github/workflows/onboarding-test.yml +++ b/.github/workflows/onboarding-test.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 - name: Build test image working-directory: tests/integration @@ -47,7 +47,7 @@ jobs: - name: Upload test logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: onboarding-test-logs path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb2c322cb0..e318134eb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,12 +39,12 @@ jobs: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm @@ -65,7 +65,7 @@ jobs: # `publish:` command. We deliberately omit `publish:` so the action # never runs `changeset publish`. npm publishing is handled by a # private cron that detects the GitHub release. - - uses: changesets/action@v1 + - uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b id: changesets with: version: pnpm changeset version diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 5f8cf574db..8fdc736615 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: fetch-depth: 0 # Full history to ensure base/head SHAs are available for PR scans @@ -80,10 +80,10 @@ jobs: if: github.event_name == 'pull_request' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Dependency Review - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 with: fail-on-severity: moderate @@ -92,13 +92,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 cache: pnpm From 334c6cbe53bb25c17350dc3332753fa360680fb6 Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Tue, 26 May 2026 23:09:08 +0530 Subject: [PATCH 3/5] fix: support new orchestrator from flat config --- packages/cli/__tests__/commands/start.test.ts | 76 +++++++++++++++++++ packages/cli/src/commands/start.ts | 50 +++++++++--- 2 files changed, 117 insertions(+), 9 deletions(-) diff --git a/packages/cli/__tests__/commands/start.test.ts b/packages/cli/__tests__/commands/start.test.ts index 53f444ef7c..72be6811d7 100644 --- a/packages/cli/__tests__/commands/start.test.ts +++ b/packages/cli/__tests__/commands/start.test.ts @@ -2829,6 +2829,82 @@ describe("start command — already-running detection", () => { expect(newKey).toMatch(/^my-app-/); }); + it("creates new orchestrator entry in the global registry when cwd config is flat", async () => { + mockIsAlreadyRunning.mockResolvedValue({ + pid: 9999, + configPath: "/fake/config.yaml", + port: 3000, + startedAt: "2026-01-01T00:00:00Z", + projects: ["agent-orchestrator_5dce9e3fe8"], + }); + + mockPromptSelect.mockResolvedValue("new"); + + const repoDir = join(tmpDir, "agent-orchestrator"); + createFakeRepo(repoDir, "https://github.com/org/agent-orchestrator.git"); + const localConfigPath = join(repoDir, "agent-orchestrator.yaml"); + writeFileSync(localConfigPath, "agent: claude-code\n"); + + const projectId = generateExternalId( + repoDir, + "https://github.com/org/agent-orchestrator.git", + ); + const globalConfigPath = process.env["AO_GLOBAL_CONFIG"]!; + const { stringify: yamlStringify } = await import("yaml"); + writeFileSync( + globalConfigPath, + yamlStringify( + { + defaults: { + runtime: "process", + agent: "claude-code", + workspace: "worktree", + notifiers: [], + }, + projects: { + [projectId]: { + projectId, + path: repoDir, + defaultBranch: "main", + displayName: "Agent Orchestrator", + sessionPrefix: "app", + }, + }, + }, + { indent: 2 }, + ), + ); + + mockConfigRef.current = makeConfig({ + [projectId]: makeProject({ + name: "Agent Orchestrator", + path: repoDir, + sessionPrefix: "app", + }), + }); + (mockConfigRef.current as Record).configPath = localConfigPath; + + try { + await program.parseAsync(["node", "test", "start", "--no-dashboard", "--no-orchestrator"]); + } catch { + // Startup may throw after the config mutation; this test only covers + // the flat-config new-orchestrator mutation path. + } + + const updatedGlobal = parseYaml(readFileSync(globalConfigPath, "utf-8")) as { + projects: Record>; + }; + const projectKeys = Object.keys(updatedGlobal.projects); + expect(projectKeys).toHaveLength(2); + expect(projectKeys).toContain(projectId); + const newKey = projectKeys.find((key) => key !== projectId); + expect(newKey).toMatch(new RegExp(`^${projectId}-`)); + expect(updatedGlobal.projects[newKey!].path).toBe(repoDir); + + const localConfig = readFileSync(localConfigPath, "utf-8"); + expect(localConfig).not.toContain("projects:"); + }); + it("does not mutate YAML when non-TTY caller detects already running (path arg)", async () => { mockIsAlreadyRunning.mockResolvedValue({ pid: 9999, diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 4d5fd5ead4..fb27adce8a 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1630,12 +1630,44 @@ export function registerStart(program: Command): void { // ── Handle "new orchestrator" choice (deferred from already-running check) ── if (startNewOrchestrator) { - const rawYaml = readFileSync(config.configPath, "utf-8"); - const rawConfig = yamlParse(rawYaml); + let mutationConfigPath = config.configPath; + let rawYaml = readFileSync(mutationConfigPath, "utf-8"); + let rawConfig = yamlParse(rawYaml) as Record | null; + let projects = + rawConfig && + typeof rawConfig === "object" && + rawConfig["projects"] && + typeof rawConfig["projects"] === "object" + ? (rawConfig["projects"] as Record | undefined>) + : null; + + if (!projects && !isCanonicalGlobalConfigPath(mutationConfigPath)) { + const globalPath = getGlobalConfigPath(); + if (existsSync(globalPath)) { + mutationConfigPath = globalPath; + rawYaml = readFileSync(mutationConfigPath, "utf-8"); + rawConfig = yamlParse(rawYaml) as Record | null; + projects = + rawConfig && + typeof rawConfig === "object" && + rawConfig["projects"] && + typeof rawConfig["projects"] === "object" + ? (rawConfig["projects"] as Record< + string, + Record | undefined + >) + : null; + } + } + + if (!rawConfig || !projects || !projects[projectId]) { + throw new Error(`Project "${projectId}" not found in a writable project registry.`); + } // Collect existing prefixes to avoid collisions const existingPrefixes = new Set( - Object.values(rawConfig.projects as Record>) + Object.values(projects) + .filter((p): p is Record => p !== undefined) .map((p) => p.sessionPrefix as string) .filter(Boolean), ); @@ -1646,18 +1678,18 @@ export function registerStart(program: Command): void { const suffix = Math.random().toString(36).slice(2, 6); newId = `${projectId}-${suffix}`; newPrefix = generateSessionPrefix(newId); - } while (rawConfig.projects[newId] || existingPrefixes.has(newPrefix)); + } while (projects[newId] || existingPrefixes.has(newPrefix)); - rawConfig.projects[newId] = { - ...rawConfig.projects[projectId], + projects[newId] = { + ...projects[projectId], sessionPrefix: newPrefix, }; - const nextYaml = isCanonicalGlobalConfigPath(config.configPath) + const nextYaml = isCanonicalGlobalConfigPath(mutationConfigPath) ? yamlStringify(rawConfig, { indent: 2 }) : configToYaml(rawConfig as Record); - writeFileSync(config.configPath, nextYaml); + writeFileSync(mutationConfigPath, nextYaml); console.log(chalk.green(`\n✓ New orchestrator "${newId}" added to config\n`)); - config = loadConfig(config.configPath); + config = loadConfig(mutationConfigPath); projectId = newId; project = config.projects[newId]; } From cd40abe3e7e2c7c2f7cd98b39d6a4aa502cee6bf Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Wed, 27 May 2026 01:38:58 +0530 Subject: [PATCH 4/5] ci: skip dependency review on unsupported forks --- .github/workflows/security.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8fdc736615..8ea29c2fec 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -77,7 +77,10 @@ jobs: dependency-review: name: Dependency Review runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + # The dependency-review API requires dependency graph support (and GitHub + # Advanced Security for private repos). Keep it enabled for upstream while + # allowing mirrors/forks without that feature to run the rest of CI. + if: github.event_name == 'pull_request' && github.repository == 'ComposioHQ/agent-orchestrator' steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 From f9a641620f437759d09c6fb7e0ebb78a6baf11e4 Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Wed, 27 May 2026 01:46:48 +0530 Subject: [PATCH 5/5] revert: run dependency review on all PRs --- .github/workflows/security.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8ea29c2fec..8fdc736615 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -77,10 +77,7 @@ jobs: dependency-review: name: Dependency Review runs-on: ubuntu-latest - # The dependency-review API requires dependency graph support (and GitHub - # Advanced Security for private repos). Keep it enabled for upstream while - # allowing mirrors/forks without that feature to run the rest of CI. - if: github.event_name == 'pull_request' && github.repository == 'ComposioHQ/agent-orchestrator' + if: github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683