feat: Make /init an interactive flow conducted by the primary agent [LET-7891] (#1356)
This commit is contained in:
@@ -95,23 +95,22 @@ describe("auto-init lifecycle guards", () => {
|
||||
expect(setDelete).toBeGreaterThan(firedCheck);
|
||||
});
|
||||
|
||||
test("manual /init clears pending auto-init for current agent after spawn", () => {
|
||||
test("manual /init clears pending auto-init for current agent", () => {
|
||||
const appSource = readSource("../../cli/App.tsx");
|
||||
|
||||
// The /init handler must delete the current agent from the pending set,
|
||||
// but only after the background subagent has been spawned (inside the try).
|
||||
// The /init handler must delete the current agent from the pending set
|
||||
// before the interactive processConversation call.
|
||||
const initHandlerIdx = appSource.indexOf('trimmed === "/init"');
|
||||
expect(initHandlerIdx).toBeGreaterThan(-1);
|
||||
|
||||
// Search from the /init handler to the end of the block
|
||||
const afterInit = appSource.slice(initHandlerIdx);
|
||||
const spawnIdx = afterInit.indexOf("spawnBackgroundSubagentTask({");
|
||||
const deleteIdx = afterInit.indexOf(
|
||||
"autoInitPendingAgentIdsRef.current.delete(agentId)",
|
||||
);
|
||||
expect(spawnIdx).toBeGreaterThan(-1);
|
||||
const processIdx = afterInit.indexOf("processConversation(");
|
||||
expect(deleteIdx).toBeGreaterThan(-1);
|
||||
expect(deleteIdx).toBeGreaterThan(spawnIdx);
|
||||
expect(processIdx).toBeGreaterThan(-1);
|
||||
expect(deleteIdx).toBeLessThan(processIdx);
|
||||
});
|
||||
|
||||
test("fireAutoInit returns false (not throw) when init subagent is active", () => {
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildMemoryInitRuntimePrompt } from "../../cli/helpers/initCommand";
|
||||
import {
|
||||
buildInitMessage,
|
||||
buildShallowInitPrompt,
|
||||
} from "../../cli/helpers/initCommand";
|
||||
|
||||
describe("init background subagent wiring", () => {
|
||||
describe("init wiring", () => {
|
||||
const readSource = (relativePath: string) =>
|
||||
readFileSync(
|
||||
fileURLToPath(new URL(relativePath, import.meta.url)),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
test("App.tsx checks pending approvals before either branch", () => {
|
||||
test("App.tsx checks pending approvals before /init runs", () => {
|
||||
const appSource = readSource("../../cli/App.tsx");
|
||||
|
||||
// The approval check must appear before the MemFS branch
|
||||
const approvalIdx = appSource.indexOf(
|
||||
"checkPendingApprovalsForSlashCommand",
|
||||
appSource.indexOf('trimmed === "/init"'),
|
||||
);
|
||||
const memfsBranchIdx = appSource.indexOf(
|
||||
"isMemfsEnabled",
|
||||
const initMessageIdx = appSource.indexOf(
|
||||
"buildInitMessage",
|
||||
appSource.indexOf('trimmed === "/init"'),
|
||||
);
|
||||
expect(approvalIdx).toBeGreaterThan(-1);
|
||||
expect(memfsBranchIdx).toBeGreaterThan(-1);
|
||||
expect(approvalIdx).toBeLessThan(memfsBranchIdx);
|
||||
expect(initMessageIdx).toBeGreaterThan(-1);
|
||||
expect(approvalIdx).toBeLessThan(initMessageIdx);
|
||||
});
|
||||
|
||||
test("App.tsx branches on MemFS: background subagent vs legacy processConversation", () => {
|
||||
test("App.tsx uses processConversation for /init", () => {
|
||||
const appSource = readSource("../../cli/App.tsx");
|
||||
|
||||
// MemFS path — background subagent
|
||||
expect(appSource).toContain("hasActiveInitSubagent()");
|
||||
expect(appSource).toContain("buildMemoryInitRuntimePrompt({");
|
||||
expect(appSource).toContain("spawnBackgroundSubagentTask({");
|
||||
expect(appSource).toContain('subagentType: "init"');
|
||||
expect(appSource).toContain("silentCompletion: true");
|
||||
expect(appSource).toContain("appendTaskNotificationEvents(");
|
||||
expect(appSource).toContain("Learning about you and your codebase");
|
||||
|
||||
// Legacy non-MemFS path — primary agent
|
||||
expect(appSource).toContain("buildLegacyInitMessage({");
|
||||
expect(appSource).toContain("buildInitMessage({");
|
||||
expect(appSource).toContain("processConversation(");
|
||||
});
|
||||
|
||||
@@ -49,10 +41,8 @@ describe("init background subagent wiring", () => {
|
||||
|
||||
expect(helperSource).toContain("export function hasActiveInitSubagent(");
|
||||
expect(helperSource).toContain("export function gatherGitContext()");
|
||||
expect(helperSource).toContain(
|
||||
"export function buildMemoryInitRuntimePrompt(",
|
||||
);
|
||||
expect(helperSource).toContain("export function buildLegacyInitMessage(");
|
||||
expect(helperSource).toContain("export function buildShallowInitPrompt(");
|
||||
expect(helperSource).toContain("export function buildInitMessage(");
|
||||
});
|
||||
|
||||
test("init.md exists as a builtin subagent", () => {
|
||||
@@ -79,36 +69,28 @@ describe("init background subagent wiring", () => {
|
||||
gitContext: "## Git context\nsome git info",
|
||||
};
|
||||
|
||||
test('buildMemoryInitRuntimePrompt includes "research_depth: shallow" when depth is "shallow"', () => {
|
||||
const prompt = buildMemoryInitRuntimePrompt({
|
||||
...baseArgs,
|
||||
depth: "shallow",
|
||||
});
|
||||
test("buildShallowInitPrompt produces shallow-only prompt", () => {
|
||||
const prompt = buildShallowInitPrompt(baseArgs);
|
||||
expect(prompt).toContain("research_depth: shallow");
|
||||
expect(prompt).toContain("Shallow init");
|
||||
expect(prompt).not.toContain("Deep init");
|
||||
});
|
||||
|
||||
test('buildMemoryInitRuntimePrompt includes "research_depth: deep" when depth is "deep"', () => {
|
||||
const prompt = buildMemoryInitRuntimePrompt({
|
||||
...baseArgs,
|
||||
depth: "deep",
|
||||
test("buildInitMessage includes memoryDir when provided", () => {
|
||||
const msg = buildInitMessage({
|
||||
gitContext: "## Git\nsome info",
|
||||
memoryDir: "/tmp/.memory",
|
||||
});
|
||||
expect(prompt).toContain("research_depth: deep");
|
||||
expect(prompt).toContain("Deep init");
|
||||
expect(prompt).not.toContain("Shallow init");
|
||||
expect(msg).toContain("Memory filesystem is enabled");
|
||||
expect(msg).toContain("/tmp/.memory");
|
||||
expect(msg).toContain("initializing-memory");
|
||||
});
|
||||
|
||||
test('buildMemoryInitRuntimePrompt defaults to "deep" when depth is omitted', () => {
|
||||
const prompt = buildMemoryInitRuntimePrompt(baseArgs);
|
||||
expect(prompt).toContain("research_depth: deep");
|
||||
expect(prompt).toContain("Deep init");
|
||||
});
|
||||
|
||||
test("App.tsx contains maybeLaunchDeepInitSubagent", () => {
|
||||
const appSource = readSource("../../cli/App.tsx");
|
||||
expect(appSource).toContain("maybeLaunchDeepInitSubagent");
|
||||
expect(appSource).toContain("Deep memory initialization");
|
||||
expect(appSource).toContain('depth: "deep"');
|
||||
test("buildInitMessage works without memoryDir", () => {
|
||||
const msg = buildInitMessage({
|
||||
gitContext: "## Git\nsome info",
|
||||
});
|
||||
expect(msg).not.toContain("Memory filesystem");
|
||||
expect(msg).toContain("initializing-memory");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,42 +24,24 @@ describe("memory subagent recompile handling", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("updates init progress and recompiles after successful shallow init", async () => {
|
||||
const progressUpdates: Array<{
|
||||
agentId: string;
|
||||
update: Record<string, boolean>;
|
||||
}> = [];
|
||||
|
||||
test("recompiles system prompt after successful init", async () => {
|
||||
const message = await handleMemorySubagentCompletion(
|
||||
{
|
||||
agentId: "agent-init-1",
|
||||
conversationId: "conv-init-1",
|
||||
subagentType: "init",
|
||||
initDepth: "shallow",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
recompileByConversation: new Map(),
|
||||
recompileQueuedByConversation: new Set(),
|
||||
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
|
||||
updateInitProgress: (agentId, update) => {
|
||||
progressUpdates.push({
|
||||
agentId,
|
||||
update: update as Record<string, boolean>,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(message).toBe(
|
||||
"Built a memory palace of you. Visit it with /palace.",
|
||||
);
|
||||
expect(progressUpdates).toEqual([
|
||||
{
|
||||
agentId: "agent-init-1",
|
||||
update: { shallowCompleted: true },
|
||||
},
|
||||
]);
|
||||
expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith(
|
||||
"conv-init-1",
|
||||
{},
|
||||
@@ -79,7 +61,6 @@ describe("memory subagent recompile handling", () => {
|
||||
recompileByConversation,
|
||||
recompileQueuedByConversation,
|
||||
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
|
||||
updateInitProgress: () => {},
|
||||
};
|
||||
|
||||
const first = handleMemorySubagentCompletion(
|
||||
@@ -145,7 +126,6 @@ describe("memory subagent recompile handling", () => {
|
||||
recompileByConversation: new Map<string, Promise<void>>(),
|
||||
recompileQueuedByConversation: new Set<string>(),
|
||||
recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock,
|
||||
updateInitProgress: () => {},
|
||||
};
|
||||
|
||||
const [firstMessage, secondMessage] = await Promise.all([
|
||||
|
||||
@@ -10,11 +10,6 @@ import {
|
||||
reflectionSettingsToLegacyMode,
|
||||
shouldFireStepCountTrigger,
|
||||
} from "../../cli/helpers/memoryReminder";
|
||||
import {
|
||||
type SharedReminderContext,
|
||||
sharedReminderProviders,
|
||||
} from "../../reminders/engine";
|
||||
import { createSharedReminderState } from "../../reminders/state";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
|
||||
const originalGetLocalProjectSettings = settingsManager.getLocalProjectSettings;
|
||||
@@ -175,121 +170,3 @@ describe("memoryReminder", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deep-init trigger", () => {
|
||||
const deepInitProvider = sharedReminderProviders["deep-init"];
|
||||
|
||||
function makeContext(
|
||||
overrides: Partial<{
|
||||
shallowInitCompleted: boolean;
|
||||
deepInitFired: boolean;
|
||||
turnCount: number;
|
||||
memfsEnabled: boolean;
|
||||
callback: (() => Promise<boolean>) | undefined;
|
||||
}> = {},
|
||||
): SharedReminderContext {
|
||||
const state = createSharedReminderState();
|
||||
state.shallowInitCompleted = overrides.shallowInitCompleted ?? false;
|
||||
state.deepInitFired = overrides.deepInitFired ?? false;
|
||||
state.turnCount = overrides.turnCount ?? 0;
|
||||
|
||||
const memfsEnabled = overrides.memfsEnabled ?? true;
|
||||
(settingsManager as typeof settingsManager).isMemfsEnabled = (() =>
|
||||
memfsEnabled) as typeof settingsManager.isMemfsEnabled;
|
||||
|
||||
return {
|
||||
mode: "interactive",
|
||||
agent: { id: "test-agent", name: "test" },
|
||||
state,
|
||||
sessionContextReminderEnabled: false,
|
||||
reflectionSettings: {
|
||||
trigger: "step-count",
|
||||
behavior: "auto-launch",
|
||||
stepCount: 25,
|
||||
},
|
||||
skillSources: [],
|
||||
resolvePlanModeReminder: async () => "",
|
||||
maybeLaunchDeepInitSubagent: overrides.callback,
|
||||
};
|
||||
}
|
||||
|
||||
test("does not fire before turn 8", async () => {
|
||||
let launched = false;
|
||||
const ctx = makeContext({
|
||||
shallowInitCompleted: true,
|
||||
turnCount: 7,
|
||||
callback: async () => {
|
||||
launched = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const result = await deepInitProvider(ctx);
|
||||
expect(result).toBeNull();
|
||||
expect(launched).toBe(false);
|
||||
});
|
||||
|
||||
// Deep init auto-launch is currently disabled (reflection + deep init
|
||||
// at similar turn counts is too chaotic). This test documents the
|
||||
// disabled behavior; re-enable when subagent prompts are tuned.
|
||||
test("is currently disabled — does not launch even when conditions are met", async () => {
|
||||
let launched = false;
|
||||
const ctx = makeContext({
|
||||
shallowInitCompleted: true,
|
||||
turnCount: 8,
|
||||
callback: async () => {
|
||||
launched = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const result = await deepInitProvider(ctx);
|
||||
expect(result).toBeNull();
|
||||
expect(launched).toBe(false);
|
||||
});
|
||||
|
||||
test("does not re-fire once deepInitFired is true", async () => {
|
||||
let launched = false;
|
||||
const ctx = makeContext({
|
||||
shallowInitCompleted: true,
|
||||
deepInitFired: true,
|
||||
turnCount: 10,
|
||||
callback: async () => {
|
||||
launched = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const result = await deepInitProvider(ctx);
|
||||
expect(result).toBeNull();
|
||||
expect(launched).toBe(false);
|
||||
});
|
||||
|
||||
test("does not fire when shallowInitCompleted is false", async () => {
|
||||
let launched = false;
|
||||
const ctx = makeContext({
|
||||
shallowInitCompleted: false,
|
||||
turnCount: 10,
|
||||
callback: async () => {
|
||||
launched = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const result = await deepInitProvider(ctx);
|
||||
expect(result).toBeNull();
|
||||
expect(launched).toBe(false);
|
||||
});
|
||||
|
||||
test("does not fire when memfs is disabled", async () => {
|
||||
let launched = false;
|
||||
const ctx = makeContext({
|
||||
shallowInitCompleted: true,
|
||||
turnCount: 8,
|
||||
memfsEnabled: false,
|
||||
callback: async () => {
|
||||
launched = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const result = await deepInitProvider(ctx);
|
||||
expect(result).toBeNull();
|
||||
expect(launched).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user