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
49 changes: 18 additions & 31 deletions .ade/.gitignore
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
# Machine-local ADE state
local.yaml
local.secret.yaml
ade.db
ade.db-*
ade.db-wal
embeddings.db
ade.sock
artifacts/
transcripts/
cache/
worktrees/
secrets/
# ADE ignores local runtime state by default.
*

# Local-only generated runtime docs/state
agents/
cto/CURRENT.md
cto/MEMORY.md
cto/core-memory.json
cto/daily/
cto/sessions.jsonl
cto/subordinate-activity.jsonl
cto/openclaw-history.json
cto/openclaw-idempotency.json
cto/openclaw-outbox.json
cto/openclaw-routes.json
cto/openclaw-device.json
context/
memory/
history/
reflections/
context/*.ade.md
# Shared ADE project config
!.gitignore
!ade.yaml
!cto/
!cto/identity.yaml

# Shared user-authored ADE assets
!templates/
!templates/**
!skills/
!skills/**
!workflows/
!workflows/linear/
!workflows/linear/**
!project-icons/
!project-icons/**
74 changes: 73 additions & 1 deletion apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,65 @@ describe("ADE CLI", () => {
});
});

it("automations create with implicit reuse accepts --lane", () => {
const plan = buildCliPlan([
"automations",
"create",
"--text",
"id: r1\n",
"--lane",
"lane-99",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps[0]?.params).toMatchObject({
arguments: {
args: {
draft: {
execution: { targetLaneId: "lane-99" },
},
},
},
});
});

it("automations create accepts require-on-trigger lane mode without a target lane", () => {
const plan = buildCliPlan([
"automations",
"create",
"--text",
"id: r1\n",
"--lane-mode",
"require-on-trigger",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps[0]?.params).toMatchObject({
arguments: {
args: {
draft: {
execution: { laneMode: "require-on-trigger" },
},
},
},
});
});

it("automations create rejects --lane with --lane-mode require-on-trigger", () => {
expect(() =>
buildCliPlan([
"automations",
"create",
"--text",
"id: r1\n",
"--lane-mode",
"require-on-trigger",
"--lane",
"lane-1",
]),
).toThrow(/--lane is only valid with --lane-mode reuse/);
});

it("automations create with --lane-name-preset custom accepts --lane-name-template", () => {
const plan = buildCliPlan([
"automations",
Expand Down Expand Up @@ -1602,7 +1661,7 @@ describe("ADE CLI", () => {
"--lane-mode",
"bogus",
]),
).toThrow(/--lane-mode must be one of create, reuse/);
).toThrow(/--lane-mode must be one of create, reuse, require-on-trigger/);
});

it("automations runs accepts a --status filter", () => {
Expand Down Expand Up @@ -1731,6 +1790,19 @@ describe("ADE CLI", () => {
});
});

it("automations trigger aliases run and forwards --lane as laneId", () => {
const plan = buildCliPlan(["automations", "trigger", "rule-42", "--lane", "lane-7"]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps[0]?.params).toMatchObject({
arguments: {
domain: "automations",
action: "triggerManually",
args: { id: "rule-42", laneId: "lane-7" },
},
});
});

it("automations runs passes through --rule and --limit as filters", () => {
const plan = buildCliPlan([
"automations",
Expand Down
22 changes: 14 additions & 8 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1209,13 +1209,15 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade automations update <id> --from-file <path>
$ ade automations delete <id> Remove a local rule
$ ade automations toggle <id> --enabled true|false
$ ade automations run <id> [--dry-run] Trigger a rule manually
$ ade automations run <id> [--lane <id>] [--dry-run]
$ ade automations trigger <id> [--lane <id>]
Trigger a rule manually
Comment thread
coderabbitai[bot] marked this conversation as resolved.
$ ade automations runs [--rule <id>] [--status <s>] [--limit 50]
$ ade automations run-show <runId> [--json] Inspect a run
$ ade automations example Print an example rule (stdout)

Lane mode flags (apply to create/update on top of --from-file/--stdin/--text):
--lane-mode <create|reuse> Spawn a new lane per run, or reuse one
--lane-mode <create|reuse|require-on-trigger> Create, reuse, or require lane at trigger time
--lane <id> Target lane (only with --lane-mode reuse)
--lane-name-preset <issue-title|issue-num-title|pr-title-author|custom>
--lane-name-template <string> Template (only with preset custom)
Expand Down Expand Up @@ -3706,7 +3708,7 @@ function parseDraftInput(args: string[]): JsonObject {
return parsed;
}

const AUTOMATION_LANE_MODES = ["create", "reuse"] as const;
const AUTOMATION_LANE_MODES = ["create", "reuse", "require-on-trigger"] as const;
const AUTOMATION_LANE_NAME_PRESETS = ["issue-title", "issue-num-title", "pr-title-author", "custom"] as const;
const AUTOMATION_RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled", "paused", "all"] as const;

Expand Down Expand Up @@ -3737,20 +3739,24 @@ function applyLaneFlagsToDraft(draft: JsonObject, args: string[]): JsonObject {
return draft;
}

if (laneId != null && laneMode === "create") {
const existingExecution = isRecord(draft.execution) ? draft.execution : {};
const effectiveLaneMode =
laneMode
?? (asString(existingExecution.laneMode) as AutomationLaneModeFlag | null);

if (laneId != null && effectiveLaneMode != null && effectiveLaneMode !== "reuse") {
throw new CliUsageError("--lane is only valid with --lane-mode reuse.");
}
if (preset != null && laneMode === "reuse") {
if (preset != null && effectiveLaneMode !== "create") {
throw new CliUsageError("--lane-name-preset is only valid with --lane-mode create.");
}
if (template != null && preset != null && preset !== "custom") {
throw new CliUsageError("--lane-name-template is only valid with --lane-name-preset custom.");
}
if (template != null && preset == null && laneMode !== "create") {
if (template != null && preset == null && effectiveLaneMode !== "create") {
throw new CliUsageError("--lane-name-template requires --lane-mode create (with --lane-name-preset custom).");
}

const existingExecution = isRecord(draft.execution) ? draft.execution : {};
const execution: JsonObject = { ...existingExecution };
if (laneMode != null) execution.laneMode = laneMode;
if (laneId != null) execution.targetLaneId = laneId;
Expand Down Expand Up @@ -3867,7 +3873,7 @@ function buildAutomationsPlan(args: string[]): CliPlan {
};
}

if (sub === "run") {
if (sub === "run" || sub === "trigger") {
const id = requireValue(readValue(args, ["--id"]) ?? firstPositional(args), "rule id");
const dryRun = readFlag(args, ["--dry-run"]);
const laneId = readLaneId(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,86 @@ describe("automationPlannerService.validateDraft", () => {
expect((res.normalized?.actions[2] as any).targetLaneId).toBe("lane-conflict");
});

it("preserves require-on-trigger lane mode without requiring a target lane", () => {
const { planner } = getPlanner({ suites: [] });
const draft = createDraft({
name: "Require trigger lane",
execution: { kind: "agent-session", laneMode: "require-on-trigger" } as any,
prompt: "Use the lane supplied by the caller.",
});

const res = planner.validateDraft({ draft, confirmations: [] });
expect(res.ok).toBe(true);
expect(res.normalized?.execution).toMatchObject({
kind: "agent-session",
laneMode: "require-on-trigger",
});
});

it("normalizes legacy prompt-at-run lane mode to require-on-trigger", () => {
const { planner } = getPlanner({ suites: [] });
const draft = createDraft({
name: "Legacy prompt at run",
execution: { kind: "agent-session", laneMode: "prompt-at-run" } as any,
prompt: "Use the selected lane.",
});

const res = planner.validateDraft({ draft, confirmations: [] });
expect(res.ok).toBe(true);
expect(res.normalized?.execution).toMatchObject({
kind: "agent-session",
laneMode: "require-on-trigger",
});
});

it("rejects targetLaneId when lane mode requires the trigger lane", () => {
const { planner } = getPlanner({ suites: [] });
const draft = createDraft({
name: "Conflicting trigger lane",
execution: {
kind: "agent-session",
laneMode: "require-on-trigger",
targetLaneId: "lane-fixed",
} as any,
prompt: "This should choose at trigger time.",
});

const res = planner.validateDraft({ draft, confirmations: [] });
expect(res.ok).toBe(false);
expect(res.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
level: "error",
path: "execution.targetLaneId",
}),
]),
);
});

it("rejects per-action targetLaneId when lane mode requires the trigger lane", () => {
const { planner } = getPlanner({ suites: [] });
const draft = createDraft({
name: "Conflicting step lane",
execution: {
kind: "built-in",
laneMode: "require-on-trigger",
} as any,
actions: [{ type: "run-command", command: "pwd", targetLaneId: "lane-fixed" } as any],
legacyActions: [{ type: "run-command", command: "pwd", targetLaneId: "lane-fixed" } as any],
});

const res = planner.validateDraft({ draft, confirmations: ["confirm.run-command"] });
expect(res.ok).toBe(false);
expect(res.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
level: "error",
path: "actions[0].targetLaneId",
}),
]),
);
});

it("validates run-command cwd against the per-action targetLaneId before draft execution lane", () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-automation-planner-action-lane-"));
const actionLane = fs.mkdtempSync(path.join(os.tmpdir(), "ade-automation-planner-action-lane-target-"));
Expand Down Expand Up @@ -396,6 +476,36 @@ describe("automationPlannerService.validateDraft", () => {
fs.rmSync(draftLane, { recursive: true, force: true });
}
});

it("preserves output disposition and verification settings", () => {
const { planner } = getPlanner({ suites: [] });
const draft = createDraft({
name: "Publish settings",
execution: { kind: "agent-session" } as any,
prompt: "Prepare a draft PR.",
outputs: {
disposition: "open-pr-draft",
createArtifact: false,
notificationChannel: "automation-alerts",
},
verification: {
verifyBeforePublish: true,
mode: "dry-run",
},
});

const res = planner.validateDraft({ draft, confirmations: [] });
expect(res.ok).toBe(true);
expect(res.normalized?.outputs).toMatchObject({
disposition: "open-pr-draft",
createArtifact: false,
notificationChannel: "automation-alerts",
});
expect(res.normalized?.verification).toMatchObject({
verifyBeforePublish: true,
mode: "dry-run",
});
});
});

function createDraft(
Expand Down
Loading
Loading