mirror of
https://github.com/Hestia-Homes/agentic-toolkit.git
synced 2026-06-08 11:37:26 +00:00
Initial implementation of Domna's agentic toolkit per PRD #1: - Runner CLI (src/cli.ts) wrapping sandcastle.run() with Docker provider - Pure modules: PhaseScheduler, PromptBuilder, FailureHandler with tests - Project Status v2 GraphQL client + parsers with tests - BranchManager (git/gh wrapper) and LoopOrchestrator (per-tick algorithm) - Variant-aware: per-ticket (one PR per issue, phase-gated, exit between phases) vs single-pr (one PR for the whole DAG, halt on failure) - /to-project skill that creates a repo-level project, configures the Status schema the runner expects, and sets initial issue statuses - setup.sh that installs Matt Pocock skills + Domna skills via npx skills Out of scope at v1: remote runners, Slack notifications, stacked PRs, cross-repo projects, SHA-pinning of upstream skills (tracks HEAD until the skills CLI supports repo#sha). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
3 KiB
TypeScript
122 lines
3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ProjectIssue } from "../types.js";
|
|
import { currentPhase, schedule } from "./phase-scheduler.js";
|
|
|
|
const issue = (
|
|
n: number,
|
|
blockedBy: number[] = [],
|
|
overrides: Partial<ProjectIssue> = {},
|
|
): ProjectIssue => ({
|
|
number: n,
|
|
nodeId: `node-${n}`,
|
|
title: `Issue ${n}`,
|
|
body: "",
|
|
kind: "AFK",
|
|
status: "Ready",
|
|
blockedBy,
|
|
...overrides,
|
|
});
|
|
|
|
describe("schedule", () => {
|
|
it("puts unblocked issues in phase 0", () => {
|
|
const phases = schedule([issue(1), issue(2), issue(3)]);
|
|
expect(phases).toHaveLength(1);
|
|
expect(phases[0]?.issues.map((i) => i.number)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it("orders a linear chain into one phase per node", () => {
|
|
const phases = schedule([
|
|
issue(1),
|
|
issue(2, [1]),
|
|
issue(3, [2]),
|
|
]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[1],
|
|
[2],
|
|
[3],
|
|
]);
|
|
});
|
|
|
|
it("groups fan-out children into the same phase", () => {
|
|
const phases = schedule([
|
|
issue(1),
|
|
issue(2, [1]),
|
|
issue(3, [1]),
|
|
issue(4, [1]),
|
|
]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[1],
|
|
[2, 3, 4],
|
|
]);
|
|
});
|
|
|
|
it("handles a diamond DAG", () => {
|
|
const phases = schedule([
|
|
issue(1),
|
|
issue(2, [1]),
|
|
issue(3, [1]),
|
|
issue(4, [2, 3]),
|
|
]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[1],
|
|
[2, 3],
|
|
[4],
|
|
]);
|
|
});
|
|
|
|
it("treats blockers outside the input set as satisfied", () => {
|
|
const phases = schedule([issue(2, [99]), issue(3, [2])]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[2],
|
|
[3],
|
|
]);
|
|
});
|
|
|
|
it("excludes Done issues from phases but keeps their blockers satisfied", () => {
|
|
const phases = schedule([
|
|
issue(1, [], { status: "Done" }),
|
|
issue(2, [1]),
|
|
issue(3, [2]),
|
|
]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[2],
|
|
[3],
|
|
]);
|
|
});
|
|
|
|
it("partitions disconnected components in parallel", () => {
|
|
const phases = schedule([
|
|
issue(1),
|
|
issue(2, [1]),
|
|
issue(10),
|
|
issue(11, [10]),
|
|
]);
|
|
expect(phases.map((p) => p.issues.map((i) => i.number))).toEqual([
|
|
[1, 10],
|
|
[2, 11],
|
|
]);
|
|
});
|
|
|
|
it("throws on a cycle", () => {
|
|
expect(() => schedule([issue(1, [2]), issue(2, [1])])).toThrow(/Cycle/);
|
|
});
|
|
});
|
|
|
|
describe("currentPhase", () => {
|
|
it("returns the first phase containing any non-Done issue", () => {
|
|
const phases = schedule([
|
|
issue(1, [], { status: "Done" }),
|
|
issue(2, [1], { status: "In progress" }),
|
|
issue(3, [2]),
|
|
]);
|
|
expect(currentPhase(phases)?.issues.map((i) => i.number)).toEqual([2]);
|
|
});
|
|
|
|
it("returns undefined when all phases are Done", () => {
|
|
const phases = schedule([
|
|
issue(1, [], { status: "Done" }),
|
|
issue(2, [1], { status: "Done" }),
|
|
]);
|
|
expect(currentPhase(phases)).toBeUndefined();
|
|
});
|
|
});
|