ai-foreman 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
2
+ export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
3
+ import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, TurnResult } from "./types.js";
4
+ /**
5
+ * Pure function: build the `options` object passed to `query()`.
6
+ * Extracted so tests can assert on the shape without making a live SDK call.
7
+ * `canUseTool` is omitted here — it's a closure that the constructor adds.
8
+ */
9
+ export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">): Record<string, unknown>;
10
+ /**
11
+ * Drives Claude Code through the Claude Agent SDK in streaming-input mode:
12
+ * one persistent session, follow-up turns pushed as user messages, permission
13
+ * requests routed to the foreman's handler via `canUseTool`.
14
+ */
15
+ export declare class ClaudeAdapter implements BuilderAdapter {
16
+ readonly agent: "claude";
17
+ private readonly inbox;
18
+ private readonly eventQueue;
19
+ private readonly query;
20
+ private readonly abort;
21
+ private readonly pumpDone;
22
+ private _sessionId?;
23
+ private pending?;
24
+ private closed;
25
+ static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
26
+ private constructor();
27
+ /** Background loop: consume the SDK message stream until it ends. */
28
+ private pump;
29
+ private handle;
30
+ sendTurn(text: string): Promise<TurnResult>;
31
+ sessionId(): string | undefined;
32
+ events(): AsyncIterable<BuilderEvent>;
33
+ close(): Promise<void>;
34
+ }
@@ -1,4 +1,15 @@
1
- import { query } from "@anthropic-ai/claude-agent-sdk";
1
+ /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
2
+ export async function requireClaudeSDK() {
3
+ try {
4
+ return await import("@anthropic-ai/claude-agent-sdk");
5
+ }
6
+ catch {
7
+ throw new Error("Claude Agent SDK not installed.\n" +
8
+ "To use Claude Code as your agent runtime, run:\n" +
9
+ " npm install @anthropic-ai/claude-agent-sdk\n" +
10
+ "Or use Codex only: ai-foreman start --agent codex");
11
+ }
12
+ }
2
13
  import { AsyncQueue } from "../util/asyncQueue.js";
3
14
  /**
4
15
  * Pure function: build the `options` object passed to `query()`.
@@ -38,7 +49,12 @@ export class ClaudeAdapter {
38
49
  _sessionId;
39
50
  pending;
40
51
  closed = false;
41
- constructor(opts) {
52
+ static async create(opts) {
53
+ const { query } = await requireClaudeSDK();
54
+ return new ClaudeAdapter(opts, query);
55
+ }
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ constructor(opts, query) {
42
58
  this.query = query({
43
59
  prompt: this.inbox,
44
60
  options: {
@@ -0,0 +1,46 @@
1
+ import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, TurnResult } from "./types.js";
2
+ /** Result of parsing one JSONL line from `codex exec --json` output. */
3
+ export interface CodexLineResult {
4
+ /** Events to emit to the BuilderEvent stream. */
5
+ events: BuilderEvent[];
6
+ /** If set, update the stored session ID to this value. */
7
+ sessionId?: string;
8
+ /** If set, append this to the turn's accumulated text. */
9
+ text?: string;
10
+ }
11
+ /**
12
+ * Pure function: parse one JSONL event from `codex exec --json` output into
13
+ * structured actions. Exported for unit testing; production code calls this
14
+ * via handleEvent().
15
+ */
16
+ export declare function parseCodexLine(raw: Record<string, unknown>): CodexLineResult;
17
+ /**
18
+ * Drives Codex CLI as a subprocess. Each sendTurn() spawns `codex exec --json`
19
+ * and reads JSONL from stdout. Session continuity is maintained via the
20
+ * thread_id captured from the first run and passed as `resume <id>` on
21
+ * subsequent turns.
22
+ *
23
+ * NOTE: opts.permission is accepted for interface conformance but is not
24
+ * invoked. Codex manages its own tool sandboxing via --sandbox workspace-write.
25
+ */
26
+ export declare class CodexAdapter implements BuilderAdapter {
27
+ private readonly opts;
28
+ readonly agent: "codex";
29
+ private readonly eventQueue;
30
+ private _sessionId?;
31
+ private _closed;
32
+ private _activeProc?;
33
+ constructor(opts: BuilderAdapterOptions);
34
+ /**
35
+ * Prepend role system text to a turn instruction when `systemPromptAppend` is
36
+ * set. Codex reads everything from the prompt (no persistent system prompt), so
37
+ * role guidance must ride along with each turn.
38
+ */
39
+ buildInstruction(instruction: string): string;
40
+ /** Build the `codex exec` argument list for the given instruction. */
41
+ buildArgs(instruction: string): string[];
42
+ sendTurn(instruction: string): Promise<TurnResult>;
43
+ sessionId(): string | undefined;
44
+ events(): AsyncIterable<BuilderEvent>;
45
+ close(): Promise<void>;
46
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Agent-agnostic interface over a coding agent ("builder").
3
+ */
4
+ /** A permission request surfaced by a builder before it runs a tool. */
5
+ export interface PermissionRequest {
6
+ toolName: string;
7
+ input: Record<string, unknown>;
8
+ /** Human-readable prompt from the agent, when available. */
9
+ title?: string;
10
+ }
11
+ /** The foreman's verdict on a permission request. */
12
+ export type PermissionDecision = {
13
+ behavior: "allow";
14
+ } | {
15
+ behavior: "deny";
16
+ message: string;
17
+ };
18
+ /** Decides each permission request. Supplied by the foreman, called by the adapter. */
19
+ export type PermissionHandler = (req: PermissionRequest) => Promise<PermissionDecision>;
20
+ /** Result of a single completed turn. */
21
+ export interface TurnResult {
22
+ /** Final assistant message text — where the STEP_STATUS marker lives. */
23
+ text: string;
24
+ isError: boolean;
25
+ numTurns: number;
26
+ costUsd: number;
27
+ }
28
+ /** Observability events emitted while a builder works. */
29
+ export type BuilderEvent = {
30
+ kind: "text";
31
+ text: string;
32
+ } | {
33
+ kind: "tool";
34
+ name: string;
35
+ input: unknown;
36
+ } | {
37
+ kind: "turn-complete";
38
+ result: TurnResult;
39
+ } | {
40
+ kind: "error";
41
+ message: string;
42
+ };
43
+ export type EffortLevel = "low" | "medium" | "high" | "xhigh";
44
+ export interface BuilderAdapterOptions {
45
+ /** Working directory the builder operates in. */
46
+ cwd: string;
47
+ /** Permission decision callback. */
48
+ permission: PermissionHandler;
49
+ /** Resume a prior session instead of starting fresh. */
50
+ resumeSessionId?: string;
51
+ /** Override the model; omit for the agent's default. */
52
+ model?: string;
53
+ /** Reasoning effort level. Claude also accepts "max"; Codex supports up to "xhigh". */
54
+ effort?: EffortLevel;
55
+ /** Fast mode: lower latency at the cost of some quality. */
56
+ fast?: boolean;
57
+ /** Role system text to append to the harness system prompt (from .rafi/compiled or library). */
58
+ systemPromptAppend?: string;
59
+ /** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
60
+ skills?: string[];
61
+ }
62
+ export interface BuilderAdapter {
63
+ readonly agent: "claude" | "codex";
64
+ /** Send one instruction; resolves when that turn completes. */
65
+ sendTurn(text: string): Promise<TurnResult>;
66
+ /** Current session id, once known — used for resume. */
67
+ sessionId(): string | undefined;
68
+ /** Stream of observability events. Iterate to drive a live view. */
69
+ events(): AsyncIterable<BuilderEvent>;
70
+ /** Shut the builder down and release resources. */
71
+ close(): Promise<void>;
72
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildDoctorCommand(): Command;
@@ -0,0 +1,71 @@
1
+ import { Command } from "commander";
2
+ import { resolve, join } from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { loadConfig } from "../config.js";
6
+ import { isTicketsInitialized } from "../tickets/config.js";
7
+ import { cmdValidate } from "../tickets/commands.js";
8
+ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))?.version;
9
+ function commandVersion(command) {
10
+ const result = spawnSync(command, ["--version"], {
11
+ encoding: "utf8",
12
+ timeout: 3000,
13
+ });
14
+ if (result.error)
15
+ return { ok: false, detail: result.error.message };
16
+ if (result.status !== 0)
17
+ return { ok: false, detail: result.stderr.trim() };
18
+ return { ok: true, detail: (result.stdout.trim() || result.stderr.trim()).slice(0, 120) };
19
+ }
20
+ export function buildDoctorCommand() {
21
+ return new Command("doctor")
22
+ .description("Check Foreman, agent CLIs, config, and optional ticket tracker readiness.")
23
+ .argument("[project]", "path to the project directory", ".")
24
+ .action((project) => {
25
+ const cwd = resolve(project);
26
+ let errors = 0;
27
+ const report = (ok, label, detail) => {
28
+ console.log(`${ok ? "ok" : "!!"} ${label}${detail ? ` — ${detail}` : ""}`);
29
+ if (!ok)
30
+ errors++;
31
+ };
32
+ const warn = (label, detail) => {
33
+ console.log(`-- ${label}${detail ? ` — ${detail}` : ""}`);
34
+ };
35
+ report(Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20, "node >=20", process.version);
36
+ report(Boolean(PACKAGE_VERSION), "foreman package version", PACKAGE_VERSION);
37
+ report(existsSync(cwd), "project directory exists", cwd);
38
+ if (!existsSync(cwd))
39
+ process.exit(1);
40
+ try {
41
+ loadConfig(join(cwd, "foreman.yaml"));
42
+ report(true, "foreman.yaml", existsSync(join(cwd, "foreman.yaml")) ? "valid" : "not present, using defaults");
43
+ }
44
+ catch (err) {
45
+ report(false, "foreman.yaml", err instanceof Error ? err.message : String(err));
46
+ }
47
+ const claude = commandVersion("claude");
48
+ if (claude.ok)
49
+ warn("claude CLI found", claude.detail);
50
+ else
51
+ warn("claude CLI not found", "Claude adapter may still work through the SDK if credentials are configured");
52
+ const codex = commandVersion("codex");
53
+ if (codex.ok)
54
+ warn("codex CLI found", codex.detail);
55
+ else
56
+ warn("codex CLI not found", "required only for --agent codex");
57
+ if (isTicketsInitialized(cwd)) {
58
+ try {
59
+ const result = cmdValidate(cwd);
60
+ report(result.clean, ".tickets validation", result.clean ? "clean" : `${result.issues.length} issue(s)`);
61
+ }
62
+ catch (err) {
63
+ report(false, ".tickets validation", err instanceof Error ? err.message : String(err));
64
+ }
65
+ }
66
+ else {
67
+ warn(".tickets", "not initialized; start will run in plain mode");
68
+ }
69
+ process.exit(errors === 0 ? 0 : 1);
70
+ });
71
+ }
@@ -0,0 +1,3 @@
1
+ import type { BuilderEvent } from "../adapters/types.js";
2
+ /** Print a compact live feed of builder activity. */
3
+ export declare function printEvents(events: AsyncIterable<BuilderEvent>): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildStartCommand(): Command;
@@ -0,0 +1,181 @@
1
+ import { Command } from "commander";
2
+ import { resolve, join, relative } from "node:path";
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { select, text, isCancel } from "@clack/prompts";
5
+ import { loadConfig } from "../config.js";
6
+ import { Log } from "../log.js";
7
+ import { PermissionPolicy } from "../permissions/policy.js";
8
+ import { ClaudeAdapter } from "../adapters/claude.js";
9
+ import { CodexAdapter } from "../adapters/codex.js";
10
+ import { Foreman, createPermissionHandler } from "../foreman.js";
11
+ import { printEvents } from "./events.js";
12
+ import { loadRoleBundle } from "../roles.js";
13
+ function fail(message) {
14
+ console.error(`foreman: ${message}`);
15
+ process.exit(1);
16
+ }
17
+ function findLastSessionId(dir) {
18
+ if (!existsSync(dir))
19
+ return undefined;
20
+ const logs = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort();
21
+ if (logs.length === 0)
22
+ return undefined;
23
+ const lines = readFileSync(join(dir, logs[logs.length - 1]), "utf8")
24
+ .split("\n")
25
+ .filter(Boolean);
26
+ for (let i = lines.length - 1; i >= 0; i--) {
27
+ const r = JSON.parse(lines[i]);
28
+ if (r.event === "batch-end" && typeof r.sessionId === "string") {
29
+ return r.sessionId;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+ export function buildStartCommand() {
35
+ return new Command("start")
36
+ .description("Enlist a builder and drive it through a batch of N steps.")
37
+ .argument("<project>", "path to the project directory the builder works in")
38
+ .requiredOption("-s, --steps <n>", "number of steps to drive")
39
+ .option("-a, --agent <agent>", "builder agent (claude | codex)", "claude")
40
+ .option("-m, --model <model>", "override the builder's model")
41
+ .option("-r, --resume <sessionId>", "resume a prior builder session")
42
+ .option("--continue", "resume the most recent logged session for this project")
43
+ .option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
44
+ .option("-y, --yes", "skip pre-flight confirmation prompt")
45
+ .option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
46
+ .option("--fast", "fast mode — lower latency (maps to effort=low for codex)")
47
+ .option("--no-qa", "disable per-ticket QA review (enabled by default)")
48
+ .action(async (project, opts) => {
49
+ const steps = Number.parseInt(opts.steps, 10);
50
+ if (!Number.isInteger(steps) || steps < 1) {
51
+ fail("--steps must be a positive integer");
52
+ }
53
+ const VALID_AGENTS = ["claude", "codex"];
54
+ if (!VALID_AGENTS.includes(opts.agent)) {
55
+ fail(`unknown agent "${opts.agent}" — choose: ${VALID_AGENTS.join(" | ")}`);
56
+ }
57
+ const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
58
+ if (opts.effort && !VALID_EFFORT.includes(opts.effort)) {
59
+ fail(`unknown effort "${opts.effort}" — choose: ${VALID_EFFORT.join(" | ")}`);
60
+ }
61
+ const cwd = resolve(project);
62
+ if (!existsSync(cwd))
63
+ fail(`project directory not found: ${cwd}`);
64
+ const TRACKER_SEARCH_PATHS = ["docs/ticket-progress.md", "ticket-progress.md"];
65
+ let ticketsContent;
66
+ let trackerRelPath;
67
+ if (opts.tickets) {
68
+ const ticketPath = resolve(opts.tickets);
69
+ if (!existsSync(ticketPath))
70
+ fail(`ticket file not found: ${ticketPath}`);
71
+ ticketsContent = readFileSync(ticketPath, "utf8");
72
+ trackerRelPath = relative(cwd, ticketPath);
73
+ }
74
+ else {
75
+ for (const rel of TRACKER_SEARCH_PATHS) {
76
+ const abs = join(cwd, rel);
77
+ if (existsSync(abs)) {
78
+ ticketsContent = readFileSync(abs, "utf8");
79
+ trackerRelPath = rel;
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ if (opts.resume && opts.continue) {
85
+ fail("choose either --resume <sessionId> or --continue, not both");
86
+ }
87
+ const resumeSessionId = opts.resume ??
88
+ (opts.continue ? findLastSessionId(join(cwd, ".foreman")) : undefined);
89
+ if (opts.continue && !resumeSessionId) {
90
+ fail(`no previous session id found under ${join(cwd, ".foreman")}`);
91
+ }
92
+ const config = loadConfig(join(cwd, "foreman.yaml"));
93
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
94
+ const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
95
+ const log = new Log(logPath);
96
+ const policy = new PermissionPolicy(config.permissions, cwd);
97
+ const roleBundle = loadRoleBundle("builder", { projectDir: cwd });
98
+ const adapterOpts = {
99
+ cwd,
100
+ model: opts.model,
101
+ resumeSessionId,
102
+ permission: createPermissionHandler(policy, log),
103
+ effort: opts.effort,
104
+ fast: opts.fast,
105
+ systemPromptAppend: roleBundle.system || undefined,
106
+ skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
107
+ };
108
+ const builder = opts.agent === "codex"
109
+ ? new CodexAdapter(adapterOpts)
110
+ : await ClaudeAdapter.create(adapterOpts);
111
+ const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
112
+ const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd);
113
+ const modifiers = [
114
+ opts.model ? `model=${opts.model}` : null,
115
+ opts.effort ? `effort=${opts.effort}` : null,
116
+ opts.fast ? "fast" : null,
117
+ qaEnabled ? null : "qa=off",
118
+ ].filter(Boolean).join(" ");
119
+ console.log(`foreman: driving a ${opts.agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
120
+ console.log(`foreman: project ${cwd}`);
121
+ if (trackerRelPath)
122
+ console.log(`foreman: tracker ${trackerRelPath}`);
123
+ console.log(`foreman: log ${logPath}\n`);
124
+ const viewer = printEvents(builder.events());
125
+ try {
126
+ console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
127
+ await foreman.runPreflight(steps, ticketsContent);
128
+ if (!opts.yes) {
129
+ while (true) {
130
+ console.log();
131
+ const action = await select({
132
+ message: "How does this plan look?",
133
+ options: [
134
+ { value: "proceed", label: "Proceed — start implementing" },
135
+ { value: "feedback", label: "Give feedback — revise the plan" },
136
+ { value: "cancel", label: "Cancel" },
137
+ ],
138
+ });
139
+ if (isCancel(action) || action === "cancel") {
140
+ console.log("ai-foreman: cancelled");
141
+ await builder.close();
142
+ await viewer;
143
+ process.exit(0);
144
+ }
145
+ if (action === "proceed") {
146
+ console.log();
147
+ break;
148
+ }
149
+ const fb = await text({
150
+ message: "Your feedback:",
151
+ validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
152
+ });
153
+ if (isCancel(fb)) {
154
+ console.log("ai-foreman: cancelled");
155
+ await builder.close();
156
+ await viewer;
157
+ process.exit(0);
158
+ }
159
+ console.log();
160
+ await foreman.sendPreflightFeedback(String(fb));
161
+ }
162
+ }
163
+ const result = await foreman.runBatch(steps, trackerRelPath);
164
+ await builder.close();
165
+ await viewer;
166
+ console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
167
+ console.log(`foreman: outcome — ${result.outcome}`);
168
+ if (result.detail)
169
+ console.log(`foreman: ${result.detail}`);
170
+ const sid = builder.sessionId();
171
+ if (sid)
172
+ console.log(`foreman: resume this builder with --resume ${sid}`);
173
+ process.exit(result.outcome === "needs-human" ? 2 : 0);
174
+ }
175
+ catch (err) {
176
+ await builder.close().catch(() => { });
177
+ log.write("error", { message: String(err) });
178
+ fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
179
+ }
180
+ });
181
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildStatusCommand(): Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import { resolve, join } from "node:path";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ function fail(message) {
5
+ console.error(`foreman: ${message}`);
6
+ process.exit(1);
7
+ }
8
+ export function buildStatusCommand() {
9
+ return new Command("status")
10
+ .description("Summarize the most recent foreman run for a project.")
11
+ .argument("<project>", "path to the project directory")
12
+ .action((project) => {
13
+ const dir = join(resolve(project), ".foreman");
14
+ if (!existsSync(dir))
15
+ fail(`no foreman runs found under ${dir}`);
16
+ const logs = readdirSync(dir)
17
+ .filter((f) => f.endsWith(".jsonl"))
18
+ .sort();
19
+ if (logs.length === 0)
20
+ fail(`no foreman runs found under ${dir}`);
21
+ const latest = join(dir, logs[logs.length - 1]);
22
+ const records = readFileSync(latest, "utf8")
23
+ .split("\n")
24
+ .filter(Boolean)
25
+ .map((line) => JSON.parse(line));
26
+ const steps = records.filter((r) => r.event === "step");
27
+ const escalations = records.filter((r) => r.event === "escalation");
28
+ const batchEnd = records.reverse().find((r) => r.event === "batch-end");
29
+ console.log(`foreman: latest run ${logs[logs.length - 1]}`);
30
+ console.log(`foreman: ${steps.length} step record(s), ${escalations.length} escalation(s)`);
31
+ if (batchEnd) {
32
+ console.log(`foreman: outcome — ${batchEnd.outcome} (${batchEnd.completed}/${batchEnd.requested})`);
33
+ if (batchEnd.detail)
34
+ console.log(`foreman: ${batchEnd.detail}`);
35
+ }
36
+ else {
37
+ console.log("ai-foreman: run is still in progress or did not finish");
38
+ }
39
+ for (const esc of escalations) {
40
+ console.log(` escalated: ${esc.tool} — ${esc.reason}`);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function buildPopulateInstruction(): string;
3
+ export declare function buildTicketsCommand(): Command;
@@ -144,7 +144,7 @@ export function buildTicketsCommand() {
144
144
  };
145
145
  const builder = agent === "codex"
146
146
  ? new CodexAdapter(adapterOpts)
147
- : new ClaudeAdapter(adapterOpts);
147
+ : await ClaudeAdapter.create(adapterOpts);
148
148
  const viewer = printEvents(builder.events());
149
149
  const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, dir);
150
150
  console.log(`foreman tickets: populating tickets with ${agent}`);
@@ -0,0 +1,27 @@
1
+ export interface PermissionConfig {
2
+ /** Bash command prefixes that are auto-approved when every segment matches. */
3
+ allowBash: string[];
4
+ /** Bash command substrings that always escalate (checked first). */
5
+ escalateBash: string[];
6
+ /** Tool names auto-approved when their target stays inside the worktree. */
7
+ allowTools: string[];
8
+ /** Tool names that always escalate. */
9
+ escalateTools: string[];
10
+ }
11
+ export interface NotificationsConfig {
12
+ /** Fire a desktop notification when the builder needs user input. Default: false. */
13
+ enabled: boolean;
14
+ }
15
+ export interface QaConfig {
16
+ /** Run a QA review pass after each completed ticket or step. Default: true. */
17
+ enabled: boolean;
18
+ }
19
+ export interface ForemanConfig {
20
+ permissions: PermissionConfig;
21
+ notifications: NotificationsConfig;
22
+ qa: QaConfig;
23
+ }
24
+ /** Built-in defaults; foreman.yaml overrides any field present. */
25
+ export declare const DEFAULT_CONFIG: ForemanConfig;
26
+ /** Load foreman.yaml if present and deep-merge it over the defaults. */
27
+ export declare function loadConfig(path?: string): ForemanConfig;
@@ -0,0 +1,74 @@
1
+ import type { BuilderAdapter, PermissionHandler, TurnResult } from "./adapters/types.js";
2
+ import type { PermissionPolicy } from "./permissions/policy.js";
3
+ import type { Log } from "./log.js";
4
+ /** Parsed STEP_STATUS marker from a builder's turn. */
5
+ export interface StepStatus {
6
+ kind: "done" | "blocked" | "plan_complete" | "needs_input" | "qa_pass" | "qa_fail" | "unknown";
7
+ summary?: string;
8
+ next?: string;
9
+ reason?: string;
10
+ question?: string;
11
+ choices?: string[];
12
+ issues?: string;
13
+ ticket?: string;
14
+ error?: string;
15
+ }
16
+ /**
17
+ * Pull the STEP_STATUS marker out of a turn's final text.
18
+ * Format: `STEP_STATUS: <kind> | key="value" key="value"`.
19
+ */
20
+ export declare function parseStepStatus(text: string): StepStatus;
21
+ /** Heuristic: did a marker-less turn end by asking the human something? */
22
+ export declare function looksLikeQuestion(text: string): boolean;
23
+ export { MARKER_SPEC } from "./markers.js";
24
+ export { QA_MARKER_SPEC } from "./markers.js";
25
+ /** Instruction sent on the first turn of a batch. */
26
+ export declare function buildPrimer(n: number, trackerPath?: string, ticketsEnabled?: boolean): string;
27
+ /** Instruction sent on turns 2–N. */
28
+ export declare function buildNextStepInstruction(i: number, n: number): string;
29
+ /** QA review turn: ask the builder to triple-check the ticket or step it just completed. */
30
+ export declare function buildQaInstruction(): string;
31
+ /** Follow-up turn after qa_fail: have the builder implement the listed fixes. */
32
+ export declare function buildQaFixInstruction(issues: string): string;
33
+ /** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
34
+ export declare function buildPlanningTurn(n: number, ticketsContent?: string): string;
35
+ /** Builds the permission callback: classify, log, allow or escalate. */
36
+ export declare function createPermissionHandler(policy: PermissionPolicy, log: Log): PermissionHandler;
37
+ export interface BatchResult {
38
+ completed: number;
39
+ requested: number;
40
+ outcome: "all-done" | "plan-complete" | "blocked" | "needs-human";
41
+ detail?: string;
42
+ }
43
+ /** Drives one builder through a batch of N steps via the STEP_STATUS protocol. */
44
+ export declare class Foreman {
45
+ private readonly builder;
46
+ private readonly log;
47
+ private readonly notificationsEnabled;
48
+ private readonly qaEnabled;
49
+ private readonly qaMaxCycles;
50
+ private readonly projectDir?;
51
+ private readonly ticketsEnabled;
52
+ constructor(builder: BuilderAdapter, log: Log, notificationsEnabled?: boolean, qaEnabled?: boolean, qaMaxCycles?: number, projectDir?: string | undefined);
53
+ /**
54
+ * Send one instruction and resolve any needs_input exchanges before returning.
55
+ * The returned result and status always reflect the final (non-needs_input) turn.
56
+ */
57
+ private doTurn;
58
+ /** Send a planning turn and return the builder's response text. Does not count toward steps. */
59
+ runPreflight(n: number, ticketsContent?: string): Promise<string>;
60
+ /** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
61
+ sendPreflightFeedback(feedback: string): Promise<void>;
62
+ /** Send one custom instruction through the same needs_input loop as a batch turn. */
63
+ runInstruction(instruction: string): Promise<{
64
+ result: TurnResult;
65
+ status: StepStatus;
66
+ }>;
67
+ /**
68
+ * Run a QA review pass on the ticket the builder just completed.
69
+ * Loops on qa_fail → fix → re-QA until qa_pass or the cycle cap is reached.
70
+ * QA turns are free — they do not advance the step counter.
71
+ */
72
+ private runQa;
73
+ runBatch(n: number, trackerPath?: string): Promise<BatchResult>;
74
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};