ai-foreman 1.6.2 → 1.7.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 +2 -2
- package/dist/adapters/claude.d.ts +5 -1
- package/dist/adapters/claude.js +41 -0
- package/dist/adapters/codex.d.ts +26 -28
- package/dist/adapters/codex.js +209 -148
- package/dist/adapters/recovering.d.ts +3 -1
- package/dist/adapters/recovering.js +2 -0
- package/dist/adapters/types.d.ts +29 -0
- package/dist/branch/planner.js +1 -1
- package/dist/branch/runner.d.ts +6 -0
- package/dist/branch/runner.js +149 -16
- package/dist/buildRuns.d.ts +9 -2
- package/dist/buildRuns.js +110 -7
- package/dist/cli/start.d.ts +4 -0
- package/dist/cli/start.js +283 -28
- package/dist/cli/status.js +25 -2
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +67 -27
- package/dist/foreman.d.ts +18 -3
- package/dist/foreman.js +112 -5
- package/dist/log.d.ts +1 -1
- package/dist/operationRecovery.d.ts +25 -0
- package/dist/operationRecovery.js +46 -0
- package/dist/outcomes.d.ts +16 -0
- package/dist/outcomes.js +27 -0
- package/dist/qaReview.d.ts +52 -0
- package/dist/qaReview.js +103 -0
- package/dist/qaSnapshot.d.ts +19 -0
- package/dist/qaSnapshot.js +88 -0
- package/dist/sessionLifecycle.d.ts +37 -0
- package/dist/sessionLifecycle.js +104 -0
- package/dist/sources/sourceRegistry.d.ts +49 -0
- package/dist/sources/sourceRegistry.js +577 -0
- package/dist/statusReporter.d.ts +40 -0
- package/dist/statusReporter.js +31 -0
- package/dist/ticketPopulation.d.ts +54 -0
- package/dist/ticketPopulation.js +209 -0
- package/dist/tickets/blockers.d.ts +1 -1
- package/dist/tickets/blockers.js +2 -0
- package/dist/tickets/commands.d.ts +1 -0
- package/dist/tickets/commands.js +31 -1
- package/dist/tickets/delivery.d.ts +36 -0
- package/dist/tickets/delivery.js +204 -4
- package/dist/tickets/queue.d.ts +3 -0
- package/dist/tickets/queue.js +50 -1
- package/dist/tickets/setupConfig.js +30 -2
- package/dist/tickets/sourceFetch.js +6 -1
- package/dist/tickets/stateDb.d.ts +2 -1
- package/dist/tickets/stateDb.js +24 -1
- package/dist/tickets/ticketSchema.d.ts +53 -0
- package/dist/tickets/ticketSchema.js +11 -0
- package/dist/tickets/validate.js +1 -1
- package/dist/workflowDb.d.ts +113 -0
- package/dist/workflowDb.js +261 -0
- package/package.json +20 -3
package/dist/tickets/queue.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TicketDef } from "./ticketSchema.js";
|
|
2
2
|
import type { TicketState } from "./stateDb.js";
|
|
3
|
+
import type { DeliveryConfig } from "./delivery.js";
|
|
3
4
|
export interface QueueRow {
|
|
4
5
|
rank: number;
|
|
5
6
|
ticket: string;
|
|
@@ -34,3 +35,5 @@ export interface ActiveStatusRow {
|
|
|
34
35
|
}
|
|
35
36
|
export declare function buildNextQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, queueLimit: number): QueueRow[];
|
|
36
37
|
export declare function buildActiveStatusRows(queueRows: QueueRow[], ticketDefs: TicketDef[], states: Map<string, TicketState>): ActiveStatusRow[];
|
|
38
|
+
/** Human-readable queue blocks keep a whole unfinished stack and its resume point visible. */
|
|
39
|
+
export declare function formatStackAwareQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, delivery?: DeliveryConfig): string[];
|
package/dist/tickets/queue.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveBlockers, computeDisplayStatus } from "./blockers.js";
|
|
2
|
+
import { normalizeStackNodes } from "./delivery.js";
|
|
2
3
|
function defaultNextAction(ticket, displayStatus, blockedBy) {
|
|
3
4
|
if (displayStatus === "blocked")
|
|
4
5
|
return `Resolve blockers: ${blockedBy.join(", ")}`;
|
|
@@ -11,7 +12,7 @@ export function buildNextQueue(ticketDefs, states, queueLimit) {
|
|
|
11
12
|
.filter((t) => {
|
|
12
13
|
const s = states.get(t.id);
|
|
13
14
|
const status = s?.status ?? "planned";
|
|
14
|
-
return status !== "done" && status !== "canceled";
|
|
15
|
+
return status !== "done" && status !== "canceled" && status !== "obsolete";
|
|
15
16
|
})
|
|
16
17
|
.sort((a, b) => a.order - b.order);
|
|
17
18
|
const window = remaining.slice(0, Math.min(queueLimit, remaining.length));
|
|
@@ -60,3 +61,51 @@ export function buildActiveStatusRows(queueRows, ticketDefs, states) {
|
|
|
60
61
|
};
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
/** Human-readable queue blocks keep a whole unfinished stack and its resume point visible. */
|
|
65
|
+
export function formatStackAwareQueue(ticketDefs, states, delivery) {
|
|
66
|
+
if (!delivery?.stacks?.length)
|
|
67
|
+
return buildNextQueue(ticketDefs, states, Number.MAX_SAFE_INTEGER).map((row) => `${row.ticket} ${row.status} ${row.title}`);
|
|
68
|
+
const defs = new Map(ticketDefs.map((ticket) => [ticket.id, ticket]));
|
|
69
|
+
const unitById = new Map(delivery.units.map((unit) => [unit.id, unit]));
|
|
70
|
+
const stackedTickets = new Set();
|
|
71
|
+
const lines = [];
|
|
72
|
+
for (const stack of delivery.stacks) {
|
|
73
|
+
const nodes = normalizeStackNodes(stack, delivery.units, ticketDefs);
|
|
74
|
+
const ticketIds = stack.units.flatMap((id) => unitById.get(id)?.tickets ?? []);
|
|
75
|
+
ticketIds.forEach((id) => stackedTickets.add(id));
|
|
76
|
+
const completed = ticketIds.filter((id) => terminal(states.get(id)?.status));
|
|
77
|
+
if (completed.length === ticketIds.length && (stack.status ?? "planned") === "merged")
|
|
78
|
+
continue;
|
|
79
|
+
const next = ticketIds.find((id) => !terminal(states.get(id)?.status));
|
|
80
|
+
const blockers = next && defs.get(next) ? resolveBlockers(defs.get(next), states) : [];
|
|
81
|
+
const state = deriveStackState(stack, ticketIds, states, blockers);
|
|
82
|
+
lines.push(`=== STACK START: ${stack.name} (${stack.id}) ===`);
|
|
83
|
+
lines.push(`status=${state}; size=${ticketIds.length}; pr_depth=${nodes.length}; blockers=${blockers.join(",") || "none"}; completed_prefix=${completed.join(",") || "none"}; next=${next ?? "none"}`);
|
|
84
|
+
lines.push(`reviews=${stack.review_links?.join(",") || "none"}; remote_checked=${stack.remote_checked_at ?? "never"}${stack.remote_stale ? " (stale; run --refresh for a live check)" : ""}`);
|
|
85
|
+
for (const id of ticketIds)
|
|
86
|
+
lines.push(`${id} ${states.get(id)?.status ?? "planned"} ${defs.get(id)?.title ?? "unknown ticket"}`);
|
|
87
|
+
lines.push(`=== STACK END: ${stack.name} (${stack.id}) ===`);
|
|
88
|
+
}
|
|
89
|
+
for (const ticket of ticketDefs.sort((a, b) => a.order - b.order)) {
|
|
90
|
+
if (stackedTickets.has(ticket.id) || terminal(states.get(ticket.id)?.status))
|
|
91
|
+
continue;
|
|
92
|
+
const blockers = resolveBlockers(ticket, states);
|
|
93
|
+
lines.push(`${ticket.id} ${computeDisplayStatus(states.get(ticket.id)?.status ?? "planned", blockers)} ${ticket.title}`);
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
function terminal(status) { return status === "done" || status === "canceled" || status === "obsolete"; }
|
|
98
|
+
function deriveStackState(stack, tickets, states, blockers) {
|
|
99
|
+
if (stack.status === "merged")
|
|
100
|
+
return "merged";
|
|
101
|
+
if (stack.status === "awaiting_review")
|
|
102
|
+
return "awaiting_review";
|
|
103
|
+
if (blockers.length)
|
|
104
|
+
return "blocked";
|
|
105
|
+
const completed = tickets.filter((id) => terminal(states.get(id)?.status)).length;
|
|
106
|
+
if (completed === 0)
|
|
107
|
+
return stack.status ?? "planned";
|
|
108
|
+
if (completed < tickets.length)
|
|
109
|
+
return "partial";
|
|
110
|
+
return "awaiting_review";
|
|
111
|
+
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse, stringify } from "yaml";
|
|
5
5
|
import { loadDefaults } from "special-agents";
|
|
6
|
+
import { appendLegacyTicketSources, loadSourceRegistry } from "../sources/sourceRegistry.js";
|
|
6
7
|
export const RAFI_CONFIG_FILE = "rafi-config.yaml";
|
|
7
8
|
const AGENT_NAMES = ["builder", "qa", "planner", "ticket-maker", "uninstaller"];
|
|
8
9
|
const SKILL_NAMES = [
|
|
@@ -48,7 +49,10 @@ export function loadTicketSetupConfig(projectDir) {
|
|
|
48
49
|
const raw = loadRafiConfigObject(projectDir);
|
|
49
50
|
if (!raw || raw.tickets === undefined)
|
|
50
51
|
return undefined;
|
|
51
|
-
|
|
52
|
+
const setup = normalizeTicketsSetupConfig(raw.tickets, `${RAFI_CONFIG_FILE}.tickets`);
|
|
53
|
+
if (setup.sources.length === 0)
|
|
54
|
+
setup.sources = legacySourcesFromRegistry(loadSourceRegistry(projectDir).registry.entries);
|
|
55
|
+
return setup;
|
|
52
56
|
}
|
|
53
57
|
export function loadTicketSetupConfigWithDefaults(projectDir) {
|
|
54
58
|
return loadTicketSetupConfig(projectDir) ?? cloneTicketSetup(DEFAULT_TICKET_SETUP);
|
|
@@ -59,7 +63,17 @@ export function hasTicketSetupConfig(projectDir) {
|
|
|
59
63
|
}
|
|
60
64
|
export function saveTicketSetupConfig(projectDir, setup, opts = {}) {
|
|
61
65
|
const config = loadRafiConfigObject(projectDir) ?? minimalRafiConfig(projectDir, opts);
|
|
62
|
-
|
|
66
|
+
const normalized = normalizeTicketsSetupConfig(setup, "tickets");
|
|
67
|
+
const ticketConfig = denormalizeTicketsSetupConfig(normalized);
|
|
68
|
+
delete ticketConfig.sources;
|
|
69
|
+
config.tickets = ticketConfig;
|
|
70
|
+
config.sources = appendLegacyTicketSources(loadSourceRegistry(projectDir).registry, normalized.sources);
|
|
71
|
+
const planning = config.planning && typeof config.planning === "object" && !Array.isArray(config.planning) ? config.planning : undefined;
|
|
72
|
+
if (planning) {
|
|
73
|
+
delete planning.sources;
|
|
74
|
+
if (Object.keys(planning).length === 0)
|
|
75
|
+
delete config.planning;
|
|
76
|
+
}
|
|
63
77
|
mkdirSync(projectDir, { recursive: true });
|
|
64
78
|
writeFileSync(join(projectDir, RAFI_CONFIG_FILE), stringify(config, { lineWidth: 100 }), "utf8");
|
|
65
79
|
}
|
|
@@ -424,3 +438,17 @@ function cloneTicketSetup(setup) {
|
|
|
424
438
|
function unique(values) {
|
|
425
439
|
return [...new Set(values)];
|
|
426
440
|
}
|
|
441
|
+
function legacySourcesFromRegistry(entries) {
|
|
442
|
+
const out = [];
|
|
443
|
+
for (const entry of entries.filter((item) => item.active)) {
|
|
444
|
+
if (entry.type === "local" && entry.locator.path)
|
|
445
|
+
out.push({ type: "local", paths: [entry.locator.path] });
|
|
446
|
+
else if (entry.type === "url" && entry.locator.url)
|
|
447
|
+
out.push({ type: "url", url: entry.locator.url });
|
|
448
|
+
else if (entry.type === "linear")
|
|
449
|
+
out.push({ type: "linear", api_key_env: entry.locator.api_key_env ?? "LINEAR_API_KEY", team_key: entry.locator.team_key, filter: entry.locator.filter });
|
|
450
|
+
else if (entry.type === "jira" && entry.locator.site && entry.locator.jql)
|
|
451
|
+
out.push({ type: "jira", site: entry.locator.site, email_env: entry.locator.email_env ?? "JIRA_EMAIL", token_env: entry.locator.token_env ?? "JIRA_API_TOKEN", jql: entry.locator.jql });
|
|
452
|
+
}
|
|
453
|
+
return dedupeTicketSources(out);
|
|
454
|
+
}
|
|
@@ -23,7 +23,7 @@ export async function fetchAndSnapshotUrl(projectDir, input, limits = {}) {
|
|
|
23
23
|
response = await fetch(current, {
|
|
24
24
|
redirect: "manual",
|
|
25
25
|
signal: controller.signal,
|
|
26
|
-
headers: { Accept: "text/html,text/plain,text/markdown,application/pdf;q=0.9" },
|
|
26
|
+
headers: { Accept: "text/html,text/plain,text/markdown,application/json,application/yaml,text/yaml,application/pdf;q=0.9" },
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
finally {
|
|
@@ -154,6 +154,11 @@ function detectContentType(header, bytes, url) {
|
|
|
154
154
|
return "html";
|
|
155
155
|
if (["text/markdown", "text/x-markdown"].includes(mime) || /\.md(?:own)?$/i.test(new URL(url).pathname))
|
|
156
156
|
return "markdown";
|
|
157
|
+
if (["application/json", "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml"].includes(mime) || /\.(?:json|ya?ml)$/i.test(new URL(url).pathname)) {
|
|
158
|
+
if (bytes.subarray(0, Math.min(bytes.length, 1024)).includes(0))
|
|
159
|
+
throw new Error("binary URL content is not supported");
|
|
160
|
+
return "text";
|
|
161
|
+
}
|
|
157
162
|
if (!mime || mime.startsWith("text/") || mime === "application/octet-stream") {
|
|
158
163
|
if (bytes.subarray(0, Math.min(bytes.length, 1024)).includes(0))
|
|
159
164
|
throw new Error("binary URL content is not supported");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TicketStatus = "planned" | "next" | "in_progress" | "blocked" | "done" | "canceled";
|
|
1
|
+
export type TicketStatus = "planned" | "next" | "in_progress" | "blocked" | "done" | "canceled" | "obsolete";
|
|
2
2
|
export type ValidationResult = "passed" | "failed" | "not_run" | "not_applicable";
|
|
3
3
|
export interface TicketState {
|
|
4
4
|
ticket_id: string;
|
|
@@ -92,6 +92,7 @@ export interface OperationReceipt {
|
|
|
92
92
|
export declare class StateDb {
|
|
93
93
|
private db;
|
|
94
94
|
constructor(dbPath: string);
|
|
95
|
+
private migrateObsoleteStatus;
|
|
95
96
|
close(): void;
|
|
96
97
|
getState(ticketId: string): TicketState | undefined;
|
|
97
98
|
getAllStates(): Map<string, TicketState>;
|
package/dist/tickets/stateDb.js
CHANGED
|
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS ticket_state (
|
|
|
25
25
|
validation_notes TEXT,
|
|
26
26
|
updated_at TEXT NOT NULL,
|
|
27
27
|
updated_by TEXT,
|
|
28
|
-
CHECK (status IN ('planned','next','in_progress','blocked','done','canceled')),
|
|
28
|
+
CHECK (status IN ('planned','next','in_progress','blocked','done','canceled','obsolete')),
|
|
29
29
|
CHECK (validation_result IS NULL OR validation_result IN ('passed','failed','not_run','not_applicable'))
|
|
30
30
|
);
|
|
31
31
|
|
|
@@ -120,6 +120,29 @@ export class StateDb {
|
|
|
120
120
|
this.db = new Database(dbPath);
|
|
121
121
|
this.db.pragma("journal_mode = WAL");
|
|
122
122
|
this.db.exec(INIT_SQL);
|
|
123
|
+
this.migrateObsoleteStatus();
|
|
124
|
+
}
|
|
125
|
+
migrateObsoleteStatus() {
|
|
126
|
+
const row = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='ticket_state'").get();
|
|
127
|
+
if (!row || row.sql.includes("'obsolete'"))
|
|
128
|
+
return;
|
|
129
|
+
this.db.pragma("foreign_keys = OFF");
|
|
130
|
+
this.db.exec(`
|
|
131
|
+
DROP INDEX IF EXISTS idx_ticket_state_status;
|
|
132
|
+
ALTER TABLE ticket_state RENAME TO ticket_state_legacy;
|
|
133
|
+
CREATE TABLE ticket_state (
|
|
134
|
+
ticket_id TEXT PRIMARY KEY,status TEXT NOT NULL DEFAULT 'planned',owner TEXT,current_step TEXT,next_action TEXT,
|
|
135
|
+
blocked_by_json TEXT NOT NULL DEFAULT '[]',blocker_type TEXT,blocker_notes TEXT,first_blocked_at TEXT,last_checked_at TEXT,
|
|
136
|
+
last_worked_at TEXT,completed_at TEXT,attempt_count INTEGER NOT NULL DEFAULT 0,last_error TEXT,evidence TEXT,
|
|
137
|
+
validation_result TEXT,validation_commands TEXT,validation_notes TEXT,updated_at TEXT NOT NULL,updated_by TEXT,
|
|
138
|
+
CHECK (status IN ('planned','next','in_progress','blocked','done','canceled','obsolete')),
|
|
139
|
+
CHECK (validation_result IS NULL OR validation_result IN ('passed','failed','not_run','not_applicable'))
|
|
140
|
+
);
|
|
141
|
+
INSERT INTO ticket_state SELECT * FROM ticket_state_legacy;
|
|
142
|
+
DROP TABLE ticket_state_legacy;
|
|
143
|
+
CREATE INDEX idx_ticket_state_status ON ticket_state(status);
|
|
144
|
+
`);
|
|
145
|
+
this.db.pragma("foreign_keys = ON");
|
|
123
146
|
}
|
|
124
147
|
close() {
|
|
125
148
|
this.db.close();
|
|
@@ -11,10 +11,16 @@ export interface TicketExternalRef {
|
|
|
11
11
|
export interface TicketSourceRef {
|
|
12
12
|
source: string;
|
|
13
13
|
item: string;
|
|
14
|
+
source_id?: string;
|
|
14
15
|
url?: string | null;
|
|
15
16
|
fingerprint?: string | null;
|
|
16
17
|
note?: string | null;
|
|
17
18
|
}
|
|
19
|
+
export interface TicketPlanRef {
|
|
20
|
+
plan_id: string;
|
|
21
|
+
revision: number;
|
|
22
|
+
slice_ref: string;
|
|
23
|
+
}
|
|
18
24
|
export interface TicketDef {
|
|
19
25
|
id: string;
|
|
20
26
|
order: number;
|
|
@@ -34,6 +40,7 @@ export interface TicketDef {
|
|
|
34
40
|
source_refs?: TicketSourceRef[];
|
|
35
41
|
superseded_by?: string[];
|
|
36
42
|
supersedes?: string[];
|
|
43
|
+
plan_ref?: TicketPlanRef;
|
|
37
44
|
}
|
|
38
45
|
export declare const TICKET_JSON_SCHEMA: {
|
|
39
46
|
readonly type: "object";
|
|
@@ -143,6 +150,10 @@ export declare const TICKET_JSON_SCHEMA: {
|
|
|
143
150
|
readonly type: "string";
|
|
144
151
|
readonly minLength: 1;
|
|
145
152
|
};
|
|
153
|
+
readonly source_id: {
|
|
154
|
+
readonly type: "string";
|
|
155
|
+
readonly pattern: "^src_[A-Za-z0-9_-]+$";
|
|
156
|
+
};
|
|
146
157
|
readonly url: {
|
|
147
158
|
readonly type: readonly ["string", "null"];
|
|
148
159
|
};
|
|
@@ -169,6 +180,25 @@ export declare const TICKET_JSON_SCHEMA: {
|
|
|
169
180
|
};
|
|
170
181
|
readonly uniqueItems: true;
|
|
171
182
|
};
|
|
183
|
+
readonly plan_ref: {
|
|
184
|
+
readonly type: "object";
|
|
185
|
+
readonly required: readonly ["plan_id", "revision", "slice_ref"];
|
|
186
|
+
readonly additionalProperties: false;
|
|
187
|
+
readonly properties: {
|
|
188
|
+
readonly plan_id: {
|
|
189
|
+
readonly type: "string";
|
|
190
|
+
readonly minLength: 1;
|
|
191
|
+
};
|
|
192
|
+
readonly revision: {
|
|
193
|
+
readonly type: "integer";
|
|
194
|
+
readonly minimum: 1;
|
|
195
|
+
};
|
|
196
|
+
readonly slice_ref: {
|
|
197
|
+
readonly type: "string";
|
|
198
|
+
readonly minLength: 1;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
};
|
|
172
202
|
};
|
|
173
203
|
};
|
|
174
204
|
export declare const TICKETS_FILE_SCHEMA: {
|
|
@@ -285,6 +315,10 @@ export declare const TICKETS_FILE_SCHEMA: {
|
|
|
285
315
|
readonly type: "string";
|
|
286
316
|
readonly minLength: 1;
|
|
287
317
|
};
|
|
318
|
+
readonly source_id: {
|
|
319
|
+
readonly type: "string";
|
|
320
|
+
readonly pattern: "^src_[A-Za-z0-9_-]+$";
|
|
321
|
+
};
|
|
288
322
|
readonly url: {
|
|
289
323
|
readonly type: readonly ["string", "null"];
|
|
290
324
|
};
|
|
@@ -311,6 +345,25 @@ export declare const TICKETS_FILE_SCHEMA: {
|
|
|
311
345
|
};
|
|
312
346
|
readonly uniqueItems: true;
|
|
313
347
|
};
|
|
348
|
+
readonly plan_ref: {
|
|
349
|
+
readonly type: "object";
|
|
350
|
+
readonly required: readonly ["plan_id", "revision", "slice_ref"];
|
|
351
|
+
readonly additionalProperties: false;
|
|
352
|
+
readonly properties: {
|
|
353
|
+
readonly plan_id: {
|
|
354
|
+
readonly type: "string";
|
|
355
|
+
readonly minLength: 1;
|
|
356
|
+
};
|
|
357
|
+
readonly revision: {
|
|
358
|
+
readonly type: "integer";
|
|
359
|
+
readonly minimum: 1;
|
|
360
|
+
};
|
|
361
|
+
readonly slice_ref: {
|
|
362
|
+
readonly type: "string";
|
|
363
|
+
readonly minLength: 1;
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
};
|
|
314
367
|
};
|
|
315
368
|
};
|
|
316
369
|
};
|
|
@@ -43,6 +43,7 @@ export const TICKET_JSON_SCHEMA = {
|
|
|
43
43
|
properties: {
|
|
44
44
|
source: { type: "string", minLength: 1 },
|
|
45
45
|
item: { type: "string", minLength: 1 },
|
|
46
|
+
source_id: { type: "string", pattern: "^src_[A-Za-z0-9_-]+$" },
|
|
46
47
|
url: { type: ["string", "null"] },
|
|
47
48
|
fingerprint: { type: ["string", "null"] },
|
|
48
49
|
note: { type: ["string", "null"] },
|
|
@@ -51,6 +52,16 @@ export const TICKET_JSON_SCHEMA = {
|
|
|
51
52
|
},
|
|
52
53
|
superseded_by: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
53
54
|
supersedes: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
55
|
+
plan_ref: {
|
|
56
|
+
type: "object",
|
|
57
|
+
required: ["plan_id", "revision", "slice_ref"],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
properties: {
|
|
60
|
+
plan_id: { type: "string", minLength: 1 },
|
|
61
|
+
revision: { type: "integer", minimum: 1 },
|
|
62
|
+
slice_ref: { type: "string", minLength: 1 },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
54
65
|
},
|
|
55
66
|
};
|
|
56
67
|
export const TICKETS_FILE_SCHEMA = {
|
package/dist/tickets/validate.js
CHANGED
|
@@ -40,7 +40,7 @@ export function runAllValidation(config, projectDir, ticketDefs, states, db) {
|
|
|
40
40
|
const queueRows = buildNextQueue(ticketDefs, states, config.implementationLimit);
|
|
41
41
|
const remaining = ticketDefs.filter((t) => {
|
|
42
42
|
const s = states.get(t.id);
|
|
43
|
-
return s?.status !== "done" && s?.status !== "canceled";
|
|
43
|
+
return s?.status !== "done" && s?.status !== "canceled" && s?.status !== "obsolete";
|
|
44
44
|
});
|
|
45
45
|
const expectedLen = Math.min(config.implementationLimit, remaining.length);
|
|
46
46
|
if (queueRows.length !== expectedLen) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { OperationLifecycle, ResolvedAgentSettings, WorkflowIssue } from "rafi-spec";
|
|
2
|
+
export declare const WORKFLOW_DB_FILE = ".rafi/recovery.sqlite3";
|
|
3
|
+
export type WorkflowKind = "plan" | "ticket-plan" | "ticket-populate" | "uninstall" | "build" | "qa-remediation" | "recovery" | "legacy";
|
|
4
|
+
export type WorkflowRunStatus = "running" | "paused" | "blocked" | "completed" | "failed" | "cancelled" | "superseded";
|
|
5
|
+
export interface WorkflowRunSnapshot {
|
|
6
|
+
runId: string;
|
|
7
|
+
kind: WorkflowKind;
|
|
8
|
+
status: WorkflowRunStatus;
|
|
9
|
+
checkpoint: string;
|
|
10
|
+
originalWork: unknown;
|
|
11
|
+
remainingWork: unknown;
|
|
12
|
+
state: Record<string, unknown>;
|
|
13
|
+
leaseGeneration?: number;
|
|
14
|
+
legacy: boolean;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export interface OperationRecord {
|
|
19
|
+
idempotencyKey: string;
|
|
20
|
+
runId: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
status: OperationLifecycle;
|
|
23
|
+
intent: unknown;
|
|
24
|
+
result?: unknown;
|
|
25
|
+
externalId?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ProjectLease {
|
|
31
|
+
owner: string;
|
|
32
|
+
generation: number;
|
|
33
|
+
pid: number;
|
|
34
|
+
host: string;
|
|
35
|
+
processStart: string;
|
|
36
|
+
heartbeatAt: string;
|
|
37
|
+
runId: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PublicationTransaction {
|
|
40
|
+
transactionId: string;
|
|
41
|
+
runId: string;
|
|
42
|
+
status: "prepared" | "staged" | "tracker_committed" | "published" | "committed" | "rolled_back";
|
|
43
|
+
intent: unknown;
|
|
44
|
+
previousDigests: unknown;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
updatedAt: string;
|
|
47
|
+
}
|
|
48
|
+
/** Authoritative persistence for every resumable workflow in a project. */
|
|
49
|
+
export declare class WorkflowDb {
|
|
50
|
+
readonly projectDir: string;
|
|
51
|
+
readonly path: string;
|
|
52
|
+
private readonly db;
|
|
53
|
+
constructor(projectDir: string, path?: string);
|
|
54
|
+
close(): void;
|
|
55
|
+
createRun(input: {
|
|
56
|
+
runId?: string;
|
|
57
|
+
kind: WorkflowKind;
|
|
58
|
+
checkpoint?: string;
|
|
59
|
+
originalWork?: unknown;
|
|
60
|
+
remainingWork?: unknown;
|
|
61
|
+
state?: Record<string, unknown>;
|
|
62
|
+
legacy?: boolean;
|
|
63
|
+
}, now?: Date): WorkflowRunSnapshot;
|
|
64
|
+
getRun(runId: string): WorkflowRunSnapshot | undefined;
|
|
65
|
+
activeRuns(): WorkflowRunSnapshot[];
|
|
66
|
+
transition(runId: string, update: {
|
|
67
|
+
status?: WorkflowRunStatus;
|
|
68
|
+
checkpoint: string;
|
|
69
|
+
remainingWork?: unknown;
|
|
70
|
+
state?: Record<string, unknown>;
|
|
71
|
+
event?: string;
|
|
72
|
+
payload?: unknown;
|
|
73
|
+
}, now?: Date): WorkflowRunSnapshot;
|
|
74
|
+
events(runId: string): Array<{
|
|
75
|
+
sequence: number;
|
|
76
|
+
type: string;
|
|
77
|
+
checkpoint: string;
|
|
78
|
+
payload: unknown;
|
|
79
|
+
at: string;
|
|
80
|
+
}>;
|
|
81
|
+
recordSettings(runId: string, role: string, boundary: number, settings: ResolvedAgentSettings, now?: Date): void;
|
|
82
|
+
recordProjectSettingsRevision(revision: number, defaults: unknown, now?: Date): void;
|
|
83
|
+
recordTelemetry(runId: string, snapshot: unknown, now?: Date): void;
|
|
84
|
+
recordSession(runId: string, role: string, stream: string, sessionId: string | undefined, transition: string, settings: ResolvedAgentSettings, now?: Date): void;
|
|
85
|
+
recordIssue(runId: string, issue: WorkflowIssue): number;
|
|
86
|
+
issues(runId: string): WorkflowIssue[];
|
|
87
|
+
planOperation(input: {
|
|
88
|
+
runId: string;
|
|
89
|
+
idempotencyKey: string;
|
|
90
|
+
kind: string;
|
|
91
|
+
intent: unknown;
|
|
92
|
+
}, now?: Date): OperationRecord;
|
|
93
|
+
updateOperation(idempotencyKey: string, status: OperationLifecycle, details?: {
|
|
94
|
+
result?: unknown;
|
|
95
|
+
externalId?: string;
|
|
96
|
+
error?: string;
|
|
97
|
+
}, now?: Date): OperationRecord;
|
|
98
|
+
operation(idempotencyKey: string): OperationRecord | undefined;
|
|
99
|
+
operations(runId: string): OperationRecord[];
|
|
100
|
+
putEvidence(kind: "handoff" | "diff" | "test", value: string | Buffer, now?: Date): string;
|
|
101
|
+
getEvidence(digest: string): Buffer | undefined;
|
|
102
|
+
beginPublication(runId: string, intent: unknown, previousDigests: unknown, now?: Date): PublicationTransaction;
|
|
103
|
+
updatePublication(transactionId: string, status: PublicationTransaction["status"], now?: Date): PublicationTransaction;
|
|
104
|
+
publication(transactionId: string): PublicationTransaction | undefined;
|
|
105
|
+
incompletePublications(): PublicationTransaction[];
|
|
106
|
+
acquireLease(runId: string, owner?: string, now?: Date, staleMs?: number): ProjectLease;
|
|
107
|
+
heartbeatLease(lease: ProjectLease, now?: Date): ProjectLease;
|
|
108
|
+
releaseLease(lease: ProjectLease, now?: Date): void;
|
|
109
|
+
currentLease(): ProjectLease | undefined;
|
|
110
|
+
importLegacyOnce(now?: Date): number;
|
|
111
|
+
private insertEvent;
|
|
112
|
+
private migrate;
|
|
113
|
+
}
|