ai-foreman 1.7.4 → 1.7.6
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 +6 -1
- package/dist/activity.d.ts +63 -0
- package/dist/activity.js +244 -0
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +41 -1
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +65 -5
- package/dist/adapters/recovering.js +7 -1
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +5 -0
- package/dist/agentRun.js +5 -1
- package/dist/branch/github.js +5 -3
- package/dist/branch/gitlab.js +5 -3
- package/dist/branch/planner.d.ts +5 -0
- package/dist/branch/planner.js +42 -3
- package/dist/branch/presentation.d.ts +17 -0
- package/dist/branch/presentation.js +38 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +2 -0
- package/dist/branch/runner.js +44 -25
- package/dist/buildRuns.d.ts +41 -17
- package/dist/buildRuns.js +243 -22
- package/dist/cli/events.js +24 -5
- package/dist/cli/runtimeAuthPrompt.js +5 -1
- package/dist/cli/start.js +138 -45
- package/dist/cli/tickets.d.ts +10 -1
- package/dist/cli/tickets.js +418 -79
- package/dist/foreman.d.ts +8 -5
- package/dist/foreman.js +38 -14
- package/dist/index.js +3 -1
- package/dist/providerQuestions.d.ts +23 -0
- package/dist/providerQuestions.js +24 -0
- package/dist/qaReview.d.ts +1 -0
- package/dist/qaReview.js +23 -15
- package/dist/qaSnapshot.d.ts +14 -0
- package/dist/qaSnapshot.js +117 -1
- package/dist/runtimeReadiness.js +5 -0
- package/dist/sources/sourceRegistry.js +13 -6
- package/dist/tickets/details.d.ts +17 -0
- package/dist/tickets/details.js +94 -0
- package/dist/tickets/eligibility.d.ts +28 -0
- package/dist/tickets/eligibility.js +71 -0
- package/dist/tickets/importer.js +13 -8
- package/dist/tickets/reset.d.ts +27 -0
- package/dist/tickets/reset.js +213 -0
- package/dist/tickets/reviewStandards.d.ts +7 -0
- package/dist/tickets/reviewStandards.js +53 -0
- package/dist/tickets/setupConfig.d.ts +22 -0
- package/dist/tickets/setupConfig.js +105 -1
- package/dist/tickets/sourceFetch.js +19 -0
- package/dist/tickets/stateDb.d.ts +8 -1
- package/dist/tickets/stateDb.js +54 -23
- package/dist/workflowDb.d.ts +3 -0
- package/dist/workflowDb.js +26 -0
- package/package.json +15 -3
|
@@ -16,6 +16,7 @@ const SKILL_NAMES = [
|
|
|
16
16
|
];
|
|
17
17
|
export const DEFAULT_TICKET_SETUP = {
|
|
18
18
|
sources: [],
|
|
19
|
+
limits: { implementation: 500, view: 20_000 },
|
|
19
20
|
populate: {
|
|
20
21
|
source_handling: "saved",
|
|
21
22
|
agent_preference: "configured",
|
|
@@ -33,6 +34,25 @@ export const DEFAULT_TICKET_SETUP = {
|
|
|
33
34
|
cleanup: true,
|
|
34
35
|
auto_merge_wait: false,
|
|
35
36
|
auto_merge_timeout_minutes: null,
|
|
37
|
+
base_branch: "main",
|
|
38
|
+
branch_policy: {
|
|
39
|
+
mode: "global",
|
|
40
|
+
global_strategy: "branch-per-ticket",
|
|
41
|
+
by_size: { XS: "shared", S: "shared", M: "per-ticket", L: "per-ticket", XL: "per-ticket" },
|
|
42
|
+
},
|
|
43
|
+
review: {
|
|
44
|
+
title_style: "ticket-title",
|
|
45
|
+
title_template: null,
|
|
46
|
+
description_sections: ["Summary", "Linked ticket", "Changes made", "Tests and validation performed", "Risks or rollback notes", "Checklist"],
|
|
47
|
+
},
|
|
48
|
+
validation_checklist: [
|
|
49
|
+
"Acceptance criteria satisfied",
|
|
50
|
+
"Required tests pass",
|
|
51
|
+
"Configured project checks pass",
|
|
52
|
+
"Documentation updated when needed",
|
|
53
|
+
"Database and deployment changes reviewed when present",
|
|
54
|
+
"Validation evidence attached",
|
|
55
|
+
],
|
|
36
56
|
},
|
|
37
57
|
};
|
|
38
58
|
export function loadRafiConfigObject(projectDir) {
|
|
@@ -77,6 +97,18 @@ export function saveTicketSetupConfig(projectDir, setup, opts = {}) {
|
|
|
77
97
|
mkdirSync(projectDir, { recursive: true });
|
|
78
98
|
writeFileSync(join(projectDir, RAFI_CONFIG_FILE), stringify(config, { lineWidth: 100 }), "utf8");
|
|
79
99
|
}
|
|
100
|
+
/** Synchronize pending setup limits into the tracker, once the tracker exists. */
|
|
101
|
+
export function syncTicketLimits(projectDir, limits) {
|
|
102
|
+
const path = join(projectDir, ".tickets", "config.yaml");
|
|
103
|
+
if (!existsSync(path))
|
|
104
|
+
return;
|
|
105
|
+
const raw = parse(readFileSync(path, "utf8"));
|
|
106
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
107
|
+
throw new Error(`${path}: expected a YAML object`);
|
|
108
|
+
raw.implementation_limit = positiveInteger(limits.implementation, 500, "tickets.limits.implementation");
|
|
109
|
+
raw.view_limit = positiveInteger(limits.view, 20_000, "tickets.limits.view");
|
|
110
|
+
writeFileSync(path, stringify(raw, { lineWidth: 100 }), "utf8");
|
|
111
|
+
}
|
|
80
112
|
export function ensureRafiConfigForTicketSetup(projectDir, opts = {}) {
|
|
81
113
|
const existing = loadRafiConfigObject(projectDir);
|
|
82
114
|
if (existing)
|
|
@@ -95,6 +127,7 @@ export function normalizeTicketsSetupConfig(value, label = "tickets") {
|
|
|
95
127
|
const raw = value;
|
|
96
128
|
const setup = {
|
|
97
129
|
sources: normalizeSources(raw.sources, `${label}.sources`),
|
|
130
|
+
limits: normalizeLimits(raw.limits, `${label}.limits`),
|
|
98
131
|
populate: normalizePopulate(raw.populate, `${label}.populate`),
|
|
99
132
|
build: normalizeBuild(raw.build, `${label}.build`),
|
|
100
133
|
};
|
|
@@ -103,6 +136,7 @@ export function normalizeTicketsSetupConfig(value, label = "tickets") {
|
|
|
103
136
|
export function denormalizeTicketsSetupConfig(setup) {
|
|
104
137
|
return {
|
|
105
138
|
sources: setup.sources.map((source) => ({ ...source })),
|
|
139
|
+
limits: { ...setup.limits },
|
|
106
140
|
populate: { ...setup.populate },
|
|
107
141
|
build: { ...setup.build },
|
|
108
142
|
};
|
|
@@ -111,6 +145,7 @@ export function mergeTicketSetup(current, patch) {
|
|
|
111
145
|
const base = current ? cloneTicketSetup(current) : cloneTicketSetup(DEFAULT_TICKET_SETUP);
|
|
112
146
|
return normalizeTicketsSetupConfig({
|
|
113
147
|
sources: patch.sources ?? base.sources,
|
|
148
|
+
limits: patch.limits ?? base.limits,
|
|
114
149
|
populate: { ...base.populate, ...(patch.populate ?? {}) },
|
|
115
150
|
build: { ...base.build, ...(patch.build ?? {}) },
|
|
116
151
|
});
|
|
@@ -212,6 +247,22 @@ export function detectGitProvider(projectDir) {
|
|
|
212
247
|
}
|
|
213
248
|
return undefined;
|
|
214
249
|
}
|
|
250
|
+
export function detectDefaultBranch(projectDir) {
|
|
251
|
+
const attempts = [
|
|
252
|
+
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
253
|
+
["config", "--get", "init.defaultBranch"],
|
|
254
|
+
["branch", "--show-current"],
|
|
255
|
+
];
|
|
256
|
+
for (const args of attempts) {
|
|
257
|
+
try {
|
|
258
|
+
const value = execFileSync("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
259
|
+
if (value)
|
|
260
|
+
return value.replace(/^origin\//, "");
|
|
261
|
+
}
|
|
262
|
+
catch { /* try the next safe source */ }
|
|
263
|
+
}
|
|
264
|
+
return "main";
|
|
265
|
+
}
|
|
215
266
|
export function recommendedBuildDefaults(projectDir) {
|
|
216
267
|
const provider = detectGitProvider(projectDir);
|
|
217
268
|
return {
|
|
@@ -221,6 +272,7 @@ export function recommendedBuildDefaults(projectDir) {
|
|
|
221
272
|
pr_ready: Boolean(provider),
|
|
222
273
|
merge_method: "squash",
|
|
223
274
|
cleanup: true,
|
|
275
|
+
base_branch: detectDefaultBranch(projectDir),
|
|
224
276
|
};
|
|
225
277
|
}
|
|
226
278
|
function minimalRafiConfig(projectDir, opts) {
|
|
@@ -354,6 +406,45 @@ function normalizeBuild(value, label) {
|
|
|
354
406
|
cleanup: booleanField(raw.cleanup, DEFAULT_TICKET_SETUP.build.cleanup, `${label}.cleanup`),
|
|
355
407
|
auto_merge_wait: booleanField(raw.auto_merge_wait, DEFAULT_TICKET_SETUP.build.auto_merge_wait, `${label}.auto_merge_wait`),
|
|
356
408
|
auto_merge_timeout_minutes: nullablePositiveInteger(raw.auto_merge_timeout_minutes, DEFAULT_TICKET_SETUP.build.auto_merge_timeout_minutes, `${label}.auto_merge_timeout_minutes`),
|
|
409
|
+
base_branch: stringField(raw.base_branch, DEFAULT_TICKET_SETUP.build.base_branch),
|
|
410
|
+
branch_policy: normalizeBranchPolicy(raw.branch_policy, raw.branch_strategy, `${label}.branch_policy`),
|
|
411
|
+
review: normalizeReview(raw.review, `${label}.review`),
|
|
412
|
+
validation_checklist: stringArray(raw.validation_checklist, DEFAULT_TICKET_SETUP.build.validation_checklist, `${label}.validation_checklist`),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function normalizeLimits(value, label) {
|
|
416
|
+
const raw = objectOrEmpty(value, label);
|
|
417
|
+
return {
|
|
418
|
+
implementation: positiveInteger(raw.implementation, DEFAULT_TICKET_SETUP.limits.implementation, `${label}.implementation`),
|
|
419
|
+
view: positiveInteger(raw.view, DEFAULT_TICKET_SETUP.limits.view, `${label}.view`),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function normalizeBranchPolicy(value, legacyStrategy, label) {
|
|
423
|
+
const raw = objectOrEmpty(value, label);
|
|
424
|
+
const sizes = objectOrEmpty(raw.by_size, `${label}.by_size`);
|
|
425
|
+
const globalStrategy = enumField(raw.global_strategy ?? legacyStrategy, ["current", "batch", "branch-per-ticket"], DEFAULT_TICKET_SETUP.build.branch_policy.global_strategy, `${label}.global_strategy`);
|
|
426
|
+
return {
|
|
427
|
+
mode: enumField(raw.mode, ["global", "size"], DEFAULT_TICKET_SETUP.build.branch_policy.mode, `${label}.mode`),
|
|
428
|
+
global_strategy: globalStrategy,
|
|
429
|
+
by_size: {
|
|
430
|
+
XS: enumField(sizes.XS, ["shared", "per-ticket"], "shared", `${label}.by_size.XS`),
|
|
431
|
+
S: enumField(sizes.S, ["shared", "per-ticket"], "shared", `${label}.by_size.S`),
|
|
432
|
+
M: enumField(sizes.M, ["shared", "per-ticket"], "per-ticket", `${label}.by_size.M`),
|
|
433
|
+
L: enumField(sizes.L, ["shared", "per-ticket"], "per-ticket", `${label}.by_size.L`),
|
|
434
|
+
XL: enumField(sizes.XL, ["shared", "per-ticket"], "per-ticket", `${label}.by_size.XL`),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function normalizeReview(value, label) {
|
|
439
|
+
const raw = objectOrEmpty(value, label);
|
|
440
|
+
const titleStyle = enumField(raw.title_style, ["ticket-title", "conventional", "none", "custom"], DEFAULT_TICKET_SETUP.build.review.title_style, `${label}.title_style`);
|
|
441
|
+
const titleTemplate = nullableString(raw.title_template);
|
|
442
|
+
if (titleStyle === "custom" && !titleTemplate)
|
|
443
|
+
throw new Error(`${label}.title_template: required for custom title style`);
|
|
444
|
+
return {
|
|
445
|
+
title_style: titleStyle,
|
|
446
|
+
title_template: titleTemplate,
|
|
447
|
+
description_sections: stringArray(raw.description_sections, DEFAULT_TICKET_SETUP.build.review.description_sections, `${label}.description_sections`),
|
|
357
448
|
};
|
|
358
449
|
}
|
|
359
450
|
function objectOrEmpty(value, label) {
|
|
@@ -418,6 +509,13 @@ function nullablePositiveInteger(value, fallback, label) {
|
|
|
418
509
|
return Number(value);
|
|
419
510
|
throw new Error(`${label}: expected a positive integer or null`);
|
|
420
511
|
}
|
|
512
|
+
function stringArray(value, fallback, label) {
|
|
513
|
+
if (value === undefined)
|
|
514
|
+
return [...fallback];
|
|
515
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim()))
|
|
516
|
+
throw new Error(`${label}: expected non-empty strings`);
|
|
517
|
+
return [...new Set(value.map((item) => String(item).trim()))];
|
|
518
|
+
}
|
|
421
519
|
function normalizeTargets(value) {
|
|
422
520
|
if (!Array.isArray(value))
|
|
423
521
|
return [];
|
|
@@ -431,8 +529,14 @@ function normalizeTargets(value) {
|
|
|
431
529
|
function cloneTicketSetup(setup) {
|
|
432
530
|
return {
|
|
433
531
|
sources: setup.sources.map((source) => ({ ...source, ...(source.type === "local" ? { paths: [...source.paths] } : {}) })),
|
|
532
|
+
limits: { ...setup.limits },
|
|
434
533
|
populate: { ...setup.populate },
|
|
435
|
-
build: {
|
|
534
|
+
build: {
|
|
535
|
+
...setup.build,
|
|
536
|
+
branch_policy: { ...setup.build.branch_policy, by_size: { ...setup.build.branch_policy.by_size } },
|
|
537
|
+
review: { ...setup.build.review, description_sections: [...setup.build.review.description_sections] },
|
|
538
|
+
validation_checklist: [...setup.build.validation_checklist],
|
|
539
|
+
},
|
|
436
540
|
};
|
|
437
541
|
}
|
|
438
542
|
function unique(values) {
|
|
@@ -5,12 +5,22 @@ import { isIP } from "node:net";
|
|
|
5
5
|
import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { readdirSync } from "node:fs";
|
|
7
7
|
import { parse as parseHtml } from "parse5";
|
|
8
|
+
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
8
9
|
export const DEFAULT_SOURCE_FETCH_LIMITS = {
|
|
9
10
|
timeoutMs: 15_000,
|
|
10
11
|
maxBytes: 10 * 1024 * 1024,
|
|
11
12
|
maxRedirects: 5,
|
|
12
13
|
};
|
|
13
14
|
export async function fetchAndSnapshotUrl(projectDir, input, limits = {}) {
|
|
15
|
+
const label = (() => { try {
|
|
16
|
+
return `fetching source ${new URL(input).hostname}`;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "fetching URL source";
|
|
20
|
+
} })();
|
|
21
|
+
return withActivityPhase(label, () => fetchAndSnapshotUrlInternal(projectDir, input, limits));
|
|
22
|
+
}
|
|
23
|
+
async function fetchAndSnapshotUrlInternal(projectDir, input, limits) {
|
|
14
24
|
const effective = { ...DEFAULT_SOURCE_FETCH_LIMITS, ...limits };
|
|
15
25
|
const requested = normalizePublicHttpUrl(input);
|
|
16
26
|
let current = requested;
|
|
@@ -126,6 +136,7 @@ async function readLimitedBody(response, maxBytes) {
|
|
|
126
136
|
if (done)
|
|
127
137
|
break;
|
|
128
138
|
total += value.length;
|
|
139
|
+
currentActivity()?.pulse(`downloaded ${formatBytes(total)}`);
|
|
129
140
|
if (total > maxBytes) {
|
|
130
141
|
await reader.cancel();
|
|
131
142
|
throw new Error(`URL content exceeds ${maxBytes} byte limit`);
|
|
@@ -174,6 +185,7 @@ async function extractSourceText(kind, bytes) {
|
|
|
174
185
|
const document = await pdfjs.getDocument({ data: bytes, useWorkerFetch: false, isEvalSupported: false }).promise;
|
|
175
186
|
const pages = [];
|
|
176
187
|
for (let pageNumber = 1; pageNumber <= document.numPages; pageNumber++) {
|
|
188
|
+
currentActivity()?.update("extracting PDF source", `page ${pageNumber}/${document.numPages}`);
|
|
177
189
|
const page = await document.getPage(pageNumber);
|
|
178
190
|
const content = await page.getTextContent();
|
|
179
191
|
pages.push(content.items.map((item) => "str" in item ? item.str : "").join(" "));
|
|
@@ -185,6 +197,13 @@ async function extractSourceText(kind, bytes) {
|
|
|
185
197
|
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
186
198
|
return kind === "html" ? htmlToText(decoded) : decoded;
|
|
187
199
|
}
|
|
200
|
+
function formatBytes(bytes) {
|
|
201
|
+
if (bytes < 1024)
|
|
202
|
+
return `${bytes} B`;
|
|
203
|
+
if (bytes < 1024 * 1024)
|
|
204
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
205
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
206
|
+
}
|
|
188
207
|
export function htmlToText(html) {
|
|
189
208
|
const doc = parseHtml(html);
|
|
190
209
|
const blocks = new Set(["address", "article", "aside", "blockquote", "br", "div", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "li", "main", "p", "section", "table", "tr"]);
|
|
@@ -89,6 +89,8 @@ export interface OperationReceipt {
|
|
|
89
89
|
completed_at: string;
|
|
90
90
|
payload_json: string;
|
|
91
91
|
}
|
|
92
|
+
/** The only valid initial/fully-reset active state for a ticket. */
|
|
93
|
+
export declare function pristineTicketState(ticketId: string, now: string, actor?: string | null): TicketState;
|
|
92
94
|
export declare class StateDb {
|
|
93
95
|
private db;
|
|
94
96
|
constructor(dbPath: string);
|
|
@@ -96,17 +98,22 @@ export declare class StateDb {
|
|
|
96
98
|
close(): void;
|
|
97
99
|
getState(ticketId: string): TicketState | undefined;
|
|
98
100
|
getAllStates(): Map<string, TicketState>;
|
|
101
|
+
deleteState(ticketId: string): void;
|
|
99
102
|
upsertState(ticketId: string, patch: Partial<Omit<TicketState, "ticket_id">>, now: string): void;
|
|
100
|
-
insertEvent(event: Omit<TicketEvent, "id">):
|
|
103
|
+
insertEvent(event: Omit<TicketEvent, "id">): number;
|
|
104
|
+
deleteEvents(ids: number[]): void;
|
|
101
105
|
getRecentEvents(limit: number): TicketEvent[];
|
|
106
|
+
getTicketEvents(ticketId: string): TicketEvent[];
|
|
102
107
|
insertValidationSnapshot(snap: Omit<ValidationSnapshot, "id">): void;
|
|
103
108
|
getRecentValidationSnapshot(): ValidationSnapshot | undefined;
|
|
109
|
+
getValidationSnapshots(scope?: string): ValidationSnapshot[];
|
|
104
110
|
insertFutureWork(item: Omit<FutureWorkItem, "id">): number;
|
|
105
111
|
getFutureWork(): FutureWorkItem[];
|
|
106
112
|
getFutureWorkById(id: number): FutureWorkItem | undefined;
|
|
107
113
|
updateFutureWorkDisposition(id: number, disposition: string): void;
|
|
108
114
|
getRecentCompleted(): RecentCompletedContext[];
|
|
109
115
|
upsertRecentCompleted(row: RecentCompletedContext): void;
|
|
116
|
+
deleteRecentCompleted(ticketId: string): void;
|
|
110
117
|
getArchiveIndex(): ArchiveIndexEntry[];
|
|
111
118
|
insertReviewRecommendation(item: Omit<ReviewRecommendation, "id">): number;
|
|
112
119
|
getReviewRecommendations(status?: ReviewRecommendationStatus): ReviewRecommendation[];
|
package/dist/tickets/stateDb.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
+
/** The only valid initial/fully-reset active state for a ticket. */
|
|
5
|
+
export function pristineTicketState(ticketId, now, actor = null) {
|
|
6
|
+
return {
|
|
7
|
+
ticket_id: ticketId,
|
|
8
|
+
status: "planned",
|
|
9
|
+
owner: null,
|
|
10
|
+
current_step: null,
|
|
11
|
+
next_action: null,
|
|
12
|
+
blocked_by_json: "[]",
|
|
13
|
+
blocker_type: null,
|
|
14
|
+
blocker_notes: null,
|
|
15
|
+
first_blocked_at: null,
|
|
16
|
+
last_checked_at: null,
|
|
17
|
+
last_worked_at: null,
|
|
18
|
+
completed_at: null,
|
|
19
|
+
attempt_count: 0,
|
|
20
|
+
last_error: null,
|
|
21
|
+
evidence: null,
|
|
22
|
+
validation_result: null,
|
|
23
|
+
validation_commands: null,
|
|
24
|
+
validation_notes: null,
|
|
25
|
+
updated_at: now,
|
|
26
|
+
updated_by: actor,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
4
29
|
const INIT_SQL = `
|
|
5
30
|
PRAGMA foreign_keys = ON;
|
|
6
31
|
|
|
@@ -160,30 +185,12 @@ export class StateDb {
|
|
|
160
185
|
m.set(row.ticket_id, row);
|
|
161
186
|
return m;
|
|
162
187
|
}
|
|
188
|
+
deleteState(ticketId) {
|
|
189
|
+
this.db.prepare("DELETE FROM ticket_state WHERE ticket_id = ?").run(ticketId);
|
|
190
|
+
}
|
|
163
191
|
upsertState(ticketId, patch, now) {
|
|
164
192
|
const existing = this.getState(ticketId);
|
|
165
|
-
const base = existing ??
|
|
166
|
-
ticket_id: ticketId,
|
|
167
|
-
status: "planned",
|
|
168
|
-
owner: null,
|
|
169
|
-
current_step: null,
|
|
170
|
-
next_action: null,
|
|
171
|
-
blocked_by_json: "[]",
|
|
172
|
-
blocker_type: null,
|
|
173
|
-
blocker_notes: null,
|
|
174
|
-
first_blocked_at: null,
|
|
175
|
-
last_checked_at: null,
|
|
176
|
-
last_worked_at: null,
|
|
177
|
-
completed_at: null,
|
|
178
|
-
attempt_count: 0,
|
|
179
|
-
last_error: null,
|
|
180
|
-
evidence: null,
|
|
181
|
-
validation_result: null,
|
|
182
|
-
validation_commands: null,
|
|
183
|
-
validation_notes: null,
|
|
184
|
-
updated_at: now,
|
|
185
|
-
updated_by: null,
|
|
186
|
-
};
|
|
193
|
+
const base = existing ?? pristineTicketState(ticketId, now);
|
|
187
194
|
const merged = { ...base, ...patch, ticket_id: ticketId, updated_at: now };
|
|
188
195
|
if (existing) {
|
|
189
196
|
this.db.prepare(`
|
|
@@ -218,7 +225,7 @@ export class StateDb {
|
|
|
218
225
|
}
|
|
219
226
|
// ── ticket_events ─────────────────────────────────────────────────────────
|
|
220
227
|
insertEvent(event) {
|
|
221
|
-
this.db.prepare(`
|
|
228
|
+
const result = this.db.prepare(`
|
|
222
229
|
INSERT INTO ticket_events (
|
|
223
230
|
timestamp,actor,ticket_id,event_type,old_status,new_status,
|
|
224
231
|
summary,validation,evidence,payload_json
|
|
@@ -227,12 +234,23 @@ export class StateDb {
|
|
|
227
234
|
@summary,@validation,@evidence,@payload_json
|
|
228
235
|
)
|
|
229
236
|
`).run(event);
|
|
237
|
+
return Number(result.lastInsertRowid);
|
|
238
|
+
}
|
|
239
|
+
deleteEvents(ids) {
|
|
240
|
+
const remove = this.db.prepare("DELETE FROM ticket_events WHERE id = ?");
|
|
241
|
+
for (const id of ids)
|
|
242
|
+
remove.run(id);
|
|
230
243
|
}
|
|
231
244
|
getRecentEvents(limit) {
|
|
232
245
|
return this.db
|
|
233
246
|
.prepare("SELECT * FROM ticket_events ORDER BY timestamp DESC, id DESC LIMIT ?")
|
|
234
247
|
.all(limit);
|
|
235
248
|
}
|
|
249
|
+
getTicketEvents(ticketId) {
|
|
250
|
+
return this.db
|
|
251
|
+
.prepare("SELECT * FROM ticket_events WHERE ticket_id = ? ORDER BY timestamp, id")
|
|
252
|
+
.all(ticketId);
|
|
253
|
+
}
|
|
236
254
|
// ── validation_snapshots ──────────────────────────────────────────────────
|
|
237
255
|
insertValidationSnapshot(snap) {
|
|
238
256
|
this.db.prepare(`
|
|
@@ -245,6 +263,16 @@ export class StateDb {
|
|
|
245
263
|
.prepare("SELECT * FROM validation_snapshots ORDER BY timestamp DESC, id DESC LIMIT 1")
|
|
246
264
|
.get();
|
|
247
265
|
}
|
|
266
|
+
getValidationSnapshots(scope) {
|
|
267
|
+
if (scope !== undefined) {
|
|
268
|
+
return this.db
|
|
269
|
+
.prepare("SELECT * FROM validation_snapshots WHERE scope = ? ORDER BY timestamp, id")
|
|
270
|
+
.all(scope);
|
|
271
|
+
}
|
|
272
|
+
return this.db
|
|
273
|
+
.prepare("SELECT * FROM validation_snapshots ORDER BY timestamp, id")
|
|
274
|
+
.all();
|
|
275
|
+
}
|
|
248
276
|
// ── future_work ───────────────────────────────────────────────────────────
|
|
249
277
|
insertFutureWork(item) {
|
|
250
278
|
const r = this.db.prepare(`
|
|
@@ -287,6 +315,9 @@ export class StateDb {
|
|
|
287
315
|
updated_at=excluded.updated_at
|
|
288
316
|
`).run(row);
|
|
289
317
|
}
|
|
318
|
+
deleteRecentCompleted(ticketId) {
|
|
319
|
+
this.db.prepare("DELETE FROM recent_completed_context WHERE ticket_id = ?").run(ticketId);
|
|
320
|
+
}
|
|
290
321
|
// ── archive_index ─────────────────────────────────────────────────────────
|
|
291
322
|
getArchiveIndex() {
|
|
292
323
|
return this.db
|
package/dist/workflowDb.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface ProjectLease {
|
|
|
36
36
|
heartbeatAt: string;
|
|
37
37
|
runId: string;
|
|
38
38
|
}
|
|
39
|
+
/** Read the current project lease without creating or migrating recovery state. */
|
|
40
|
+
export declare function readCurrentWorkflowLease(projectDir: string): ProjectLease | undefined;
|
|
39
41
|
export interface PublicationTransaction {
|
|
40
42
|
transactionId: string;
|
|
41
43
|
runId: string;
|
|
@@ -63,6 +65,7 @@ export declare class WorkflowDb {
|
|
|
63
65
|
}, now?: Date): WorkflowRunSnapshot;
|
|
64
66
|
getRun(runId: string): WorkflowRunSnapshot | undefined;
|
|
65
67
|
activeRuns(): WorkflowRunSnapshot[];
|
|
68
|
+
resumableRuns(kind?: WorkflowKind): WorkflowRunSnapshot[];
|
|
66
69
|
transition(runId: string, update: {
|
|
67
70
|
status?: WorkflowRunStatus;
|
|
68
71
|
checkpoint: string;
|
package/dist/workflowDb.js
CHANGED
|
@@ -4,6 +4,26 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
export const WORKFLOW_DB_FILE = ".rafi/recovery.sqlite3";
|
|
7
|
+
/** Read the current project lease without creating or migrating recovery state. */
|
|
8
|
+
export function readCurrentWorkflowLease(projectDir) {
|
|
9
|
+
const path = join(resolve(projectDir), WORKFLOW_DB_FILE);
|
|
10
|
+
if (!existsSync(path))
|
|
11
|
+
return undefined;
|
|
12
|
+
let db;
|
|
13
|
+
try {
|
|
14
|
+
db = new Database(path, { readonly: true, fileMustExist: true });
|
|
15
|
+
const row = db.prepare("SELECT * FROM project_lease WHERE singleton=1").get();
|
|
16
|
+
return row ? { owner: row.owner, generation: row.generation, pid: row.pid, host: row.host, processStart: row.process_start, heartbeatAt: row.heartbeat_at, runId: row.run_id } : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (String(error).includes("no such table"))
|
|
20
|
+
return undefined;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
db?.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
7
27
|
/** Authoritative persistence for every resumable workflow in a project. */
|
|
8
28
|
export class WorkflowDb {
|
|
9
29
|
projectDir;
|
|
@@ -43,6 +63,12 @@ export class WorkflowDb {
|
|
|
43
63
|
activeRuns() {
|
|
44
64
|
return this.db.prepare("SELECT * FROM workflow_runs WHERE status IN ('running','paused','blocked') ORDER BY created_at").all().map(rowToRun);
|
|
45
65
|
}
|
|
66
|
+
resumableRuns(kind) {
|
|
67
|
+
const rows = kind
|
|
68
|
+
? this.db.prepare("SELECT * FROM workflow_runs WHERE kind=? AND status NOT IN ('completed','cancelled','superseded') ORDER BY created_at").all(kind)
|
|
69
|
+
: this.db.prepare("SELECT * FROM workflow_runs WHERE status NOT IN ('completed','cancelled','superseded') ORDER BY created_at").all();
|
|
70
|
+
return rows.map(rowToRun);
|
|
71
|
+
}
|
|
46
72
|
transition(runId, update, now = new Date()) {
|
|
47
73
|
const at = now.toISOString();
|
|
48
74
|
return this.db.transaction(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
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
|
+
"./activity.js": {
|
|
21
|
+
"types": "./dist/activity.d.ts",
|
|
22
|
+
"default": "./dist/activity.js"
|
|
23
|
+
},
|
|
20
24
|
"./agent-run.js": {
|
|
21
25
|
"types": "./dist/agentRun.d.ts",
|
|
22
26
|
"default": "./dist/agentRun.js"
|
|
@@ -25,6 +29,10 @@
|
|
|
25
29
|
"types": "./dist/interviews.d.ts",
|
|
26
30
|
"default": "./dist/interviews.js"
|
|
27
31
|
},
|
|
32
|
+
"./provider-questions.js": {
|
|
33
|
+
"types": "./dist/providerQuestions.d.ts",
|
|
34
|
+
"default": "./dist/providerQuestions.js"
|
|
35
|
+
},
|
|
28
36
|
"./runtime-readiness.js": {
|
|
29
37
|
"types": "./dist/runtimeReadiness.d.ts",
|
|
30
38
|
"default": "./dist/runtimeReadiness.js"
|
|
@@ -65,6 +73,10 @@
|
|
|
65
73
|
"types": "./dist/tickets/setupConfig.d.ts",
|
|
66
74
|
"default": "./dist/tickets/setupConfig.js"
|
|
67
75
|
},
|
|
76
|
+
"./ticket-reset.js": {
|
|
77
|
+
"types": "./dist/tickets/reset.d.ts",
|
|
78
|
+
"default": "./dist/tickets/reset.js"
|
|
79
|
+
},
|
|
68
80
|
"./sources/source-registry.js": {
|
|
69
81
|
"types": "./dist/sources/sourceRegistry.d.ts",
|
|
70
82
|
"default": "./dist/sources/sourceRegistry.js"
|
|
@@ -118,8 +130,8 @@
|
|
|
118
130
|
"@anthropic-ai/claude-agent-sdk": "^0.3.148"
|
|
119
131
|
},
|
|
120
132
|
"dependencies": {
|
|
121
|
-
"special-agents": "0.8.
|
|
122
|
-
"rafi-spec": "0.8.
|
|
133
|
+
"special-agents": "0.8.4",
|
|
134
|
+
"rafi-spec": "0.8.4",
|
|
123
135
|
"@clack/prompts": "^1.4.0",
|
|
124
136
|
"ajv": "^8.20.0",
|
|
125
137
|
"ajv-formats": "^3.0.1",
|