mirror of
https://github.com/wassname/pi-goals.git
synced 2026-09-25 14:01:03 +08:00
Improve goal reminder cadence and auto continuation
This commit is contained in:
@@ -74,11 +74,14 @@ pi -e ./src/index.ts
|
||||
3. Work. Ready is the only review action that starts work. The agent ticks subtasks, appends to
|
||||
`## Log` and `## Learnings`, fills `evidence:`, and calls `CompleteGoal` when a discriminator is
|
||||
satisfied. Every human reply and Refine note in plan mode is saved verbatim under `## Interview`.
|
||||
If it leaves the plan untouched for two turns, the working set is sent back with a short upkeep
|
||||
After eight turns without a change above `## Log`, the working set is sent back with a short upkeep
|
||||
reminder.
|
||||
|
||||
Other commands: `/goals --clear` deletes this session's active plan file; `/goals --judge <model-ref>`
|
||||
picks a sign-off judge model (default: your current session model, else pi's default). The `--` prefix
|
||||
Other commands: `/goals --clear` disconnects this session from its active plan, preserving the
|
||||
versioned file on disk; `/goals --auto [minutes|off]` continues active goals after the agent settles
|
||||
and then on that interval. It pauses after two automatic wakes with no working-plan change; `/goals
|
||||
--judge <model-ref>` picks a sign-off judge model (default: your current session model, else pi's
|
||||
default). The `--` prefix
|
||||
keeps ordinary objectives such as `judge model quality` from being parsed as commands.
|
||||
|
||||
## Prompts
|
||||
|
||||
+148
-27
@@ -4,8 +4,8 @@
|
||||
* CompleteGoal.
|
||||
*
|
||||
* PI: Each /goals call makes a new plan version, `.pi/plan/<session_id>-vN.md`. The selected version
|
||||
* stays in session state across resume and compaction. Old drafts stay available but inert, so a new
|
||||
* conversation cannot silently edit them. The filename is the arm switch: a session that never ran
|
||||
* stays in session state across resume and compaction. Old plans stay on disk but inert, so a new
|
||||
* conversation cannot silently edit them. `/goals --clear` only disconnects this session; the filename is the arm switch: a session that never ran
|
||||
* /goals has no active plan, so the widget, injections, and CompleteGoal all stay silent.
|
||||
*
|
||||
* The v1 lesson: the parser existed so TypeScript could read the plan, but almost every reader is a
|
||||
@@ -60,9 +60,12 @@ const JUDGE_TIMEOUT_MS = 600_000;
|
||||
// Plan mode is read-only by convention AND a light gate: edit/write are blocked (except the plan
|
||||
// file, the deliverable). bash stays open — the prompt says don't mutate; guide, not gate (spec D3).
|
||||
const PLAN_MODE_BLOCKED_TOOLS = ["edit", "write"];
|
||||
// Turns the plan may go untouched before it is re-sent. pi-tasks uses 4, or 2 while something is in
|
||||
// progress; here every goal is "in progress", so 2.
|
||||
const STALE_TURNS = 2;
|
||||
// A plan reminder is only useful after a substantial run of work that has not changed the working
|
||||
// set. Log and learning entries do not count as progress. Unlike pi-tasks, goals have no dedicated
|
||||
// progress tool, so this cadence repeats until the working set changes.
|
||||
const STALE_TURNS = 8;
|
||||
const AUTO_DEFAULT_INTERVAL_MS = 60 * 60 * 1_000;
|
||||
const AUTO_MAX_WAKES_WITHOUT_PROGRESS = 2;
|
||||
|
||||
// A checkbox line beginning "goal:", for the widget and the "any goals open?" reminder condition.
|
||||
// Everything else reads the file as prose.
|
||||
@@ -120,16 +123,23 @@ interface PlanState {
|
||||
/** Optional model ref for the sign-off judge; unset => current session model, else pi's default. */
|
||||
judgeModel: string | null;
|
||||
planVersion: number | null;
|
||||
/** User-enabled interval for continuing active goals after the agent settles. */
|
||||
autoIntervalMs: number | null;
|
||||
autoPaused: boolean;
|
||||
}
|
||||
|
||||
export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
let state: PlanState = { phase: null, judgeModel: null, planVersion: null };
|
||||
let state: PlanState = { phase: null, judgeModel: null, planVersion: null, autoIntervalMs: null, autoPaused: false };
|
||||
let planningContextPending = false;
|
||||
// Reminder cadence (pi-tasks style): the plan is re-sent only after it has gone untouched for
|
||||
// STALE_TURNS turns, and editing it resets the clock -- an agent that is maintaining the file
|
||||
// doesn't need to be told to. In-memory, like pi-tasks: a new session starts fresh.
|
||||
// The reminder sees only the working set. A repeated Log line must not look like progress.
|
||||
let turnsStale = 0;
|
||||
let lastSeenPlan = "";
|
||||
let lastSeenWorkingSet = "";
|
||||
let autoTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let autoWakeInFlight = false;
|
||||
let autoWakesWithoutProgress = 0;
|
||||
let autoLastWorkingSet = "";
|
||||
let autoImmediateUsed = false;
|
||||
let runStartedBackgroundWork = false;
|
||||
// Set on session start and after a compaction; drained by the next LLM call, which then carries
|
||||
// the WHOLE file (appendix included) instead of just the working set.
|
||||
let resyncReason: string | null = "New session.";
|
||||
@@ -151,6 +161,60 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
pi.appendEntry<PlanState>(STATE, state);
|
||||
}
|
||||
|
||||
function clearAutoTimer(): void {
|
||||
if (autoTimer !== null) clearTimeout(autoTimer);
|
||||
autoTimer = null;
|
||||
}
|
||||
|
||||
function activeGoals(ctx: ExtensionContext): boolean {
|
||||
return scanGoals(readPlan(ctx)).some((goal) => goal.status === "active" || goal.status === "open");
|
||||
}
|
||||
|
||||
function scheduleAutoContinue(ctx: ExtensionContext, delayMs = state.autoIntervalMs): void {
|
||||
clearAutoTimer();
|
||||
if (delayMs === null || state.phase !== "working" || state.autoIntervalMs === null || state.autoPaused || !activeGoals(ctx)) return;
|
||||
autoTimer = setTimeout(() => {
|
||||
autoTimer = null;
|
||||
if (state.phase !== "working" || state.autoPaused || !ctx.isIdle() || !activeGoals(ctx)) return;
|
||||
autoWakeInFlight = true;
|
||||
pi.sendUserMessage(
|
||||
`<system-reminder>Auto-continue is enabled by the human. Continue the active goal in ${planRel(ctx)}. Work from the open subtasks and observed artifacts. Keep the plan current, including useful Log entries. If you need a human decision, ask one direct question and leave the goal active.</system-reminder>`,
|
||||
{ deliverAs: "followUp" },
|
||||
);
|
||||
}, delayMs);
|
||||
autoTimer.unref();
|
||||
}
|
||||
|
||||
function settleAuto(ctx: ExtensionContext): void {
|
||||
if (state.phase !== "working" || state.autoIntervalMs === null || state.autoPaused || !activeGoals(ctx)) return;
|
||||
const workingSet = foldPlan(readPlan(ctx));
|
||||
const changed = workingSet !== autoLastWorkingSet;
|
||||
if (changed) {
|
||||
autoLastWorkingSet = workingSet;
|
||||
autoWakesWithoutProgress = 0;
|
||||
autoImmediateUsed = false;
|
||||
}
|
||||
if (autoWakeInFlight) {
|
||||
autoWakeInFlight = false;
|
||||
if (!changed) autoWakesWithoutProgress++;
|
||||
if (autoWakesWithoutProgress >= AUTO_MAX_WAKES_WITHOUT_PROGRESS) {
|
||||
state = { ...state, autoPaused: true };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify("Goal auto-continue paused; waiting for user after two wakes without working-plan progress.", "warning");
|
||||
return;
|
||||
}
|
||||
scheduleAutoContinue(ctx);
|
||||
return;
|
||||
}
|
||||
if (!runStartedBackgroundWork && !autoImmediateUsed) {
|
||||
autoImmediateUsed = true;
|
||||
scheduleAutoContinue(ctx, 0);
|
||||
return;
|
||||
}
|
||||
scheduleAutoContinue(ctx);
|
||||
}
|
||||
|
||||
function updateWidget(ctx: ExtensionContext): void {
|
||||
if (state.phase === "planning") {
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", "planning"));
|
||||
@@ -164,14 +228,15 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
return;
|
||||
}
|
||||
const done = goals.filter((g) => g.status === "done").length;
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `◷ ${done}/${goals.length} goals`));
|
||||
const auto = state.autoPaused ? " · waiting for user" : state.autoIntervalMs === null ? "" : ` · auto ${state.autoIntervalMs / 60_000}m`;
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `◷ ${done}/${goals.length} goals${auto}`));
|
||||
const mark: Record<GoalStatus, string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
|
||||
// Only live goals get lines so finished work never pushes current work off screen. The active
|
||||
// goal also shows its open subtasks: this file is the task list, so the widget is the task list.
|
||||
// No path line: the session id makes it 47 chars, too long to be worth a widget row. The
|
||||
// human opens the file from the Ready menu, and every injected reminder still names it.
|
||||
const plan = readPlan(ctx);
|
||||
const lines: string[] = [];
|
||||
const lines: string[] = state.autoPaused ? [ctx.ui.theme.fg("warning", "⏸ waiting for user")] : [];
|
||||
for (const g of goals.filter((g) => g.status === "active" || g.status === "open")) {
|
||||
lines.push(`${mark[g.status]} ${g.subject}`);
|
||||
if (g.status === "active") lines.push(...openSubtasks(plan, g.line).slice(0, 3).map((s) => ctx.ui.theme.fg("muted", ` ◦ ${s}`)));
|
||||
@@ -182,20 +247,49 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
// --- /goals: enter plan mode (or clear / set judge) --------------------------------------------
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
description: `Plan mode: draft goals into ${PLAN_SHAPE}, review, then work them. /goals <objective> | /goals --clear | /goals --judge <model>`,
|
||||
description: `Plan mode: draft goals into ${PLAN_SHAPE}, review, then work them. /goals <objective> | /goals --clear (disconnect) | /goals --auto [minutes|off] | /goals --judge <model>`,
|
||||
handler: async (args, ctx) => {
|
||||
const arg = args.trim();
|
||||
if (arg === "--clear") {
|
||||
if (state.planVersion === null) {
|
||||
ctx.ui.notify("No active plan to delete.", "info");
|
||||
ctx.ui.notify("No active plan to disconnect.", "info");
|
||||
return;
|
||||
}
|
||||
const currentPlan = planRel(ctx);
|
||||
rmSync(planPath(ctx), { force: true });
|
||||
state = { ...state, phase: null, planVersion: null };
|
||||
clearAutoTimer();
|
||||
state = { ...state, phase: null, planVersion: null, autoIntervalMs: null, autoPaused: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify(`Deleted ${currentPlan}.`, "info");
|
||||
ctx.ui.notify(`Disconnected from ${currentPlan}; the file remains on disk.`, "info");
|
||||
return;
|
||||
}
|
||||
if (arg === "--auto" || arg.startsWith("--auto ")) {
|
||||
const value = arg.slice("--auto".length).trim();
|
||||
if (value === "off") {
|
||||
clearAutoTimer();
|
||||
state = { ...state, autoIntervalMs: null, autoPaused: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
ctx.ui.notify("Goal auto-continue disabled.", "info");
|
||||
return;
|
||||
}
|
||||
if (state.phase !== "working") {
|
||||
ctx.ui.notify("Approve a plan with Ready before enabling auto-continue.", "warning");
|
||||
return;
|
||||
}
|
||||
const minutes = value ? Number(value) : AUTO_DEFAULT_INTERVAL_MS / 60_000;
|
||||
if (!Number.isInteger(minutes) || minutes < 1) {
|
||||
ctx.ui.notify("Use /goals --auto [whole minutes], or /goals --auto off.", "warning");
|
||||
return;
|
||||
}
|
||||
autoWakeInFlight = false;
|
||||
autoWakesWithoutProgress = 0;
|
||||
autoLastWorkingSet = foldPlan(readPlan(ctx));
|
||||
state = { ...state, autoIntervalMs: minutes * 60_000, autoPaused: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
scheduleAutoContinue(ctx);
|
||||
ctx.ui.notify(`Goal auto-continue enabled every ${minutes}m.`, "info");
|
||||
return;
|
||||
}
|
||||
if (arg === "--judge" || arg.startsWith("--judge ")) {
|
||||
@@ -268,29 +362,38 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
|
||||
// PI: Human plan-mode replies are durable evidence of the interview, not model summaries.
|
||||
pi.on("input", async (event, ctx) => {
|
||||
if (event.source !== "extension") {
|
||||
clearAutoTimer();
|
||||
autoImmediateUsed = false;
|
||||
if (state.autoPaused) {
|
||||
state = { ...state, autoPaused: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
}
|
||||
}
|
||||
if (state.phase === "planning" && event.source !== "extension") writePlan(ctx, appendInterview(readPlan(ctx), event.text));
|
||||
});
|
||||
|
||||
// The staleness clock: editing the plan resets it, the way a task tool call resets pi-tasks'.
|
||||
// The staleness clock sees only the working set. Log updates are durable evidence, not progress.
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
const plan = readPlan(ctx);
|
||||
if (plan === lastSeenPlan) {
|
||||
const workingSet = foldPlan(readPlan(ctx));
|
||||
if (workingSet === lastSeenWorkingSet) {
|
||||
turnsStale++;
|
||||
return;
|
||||
}
|
||||
lastSeenPlan = plan;
|
||||
lastSeenWorkingSet = workingSet;
|
||||
turnsStale = 0;
|
||||
updateWidget(ctx);
|
||||
});
|
||||
|
||||
// A compaction loses context, so restore either the planning snapshot or the working plan once.
|
||||
pi.on("session_compact", async () => {
|
||||
if (state.phase === "planning") planningContextPending = true;
|
||||
else resyncReason = "The session was just compacted.";
|
||||
pi.on("agent_start", async () => {
|
||||
runStartedBackgroundWork = false;
|
||||
});
|
||||
|
||||
// PI: Block project changes while planning, but leave ordinary inspection available.
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (state.phase === "working" && (event.toolName === "subagent" || (event.toolName === "process" && (event.input as { action?: string }).action === "start"))) {
|
||||
runStartedBackgroundWork = true;
|
||||
}
|
||||
if (state.phase !== "planning") return;
|
||||
if (PLAN_MODE_BLOCKED_TOOLS.includes(event.toolName)) {
|
||||
const target = (event.input as { path?: string }).path;
|
||||
@@ -302,8 +405,18 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
});
|
||||
|
||||
// A compaction loses context, so restore either the planning snapshot or the working plan once.
|
||||
pi.on("session_compact", async () => {
|
||||
if (state.phase === "planning") planningContextPending = true;
|
||||
else resyncReason = "The session was just compacted.";
|
||||
});
|
||||
|
||||
// PI: Print after Pi settles. agent_end is still streaming, so its message queues behind the menu.
|
||||
pi.on("agent_settled", async (_event, ctx) => {
|
||||
if (state.phase === "working") {
|
||||
settleAuto(ctx);
|
||||
return;
|
||||
}
|
||||
if (state.phase !== "planning" || !ctx.hasUI) return;
|
||||
let printed = "";
|
||||
while (true) {
|
||||
@@ -356,11 +469,19 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
phase: last?.data?.phase ?? null,
|
||||
judgeModel: last?.data?.judgeModel ?? null,
|
||||
planVersion: last?.data?.planVersion ?? null,
|
||||
autoIntervalMs: last?.data?.autoIntervalMs ?? null,
|
||||
autoPaused: last?.data?.autoPaused ?? false,
|
||||
};
|
||||
lastSeenPlan = readPlan(ctx);
|
||||
lastSeenWorkingSet = foldPlan(readPlan(ctx));
|
||||
autoLastWorkingSet = lastSeenWorkingSet;
|
||||
planningContextPending = state.phase === "planning";
|
||||
resyncReason = state.phase === "working" ? "New session." : null;
|
||||
updateWidget(ctx);
|
||||
scheduleAutoContinue(ctx);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
clearAutoTimer();
|
||||
});
|
||||
|
||||
// --- the one blessed tool: CompleteGoal ---------------------------------------------------------
|
||||
|
||||
+98
-1
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import piGoalsExtension from "../src/index.js";
|
||||
|
||||
function setup(
|
||||
@@ -20,6 +20,7 @@ function setup(
|
||||
const ctx = {
|
||||
cwd,
|
||||
hasUI: true,
|
||||
isIdle: () => true,
|
||||
sessionManager: { getSessionId: () => "session-a", getEntries: () => entries },
|
||||
ui: {
|
||||
theme: { fg: (_kind: string, text: string) => text },
|
||||
@@ -92,6 +93,25 @@ describe("/goals draft flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("disconnects without deleting the active plan", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: preserve this\n");
|
||||
|
||||
await flow.commands.get("goals").handler("--clear", flow.ctx);
|
||||
|
||||
expect(readFileSync(planPath, "utf-8")).toContain("goal: preserve this");
|
||||
expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: null });
|
||||
|
||||
await flow.commands.get("goals").handler("next objective", flow.ctx);
|
||||
expect(readFileSync(join(flow.cwd, ".pi/plan/session-a-v2.md"), "utf-8")).toBe("");
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("waits for Refine notes before starting a revision turn", async () => {
|
||||
let submitNotes: (notes: string) => void;
|
||||
const flow = setup(["Refine"], [], () => new Promise((resolve) => {
|
||||
@@ -153,6 +173,83 @@ describe("/goals draft flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reminds every eight unchanged working-set turns, ignoring log-only edits", async () => {
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
for (let turn = 0; turn < 3; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n\n## Log\n- checked input\n");
|
||||
for (let turn = 0; turn < 5; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
|
||||
const reminder = await flow.hooks.get("context")({ messages: [] }, flow.ctx);
|
||||
expect(reminder.messages.at(-1).content[0].text).toContain(".pi/plan/session-a-v1.md");
|
||||
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n - [x] inspect input\n\n## Log\n- checked input\n");
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
for (let turn = 0; turn < 7; turn++) await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect((await flow.hooks.get("context")({ messages: [] }, flow.ctx)).messages).toHaveLength(0);
|
||||
await flow.hooks.get("turn_end")({}, flow.ctx);
|
||||
expect((await flow.hooks.get("context")({ messages: [] }, flow.ctx)).messages.at(-1).content[0].text).toContain("make the output");
|
||||
} finally {
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-continues once on stop, then pauses after two no-progress wakes", async () => {
|
||||
vi.useFakeTimers();
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("--auto 1", flow.ctx);
|
||||
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
|
||||
expect(autoMessages()).toHaveLength(1);
|
||||
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(2);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("delays auto-continuation after a known background start", async () => {
|
||||
vi.useFakeTimers();
|
||||
const flow = setup(["Ready"]);
|
||||
try {
|
||||
await flow.commands.get("goals").handler("objective", flow.ctx);
|
||||
const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md");
|
||||
writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n");
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await flow.commands.get("goals").handler("--auto 1", flow.ctx);
|
||||
await flow.hooks.get("agent_start")({}, flow.ctx);
|
||||
await flow.hooks.get("tool_call")({ toolName: "process", input: { action: "start" } }, flow.ctx);
|
||||
await flow.hooks.get("agent_settled")({}, flow.ctx);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const autoMessages = () => flow.messages.filter((message) => message.content.includes("Auto-continue is enabled"));
|
||||
expect(autoMessages()).toHaveLength(0);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(autoMessages()).toHaveLength(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
rmSync(flow.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("gives the agent a planning snapshot and blocks work routes", async () => {
|
||||
const flow = setup([]);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user