feat(HAI-027): complete Step 2 — inject commands into triage agent context

This commit is contained in:
Dustin Byrne
2026-03-25 22:17:44 -04:00
parent e7c4977dc4
commit 289f865689
2 changed files with 115 additions and 5 deletions

View File

@@ -6,8 +6,9 @@ vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(),
}));
import { TriageProcessor } from "./triage.js";
import { TriageProcessor, buildSpecificationPrompt } from "./triage.js";
import { createHaiAgent } from "./pi.js";
import type { TaskDetail } from "@hai/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent);
@@ -29,9 +30,33 @@ function createMockStore(tasks: any[] = []) {
}),
updateTask: vi.fn().mockResolvedValue({}),
moveTask: vi.fn().mockResolvedValue({}),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
}),
} as any;
}
function createMockTaskDetail(overrides: Partial<TaskDetail> = {}): TaskDetail {
return {
id: "HAI-001",
title: "Test Task",
description: "A test task",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
prompt: "",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe("TriageProcessor with semaphore", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -146,3 +171,72 @@ describe("TriageProcessor with semaphore", () => {
expect(sem.activeCount).toBe(0);
});
});
describe("buildSpecificationPrompt", () => {
it("includes project commands when testCommand is set", () => {
const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
testCommand: "pnpm test",
});
expect(result).toContain("## Project Commands");
expect(result).toContain("**Test:** `pnpm test`");
expect(result).toContain("Use these exact commands");
});
it("includes project commands when buildCommand is set", () => {
const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
buildCommand: "pnpm build",
});
expect(result).toContain("## Project Commands");
expect(result).toContain("**Build:** `pnpm build`");
});
it("includes both commands when both are set", () => {
const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
testCommand: "npm test",
buildCommand: "npm run build",
});
expect(result).toContain("**Test:** `npm test`");
expect(result).toContain("**Build:** `npm run build`");
});
it("omits project commands section when neither command is set", () => {
const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md", {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
});
expect(result).not.toContain("## Project Commands");
});
it("omits project commands section when settings is undefined", () => {
const task = createMockTaskDetail();
const result = buildSpecificationPrompt(task, ".hai/tasks/HAI-001/PROMPT.md");
expect(result).not.toContain("## Project Commands");
});
});

View File

@@ -1,4 +1,4 @@
import type { TaskStore, Task, TaskDetail } from "@hai/core";
import type { TaskStore, Task, TaskDetail, Settings } from "@hai/core";
import { Type } from "@mariozechner/pi-ai";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { createHaiAgent } from "./pi.js";
@@ -137,6 +137,12 @@ write a PROMPT.md. Instead, write a single line to the output file:
- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
- 0-1 → Level 0, 2-3 → Level 1, 4-5 → Level 2, 6-8 → Level 3
## Project commands
When the user prompt includes a "Project Commands" section with test and/or build
commands, use those EXACT commands in the testing/verification steps and anywhere
the spec references running tests or builds. Do NOT guess or infer commands from
package.json when explicit commands are provided.
## Output
Write the PROMPT.md directly using the write tool. Nothing else.`;
@@ -206,6 +212,7 @@ export class TriageProcessor {
try {
const detail = await this.store.getTask(task.id);
const settings = await this.store.getSettings();
const promptPath = `.hai/tasks/${task.id}/PROMPT.md`;
const agentWork = async () => {
@@ -220,7 +227,7 @@ export class TriageProcessor {
});
try {
const agentPrompt = buildSpecificationPrompt(detail, promptPath);
const agentPrompt = buildSpecificationPrompt(detail, promptPath, settings);
await session.prompt(agentPrompt);
// Check if the agent flagged a duplicate
@@ -330,7 +337,16 @@ export class TriageProcessor {
}
}
function buildSpecificationPrompt(task: TaskDetail, promptPath: string): string {
export function buildSpecificationPrompt(task: TaskDetail, promptPath: string, settings?: Settings): string {
let commandsSection = "";
if (settings?.testCommand || settings?.buildCommand) {
const lines = ["## Project Commands"];
if (settings.testCommand) lines.push(`- **Test:** \`${settings.testCommand}\``);
if (settings.buildCommand) lines.push(`- **Build:** \`${settings.buildCommand}\``);
lines.push("Use these exact commands in testing/verification steps.");
commandsSection = "\n\n" + lines.join("\n");
}
return `Specify this task and write the result to \`${promptPath}\`.
## Task
@@ -345,5 +361,5 @@ ${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join("
3. The specification must be detailed enough for an autonomous AI agent to implement without asking questions
4. Name actual files, functions, and patterns from the codebase — be specific
Use the write tool to write the specification file.`;
Use the write tool to write the specification file.${commandsSection}`;
}