feat(KB-635): integrate mission workflows across CLI, extension, and engine

- Add mission CLI commands and pi extension tools for creating missions, milestones, slices, features, and task links
- Extend core mission storage and schema migrations with auto-advance support, task slice linkage, and comment normalization coverage
- Wire mission-aware scheduler and executor behavior so linked features progress with task execution and completed slices can auto-activate follow-on work
- Add regression tests for mission CLI parsing, extension behaviors, scheduler mission semantics, and executor integration
This commit is contained in:
gsxdsm
2026-04-01 13:45:56 -07:00
parent a63ee7443a
commit e8b6b98d57
13 changed files with 984 additions and 229 deletions

View File

@@ -113,6 +113,16 @@ function createMockStore() {
return store as any;
}
function createMockMissionStore(overrides: Record<string, unknown> = {}) {
return {
getFeatureByTaskId: vi.fn(),
getSlice: vi.fn(),
computeSliceStatus: vi.fn(),
updateFeatureStatus: vi.fn(),
...overrides,
} as any;
}
describe("TaskExecutor with semaphore", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -4027,6 +4037,144 @@ describe("Workflow Steps Execution", () => {
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
});
it("marks linked mission feature done when task reaches in-review", async () => {
const store = createMockStore();
const missionStore = createMockMissionStore({
getFeatureByTaskId: vi.fn().mockReturnValue({ id: "F-001", sliceId: "SL-001" }),
getSlice: vi.fn().mockReturnValueOnce({ id: "SL-001", status: "active" }).mockReturnValueOnce({ id: "SL-001", status: "complete" }),
computeSliceStatus: vi.fn().mockReturnValue("complete"),
updateFeatureStatus: vi.fn(),
});
const onSliceComplete = vi.fn();
store.getTask.mockResolvedValue({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
sliceId: "SL-001",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
createAgentWithTaskDone();
const executor = new TaskExecutor(store, "/tmp/test", { missionStore, onSliceComplete });
await executor.execute({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
sliceId: "SL-001",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any);
expect(missionStore.updateFeatureStatus).toHaveBeenCalledWith("F-001", "done");
expect(onSliceComplete).toHaveBeenCalledWith(expect.objectContaining({ id: "SL-001", status: "complete" }));
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("Slice SL-001 completed"),
"Mission feature implementation ready for review",
);
});
it("skips mission updates when linked feature slice does not match task sliceId", async () => {
const store = createMockStore();
const missionStore = createMockMissionStore({
getFeatureByTaskId: vi.fn().mockReturnValue({ id: "F-001", sliceId: "SL-OTHER" }),
updateFeatureStatus: vi.fn(),
});
store.getTask.mockResolvedValue({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
sliceId: "SL-001",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
createAgentWithTaskDone();
const executor = new TaskExecutor(store, "/tmp/test", { missionStore });
await executor.execute({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
sliceId: "SL-001",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any);
expect(missionStore.updateFeatureStatus).not.toHaveBeenCalled();
expect(missionStore.computeSliceStatus).not.toHaveBeenCalled();
});
it("does not update mission progress when agent finishes without task_done", async () => {
const store = createMockStore();
const missionStore = createMockMissionStore({
getFeatureByTaskId: vi.fn().mockReturnValue({ id: "F-001", sliceId: "SL-001" }),
computeSliceStatus: vi.fn(),
updateFeatureStatus: vi.fn(),
});
const onSliceComplete = vi.fn();
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
state: {},
},
} as any);
const executor = new TaskExecutor(store, "/tmp/test", { missionStore, onSliceComplete });
await executor.execute({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
sliceId: "SL-001",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any);
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
expect(missionStore.updateFeatureStatus).not.toHaveBeenCalled();
expect(missionStore.computeSliceStatus).not.toHaveBeenCalled();
expect(onSliceComplete).not.toHaveBeenCalled();
});
it("skips workflow steps with no prompt", async () => {
const store = createMockStore();