ai-foreman 1.3.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
@@ -258,11 +258,15 @@ Init options:
258
258
  | `-p, --project <dir>` | `./my-project` / `../repo` | current directory | Project to initialize. |
259
259
  | `--app-name <name>` | `"My App"` | none | Application name stored in tracker config. |
260
260
  | `--timezone <tz>` | `America/Chicago` / `UTC` | `UTC` | IANA timezone. |
261
- | `--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`. |
262
264
  | `--docs-root <dir>` | `docs` / `docs-rafi` | `rafi-config.yaml` docs.root, then `docs` | Root for generated progress/archive docs. |
263
265
 
264
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`.
265
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
+
266
270
  This creates:
267
271
 
268
272
  ```txt
@@ -8,7 +8,7 @@ import { Log } from "../log.js";
8
8
  import { Foreman } from "../foreman.js";
9
9
  import { printEvents } from "./events.js";
10
10
  import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDiscover, cmdAcceptFutureWork, cmdReorder, cmdRender, cmdValidate, cmdQueue, cmdArchive, } from "../tickets/commands.js";
11
- import { isTicketsInitialized, loadTicketsConfig } from "../tickets/config.js";
11
+ import { DEFAULT_TICKETS_CONFIG, isTicketsInitialized, loadTicketsConfig } from "../tickets/config.js";
12
12
  import { validateDocsRoot } from "../tickets/config.js";
13
13
  import { importFromMarkdown } from "../tickets/importer.js";
14
14
  import { formatValidationIssues } from "../tickets/validate.js";
@@ -23,6 +23,10 @@ function cwd(opts) {
23
23
  function unique(values) {
24
24
  return [...new Set(values)];
25
25
  }
26
+ function optionWasProvided(command, name) {
27
+ const source = command.getOptionValueSource(name);
28
+ return source !== undefined && source !== "default";
29
+ }
26
30
  const TICKET_POPULATE_ROLE = "ticket-maker";
27
31
  const RAFI_CONFIG_FILES = ["rafi-config.yaml", "project.yaml"];
28
32
  function validateEffort(effort) {
@@ -134,15 +138,24 @@ export function buildTicketsCommand() {
134
138
  .option("-p, --project <dir>", "project directory (default: cwd)")
135
139
  .option("--app-name <name>", "application name")
136
140
  .option("--timezone <tz>", "IANA timezone (e.g. America/Chicago)", "UTC")
137
- .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")
138
144
  .option("--docs-root <dir>", "repo-relative directory for generated ticket docs")
139
- .action((opts) => {
145
+ .action((opts, command) => {
140
146
  const dir = cwd(opts);
141
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
+ }
142
153
  cmdInit(dir, {
143
154
  appName: opts.appName,
144
155
  timezone: opts.timezone,
145
- 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,
146
159
  docsRoot: opts.docsRoot,
147
160
  });
148
161
  console.log(`foreman tickets: initialized .tickets/ in ${dir}`);
@@ -489,12 +502,12 @@ export function buildTicketsCommand() {
489
502
  // ── queue ─────────────────────────────────────────────────────────────────────
490
503
  tickets
491
504
  .command("queue")
492
- .description("Print the next-N queue to stdout.")
505
+ .description("Print the ticket queue to stdout.")
493
506
  .option("-p, --project <dir>", "project directory (default: cwd)")
494
- .option("--limit <n>", "override queue limit")
507
+ .option("--limit <n>", "override view limit")
495
508
  .action((opts) => {
496
509
  try {
497
- const rows = cmdQueue(cwd(opts), opts.limit ? Number(opts.limit) : undefined);
510
+ const rows = cmdQueue(cwd(opts), opts.limit !== undefined ? Number(opts.limit) : undefined);
498
511
  if (rows.length === 0) {
499
512
  console.log("No remaining tickets.");
500
513
  }
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;
@@ -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.3.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",