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.
@@ -68,6 +68,19 @@ export interface ArchiveIndexEntry {
68
68
  last_updated: string | null;
69
69
  notes: string | null;
70
70
  }
71
+ export type ReviewRecommendationStatus = "pending" | "accepted" | "deferred" | "dismissed";
72
+ export interface ReviewRecommendation {
73
+ id?: number;
74
+ created_at: string;
75
+ kind: string;
76
+ status: ReviewRecommendationStatus;
77
+ summary: string;
78
+ rationale: string | null;
79
+ ticket_ids_json: string;
80
+ patch_json: string;
81
+ source_json: string;
82
+ updated_at: string | null;
83
+ }
71
84
  export declare class StateDb {
72
85
  private db;
73
86
  constructor(dbPath: string);
@@ -86,5 +99,9 @@ export declare class StateDb {
86
99
  getRecentCompleted(): RecentCompletedContext[];
87
100
  upsertRecentCompleted(row: RecentCompletedContext): void;
88
101
  getArchiveIndex(): ArchiveIndexEntry[];
102
+ insertReviewRecommendation(item: Omit<ReviewRecommendation, "id">): number;
103
+ getReviewRecommendations(status?: ReviewRecommendationStatus): ReviewRecommendation[];
104
+ getReviewRecommendation(id: number): ReviewRecommendation | undefined;
105
+ updateReviewRecommendationStatus(id: number, status: ReviewRecommendationStatus, updatedAt: string): void;
89
106
  transaction<T>(fn: () => T): T;
90
107
  }
@@ -82,11 +82,26 @@ CREATE TABLE IF NOT EXISTS archive_index (
82
82
  notes TEXT
83
83
  );
84
84
 
85
+ CREATE TABLE IF NOT EXISTS review_recommendations (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ created_at TEXT NOT NULL,
88
+ kind TEXT NOT NULL,
89
+ status TEXT NOT NULL DEFAULT 'pending',
90
+ summary TEXT NOT NULL,
91
+ rationale TEXT,
92
+ ticket_ids_json TEXT NOT NULL DEFAULT '[]',
93
+ patch_json TEXT NOT NULL DEFAULT '{}',
94
+ source_json TEXT NOT NULL DEFAULT '{}',
95
+ updated_at TEXT,
96
+ CHECK (status IN ('pending','accepted','deferred','dismissed'))
97
+ );
98
+
85
99
  CREATE INDEX IF NOT EXISTS idx_ticket_state_status ON ticket_state(status);
86
100
  CREATE INDEX IF NOT EXISTS idx_ticket_events_ticket_id ON ticket_events(ticket_id);
87
101
  CREATE INDEX IF NOT EXISTS idx_ticket_events_timestamp ON ticket_events(timestamp);
88
102
  CREATE INDEX IF NOT EXISTS idx_validation_snapshots_timestamp ON validation_snapshots(timestamp);
89
103
  CREATE INDEX IF NOT EXISTS idx_future_work_disposition ON future_work(disposition);
104
+ CREATE INDEX IF NOT EXISTS idx_review_recommendations_status ON review_recommendations(status);
90
105
  `;
91
106
  export class StateDb {
92
107
  db;
@@ -245,6 +260,37 @@ export class StateDb {
245
260
  .prepare("SELECT * FROM archive_index ORDER BY last_updated DESC")
246
261
  .all();
247
262
  }
263
+ // ── review_recommendations ────────────────────────────────────────────────
264
+ insertReviewRecommendation(item) {
265
+ const r = this.db.prepare(`
266
+ INSERT INTO review_recommendations (
267
+ created_at,kind,status,summary,rationale,ticket_ids_json,patch_json,source_json,updated_at
268
+ ) VALUES (
269
+ @created_at,@kind,@status,@summary,@rationale,@ticket_ids_json,@patch_json,@source_json,@updated_at
270
+ )
271
+ `).run(item);
272
+ return r.lastInsertRowid;
273
+ }
274
+ getReviewRecommendations(status) {
275
+ if (status) {
276
+ return this.db
277
+ .prepare("SELECT * FROM review_recommendations WHERE status = ? ORDER BY id")
278
+ .all(status);
279
+ }
280
+ return this.db
281
+ .prepare("SELECT * FROM review_recommendations ORDER BY id")
282
+ .all();
283
+ }
284
+ getReviewRecommendation(id) {
285
+ return this.db
286
+ .prepare("SELECT * FROM review_recommendations WHERE id = ?")
287
+ .get(id);
288
+ }
289
+ updateReviewRecommendationStatus(id, status, updatedAt) {
290
+ this.db
291
+ .prepare("UPDATE review_recommendations SET status = ?, updated_at = ? WHERE id = ?")
292
+ .run(status, updatedAt, id);
293
+ }
248
294
  // ── transactions ──────────────────────────────────────────────────────────
249
295
  transaction(fn) {
250
296
  return this.db.transaction(fn)();
@@ -1,6 +1,12 @@
1
1
  export type TicketPriority = "P0" | "P1" | "P2" | "P3";
2
2
  export type TicketSize = "XS" | "S" | "M" | "L" | "XL";
3
3
  export type TicketRisk = "Low" | "Medium" | "High";
4
+ export interface TicketExternalRef {
5
+ provider: "linear" | "jira" | string;
6
+ id: string;
7
+ key?: string | null;
8
+ url?: string | null;
9
+ }
4
10
  export interface TicketDef {
5
11
  id: string;
6
12
  order: number;
@@ -16,6 +22,7 @@ export interface TicketDef {
16
22
  likely_files: string[];
17
23
  rollback?: string | null;
18
24
  notes?: string | null;
25
+ external_refs?: TicketExternalRef[];
19
26
  }
20
27
  export declare const TICKET_JSON_SCHEMA: {
21
28
  readonly type: "object";
@@ -86,6 +93,30 @@ export declare const TICKET_JSON_SCHEMA: {
86
93
  readonly notes: {
87
94
  readonly type: readonly ["string", "null"];
88
95
  };
96
+ readonly external_refs: {
97
+ readonly type: "array";
98
+ readonly items: {
99
+ readonly type: "object";
100
+ readonly required: readonly ["provider", "id"];
101
+ readonly additionalProperties: true;
102
+ readonly properties: {
103
+ readonly provider: {
104
+ readonly type: "string";
105
+ readonly minLength: 1;
106
+ };
107
+ readonly id: {
108
+ readonly type: "string";
109
+ readonly minLength: 1;
110
+ };
111
+ readonly key: {
112
+ readonly type: readonly ["string", "null"];
113
+ };
114
+ readonly url: {
115
+ readonly type: readonly ["string", "null"];
116
+ };
117
+ };
118
+ };
119
+ };
89
120
  };
90
121
  };
91
122
  export declare const TICKETS_FILE_SCHEMA: {
@@ -163,6 +194,30 @@ export declare const TICKETS_FILE_SCHEMA: {
163
194
  readonly notes: {
164
195
  readonly type: readonly ["string", "null"];
165
196
  };
197
+ readonly external_refs: {
198
+ readonly type: "array";
199
+ readonly items: {
200
+ readonly type: "object";
201
+ readonly required: readonly ["provider", "id"];
202
+ readonly additionalProperties: true;
203
+ readonly properties: {
204
+ readonly provider: {
205
+ readonly type: "string";
206
+ readonly minLength: 1;
207
+ };
208
+ readonly id: {
209
+ readonly type: "string";
210
+ readonly minLength: 1;
211
+ };
212
+ readonly key: {
213
+ readonly type: readonly ["string", "null"];
214
+ };
215
+ readonly url: {
216
+ readonly type: readonly ["string", "null"];
217
+ };
218
+ };
219
+ };
220
+ };
166
221
  };
167
222
  };
168
223
  };
@@ -20,6 +20,20 @@ export const TICKET_JSON_SCHEMA = {
20
20
  likely_files: { type: "array", items: { type: "string" } },
21
21
  rollback: { type: ["string", "null"] },
22
22
  notes: { type: ["string", "null"] },
23
+ external_refs: {
24
+ type: "array",
25
+ items: {
26
+ type: "object",
27
+ required: ["provider", "id"],
28
+ additionalProperties: true,
29
+ properties: {
30
+ provider: { type: "string", minLength: 1 },
31
+ id: { type: "string", minLength: 1 },
32
+ key: { type: ["string", "null"] },
33
+ url: { type: ["string", "null"] },
34
+ },
35
+ },
36
+ },
23
37
  },
24
38
  };
25
39
  export const TICKETS_FILE_SCHEMA = {
@@ -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.4.0",
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",
@@ -69,7 +69,7 @@
69
69
  "@anthropic-ai/claude-agent-sdk": "^0.3.148"
70
70
  },
71
71
  "dependencies": {
72
- "special-agents": "0.4.0",
72
+ "special-agents": "0.5.0",
73
73
  "@clack/prompts": "^1.4.0",
74
74
  "ajv": "^8.20.0",
75
75
  "ajv-formats": "^3.0.1",