feat(KB-503): merge kb/kb-503
- feat(KB-503): complete Step 5 other command project support - fix(KB-503): align task resolution with shared project context - fix(KB-503): preserve local task command fallback - test(KB-503): complete Step 4 task command coverage - test(KB-503): harden Step 3 CLI parser coverage - fix(KB-503): keep settings set scope explicit - fix(KB-503): allow implicit project resolution for project settings - fix(KB-503): enforce explicit settings scope rules - fix(KB-503): restore shared settings resolution behavior - fix(KB-503): align settings scope and project flag behavior - feat(KB-503): complete Step 5 — add project-aware settings, git, and backup behavior - fix(KB-503): enforce settings scope for global and project modes - fix(KB-503): preserve project-scoped settings and cwd-aware git helpers - fix(KB-503): add scoped settings behavior and git project tests - feat(KB-503): complete Step 5 — add project-aware git and backup coverage - test(KB-503): cover remaining project-aware task command paths - fix(KB-503): use shared resolution flow for all task commands - fix(KB-503): restore legacy task fallback and correct task mocks - fix(KB-503): align task resolution order and kb storage paths - fix(KB-503): restore local task store fallback without project flag - test(KB-503): cover remaining project-aware task handlers - fix(KB-503): preserve branch naming and avoid duplicate log resolution - test(KB-503): expand project-aware task command coverage - fix(KB-503): restore cwd fallback for path-based task commands - fix(KB-503): add project-aware task output and tests - fix(KB-503): harden project command output and coverage - fix(KB-503): preserve legacy task resolution without project flag - test(KB-503): add bin routing coverage for project flag parsing - feat(KB-503): complete Step 3 — CLI argument parsing updates - fix(KB-503): add defaultProjectId to GlobalSettings type and fix TypeScript errors - test(KB-503): fix resolveProject mock in task tests for runTaskLogs follow mode - docs(KB-503): add multi-project CLI documentation and changeset - test(KB-503): add project-context mock to task tests - test(KB-503): update git tests for new cwd parameter - feat(KB-503): Step 3 — CLI argument parsing updates with --project flag support - feat(KB-503): Steps 1-2 — project context utilities and project subcommands
This commit is contained in:
@@ -113,16 +113,6 @@ 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();
|
||||
@@ -1137,22 +1127,11 @@ describe("TaskExecutor dependency-based worktree creation", () => {
|
||||
if (cmd === `git worktree remove "${conflictingPath}" --force`) {
|
||||
throw new Error("remove failed");
|
||||
}
|
||||
if (cmd === 'git branch -D "kb/fn-065"') {
|
||||
throw new Error("branch delete failed");
|
||||
}
|
||||
if (cmd === "git worktree list --porcelain") {
|
||||
return Buffer.from(`/tmp/test/.git/worktrees/sharp-stone\n`);
|
||||
}
|
||||
return Buffer.from("");
|
||||
});
|
||||
|
||||
await executor.execute(makeTask({ id: "FN-065" }));
|
||||
|
||||
// After 3 retry attempts, should fail with combined error message
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-065", {
|
||||
status: "failed",
|
||||
error: expect.stringContaining("Worktree conflict"),
|
||||
});
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-065", {
|
||||
status: "failed",
|
||||
error: expect.stringContaining("automatic cleanup failed"),
|
||||
@@ -1601,8 +1580,6 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Project Commands");
|
||||
expect(result).toContain("- **Build:** `pnpm build`");
|
||||
expect(result).not.toContain("- **Test:**");
|
||||
expect(result).toContain("run that exact command in this worktree before calling `task_done()`");
|
||||
expect(result).toContain("Do not claim success without a real passing run");
|
||||
});
|
||||
|
||||
it("includes both commands when both are set", () => {
|
||||
@@ -1631,14 +1608,14 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).not.toContain("## Project Commands");
|
||||
});
|
||||
|
||||
it("includes Comments section when comments has entries", () => {
|
||||
it("includes Steering Comments section when steeringComments has entries", () => {
|
||||
const task = createMockTaskDetail({
|
||||
steeringComments: [
|
||||
{
|
||||
id: "1",
|
||||
text: "Please handle the edge case",
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user",
|
||||
author: "user" as const,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1647,10 +1624,10 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Steering Comments");
|
||||
expect(result).toContain("**user**");
|
||||
expect(result).toContain("> Please handle the edge case");
|
||||
expect(result).toContain("The following comments were added");
|
||||
expect(result).toContain("The following steering comments were added by the user");
|
||||
});
|
||||
|
||||
it("formats multiple comments correctly", () => {
|
||||
it("formats multiple steering comments correctly", () => {
|
||||
const now = new Date();
|
||||
const task = createMockTaskDetail({
|
||||
steeringComments: [
|
||||
@@ -1658,13 +1635,13 @@ describe("buildExecutionPrompt", () => {
|
||||
id: "1",
|
||||
text: "First comment",
|
||||
createdAt: new Date(now.getTime() - 60000).toISOString(), // 1 minute ago
|
||||
author: "user",
|
||||
author: "user" as const,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
text: "Second comment",
|
||||
createdAt: now.toISOString(),
|
||||
author: "agent",
|
||||
author: "agent" as const,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1676,29 +1653,29 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("> Second comment");
|
||||
});
|
||||
|
||||
it("omits Comments section when steeringComments is empty", () => {
|
||||
it("omits Steering Comments section when steeringComments is empty", () => {
|
||||
const task = createMockTaskDetail({ steeringComments: [] });
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).not.toContain("## Steering Comments");
|
||||
});
|
||||
|
||||
it("omits Comments section when comments is undefined", () => {
|
||||
it("omits Steering Comments section when steeringComments is undefined", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).not.toContain("## Steering Comments");
|
||||
});
|
||||
|
||||
it("includes only the 10 most recent comments", () => {
|
||||
const comments = Array.from({ length: 15 }, (_, i) => ({
|
||||
it("includes only the 10 most recent steering comments", () => {
|
||||
const steeringComments = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
text: `Comment ${i}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user",
|
||||
author: "user" as const,
|
||||
}));
|
||||
|
||||
const task = createMockTaskDetail({ comments });
|
||||
const task = createMockTaskDetail({ steeringComments });
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
// Should include comments 5-14 (the 10 most recent), not 0-4
|
||||
@@ -1741,7 +1718,7 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Steering Comments");
|
||||
|
||||
// Verify explanatory header text
|
||||
expect(result).toContain("The following comments were added by the user during execution");
|
||||
expect(result).toContain("The following steering comments were added by the user during execution");
|
||||
expect(result).toContain("Consider adjusting your approach or replanning remaining steps based on this feedback");
|
||||
|
||||
// Verify all three comments appear with correct author badges
|
||||
@@ -4041,144 +4018,6 @@ 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();
|
||||
|
||||
@@ -4394,13 +4233,13 @@ describe("Real-time steering injection", () => {
|
||||
|
||||
// Verify steer was called with the formatted message
|
||||
expect(steerFn).toHaveBeenCalledOnce();
|
||||
expect(steerFn.mock.calls[0][0]).toContain("📣 **New feedback**");
|
||||
expect(steerFn.mock.calls[0][0]).toContain("📣 **New steering feedback**");
|
||||
expect(steerFn.mock.calls[0][0]).toContain("Please use a different approach");
|
||||
|
||||
// Verify log entry was created
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.stringContaining("Comment received mid-execution"),
|
||||
expect.stringContaining("Steering comment received mid-execution"),
|
||||
"by user"
|
||||
);
|
||||
|
||||
@@ -4618,13 +4457,6 @@ describe("Real-time steering injection", () => {
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||
comments: [{
|
||||
id: "existing-comment",
|
||||
text: "Original",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: "user",
|
||||
}],
|
||||
steeringComments: [{
|
||||
id: "existing-comment",
|
||||
text: "Original",
|
||||
|
||||
Reference in New Issue
Block a user