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
75 changes: 75 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3494,6 +3494,81 @@ describe("adeRpcServer", () => {
});
});

it("normalizes legacy string test summaries for report_result", async () => {
await withEnv({ ADE_RUN_ID: "run-1" }, async () => {
const fixture = createRuntime();
fixture.runtime.orchestratorService.getRunGraph = vi.fn(() => ({
run: { id: "run-1", missionId: "mission-1", status: "running", metadata: {} },
steps: [
{
id: "step-parent",
runId: "run-1",
missionStepId: null,
stepKey: "parent-worker",
stepIndex: 0,
title: "Parent Worker",
laneId: "lane-1",
status: "running",
joinPolicy: "all_success",
quorumCount: null,
dependencyStepIds: [],
retryLimit: 1,
retryCount: 0,
lastAttemptId: "attempt-parent",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
startedAt: new Date().toISOString(),
completedAt: null,
metadata: {}
}
],
attempts: [
{ id: "attempt-parent", stepId: "step-parent", status: "running", createdAt: new Date().toISOString() }
],
claims: [],
contextSnapshots: [],
handoffs: [],
timeline: [],
runtimeEvents: [],
completionEvaluation: null
}));

const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
await initialize(handler, {
callerId: "attempt-parent",
role: "agent",
missionId: "mission-1",
runId: "run-from-identity",
stepId: "step-parent",
attemptId: "attempt-parent"
});

const response = await callTool(handler, "report_result", {
outcome: "succeeded",
summary: "Finished with validation.",
artifacts: [],
filesChanged: [],
testsRun: [
"npm run typecheck (passed)",
"npm test (passed: 2 files, 3 tests)",
"ADE_PROJECT_ROOT=/tmp/app npm run build (passed)"
]
});

expect(response?.isError).toBeUndefined();
expect(response.structuredContent.report.testsRun).toMatchObject({
passed: 3,
failed: 0,
skipped: 0,
raw: expect.stringContaining("npm test (passed: 2 files, 3 tests)")
});
const raw = String(response.structuredContent.report.testsRun.raw ?? "");
expect(raw).toContain("npm run typecheck (passed)");
expect(raw).toContain("npm test (passed: 2 files, 3 tests)");
expect(raw).toContain("ADE_PROJECT_ROOT=/tmp/app npm run build (passed)");
});
});

it("uses trusted env run context for shared-fact writes instead of initialize payload runId", async () => {
await withEnv({ ADE_RUN_ID: "run-from-env" }, async () => {
const fixture = createRuntime();
Expand Down
69 changes: 60 additions & 9 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3990,22 +3990,73 @@ function normalizeWorkerOutcome(raw: unknown): "succeeded" | "failed" | "partial
return "partial";
}

function summarizeLegacyTestsRun(entries: unknown): Record<string, number> | null {
function parseLegacyTestCount(text: string, labels: string[]): number | null {
const escapedLabels = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const labelFirst = new RegExp(`(?:^|[^\\w])(?:${escapedLabels})\\s*:?\\s*(\\d+)\\b`, "i");
const valueFirst = new RegExp(`\\b(\\d+)\\s+(?:${escapedLabels})\\b`, "i");
const match = labelFirst.exec(text) ?? valueFirst.exec(text);
if (!match?.[1]) return null;
const value = Number(match[1]);
return Number.isFinite(value) ? value : null;
}

function summarizeLegacyTestsRun(entries: unknown): Record<string, unknown> | null {
if (!Array.isArray(entries)) return null;
let passed = 0;
let failed = 0;
let skipped = 0;
const statusCounts = { passed: 0, failed: 0, skipped: 0 };
const counted = { passed: 0, failed: 0, skipped: 0 };
const rawEntries: string[] = [];
const commands: string[] = [];
let sawNumericCount = false;
for (const entry of entries) {
const result = asOptionalTrimmedString(safeObject(entry).result)?.toLowerCase() ?? "";
const record = safeObject(entry);
const stringEntry = typeof entry === "string" ? entry.trim() : "";
const command = asOptionalTrimmedString(record.command) ?? asOptionalTrimmedString(record.name) ?? "";
const result = asOptionalTrimmedString(record.result)?.toLowerCase() ?? "";
const text = [stringEntry, command, result, asOptionalTrimmedString(record.raw) ?? ""].filter((part) => part.length > 0).join(" ");
if (text.trim().length > 0) rawEntries.push(text.trim());
if (command) commands.push(command);

const failedCount = parseLegacyTestCount(text, ["failed", "failures", "failure", "fail"]);
const skippedCount = parseLegacyTestCount(text, ["skipped", "skip"]);
const passedFromTests =
/\b(?:pass|passed|passing|success|successful)\b/i.test(text)
? /\b(\d+)\s+tests?\b/i.exec(text)?.[1]
: null;
const passedCount = passedFromTests && Number.isFinite(Number(passedFromTests))
? Number(passedFromTests)
: parseLegacyTestCount(text, ["passed", "passing", "pass"]);
if (passedCount != null || failedCount != null || skippedCount != null) {
sawNumericCount = true;
counted.passed += passedCount ?? 0;
counted.failed += failedCount ?? 0;
counted.skipped += skippedCount ?? 0;
continue;
}

if (result === "pass" || result === "passed" || result === "success" || result === "succeeded") {
passed += 1;
statusCounts.passed += 1;
} else if (!result && /\b(?:pass|passed|success|succeeded)\b/i.test(text) && !/\b(?:fail|failed|error)\b/i.test(text)) {
statusCounts.passed += 1;
} else if (result === "fail" || result === "failed" || result === "error") {
failed += 1;
statusCounts.failed += 1;
} else if (!result && /\b(?:fail|failed|failure|error)\b/i.test(text)) {
statusCounts.failed += 1;
} else {
skipped += 1;
statusCounts.skipped += 1;
}
}
return { passed, failed, skipped };
const summary = sawNumericCount
? {
passed: counted.passed,
failed: counted.failed + statusCounts.failed,
skipped: counted.skipped + statusCounts.skipped,
}
: statusCounts;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return {
...summary,
...(commands.length > 0 ? { command: commands.join("; ") } : {}),
...(rawEntries.length > 0 ? { raw: rawEntries.join("\n") } : {}),
};
}

function normalizeCoordinatorWorkerToolArgs(args: {
Expand Down
18 changes: 15 additions & 3 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { createConflictService } from "../../desktop/src/main/services/conflicts
import { createGitOperationsService } from "../../desktop/src/main/services/git/gitOperationsService";
import { createDiffService } from "../../desktop/src/main/services/diffs/diffService";
import { createMissionService } from "../../desktop/src/main/services/missions/missionService";
import type { createMissionPreflightService } from "../../desktop/src/main/services/missions/missionPreflightService";
import { createMissionPreflightService } from "../../desktop/src/main/services/missions/missionPreflightService";
import { createPtyService } from "../../desktop/src/main/services/pty/ptyService";
import { createTestService } from "../../desktop/src/main/services/tests/testService";
import { createKeybindingsService } from "../../desktop/src/main/services/keybindings/keybindingsService";
Expand Down Expand Up @@ -284,14 +284,14 @@ function ensureAdeCliShim(entryPath: string): { dir: string; path: string } | nu
fs.mkdirSync(shimDir, { recursive: true });
if (process.platform === "win32") {
const body = isJavaScriptCliEntry(entryPath)
? `@echo off\r\n"${process.execPath}" "${entryPath}" %*\r\n`
? `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${process.execPath}" "${entryPath}" %*\r\n`
: `@echo off\r\n"${entryPath}" %*\r\n`;
if (!fs.existsSync(shimPath) || fs.readFileSync(shimPath, "utf8") !== body) {
fs.writeFileSync(shimPath, body, "utf8");
}
} else {
const body = isJavaScriptCliEntry(entryPath)
? `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(entryPath)} "$@"\n`
? `#!/bin/sh\nELECTRON_RUN_AS_NODE=1 exec ${JSON.stringify(process.execPath)} ${JSON.stringify(entryPath)} "$@"\n`
: `#!/bin/sh\nexec ${JSON.stringify(entryPath)} "$@"\n`;
if (!fs.existsSync(shimPath) || fs.readFileSync(shimPath, "utf8") !== body) {
fs.writeFileSync(shimPath, body, "utf8");
Expand Down Expand Up @@ -770,6 +770,17 @@ export async function createAdeRuntime(args: {
orchestratorService,
logger,
});
const missionPreflightService = createMissionPreflightService({
logger,
projectRoot,
missionService,
laneService,
aiIntegrationService,
projectConfigService,
missionBudgetService,
humanWorkDigestService: null,
computerUseArtifactBrokerService,
});
const iosSimulatorService = chatOnlyRuntime
? null
: createIosSimulatorService({
Expand Down Expand Up @@ -1128,6 +1139,7 @@ export async function createAdeRuntime(args: {
gitService,
diffService,
missionService,
missionPreflightService,
missionBudgetService,
syncService,
syncHostService: syncService?.getHostService() ?? null,
Expand Down
34 changes: 34 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,40 @@ describe("ADE CLI", () => {
priority: "high",
},
});

const status = buildCliPlan([
"report_status",
"--text",
"Running npm run typecheck",
]);
expect(status.kind).toBe("execute");
if (status.kind !== "execute") return;
expect(status.steps[0]?.params).toEqual({
name: "report_status",
arguments: {
status: "running",
summary: "Running npm run typecheck",
nextAction: "Running npm run typecheck",
details: "Running npm run typecheck",
},
});

const statusOnly = buildCliPlan([
"report_status",
"--status",
"failed",
"--arg",
"details=structured payload",
]);
expect(statusOnly.kind).toBe("execute");
if (statusOnly.kind !== "execute") return;
expect(statusOnly.steps[0]?.params).toEqual({
name: "report_status",
arguments: {
status: "failed",
details: "structured payload",
},
});
});

it("rejects invalid JSON action shapes before execution", () => {
Expand Down
38 changes: 35 additions & 3 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ const VERSION =
BUNDLED_VERSION && BUNDLED_VERSION !== "0.0.0"
? BUNDLED_VERSION
: ENV_VERSION || BUNDLED_VERSION || "0.0.0";
const PLACEHOLDER_VERSION = "0.0.0";
const PROTOCOL_VERSION = "2025-06-18";
const SOURCE_FALLBACK_ENV = "ADE_CLI_SOURCE_FALLBACK_ACTIVE";
const CLI_ENTRY_PATH =
Expand All @@ -191,6 +192,9 @@ const WORKER_MISSION_TOOL_CLI_NAMES = new Set([
"get_pending_messages",
"stream_events",
"message_worker",
"report_status",
"report_result",
"report_validation",
]);

function resolveCliPackageRoot(entryPath: string): string {
Expand Down Expand Up @@ -2263,6 +2267,29 @@ function buildWorkerMissionToolPlan(name: string, args: string[]): CliPlan {
priority: readValue(args, ["--priority"]) ?? "normal",
});
}
if (name === "report_status") {
const explicitMessage = readValue(args, ["--text", "--message", "--summary"]);
const status = readValue(args, ["--status"]) ?? "running";
const positionalParts: string[] = [];
while (true) {
const part = firstStandalonePositional(args);
if (part == null) break;
positionalParts.push(part);
}
const message = explicitMessage ?? positionalParts.join(" ").trim();
const input: JsonObject = { status };
if (message) {
input.summary = message;
input.nextAction = message;
input.details = message;
}
return collectGenericObjectArgs(args, input);
}
if (name === "report_result" || name === "report_validation") {
return collectGenericObjectArgs(args, {
summary: readValue(args, ["--text", "--message", "--summary"]),
});
}
return collectGenericObjectArgs(args);
})();
return {
Expand Down Expand Up @@ -10224,6 +10251,11 @@ function readRuntimeInfoVersion(value: unknown): string | null {
return asString(value.runtimeInfo.version);
}

function shouldReplaceMachineRuntimeVersion(runtimeVersion: string | null): boolean {
if (VERSION === PLACEHOLDER_VERSION) return false;
return runtimeVersion == null || runtimeVersion !== VERSION;
}

async function initializeMachineRuntimeDaemon(
client: SocketJsonRpcClient,
options: GlobalOptions,
Expand Down Expand Up @@ -10303,7 +10335,7 @@ async function connectMachineRuntimeDaemon(
client,
options,
);
if (runtimeVersion && runtimeVersion !== VERSION) {
if (shouldReplaceMachineRuntimeVersion(runtimeVersion)) {
if (!allowSpawn) {
client.close();
throw new Error(
Expand All @@ -10326,7 +10358,7 @@ async function connectMachineRuntimeDaemon(
restarted,
options,
);
if (restartedVersion && restartedVersion !== VERSION) {
if (shouldReplaceMachineRuntimeVersion(restartedVersion)) {
await shutdownMachineRuntimeDaemon(restarted);
throw new Error(
`ADE runtime daemon version ${restartedVersion} does not match CLI version ${VERSION}.`,
Expand All @@ -10349,7 +10381,7 @@ async function connectMachineRuntimeDaemon(
client,
options,
);
if (runtimeVersion && runtimeVersion !== VERSION) {
if (shouldReplaceMachineRuntimeVersion(runtimeVersion)) {
await shutdownMachineRuntimeDaemon(client);
throw new Error(
`ADE runtime daemon version ${runtimeVersion} does not match CLI version ${VERSION}.`,
Expand Down
Loading
Loading