ai-foreman 1.2.0 → 1.3.1

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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ Requires:
51
51
 
52
52
  - Check the target project.
53
53
  - Initialize Foreman's local ticket tracker.
54
- - Ask agent to convert existing tickets or populate them
54
+ - Ask the ticket-maker role to convert existing tickets or populate them
55
55
  - Run foreman for 5 tickets (and QA each one when done)
56
56
 
57
57
  ```bash
@@ -65,12 +65,13 @@ What each part does:
65
65
 
66
66
  - `./my-project`: the repo Foreman will work on.
67
67
  - `tickets init`: creates the local `.tickets/` tracker.
68
- - `tickets populate`: asks Codex to convert project planning material.
68
+ - `tickets populate`: asks the ticket-maker role to convert project planning material.
69
69
  - Converted output uses Foreman's schema.
70
70
  - `start`: drives Codex through the next tickets one step at a time.
71
71
 
72
72
  `tickets populate` can convert:
73
73
 
74
+ - a managed Rafi plan at `<docs.root>/rafi-plan.md`
74
75
  - plans
75
76
  - TODOs
76
77
  - roadmap docs
@@ -159,7 +160,7 @@ gh repo view <host>/<owner>/<repo>
159
160
  | `-a, --agent <agent>` | `claude` / `codex` | single `rafi-config.yaml` target, otherwise `claude` | Builder agent. Explicit `--agent` always wins. |
160
161
  | `-m, --model <model>` | `gpt-5.5` / any supported agent model | agent default | Overrides the builder model. |
161
162
  | `--effort <level>` | Claude Code: `low` / `medium` / `high` / `xhigh`<br>Codex: `low` / `medium` / `high` / `xhigh` | agent default | Reasoning level. |
162
- | `--sources <paths...>` | `docs/tickets.md docs/plans/**` | scans relevant docs automatically | Optional files, folders, or globs for the agent to check first. Any reasonable planning format is OK. |
163
+ | `--sources <paths...>` | `docs/tickets.md docs/plans/**` | Configured Rafi `<docs.root>/rafi-plan.md`, then ticket docs root, then `docs/rafi-plan.md`, otherwise scans | Optional files, folders, or globs for the agent to check first. Any reasonable planning format is OK. |
163
164
  | `--fast` | flag | off | Lower latency. |
164
165
  | `-y, --yes` | flag | off | Skips confirmation before builder edits ticket files. |
165
166
 
@@ -257,11 +258,15 @@ Init options:
257
258
  | `-p, --project <dir>` | `./my-project` / `../repo` | current directory | Project to initialize. |
258
259
  | `--app-name <name>` | `"My App"` | none | Application name stored in tracker config. |
259
260
  | `--timezone <tz>` | `America/Chicago` / `UTC` | `UTC` | IANA timezone. |
260
- | `--queue-limit <n>` | `25` / `50` / `100` | `50` | Next-queue window size. |
261
+ | `--implementation-limit <n>` | `250` / `500` / `1000` | `500` | Implementation queue window for generated docs and Foreman selection. |
262
+ | `--view-limit <n>` | `1000` / `20000` | `20000` | Default row limit for the display queue command. |
263
+ | `--queue-limit <n>` | `250` / `500` / `1000` | none | Deprecated alias for `--implementation-limit`. |
261
264
  | `--docs-root <dir>` | `docs` / `docs-rafi` | `rafi-config.yaml` docs.root, then `docs` | Root for generated progress/archive docs. |
262
265
 
263
266
  `tickets init` reads `docs.root` from `rafi-config.yaml` when present. `--docs-root` overrides that value. The selected root must be a safe repo-relative directory, and init refuses to overwrite an existing selected `ticket-progress.md`.
264
267
 
268
+ Init writes `implementation_limit` and `view_limit` to `.tickets/config.yaml`. Existing configs with `queue_limit` still load as an implementation limit, with the old default value `50` upgraded to `500`.
269
+
265
270
  This creates:
266
271
 
267
272
  ```txt
@@ -283,7 +288,7 @@ With `--docs-root docs-rafi`, the generated tracker docs are written under `docs
283
288
 
284
289
  If the project already has planning material:
285
290
 
286
- - Ask a builder to convert it into Foreman's ticket format.
291
+ - Ask the ticket-maker role to convert it into Foreman's ticket format.
287
292
 
288
293
  Supported source material can include:
289
294
 
@@ -297,6 +302,7 @@ Supported source material can include:
297
302
  ```bash
298
303
  # Claude Code
299
304
  ai-foreman tickets populate --project ./my-project
305
+ ai-foreman tickets populate --project ./my-project --sources docs/rafi-plan.md
300
306
  ai-foreman tickets populate --project ./my-project --sources docs/tickets.md docs/plans/**
301
307
 
302
308
  # Codex
@@ -308,6 +314,7 @@ ai-foreman tickets populate --project ./my-project --agent codex --model gpt-5.5
308
314
 
309
315
  - read `.tickets/*`
310
316
  - read the configured progress doc
317
+ - prefer the configured Rafi `<docs.root>/rafi-plan.md`, then the ticket docs root, then `docs/rafi-plan.md` when no `--sources` are provided
311
318
  - read existing planning files
312
319
  - fill `.tickets/tickets.yaml`
313
320
  - render the progress doc
@@ -535,7 +542,8 @@ For Claude:
535
542
  For Codex:
536
543
 
537
544
  - Codex tool calls are not currently intercepted by Foreman.
538
- - Codex runs under `codex exec --sandbox workspace-write`.
545
+ - Runtime implementation and ticket population runs use `codex exec --sandbox workspace-write`.
546
+ - Read-only callers such as `rafi plan` can request `codex exec --sandbox read-only`.
539
547
  - Codex safety depends on Codex's own sandboxing.
540
548
 
541
549
  Default config:
@@ -21,7 +21,7 @@ export declare function parseCodexLine(raw: Record<string, unknown>): CodexLineR
21
21
  * subsequent turns.
22
22
  *
23
23
  * NOTE: opts.permission is accepted for interface conformance but is not
24
- * invoked. Codex manages its own tool sandboxing via --sandbox workspace-write.
24
+ * invoked. Codex manages its own tool sandboxing via --sandbox.
25
25
  */
26
26
  export declare class CodexAdapter implements BuilderAdapter {
27
27
  private readonly opts;
@@ -39,6 +39,7 @@ export declare class CodexAdapter implements BuilderAdapter {
39
39
  buildInstruction(instruction: string): string;
40
40
  /** Build the `codex exec` argument list for the given instruction. */
41
41
  buildArgs(instruction: string): string[];
42
+ private buildSkillsAppendix;
42
43
  sendTurn(instruction: string): Promise<TurnResult>;
43
44
  sessionId(): string | undefined;
44
45
  events(): AsyncIterable<BuilderEvent>;
@@ -1,5 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { createInterface } from "node:readline";
5
+ import { loadSkill } from "special-agents";
3
6
  import { AsyncQueue } from "../util/asyncQueue.js";
4
7
  import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
5
8
  /**
@@ -44,7 +47,7 @@ export function parseCodexLine(raw) {
44
47
  * subsequent turns.
45
48
  *
46
49
  * NOTE: opts.permission is accepted for interface conformance but is not
47
- * invoked. Codex manages its own tool sandboxing via --sandbox workspace-write.
50
+ * invoked. Codex manages its own tool sandboxing via --sandbox.
48
51
  */
49
52
  export class CodexAdapter {
50
53
  opts;
@@ -63,13 +66,22 @@ export class CodexAdapter {
63
66
  * role guidance must ride along with each turn.
64
67
  */
65
68
  buildInstruction(instruction) {
66
- return this.opts.systemPromptAppend
67
- ? `${this.opts.systemPromptAppend}\n\n${instruction}`
68
- : instruction;
69
+ return [
70
+ this.opts.systemPromptAppend,
71
+ this.buildSkillsAppendix(),
72
+ instruction,
73
+ ].filter((part) => Boolean(part)).join("\n\n");
69
74
  }
70
75
  /** Build the `codex exec` argument list for the given instruction. */
71
76
  buildArgs(instruction) {
72
- const args = ["exec", "--json", "--sandbox", "workspace-write", "-C", this.opts.cwd];
77
+ const args = [
78
+ "exec",
79
+ "--json",
80
+ "--sandbox",
81
+ this.opts.sandboxMode ?? "workspace-write",
82
+ "-C",
83
+ this.opts.cwd,
84
+ ];
73
85
  if (this.opts.model)
74
86
  args.push("-m", this.opts.model);
75
87
  if (this.opts.effort) {
@@ -84,6 +96,20 @@ export class CodexAdapter {
84
96
  args.push(instruction);
85
97
  return args;
86
98
  }
99
+ buildSkillsAppendix() {
100
+ if (!this.opts.skills || this.opts.skills.length === 0)
101
+ return undefined;
102
+ const blocks = this.opts.skills
103
+ .map((skill) => loadSkillMarkdown(this.opts.cwd, skill))
104
+ .filter((block) => Boolean(block));
105
+ if (blocks.length === 0)
106
+ return undefined;
107
+ return [
108
+ "# Preloaded Skills",
109
+ "Use the following skills for this run.",
110
+ ...blocks,
111
+ ].join("\n\n");
112
+ }
87
113
  sendTurn(instruction) {
88
114
  if (this._closed)
89
115
  return Promise.reject(new Error("builder is closed"));
@@ -158,3 +184,17 @@ export class CodexAdapter {
158
184
  return Promise.resolve();
159
185
  }
160
186
  }
187
+ function loadSkillMarkdown(cwd, skill) {
188
+ const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
189
+ if (existsSync(projectPath)) {
190
+ return `## ${skill}\n${readFileSync(projectPath, "utf8").trim()}`;
191
+ }
192
+ try {
193
+ const bundled = loadSkill(skill);
194
+ const body = bundled.body?.trim();
195
+ return body ? `## ${bundled.name}\n${body}` : undefined;
196
+ }
197
+ catch {
198
+ return undefined;
199
+ }
200
+ }
@@ -54,6 +54,8 @@ export interface BuilderAdapterOptions {
54
54
  effort?: EffortLevel;
55
55
  /** Fast mode: lower latency at the cost of some quality. */
56
56
  fast?: boolean;
57
+ /** Codex sandbox mode. Defaults to workspace-write for implementation runs. */
58
+ sandboxMode?: "workspace-write" | "read-only";
57
59
  /** Role system text to append to the harness system prompt (from .rafi/compiled or library). */
58
60
  systemPromptAppend?: string;
59
61
  /** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
@@ -0,0 +1,55 @@
1
+ import { type PermissionConfig } from "./config.js";
2
+ import { Log } from "./log.js";
3
+ import { type StepStatus } from "./foreman.js";
4
+ import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } from "./adapters/types.js";
5
+ import { type RoleBundle } from "./roles.js";
6
+ import type { AgentRuntime } from "./runtimeAuth.js";
7
+ export type { EffortLevel } from "./adapters/types.js";
8
+ export declare const VALID_EFFORT: readonly EffortLevel[];
9
+ export interface RoleBuilderOptions {
10
+ projectDir: string;
11
+ role: string;
12
+ agent?: string;
13
+ model?: string;
14
+ effort?: EffortLevel;
15
+ fast?: boolean;
16
+ yes?: boolean;
17
+ allowSwitch?: boolean;
18
+ label: string;
19
+ log: Log;
20
+ permissionConfig?: PermissionConfig;
21
+ extraSkills?: string[];
22
+ sandboxMode?: BuilderAdapterOptions["sandboxMode"];
23
+ resumeSessionId?: string;
24
+ }
25
+ export interface RoleBuilder {
26
+ builder: BuilderAdapter;
27
+ runtime: AgentRuntime;
28
+ model?: string;
29
+ effort?: EffortLevel;
30
+ roleBundle: RoleBundle;
31
+ skills: string[];
32
+ }
33
+ export interface RoleInstructionRunOptions extends Omit<RoleBuilderOptions, "log"> {
34
+ instruction: string;
35
+ logPath?: string;
36
+ logEvent?: "rafi-plan" | "ticket-populate" | "preflight";
37
+ }
38
+ export interface RoleInstructionRunResult {
39
+ turn: {
40
+ result: TurnResult;
41
+ status: StepStatus;
42
+ };
43
+ runtime: AgentRuntime;
44
+ model?: string;
45
+ effort?: EffortLevel;
46
+ logPath: string;
47
+ roleBundle: RoleBundle;
48
+ skills: string[];
49
+ }
50
+ export declare function assertEffortLevel(effort: string | undefined): asserts effort is EffortLevel | undefined;
51
+ export declare function makeLogPath(projectDir: string, label: string): string;
52
+ export declare function mergeSkills(roleSkills: readonly string[], extraSkills?: readonly string[]): string[];
53
+ export declare function readOnlyPermissionConfig(): PermissionConfig;
54
+ export declare function createRoleBuilder(opts: RoleBuilderOptions): Promise<RoleBuilder>;
55
+ export declare function runRoleInstruction(opts: RoleInstructionRunOptions): Promise<RoleInstructionRunResult>;
@@ -0,0 +1,151 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadConfig, DEFAULT_CONFIG } from "./config.js";
4
+ import { Log } from "./log.js";
5
+ import { PermissionPolicy } from "./permissions/policy.js";
6
+ import { ClaudeAdapter } from "./adapters/claude.js";
7
+ import { CodexAdapter } from "./adapters/codex.js";
8
+ import { Foreman, createPermissionHandler } from "./foreman.js";
9
+ import { printEvents } from "./cli/events.js";
10
+ import { loadRoleBundle } from "./roles.js";
11
+ import { ensureRuntimeReadyForCommand } from "./cli/runtimeAuthPrompt.js";
12
+ import { resolveAgentForProject } from "./cli/runtimeSelection.js";
13
+ export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
14
+ export function assertEffortLevel(effort) {
15
+ if (effort && !VALID_EFFORT.includes(effort)) {
16
+ throw new Error(`unknown effort "${effort}" - choose: ${VALID_EFFORT.join(" | ")}`);
17
+ }
18
+ }
19
+ export function makeLogPath(projectDir, label) {
20
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
21
+ return join(projectDir, ".foreman", `${stamp}-${label}.jsonl`);
22
+ }
23
+ export function mergeSkills(roleSkills, extraSkills = []) {
24
+ return [...new Set([...roleSkills, ...extraSkills])];
25
+ }
26
+ export function readOnlyPermissionConfig() {
27
+ return {
28
+ allowBash: [
29
+ "pwd",
30
+ "ls",
31
+ "cat ",
32
+ "sed -n ",
33
+ "grep ",
34
+ "rg ",
35
+ "head ",
36
+ "tail ",
37
+ "wc ",
38
+ "git status",
39
+ "git diff --stat",
40
+ "git diff --name-only",
41
+ "git log --oneline",
42
+ "git show --stat",
43
+ "git ls-files",
44
+ "git grep",
45
+ ],
46
+ escalateBash: [
47
+ ...DEFAULT_CONFIG.permissions.escalateBash,
48
+ " --output",
49
+ " -o",
50
+ " --exec",
51
+ " --format",
52
+ "git add",
53
+ "git commit",
54
+ "git checkout",
55
+ "git restore",
56
+ "git stash",
57
+ "npm install",
58
+ "npm run",
59
+ "npm ci",
60
+ "pnpm ",
61
+ "yarn ",
62
+ "mkdir ",
63
+ "touch ",
64
+ "mv ",
65
+ "cp ",
66
+ "rm ",
67
+ "ai-foreman tickets",
68
+ "foreman tickets",
69
+ ],
70
+ strictShellRedirection: true,
71
+ allowTools: ["Read", "Glob", "Grep", "TodoWrite"],
72
+ escalateTools: [
73
+ ...new Set([
74
+ ...DEFAULT_CONFIG.permissions.escalateTools,
75
+ "Edit",
76
+ "Write",
77
+ "MultiEdit",
78
+ "NotebookEdit",
79
+ ]),
80
+ ],
81
+ };
82
+ }
83
+ export async function createRoleBuilder(opts) {
84
+ if (!existsSync(opts.projectDir)) {
85
+ throw new Error(`project directory not found: ${opts.projectDir}`);
86
+ }
87
+ assertEffortLevel(opts.effort);
88
+ let runtime = resolveAgentForProject(opts.projectDir, opts.agent);
89
+ const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
90
+ const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
91
+ const initialModel = opts.model ?? roleBundle.model ?? undefined;
92
+ const effort = opts.effort ?? roleBundle.effort ?? undefined;
93
+ const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
94
+ label: opts.label,
95
+ yes: Boolean(opts.yes),
96
+ allowSwitch: opts.allowSwitch,
97
+ model: initialModel,
98
+ });
99
+ runtime = ready.runtime;
100
+ const model = ready.model;
101
+ const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
102
+ const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
103
+ const adapterOpts = {
104
+ cwd: opts.projectDir,
105
+ model,
106
+ resumeSessionId: opts.resumeSessionId,
107
+ permission: createPermissionHandler(policy, opts.log),
108
+ effort,
109
+ fast: opts.fast,
110
+ sandboxMode: opts.sandboxMode,
111
+ systemPromptAppend: roleBundle.system || undefined,
112
+ skills: skills.length > 0 ? skills : undefined,
113
+ };
114
+ const builder = runtime === "codex"
115
+ ? new CodexAdapter(adapterOpts)
116
+ : await ClaudeAdapter.create(adapterOpts);
117
+ return { builder, runtime, model, effort, roleBundle, skills };
118
+ }
119
+ export async function runRoleInstruction(opts) {
120
+ const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
121
+ const log = new Log(logPath);
122
+ const { builder, runtime, model, effort, roleBundle, skills } = await createRoleBuilder({
123
+ ...opts,
124
+ log,
125
+ });
126
+ const viewer = printEvents(builder.events());
127
+ const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
128
+ const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, opts.projectDir);
129
+ try {
130
+ const turn = await foreman.runInstruction(opts.instruction);
131
+ await builder.close();
132
+ await viewer;
133
+ if (opts.logEvent) {
134
+ log.write(opts.logEvent, {
135
+ role: opts.role,
136
+ statusKind: turn.status.kind,
137
+ summary: turn.status.summary,
138
+ reason: turn.status.reason,
139
+ costUsd: turn.result.costUsd,
140
+ isError: turn.result.isError,
141
+ });
142
+ }
143
+ return { turn, runtime, model, effort, logPath, roleBundle, skills };
144
+ }
145
+ catch (err) {
146
+ await builder.close().catch(() => { });
147
+ await viewer.catch(() => { });
148
+ log.write("error", { message: String(err) });
149
+ throw err;
150
+ }
151
+ }
@@ -1,3 +1,17 @@
1
1
  import { Command } from "commander";
2
+ import { Log } from "../log.js";
3
+ import type { EffortLevel } from "../adapters/types.js";
4
+ import { type RoleBuilderOptions } from "../agentRun.js";
5
+ import type { TicketsConfig } from "../tickets/config.js";
2
6
  export declare function buildPopulateInstruction(sourceHints?: string[], progressDoc?: string): string;
7
+ export declare function resolvePopulateSources(projectDir: string, explicitSources: string[] | undefined, ticketsConfig: TicketsConfig): string[] | undefined;
8
+ export declare function buildPopulateAgentRunOptions(opts: {
9
+ projectDir: string;
10
+ agent?: string;
11
+ model?: string;
12
+ effort?: EffortLevel;
13
+ fast?: boolean;
14
+ yes?: boolean;
15
+ log: Log;
16
+ }): RoleBuilderOptions;
3
17
  export declare function buildTicketsCommand(): Command;
@@ -1,20 +1,18 @@
1
1
  import { Command } from "commander";
2
- import { existsSync } from "node:fs";
3
- import { isAbsolute, join, resolve } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
4
5
  import { select, isCancel } from "@clack/prompts";
5
6
  import { loadConfig } from "../config.js";
6
7
  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";
8
+ import { Foreman } from "../foreman.js";
11
9
  import { printEvents } from "./events.js";
12
10
  import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDiscover, cmdAcceptFutureWork, cmdReorder, cmdRender, cmdValidate, cmdQueue, cmdArchive, } from "../tickets/commands.js";
13
- import { isTicketsInitialized, loadTicketsConfig } from "../tickets/config.js";
11
+ import { DEFAULT_TICKETS_CONFIG, isTicketsInitialized, loadTicketsConfig } from "../tickets/config.js";
12
+ import { validateDocsRoot } from "../tickets/config.js";
14
13
  import { importFromMarkdown } from "../tickets/importer.js";
15
14
  import { formatValidationIssues } from "../tickets/validate.js";
16
- import { ensureRuntimeReadyForCommand } from "./runtimeAuthPrompt.js";
17
- import { resolveAgentForProject } from "./runtimeSelection.js";
15
+ import { assertEffortLevel, createRoleBuilder, makeLogPath, } from "../agentRun.js";
18
16
  function fail(msg) {
19
17
  console.error(`foreman tickets: ${msg}`);
20
18
  process.exit(1);
@@ -22,15 +20,22 @@ function fail(msg) {
22
20
  function cwd(opts) {
23
21
  return resolve(opts.project ?? ".");
24
22
  }
25
- const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
23
+ function unique(values) {
24
+ return [...new Set(values)];
25
+ }
26
+ function optionWasProvided(command, name) {
27
+ const source = command.getOptionValueSource(name);
28
+ return source !== undefined && source !== "default";
29
+ }
30
+ const TICKET_POPULATE_ROLE = "ticket-maker";
31
+ const RAFI_CONFIG_FILES = ["rafi-config.yaml", "project.yaml"];
26
32
  function validateEffort(effort) {
27
- if (effort && !VALID_EFFORT.includes(effort)) {
28
- fail(`unknown effort "${effort}" — choose: ${VALID_EFFORT.join(" | ")}`);
33
+ try {
34
+ assertEffortLevel(effort);
35
+ }
36
+ catch (err) {
37
+ fail(err instanceof Error ? err.message : String(err));
29
38
  }
30
- }
31
- function makeLogPath(projectDir, label) {
32
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
33
- return join(projectDir, ".foreman", `${stamp}-${label}.jsonl`);
34
39
  }
35
40
  export function buildPopulateInstruction(sourceHints, progressDoc = "docs/ticket-progress.md") {
36
41
  const sources = sourceHints ?? [];
@@ -81,6 +86,49 @@ STEP_STATUS: done | summary="populated Foreman tickets from existing project tic
81
86
  or
82
87
  STEP_STATUS: blocked | reason="why ticket population cannot proceed"`;
83
88
  }
89
+ export function resolvePopulateSources(projectDir, explicitSources, ticketsConfig) {
90
+ if (explicitSources && explicitSources.length > 0)
91
+ return explicitSources;
92
+ const rafiPlan = resolveConfiguredRafiPlanSource(projectDir, "rafi-config.yaml");
93
+ if (rafiPlan)
94
+ return [rafiPlan];
95
+ const legacyPlan = resolveConfiguredRafiPlanSource(projectDir, "project.yaml");
96
+ if (legacyPlan)
97
+ return [legacyPlan];
98
+ const candidates = unique([
99
+ join(dirname(ticketsConfig.paths.progressDoc), "rafi-plan.md"),
100
+ "docs/rafi-plan.md",
101
+ ]);
102
+ const plan = candidates.find((candidate) => existsSync(join(projectDir, candidate)));
103
+ return plan ? [plan] : undefined;
104
+ }
105
+ function resolveConfiguredRafiPlanSource(projectDir, file) {
106
+ const configPath = join(projectDir, file);
107
+ if (!existsSync(configPath))
108
+ return undefined;
109
+ const raw = parseYaml(readFileSync(configPath, "utf8"));
110
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
111
+ throw new Error(`${file}: expected a YAML object`);
112
+ }
113
+ const docs = raw.docs;
114
+ if (typeof docs?.root !== "string")
115
+ return undefined;
116
+ const candidate = `${validateDocsRoot(projectDir, docs.root)}/rafi-plan.md`;
117
+ return existsSync(join(projectDir, candidate)) ? candidate : undefined;
118
+ }
119
+ export function buildPopulateAgentRunOptions(opts) {
120
+ return {
121
+ projectDir: opts.projectDir,
122
+ role: TICKET_POPULATE_ROLE,
123
+ agent: opts.agent,
124
+ model: opts.model,
125
+ effort: opts.effort,
126
+ fast: opts.fast,
127
+ yes: opts.yes,
128
+ label: "tickets populate",
129
+ log: opts.log,
130
+ };
131
+ }
84
132
  export function buildTicketsCommand() {
85
133
  const tickets = new Command("tickets").description("Manage the structured ticket tracker for a project.");
86
134
  // ── init ────────────────────────────────────────────────────────────────────
@@ -90,15 +138,24 @@ export function buildTicketsCommand() {
90
138
  .option("-p, --project <dir>", "project directory (default: cwd)")
91
139
  .option("--app-name <name>", "application name")
92
140
  .option("--timezone <tz>", "IANA timezone (e.g. America/Chicago)", "UTC")
93
- .option("--queue-limit <n>", "next-queue window size", "50")
141
+ .option("--implementation-limit <n>", "implementation queue window size", String(DEFAULT_TICKETS_CONFIG.implementationLimit))
142
+ .option("--view-limit <n>", "ticket queue display limit", String(DEFAULT_TICKETS_CONFIG.viewLimit))
143
+ .option("--queue-limit <n>", "deprecated alias for --implementation-limit")
94
144
  .option("--docs-root <dir>", "repo-relative directory for generated ticket docs")
95
- .action((opts) => {
145
+ .action((opts, command) => {
96
146
  const dir = cwd(opts);
97
147
  try {
148
+ const hasImplementationLimit = optionWasProvided(command, "implementationLimit");
149
+ const hasLegacyQueueLimit = optionWasProvided(command, "queueLimit");
150
+ if (hasImplementationLimit && hasLegacyQueueLimit) {
151
+ fail("--implementation-limit and deprecated --queue-limit cannot both be passed");
152
+ }
98
153
  cmdInit(dir, {
99
154
  appName: opts.appName,
100
155
  timezone: opts.timezone,
101
- queueLimit: Number(opts.queueLimit),
156
+ implementationLimit: hasImplementationLimit ? Number(opts.implementationLimit) : undefined,
157
+ viewLimit: optionWasProvided(command, "viewLimit") ? Number(opts.viewLimit) : undefined,
158
+ queueLimit: hasLegacyQueueLimit ? Number(opts.queueLimit) : undefined,
102
159
  docsRoot: opts.docsRoot,
103
160
  });
104
161
  console.log(`foreman tickets: initialized .tickets/ in ${dir}`);
@@ -111,7 +168,7 @@ export function buildTicketsCommand() {
111
168
  // ── populate ────────────────────────────────────────────────────────────────
112
169
  tickets
113
170
  .command("populate")
114
- .description("Ask a builder to populate .tickets/tickets.yaml from existing project ticket/backlog docs.")
171
+ .description("Ask the ticket-maker role to populate .tickets/tickets.yaml from existing project ticket/backlog docs.")
115
172
  .option("-p, --project <dir>", "project directory (default: cwd)")
116
173
  .option("-a, --agent <agent>", "builder agent (claude | codex)")
117
174
  .option("-m, --model <model>", "override the builder's model")
@@ -124,14 +181,6 @@ export function buildTicketsCommand() {
124
181
  validateEffort(opts.effort);
125
182
  if (!existsSync(dir))
126
183
  fail(`project directory not found: ${dir}`);
127
- let agent;
128
- try {
129
- agent = resolveAgentForProject(dir, opts.agent);
130
- }
131
- catch (err) {
132
- fail(err instanceof Error ? err.message : String(err));
133
- }
134
- let model = opts.model;
135
184
  if (!isTicketsInitialized(dir)) {
136
185
  fail(`ticket tracker is not initialized in ${dir}; run \`foreman tickets init --project ${dir}\` first`);
137
186
  }
@@ -148,35 +197,34 @@ export function buildTicketsCommand() {
148
197
  process.exit(0);
149
198
  }
150
199
  }
151
- const config = loadConfig(join(dir, "foreman.yaml"));
152
200
  const logPath = makeLogPath(dir, "tickets-populate");
153
201
  const log = new Log(logPath);
154
- const policy = new PermissionPolicy(config.permissions, dir);
155
- const ready = await ensureRuntimeReadyForCommand(dir, agent, {
156
- label: "tickets populate",
157
- yes: Boolean(opts.yes),
158
- model,
159
- });
160
- agent = ready.runtime;
161
- model = ready.model;
162
- const adapterOpts = {
163
- cwd: dir,
164
- model,
165
- permission: createPermissionHandler(policy, log),
166
- effort: opts.effort,
167
- fast: opts.fast,
168
- };
169
- const builder = agent === "codex"
170
- ? new CodexAdapter(adapterOpts)
171
- : await ClaudeAdapter.create(adapterOpts);
172
- const viewer = printEvents(builder.events());
173
- const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, dir);
174
- console.log(`foreman tickets: populating tickets with ${agent}`);
175
- console.log(`foreman tickets: project ${dir}`);
176
- console.log(`foreman tickets: log ${logPath}\n`);
202
+ const config = loadConfig(join(dir, "foreman.yaml"));
203
+ let builder;
204
+ let viewer;
177
205
  try {
206
+ const roleBuilder = await createRoleBuilder(buildPopulateAgentRunOptions({
207
+ projectDir: dir,
208
+ agent: opts.agent,
209
+ model: opts.model,
210
+ effort: opts.effort,
211
+ fast: opts.fast,
212
+ yes: Boolean(opts.yes),
213
+ log,
214
+ }));
215
+ builder = roleBuilder.builder;
216
+ viewer = printEvents(builder.events());
217
+ const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, dir);
218
+ console.log(`foreman tickets: populating tickets with ${roleBuilder.runtime}`);
219
+ console.log(`foreman tickets: project ${dir}`);
220
+ console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
221
+ console.log(`foreman tickets: log ${logPath}\n`);
178
222
  const ticketsConfig = loadTicketsConfig(dir);
179
- const turn = await foreman.runInstruction(buildPopulateInstruction(opts.sources, ticketsConfig.paths.progressDoc));
223
+ const sourceHints = resolvePopulateSources(dir, opts.sources, ticketsConfig);
224
+ if (sourceHints?.length) {
225
+ console.log(`foreman tickets: source hints ${sourceHints.join(" ")}`);
226
+ }
227
+ const turn = await foreman.runInstruction(buildPopulateInstruction(sourceHints, ticketsConfig.paths.progressDoc));
180
228
  await builder.close();
181
229
  await viewer;
182
230
  log.write("ticket-populate", {
@@ -211,7 +259,8 @@ export function buildTicketsCommand() {
211
259
  console.log(`foreman tickets: populated .tickets/tickets.yaml and rendered ${ticketsConfig.paths.progressDoc}`);
212
260
  }
213
261
  catch (err) {
214
- await builder.close().catch(() => { });
262
+ await builder?.close().catch(() => { });
263
+ await viewer?.catch(() => { });
215
264
  fail(String(err instanceof Error ? err.message : err));
216
265
  }
217
266
  });
@@ -453,12 +502,12 @@ export function buildTicketsCommand() {
453
502
  // ── queue ─────────────────────────────────────────────────────────────────────
454
503
  tickets
455
504
  .command("queue")
456
- .description("Print the next-N queue to stdout.")
505
+ .description("Print the ticket queue to stdout.")
457
506
  .option("-p, --project <dir>", "project directory (default: cwd)")
458
- .option("--limit <n>", "override queue limit")
507
+ .option("--limit <n>", "override view limit")
459
508
  .action((opts) => {
460
509
  try {
461
- const rows = cmdQueue(cwd(opts), opts.limit ? Number(opts.limit) : undefined);
510
+ const rows = cmdQueue(cwd(opts), opts.limit !== undefined ? Number(opts.limit) : undefined);
462
511
  if (rows.length === 0) {
463
512
  console.log("No remaining tickets.");
464
513
  }
package/dist/config.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface PermissionConfig {
3
3
  allowBash: string[];
4
4
  /** Bash command substrings that always escalate (checked first). */
5
5
  escalateBash: string[];
6
+ /** Escalate shell redirection even when it is attached to an allowed command. */
7
+ strictShellRedirection?: boolean;
6
8
  /** Tool names auto-approved when their target stays inside the worktree. */
7
9
  allowTools: string[];
8
10
  /** Tool names that always escalate. */
package/dist/config.js CHANGED
@@ -98,6 +98,9 @@ function validateConfig(raw, path) {
98
98
  throw new Error(`${path}: permissions.${key} must be an array of strings`);
99
99
  }
100
100
  }
101
+ if (permissions.strictShellRedirection !== undefined && typeof permissions.strictShellRedirection !== "boolean") {
102
+ throw new Error(`${path}: permissions.strictShellRedirection must be a boolean`);
103
+ }
101
104
  }
102
105
  if (cfg.notifications !== undefined) {
103
106
  validateBooleanObject(cfg.notifications, "notifications", path);
package/dist/foreman.js CHANGED
@@ -2,7 +2,7 @@ import { select, isCancel } from "@clack/prompts";
2
2
  import { MARKER_SPEC, QA_MARKER_SPEC } from "./markers.js";
3
3
  import { fireNotification } from "./notify.js";
4
4
  import { isTicketsInitialized } from "./tickets/config.js";
5
- import { cmdUpdate, cmdComplete, cmdBlock, cmdQueue } from "./tickets/commands.js";
5
+ import { cmdUpdate, cmdComplete, cmdBlock, cmdImplementationQueue } from "./tickets/commands.js";
6
6
  /** Phrases that suggest the builder ended its turn by asking the human. */
7
7
  const QUESTION_HINTS = [
8
8
  "should i",
@@ -324,7 +324,7 @@ export class Foreman {
324
324
  let pendingTicketId;
325
325
  if (this.ticketsEnabled && this.projectDir) {
326
326
  try {
327
- const queue = cmdQueue(this.projectDir);
327
+ const queue = cmdImplementationQueue(this.projectDir);
328
328
  const next = queue.find((r) => r.status === "next");
329
329
  if (next) {
330
330
  pendingTicketId = next.ticket;
package/dist/log.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** A single structured log record. */
2
2
  export interface LogRecord {
3
3
  ts: string;
4
- event: "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "step" | "permission" | "escalation" | "needs_input" | "preflight" | "ticket-populate" | "qa" | "qa-fix" | "error";
4
+ event: "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "step" | "permission" | "escalation" | "needs_input" | "preflight" | "rafi-plan" | "ticket-populate" | "qa" | "qa-fix" | "error";
5
5
  [key: string]: unknown;
6
6
  }
7
7
  /** Append-only JSONL logger. One line per record, plus an echo to the console. */
@@ -4,10 +4,12 @@ const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
4
4
  /** Shell operators we split on to vet each segment of a chained command. */
5
5
  const SHELL_SPLIT = /\s*(?:&&|\|\||;|\|)\s*/;
6
6
  /** Shell features that make a prefix allow-list too easy to bypass. */
7
- const ESCALATE_SHELL_SYNTAX = [
8
- { pattern: /\s\d?>|\s>>|\s</, reason: "shell redirection is not auto-approved" },
9
- { pattern: /\$\(|`/, reason: "shell substitution is not auto-approved" },
10
- ];
7
+ const SHELL_REDIRECTION = {
8
+ standard: /\s\d?>|\s>>|\s</,
9
+ strict: /(?:^|[^\\])(?:\d+)?(?:>>?|<<?)/,
10
+ reason: "shell redirection is not auto-approved",
11
+ };
12
+ const SHELL_SUBSTITUTION = { pattern: /\$\(|`/, reason: "shell substitution is not auto-approved" };
11
13
  /**
12
14
  * Rules-based permission classifier. No LLM: routine requests are auto-approved,
13
15
  * anything risky or unrecognized escalates to the human (fail safe).
@@ -51,7 +53,13 @@ export class PermissionPolicy {
51
53
  if (hitEscalate) {
52
54
  return { decision: "escalate", reason: `command matches "${hitEscalate}"` };
53
55
  }
54
- const syntax = ESCALATE_SHELL_SYNTAX.find((s) => s.pattern.test(command));
56
+ const syntax = [
57
+ {
58
+ pattern: this.config.strictShellRedirection ? SHELL_REDIRECTION.strict : SHELL_REDIRECTION.standard,
59
+ reason: SHELL_REDIRECTION.reason,
60
+ },
61
+ SHELL_SUBSTITUTION,
62
+ ].find((s) => s.pattern.test(command));
55
63
  if (syntax) {
56
64
  return { decision: "escalate", reason: syntax.reason };
57
65
  }
@@ -14,6 +14,9 @@ export declare function openContext(projectDir: string): TicketContext;
14
14
  export interface InitOptions {
15
15
  appName?: string;
16
16
  timezone?: string;
17
+ implementationLimit?: number;
18
+ viewLimit?: number;
19
+ /** @deprecated Use implementationLimit. */
17
20
  queueLimit?: number;
18
21
  docsRoot?: string;
19
22
  }
@@ -85,6 +88,7 @@ export interface ValidateResult {
85
88
  }
86
89
  export declare function cmdValidate(projectDir: string): ValidateResult;
87
90
  export declare function cmdQueue(projectDir: string, limit?: number): import("./queue.js").QueueRow[];
91
+ export declare function cmdImplementationQueue(projectDir: string): import("./queue.js").QueueRow[];
88
92
  export declare function cmdArchive(projectDir: string, _opts: {
89
93
  olderThanDays?: number;
90
94
  }): void;
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { stringify } from "yaml";
4
- import { initDocsRoot, loadTicketsConfig, pathExistsOrSymlink, resolveTicketPaths, isTicketsInitialized, } from "./config.js";
4
+ import { DEFAULT_TICKETS_CONFIG, initDocsRoot, loadTicketsConfig, pathExistsOrSymlink, resolveTicketPaths, isTicketsInitialized, } from "./config.js";
5
5
  import { loadTickets, saveTickets } from "./ticketLoader.js";
6
6
  import { StateDb } from "./stateDb.js";
7
7
  import { nowTimestamp, logEvent } from "./events.js";
@@ -32,6 +32,9 @@ export function cmdInit(projectDir, opts) {
32
32
  if (isTicketsInitialized(projectDir)) {
33
33
  throw new Error(".tickets/config.yaml already exists — already initialized.");
34
34
  }
35
+ if (opts.implementationLimit !== undefined && opts.queueLimit !== undefined) {
36
+ throw new Error("--implementation-limit and deprecated --queue-limit cannot both be set");
37
+ }
35
38
  const ticketsDir = join(projectDir, ".tickets");
36
39
  const docsRoot = initDocsRoot(projectDir, opts.docsRoot);
37
40
  const progressDoc = `${docsRoot}/ticket-progress.md`;
@@ -46,7 +49,8 @@ export function cmdInit(projectDir, opts) {
46
49
  mkdirSync(join(projectDir, docsRoot), { recursive: true });
47
50
  const config = {
48
51
  app_name: opts.appName ?? "My App",
49
- queue_limit: opts.queueLimit ?? 50,
52
+ implementation_limit: opts.implementationLimit ?? opts.queueLimit ?? DEFAULT_TICKETS_CONFIG.implementationLimit,
53
+ view_limit: opts.viewLimit ?? DEFAULT_TICKETS_CONFIG.viewLimit,
50
54
  timezone: opts.timezone ?? "UTC",
51
55
  timestamp_format: "iso8601_offset",
52
56
  paths: {
@@ -424,7 +428,13 @@ export function cmdValidate(projectDir) {
424
428
  export function cmdQueue(projectDir, limit) {
425
429
  return withContext(projectDir, (ctx) => {
426
430
  const states = ctx.db.getAllStates();
427
- return buildNextQueue(ctx.tickets, states, limit ?? ctx.config.queueLimit);
431
+ return buildNextQueue(ctx.tickets, states, limit ?? ctx.config.viewLimit);
432
+ });
433
+ }
434
+ export function cmdImplementationQueue(projectDir) {
435
+ return withContext(projectDir, (ctx) => {
436
+ const states = ctx.db.getAllStates();
437
+ return buildNextQueue(ctx.tickets, states, ctx.config.implementationLimit);
428
438
  });
429
439
  }
430
440
  // ── archive ───────────────────────────────────────────────────────────────────
@@ -22,7 +22,8 @@ export interface TicketsBehavior {
22
22
  }
23
23
  export interface TicketsConfig {
24
24
  appName: string;
25
- queueLimit: number;
25
+ implementationLimit: number;
26
+ viewLimit: number;
26
27
  timezone: string;
27
28
  paths: TicketsPaths;
28
29
  rendering: TicketsRendering;
@@ -3,7 +3,8 @@ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:pa
3
3
  import { parse } from "yaml";
4
4
  export const DEFAULT_TICKETS_CONFIG = {
5
5
  appName: "My App",
6
- queueLimit: 50,
6
+ implementationLimit: 500,
7
+ viewLimit: 20_000,
7
8
  timezone: "UTC",
8
9
  paths: {
9
10
  tickets: ".tickets/tickets.yaml",
@@ -126,7 +127,8 @@ export function loadTicketsConfig(projectDir) {
126
127
  assertPlainObject(raw.behavior, "behavior", configPath);
127
128
  const config = {
128
129
  appName: raw.app_name ?? DEFAULT_TICKETS_CONFIG.appName,
129
- queueLimit: raw.queue_limit ?? DEFAULT_TICKETS_CONFIG.queueLimit,
130
+ implementationLimit: resolveImplementationLimit(raw),
131
+ viewLimit: raw.view_limit ?? DEFAULT_TICKETS_CONFIG.viewLimit,
130
132
  timezone: raw.timezone ?? DEFAULT_TICKETS_CONFIG.timezone,
131
133
  paths: {
132
134
  ...DEFAULT_TICKETS_CONFIG.paths,
@@ -144,6 +146,17 @@ export function loadTicketsConfig(projectDir) {
144
146
  validateTicketsConfig(config, configPath);
145
147
  return config;
146
148
  }
149
+ function resolveImplementationLimit(raw) {
150
+ if (raw.implementation_limit !== undefined) {
151
+ return raw.implementation_limit;
152
+ }
153
+ if (raw.queue_limit !== undefined) {
154
+ return raw.queue_limit === 50
155
+ ? DEFAULT_TICKETS_CONFIG.implementationLimit
156
+ : raw.queue_limit;
157
+ }
158
+ return DEFAULT_TICKETS_CONFIG.implementationLimit;
159
+ }
147
160
  function assertPlainObject(value, name, configPath) {
148
161
  if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value))) {
149
162
  throw new Error(`${configPath}: ${name} must be an object`);
@@ -153,8 +166,11 @@ function validateTicketsConfig(config, configPath) {
153
166
  if (!config.appName || typeof config.appName !== "string") {
154
167
  throw new Error(`${configPath}: app_name must be a non-empty string`);
155
168
  }
156
- if (!Number.isInteger(config.queueLimit) || config.queueLimit < 1) {
157
- throw new Error(`${configPath}: queue_limit must be a positive integer`);
169
+ if (!Number.isInteger(config.implementationLimit) || config.implementationLimit < 1) {
170
+ throw new Error(`${configPath}: implementation_limit must be a positive integer`);
171
+ }
172
+ if (!Number.isInteger(config.viewLimit) || config.viewLimit < 1) {
173
+ throw new Error(`${configPath}: view_limit must be a positive integer`);
158
174
  }
159
175
  if (!config.timezone || typeof config.timezone !== "string") {
160
176
  throw new Error(`${configPath}: timezone must be a non-empty IANA timezone string`);
@@ -59,7 +59,7 @@ export const DEFAULT_TRACKER_RULES = renderTrackerRules();
59
59
  export function renderProgressDoc(input) {
60
60
  const { config, projectDir, ticketDefs, states, db } = input;
61
61
  const now = input.now ?? DateTime.now().setZone(config.timezone).toISO();
62
- const queueRows = buildNextQueue(ticketDefs, states, config.queueLimit);
62
+ const queueRows = buildNextQueue(ticketDefs, states, config.implementationLimit);
63
63
  const activeRows = buildActiveStatusRows(queueRows, ticketDefs, states);
64
64
  const futureWork = db.getFutureWork();
65
65
  const recentEvents = db.getRecentEvents(config.rendering.maxWorkLogRows);
@@ -82,7 +82,8 @@ export function renderProgressDoc(input) {
82
82
  lines.push("## Tracker Metadata");
83
83
  lines.push(section("TRACKER_METADATA", table(["Field", "Value"], [
84
84
  ["App", config.appName],
85
- ["Queue Limit", String(config.queueLimit)],
85
+ ["Implementation Limit", String(config.implementationLimit)],
86
+ ["View Limit", String(config.viewLimit)],
86
87
  ["Timezone", config.timezone],
87
88
  ["Generated At", now],
88
89
  ["Remaining Tickets", String(queueRows.length > 0 ? "≥" + queueRows.length : "0")],
@@ -25,12 +25,12 @@ export function runAllValidation(config, projectDir, ticketDefs, states, db) {
25
25
  }
26
26
  }
27
27
  // Pass 3: queue validation
28
- const queueRows = buildNextQueue(ticketDefs, states, config.queueLimit);
28
+ const queueRows = buildNextQueue(ticketDefs, states, config.implementationLimit);
29
29
  const remaining = ticketDefs.filter((t) => {
30
30
  const s = states.get(t.id);
31
31
  return s?.status !== "done" && s?.status !== "canceled";
32
32
  });
33
- const expectedLen = Math.min(config.queueLimit, remaining.length);
33
+ const expectedLen = Math.min(config.implementationLimit, remaining.length);
34
34
  if (queueRows.length !== expectedLen) {
35
35
  issues.push({ pass: 3, severity: "error", message: `[queue] expected ${expectedLen} rows, got ${queueRows.length}` });
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-foreman",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,6 +17,10 @@
17
17
  "ai-foreman": "dist/index.js"
18
18
  },
19
19
  "exports": {
20
+ "./agent-run.js": {
21
+ "types": "./dist/agentRun.d.ts",
22
+ "default": "./dist/agentRun.js"
23
+ },
20
24
  "./cli/tickets.js": {
21
25
  "types": "./dist/cli/tickets.d.ts",
22
26
  "default": "./dist/cli/tickets.js"