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
5 changes: 0 additions & 5 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,16 +420,11 @@ ade chat message session-id --kind auto --text "status/context"
ade chat steer session-id --text "active-turn context"
ade chat note "running e2e shard 2/4" # update the caller's Work sidebar status; add --session <id> to target explicitly
ade chat ask "Which account should I use?" # escalate a blocking question; add --session <id> to target explicitly
ade chat settle --outcome "opened PR #841, CI green" # settle only when runtime lifecycle checks pass; exits 1 with exact blockers otherwise
ade chat unsettle # return the caller to the active lifecycle; add --session <id> to target explicitly
ade session show session-id --text # settle/snooze state, and why a snoozed row came back
ade session snooze session-id --for 1h # 30m|1h|4h|1d|1.5h; a bare number means minutes; relative durations cap at 30d
ade session snooze session-id --until 2026-07-26T18:00:00Z # explicit ISO-8601 deadline (must be in the future)
ade session snooze session-id --until-asked # open-ended, matching the desktop/iOS "Until I'm asked" preset: only a hand-raise brings it back
ade session wake session-id --reason manual # timer|needs_you|error|turn_complete|manual
ade session settle session-id --outcome "CI green" # same as `ade chat settle`, but works for CLI/terminal sessions too
ade session settle session-id --keep-active # pin active instead; the only way to hold a clean-exit row out of the quiet tier
ade session unsettle session-id
ade session clear-woke session-id # drop the "woke early" marker after visiting the row
ade session actions --text # raw session service actions
ade chat schedules session-id --pause # pause this agent session's durable wakeups/cron/loops (omit flag to inspect, --resume to re-arm)
Expand Down
41 changes: 20 additions & 21 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1555,19 +1555,6 @@ describe("adeRpcServer", () => {
"Waiting for release choice",
),
},
{
action: "settleSelfSession",
args: { outcome: "Shipped" },
assert: () => expect(runtime.sessionService.settleSession).toHaveBeenCalledWith(
"chat-1",
{ outcome: "Shipped", source: "agent_explicit" },
),
},
{
action: "unsettleSelfSession",
args: {},
assert: () => expect(runtime.sessionService.unsettleSession).toHaveBeenCalledWith("chat-1"),
},
];
for (const lifecycle of lifecycleCalls) {
const result = await callTool(handler, "run_ade_action", {
Expand All @@ -1590,12 +1577,24 @@ describe("adeRpcServer", () => {
"Cross-session write",
);

const manualSettleDenied = await callTool(handler, "run_ade_action", {
domain: "session",
action: "settleSession",
args: { sessionId: "chat-1", outcome: "Bypass blockers" },
});
expect(manualSettleDenied.isError).toBe(true);
// Settlement is user- and PR-merge-driven only (2026-07). A session-bound
// caller gets no settle writer at all: the caller-scoped `*SelfSession`
// pair was deleted, and every survivor is CTO-only while binding caps the
// caller below cto.
for (const settleAttempt of [
{ action: "settleSession", args: { sessionId: "chat-1", outcome: "Bypass blockers" } },
{ action: "unsettleSession", args: { sessionId: "chat-1" } },
{ action: "settleSelfSession", args: { sessionId: "chat-1", outcome: "Shipped" } },
{ action: "unsettleSelfSession", args: { sessionId: "chat-1" } },
{ action: "settleSessions", args: { sessionIds: ["chat-1"] } },
{ action: "unsettleSessions", args: { sessionIds: ["chat-1"] } },
{ action: "setSettleOverride", args: { sessionId: "chat-1", override: "settled" } },
]) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const denialResult = await callTool(handler, "run_ade_action", settleAttempt);
expect(denialResult.isError).toBe(true);
}
expect(runtime.sessionService.settleSession).not.toHaveBeenCalled();
expect(runtime.sessionService.unsettleSession).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -2236,7 +2235,7 @@ describe("adeRpcServer", () => {
expect(finalArg).toContain("clean up processes you start");
expect(finalArg).toContain("ade chat note");
expect(finalArg).toContain("ade chat ask");
expect(finalArg).toContain("ade chat settle");
expect(finalArg).toContain("You cannot settle or unsettle a session");
expect(finalArg.endsWith("Implement API wiring")).toBe(true);
expect(response.structuredContent.startupCommand).toContain("claude");
expect(response.structuredContent.startupCommand).toContain("--model");
Expand Down Expand Up @@ -2268,7 +2267,7 @@ describe("adeRpcServer", () => {
const finalArg = createCall.args?.at(-1) ?? "";
expect(finalArg).toContain("ade chat note");
expect(finalArg).toContain("ade chat ask");
expect(finalArg).toContain("ade chat settle");
expect(finalArg).toContain("You cannot settle or unsettle a session");
expect(createCall.args).not.toContain("--full-auto");
expect(createCall.startupCommand).toContain("--sandbox workspace-write --ask-for-approval on-request");
expect(createCall.startupCommand).not.toContain("--full-auto");
Expand Down
8 changes: 6 additions & 2 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2675,8 +2675,12 @@ const SCOPED_CHAT_ACTIONS = new Set([
"setScheduledWorkPaused",
"requestSessionAttention",
"setSessionStatusNote",
"settleSelfSession",
"unsettleSelfSession",
// `settleSelfSession` / `unsettleSelfSession` used to be scoped here so a
// bound agent could only settle its OWN row. Both actions were removed in
// 2026-07: "is this work finished" is a subjective judgment agents are
// unreliable at, so the surviving settle writers (`session.settleSession`,
// `session.unsettleSession`, the bulk pair, `session.setSettleOverride`) are
// all CTO-only and refuse a session-bound agent outright — no scoping needed.
"interrupt",
"interruptWithQueueMode",
"restoreCancelledQueue",
Expand Down
78 changes: 14 additions & 64 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3060,16 +3060,6 @@ describe("ADE CLI", () => {
action: "setSessionStatusNote",
args: { note: "running e2e shard 2/4" },
},
{
command: ["settle", "--outcome", "opened PR #841, CI green"],
action: "settleSelfSession",
args: { outcome: "opened PR #841, CI green" },
},
{
command: ["unsettle"],
action: "unsettleSelfSession",
args: {},
},
];

for (const testCase of cases) {
Expand All @@ -3083,41 +3073,6 @@ describe("ADE CLI", () => {
},
});
}
const settlePlan = expectExecutePlan(buildCliPlan([
"chat",
"settle",
"--outcome",
"done",
]));
expect(settlePlan.formatter).toBe("session-settlement");
expect(settlePlan.exitCodeFromResult?.({ ok: false, blockers: [] })).toBe(1);
expect(settlePlan.exitCodeFromResult?.({ ok: true })).toBe(0);
expect(() => buildCliPlan(["chat", "settle"]))
.toThrow(/outcome is required/i);
expect(() => buildCliPlan(["chat", "settle", "--outcome", ""]))
.toThrow(/outcome is required/i);

const blockedText = formatOutput({
ok: false,
sessionId: "session-x",
blockers: [
{
code: "pending_input",
message: "Resolve the pending input before settling.",
},
{
code: "scheduled_work_active",
message: "Cancel or complete scheduled work before settling.",
},
],
}, { text: true } as any, settlePlan.formatter);
expect(blockedText).toContain("Session session-x was not settled.");
expect(blockedText).toContain(
"- pending_input: Resolve the pending input before settling.",
);
expect(blockedText).toContain(
"- scheduled_work_active: Cancel or complete scheduled work before settling.",
);

const clearNote = expectExecutePlan(buildCliPlan(["chat", "note", ""]));
expect(clearNote.steps[0]?.params).toMatchObject({
Expand Down Expand Up @@ -3171,19 +3126,20 @@ describe("ADE CLI", () => {
if (help.kind === "help") {
expect(help.text).toContain("ade chat note");
expect(help.text).toContain("ade chat ask");
expect(help.text).toContain("ade chat settle");
expect(help.text).toContain("ade chat unsettle");
expect(help.text).toContain("runtime lifecycle checks");
expect(help.text).toContain("prints exact blockers");
// Settling is user-/PR-merge-driven only; the help must say so rather
// than advertise a command that no longer exists.
expect(help.text).toContain("'chat settle' / 'chat unsettle' were removed");
expect(help.text).toContain("--session <id>");
}
expect(() => buildCliPlan(["chat", "settle"]))
.toThrow(/only the user .* settles a session.*chat note/i);
expect(() => buildCliPlan(["chat", "unsettle"]))
.toThrow(/only the user .* settles a session.*chat note/i);
});

it.each([
["ask", ["q"], "requestSessionAttention", { message: "q" }],
["note", ["working"], "setSessionStatusNote", { note: "working" }],
["settle", ["--outcome", "done"], "settleSelfSession", { outcome: "done" }],
["unsettle", [], "unsettleSelfSession", {}],
])(
"passes --session through for chat %s",
(subcommand, commandArgs, action, expectedArgs) => {
Expand Down Expand Up @@ -3319,15 +3275,6 @@ describe("ADE CLI", () => {
sessionId: "session-x",
reason: "needs_you",
}],
[["settle", "session-x", "--outcome", "done"], "settleSelfSession", {
sessionId: "session-x",
outcome: "done",
}],
[["settle", "session-x", "--keep-active"], "setSettleOverride", {
sessionId: "session-x",
override: "active",
}],
[["unsettle", "session-x"], "unsettleSelfSession", { sessionId: "session-x" }],
[["clear-woke", "session-x"], "clearWokeMarker", { sessionId: "session-x" }],
[["show", "session-x"], "get", { sessionId: "session-x" }],
])("plans ade session %s", (commandArgs, action, expectedArgs) => {
Expand All @@ -3351,9 +3298,9 @@ describe("ADE CLI", () => {
const previous = process.env.ADE_CHAT_SESSION_ID;
process.env.ADE_CHAT_SESSION_ID = "session-env";
try {
const envPlan = expectExecutePlan(buildCliPlan(["session", "unsettle"]));
const envPlan = expectExecutePlan(buildCliPlan(["session", "clear-woke"]));
expect(envPlan.steps[0]?.params).toMatchObject({
arguments: { action: "unsettleSelfSession", args: { sessionId: "session-env" } },
arguments: { action: "clearWokeMarker", args: { sessionId: "session-env" } },
});
} finally {
if (previous === undefined) delete process.env.ADE_CHAT_SESSION_ID;
Expand Down Expand Up @@ -3382,11 +3329,14 @@ describe("ADE CLI", () => {
expect(help.text).toContain("ade session snooze <id> --for 1h");
expect(help.text).toContain("ade session wake <id>");
expect(help.text).toContain("--until-asked");
expect(help.text).toContain("--keep-active");
// Settle is gone from this family and the help says why.
expect(help.text).not.toContain("ade session settle");
expect(help.text).toContain("'settle' and 'unsettle' were removed");
}
const top = buildCliPlan([]);
if (top.kind === "help") {
expect(top.text).toContain("ade session snooze | wake | settle | unsettle");
expect(top.text).toContain("ade session show | snooze | wake | clear-woke");
expect(top.text).not.toContain("ade session snooze | wake | settle | unsettle");
}
});
});
Expand Down
Loading