ai-foreman 1.3.0 → 1.4.0

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/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" | "rafi-plan" | "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" | "gitlab-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "pr-auto-merge-enabled" | "pr-auto-merge-failed" | "mr-created" | "mr-existing" | "mr-failed" | "mr-auto-merge-enabled" | "mr-auto-merge-failed" | "branch-auto-merge-wait" | "branch-direct-merge" | "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. */
@@ -3,6 +3,7 @@ import type { ValidationResult } from "./stateDb.js";
3
3
  import { type TicketsConfig, resolveTicketPaths } from "./config.js";
4
4
  import { StateDb } from "./stateDb.js";
5
5
  import { runAllValidation } from "./validate.js";
6
+ import { type ReviewCommandOptions, type ReviewCommandResult } from "./recommendations.js";
6
7
  export interface TicketContext {
7
8
  config: TicketsConfig;
8
9
  projectDir: string;
@@ -14,6 +15,9 @@ export declare function openContext(projectDir: string): TicketContext;
14
15
  export interface InitOptions {
15
16
  appName?: string;
16
17
  timezone?: string;
18
+ implementationLimit?: number;
19
+ viewLimit?: number;
20
+ /** @deprecated Use implementationLimit. */
17
21
  queueLimit?: number;
18
22
  docsRoot?: string;
19
23
  }
@@ -85,6 +89,8 @@ export interface ValidateResult {
85
89
  }
86
90
  export declare function cmdValidate(projectDir: string): ValidateResult;
87
91
  export declare function cmdQueue(projectDir: string, limit?: number): import("./queue.js").QueueRow[];
92
+ export declare function cmdImplementationQueue(projectDir: string): import("./queue.js").QueueRow[];
88
93
  export declare function cmdArchive(projectDir: string, _opts: {
89
94
  olderThanDays?: number;
90
95
  }): void;
96
+ export declare function cmdReview(projectDir: string, opts?: ReviewCommandOptions): ReviewCommandResult;
@@ -1,13 +1,14 @@
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";
8
8
  import { renderAndWrite, renderTrackerRules } from "./renderMarkdown.js";
9
9
  import { runAllValidation } from "./validate.js";
10
10
  import { buildNextQueue } from "./queue.js";
11
+ import { cmdReviewRecommendations } from "./recommendations.js";
11
12
  export function openContext(projectDir) {
12
13
  const config = loadTicketsConfig(projectDir);
13
14
  const paths = resolveTicketPaths(config, projectDir);
@@ -32,6 +33,9 @@ export function cmdInit(projectDir, opts) {
32
33
  if (isTicketsInitialized(projectDir)) {
33
34
  throw new Error(".tickets/config.yaml already exists — already initialized.");
34
35
  }
36
+ if (opts.implementationLimit !== undefined && opts.queueLimit !== undefined) {
37
+ throw new Error("--implementation-limit and deprecated --queue-limit cannot both be set");
38
+ }
35
39
  const ticketsDir = join(projectDir, ".tickets");
36
40
  const docsRoot = initDocsRoot(projectDir, opts.docsRoot);
37
41
  const progressDoc = `${docsRoot}/ticket-progress.md`;
@@ -46,7 +50,8 @@ export function cmdInit(projectDir, opts) {
46
50
  mkdirSync(join(projectDir, docsRoot), { recursive: true });
47
51
  const config = {
48
52
  app_name: opts.appName ?? "My App",
49
- queue_limit: opts.queueLimit ?? 50,
53
+ implementation_limit: opts.implementationLimit ?? opts.queueLimit ?? DEFAULT_TICKETS_CONFIG.implementationLimit,
54
+ view_limit: opts.viewLimit ?? DEFAULT_TICKETS_CONFIG.viewLimit,
50
55
  timezone: opts.timezone ?? "UTC",
51
56
  timestamp_format: "iso8601_offset",
52
57
  paths: {
@@ -74,6 +79,14 @@ export function cmdInit(projectDir, opts) {
74
79
  };
75
80
  writeFileSync(join(ticketsDir, "config.yaml"), stringify(config, { lineWidth: 80 }), "utf8");
76
81
  writeFileSync(join(ticketsDir, "tickets.yaml"), "tickets: []\n", "utf8");
82
+ writeFileSync(join(ticketsDir, ".gitignore"), [
83
+ "imports/",
84
+ "ticket-state.sqlite",
85
+ "ticket-state.sqlite-shm",
86
+ "ticket-state.sqlite-wal",
87
+ "backups/",
88
+ "",
89
+ ].join("\n"), "utf8");
77
90
  writeFileSync(join(ticketsDir, "tracker-rules.md"), renderTrackerRules(progressDoc), "utf8");
78
91
  const ticketSchema = {
79
92
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -96,6 +109,20 @@ export function cmdInit(projectDir, opts) {
96
109
  likely_files: { type: "array", items: { type: "string" } },
97
110
  rollback: { type: ["string", "null"] },
98
111
  notes: { type: ["string", "null"] },
112
+ external_refs: {
113
+ type: "array",
114
+ items: {
115
+ type: "object",
116
+ required: ["provider", "id"],
117
+ additionalProperties: true,
118
+ properties: {
119
+ provider: { type: "string", minLength: 1 },
120
+ id: { type: "string", minLength: 1 },
121
+ key: { type: ["string", "null"] },
122
+ url: { type: ["string", "null"] },
123
+ },
124
+ },
125
+ },
99
126
  },
100
127
  };
101
128
  writeFileSync(join(ticketsDir, "schema", "tickets.schema.json"), JSON.stringify(ticketSchema, null, 2), "utf8");
@@ -424,7 +451,13 @@ export function cmdValidate(projectDir) {
424
451
  export function cmdQueue(projectDir, limit) {
425
452
  return withContext(projectDir, (ctx) => {
426
453
  const states = ctx.db.getAllStates();
427
- return buildNextQueue(ctx.tickets, states, limit ?? ctx.config.queueLimit);
454
+ return buildNextQueue(ctx.tickets, states, limit ?? ctx.config.viewLimit);
455
+ });
456
+ }
457
+ export function cmdImplementationQueue(projectDir) {
458
+ return withContext(projectDir, (ctx) => {
459
+ const states = ctx.db.getAllStates();
460
+ return buildNextQueue(ctx.tickets, states, ctx.config.implementationLimit);
428
461
  });
429
462
  }
430
463
  // ── archive ───────────────────────────────────────────────────────────────────
@@ -443,3 +476,7 @@ export function cmdArchive(projectDir, _opts) {
443
476
  renderAfterUpdate(ctx);
444
477
  });
445
478
  }
479
+ // ── review recommendations ──────────────────────────────────────────────────
480
+ export function cmdReview(projectDir, opts = {}) {
481
+ return cmdReviewRecommendations(projectDir, opts);
482
+ }
@@ -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`);
@@ -1 +1,56 @@
1
+ import type { TicketDef } from "./ticketSchema.js";
2
+ import type { TicketSourceConfig } from "./setupConfig.js";
1
3
  export declare function importFromMarkdown(_progressDocPath: string): never;
4
+ export interface ImportedTicketItem {
5
+ provider: "linear" | "jira";
6
+ providerId: string;
7
+ key?: string | null;
8
+ url?: string | null;
9
+ title: string;
10
+ description?: string | null;
11
+ area?: string | null;
12
+ priority?: string | number | null;
13
+ size?: string | number | null;
14
+ status?: string | null;
15
+ statusCategory?: string | null;
16
+ labels?: string[];
17
+ comments?: ImportedComment[];
18
+ createdAt?: string | null;
19
+ updatedAt?: string | null;
20
+ raw: unknown;
21
+ }
22
+ export interface ImportedComment {
23
+ author?: string | null;
24
+ body: string;
25
+ createdAt?: string | null;
26
+ }
27
+ export interface ExternalImportOptions {
28
+ importCap: number;
29
+ commentLimit: number;
30
+ recommendSplitForXl?: boolean;
31
+ }
32
+ export interface ExternalImportResult {
33
+ provider: "linear" | "jira";
34
+ sourceLabel: string;
35
+ fetched: number;
36
+ created: number;
37
+ updated: number;
38
+ snapshotPath: string;
39
+ }
40
+ export declare function importExternalSources(projectDir: string, sources: Extract<TicketSourceConfig, {
41
+ type: "linear" | "jira";
42
+ }>[], opts: ExternalImportOptions): Promise<ExternalImportResult[]>;
43
+ export declare function validateExternalSourceAccess(source: Extract<TicketSourceConfig, {
44
+ type: "linear" | "jira";
45
+ }>): Promise<void>;
46
+ export declare function fetchLinearIssues(source: Extract<TicketSourceConfig, {
47
+ type: "linear";
48
+ }>, opts: ExternalImportOptions): Promise<ImportedTicketItem[]>;
49
+ export declare function fetchJiraIssues(source: Extract<TicketSourceConfig, {
50
+ type: "jira";
51
+ }>, opts: ExternalImportOptions): Promise<ImportedTicketItem[]>;
52
+ export declare function applyImportedItems(projectDir: string, items: ImportedTicketItem[]): {
53
+ created: number;
54
+ updated: number;
55
+ tickets: TicketDef[];
56
+ };