ai-foreman 1.3.1 → 1.5.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/README.md +8 -0
- package/dist/adapters/codex.js +1 -0
- package/dist/agentRun.d.ts +1 -0
- package/dist/agentRun.js +5 -1
- package/dist/branch/git.d.ts +2 -0
- package/dist/branch/git.js +18 -1
- package/dist/branch/github.d.ts +3 -1
- package/dist/branch/github.js +32 -0
- package/dist/branch/gitlab.d.ts +15 -0
- package/dist/branch/gitlab.js +290 -0
- package/dist/branch/planner.d.ts +1 -0
- package/dist/branch/planner.js +3 -1
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +7 -1
- package/dist/branch/runner.d.ts +6 -1
- package/dist/branch/runner.js +155 -20
- package/dist/branch/types.d.ts +10 -2
- package/dist/cli/start.js +135 -7
- package/dist/cli/tickets.d.ts +11 -0
- package/dist/cli/tickets.js +767 -86
- package/dist/interviews.d.ts +68 -0
- package/dist/interviews.js +184 -0
- package/dist/log.d.ts +1 -1
- package/dist/runtimeAuth.js +1 -1
- package/dist/tickets/commands.d.ts +2 -0
- package/dist/tickets/commands.js +27 -0
- package/dist/tickets/importer.d.ts +55 -0
- package/dist/tickets/importer.js +480 -0
- package/dist/tickets/recommendations.d.ts +32 -0
- package/dist/tickets/recommendations.js +183 -0
- package/dist/tickets/renderMarkdown.js +23 -0
- package/dist/tickets/setupConfig.d.ts +83 -0
- package/dist/tickets/setupConfig.js +381 -0
- package/dist/tickets/stateDb.d.ts +17 -0
- package/dist/tickets/stateDb.js +46 -0
- package/dist/tickets/ticketSchema.d.ts +55 -0
- package/dist/tickets/ticketSchema.js +14 -0
- package/package.json +6 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const INTERVIEW_STATE_VERSION = 1;
|
|
2
|
+
export declare const INTERVIEW_DIRECTORY = ".rafi/interviews";
|
|
3
|
+
export type InterviewWorkflow = "create" | "plan" | "tickets-setup-init" | "tickets-setup-update";
|
|
4
|
+
export type InterviewStatus = "in_progress" | "needs_review" | "completed" | "incompatible";
|
|
5
|
+
export interface InterviewRuntimeState {
|
|
6
|
+
runtime?: "claude" | "codex";
|
|
7
|
+
model?: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
/** True when a session id could not be resumed and a fresh session is needed. */
|
|
10
|
+
continuityLost?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface InterviewOutputFingerprint {
|
|
13
|
+
path: string;
|
|
14
|
+
sha256: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface InterviewFailure {
|
|
17
|
+
checkpoint: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
at: string;
|
|
20
|
+
}
|
|
21
|
+
export interface InterviewRecord {
|
|
22
|
+
version: number;
|
|
23
|
+
id: string;
|
|
24
|
+
workflow: InterviewWorkflow;
|
|
25
|
+
status: InterviewStatus;
|
|
26
|
+
/** Arguments/context required to re-enter the runner. No environment secrets. */
|
|
27
|
+
invocation: Record<string, unknown>;
|
|
28
|
+
answers: Record<string, unknown>;
|
|
29
|
+
checkpoint: string;
|
|
30
|
+
runtime: InterviewRuntimeState;
|
|
31
|
+
outputs: InterviewOutputFingerprint[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
completedAt?: string;
|
|
35
|
+
failure?: InterviewFailure;
|
|
36
|
+
}
|
|
37
|
+
export interface InterviewRecordProblem {
|
|
38
|
+
path: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
}
|
|
41
|
+
export interface InterviewReadResult {
|
|
42
|
+
records: InterviewRecord[];
|
|
43
|
+
problems: InterviewRecordProblem[];
|
|
44
|
+
}
|
|
45
|
+
export declare function interviewDirectory(projectDir: string): string;
|
|
46
|
+
export declare function createInterviewRecord(input: {
|
|
47
|
+
workflow: InterviewWorkflow;
|
|
48
|
+
invocation: Record<string, unknown>;
|
|
49
|
+
answers?: Record<string, unknown>;
|
|
50
|
+
checkpoint: string;
|
|
51
|
+
outputs?: string[];
|
|
52
|
+
now?: Date;
|
|
53
|
+
}): InterviewRecord;
|
|
54
|
+
/** Ensure ignored local state exists without modifying unrelated gitignore text. */
|
|
55
|
+
export declare function ensureInterviewGitignore(projectDir: string): void;
|
|
56
|
+
export declare function saveInterviewRecord(projectDir: string, record: InterviewRecord, now?: Date): InterviewRecord;
|
|
57
|
+
export declare function checkpointInterview(projectDir: string, record: InterviewRecord, update: Partial<Pick<InterviewRecord, "answers" | "checkpoint" | "runtime" | "outputs" | "invocation" | "status">>): InterviewRecord;
|
|
58
|
+
export declare function failInterview(projectDir: string, record: InterviewRecord, checkpoint: string, error: unknown): InterviewRecord;
|
|
59
|
+
export declare function completeInterview(projectDir: string, record: InterviewRecord, now?: Date): InterviewRecord;
|
|
60
|
+
export declare function readInterviewRecords(projectDir: string): InterviewReadResult;
|
|
61
|
+
export declare function unfinishedInterviews(projectDir: string): InterviewRecord[];
|
|
62
|
+
export declare function findInterviewRecord(projectDir: string, id: string): InterviewRecord | undefined;
|
|
63
|
+
export declare function discardInterview(projectDir: string, id: string): boolean;
|
|
64
|
+
/** Remove only completed records past retention; malformed/incompatible files stay for explicit discard. */
|
|
65
|
+
export declare function pruneCompletedInterviews(projectDir: string, now?: Date): string[];
|
|
66
|
+
export declare function fingerprintOutputs(projectDir: string, paths: string[]): InterviewOutputFingerprint[];
|
|
67
|
+
export declare function outputsChanged(projectDir: string, fingerprints: InterviewOutputFingerprint[]): string[];
|
|
68
|
+
export declare function redactFailure(error: unknown): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, local state for commands that ask a human questions. This lives in
|
|
3
|
+
* ai-foreman (rather than a CLI package) because `rafi tickets` is implemented
|
|
4
|
+
* there too. Records deliberately contain decisions and small diagnostics,
|
|
5
|
+
* never a transcript or command output.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
9
|
+
import { join, relative, resolve } from "node:path";
|
|
10
|
+
export const INTERVIEW_STATE_VERSION = 1;
|
|
11
|
+
export const INTERVIEW_DIRECTORY = ".rafi/interviews";
|
|
12
|
+
const MAX_FAILURE_LENGTH = 1_000;
|
|
13
|
+
const COMPLETE_RETENTION_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
14
|
+
export function interviewDirectory(projectDir) {
|
|
15
|
+
return join(resolve(projectDir), INTERVIEW_DIRECTORY);
|
|
16
|
+
}
|
|
17
|
+
export function createInterviewRecord(input) {
|
|
18
|
+
const stamp = (input.now ?? new Date()).toISOString();
|
|
19
|
+
return {
|
|
20
|
+
version: INTERVIEW_STATE_VERSION,
|
|
21
|
+
id: randomUUID(),
|
|
22
|
+
workflow: input.workflow,
|
|
23
|
+
status: "in_progress",
|
|
24
|
+
invocation: { ...input.invocation },
|
|
25
|
+
answers: { ...(input.answers ?? {}) },
|
|
26
|
+
checkpoint: input.checkpoint,
|
|
27
|
+
runtime: {},
|
|
28
|
+
outputs: fingerprintOutputs(String(input.invocation.projectDir ?? ""), input.outputs ?? []),
|
|
29
|
+
createdAt: stamp,
|
|
30
|
+
updatedAt: stamp,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Ensure ignored local state exists without modifying unrelated gitignore text. */
|
|
34
|
+
export function ensureInterviewGitignore(projectDir) {
|
|
35
|
+
const root = resolve(projectDir);
|
|
36
|
+
const path = join(root, ".gitignore");
|
|
37
|
+
const line = ".rafi/interviews/";
|
|
38
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
39
|
+
if (current.split(/\r?\n/).some((entry) => entry.trim() === line))
|
|
40
|
+
return;
|
|
41
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
42
|
+
writeFileSync(path, `${current}${prefix}${line}\n`, "utf8");
|
|
43
|
+
}
|
|
44
|
+
export function saveInterviewRecord(projectDir, record, now = new Date()) {
|
|
45
|
+
validateRecord(record);
|
|
46
|
+
const directory = interviewDirectory(projectDir);
|
|
47
|
+
mkdirSync(directory, { recursive: true });
|
|
48
|
+
ensureInterviewGitignore(projectDir);
|
|
49
|
+
const next = { ...record, updatedAt: now.toISOString() };
|
|
50
|
+
const path = interviewRecordPath(projectDir, next.id);
|
|
51
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
52
|
+
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
53
|
+
renameSync(temp, path);
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
export function checkpointInterview(projectDir, record, update) {
|
|
57
|
+
const next = {
|
|
58
|
+
...record,
|
|
59
|
+
...update,
|
|
60
|
+
answers: update.answers ? { ...update.answers } : record.answers,
|
|
61
|
+
runtime: update.runtime ? { ...update.runtime } : record.runtime,
|
|
62
|
+
invocation: update.invocation ? { ...update.invocation } : record.invocation,
|
|
63
|
+
outputs: update.outputs ? update.outputs.map((output) => ({ ...output })) : record.outputs,
|
|
64
|
+
};
|
|
65
|
+
return saveInterviewRecord(projectDir, next);
|
|
66
|
+
}
|
|
67
|
+
export function failInterview(projectDir, record, checkpoint, error) {
|
|
68
|
+
const at = new Date().toISOString();
|
|
69
|
+
return saveInterviewRecord(projectDir, {
|
|
70
|
+
...record,
|
|
71
|
+
status: record.status === "needs_review" ? "needs_review" : "in_progress",
|
|
72
|
+
checkpoint,
|
|
73
|
+
failure: { checkpoint, at, summary: redactFailure(error) },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function completeInterview(projectDir, record, now = new Date()) {
|
|
77
|
+
const stamp = now.toISOString();
|
|
78
|
+
return saveInterviewRecord(projectDir, {
|
|
79
|
+
...record,
|
|
80
|
+
status: "completed",
|
|
81
|
+
completedAt: stamp,
|
|
82
|
+
failure: undefined,
|
|
83
|
+
}, now);
|
|
84
|
+
}
|
|
85
|
+
export function readInterviewRecords(projectDir) {
|
|
86
|
+
const directory = interviewDirectory(projectDir);
|
|
87
|
+
if (!existsSync(directory))
|
|
88
|
+
return { records: [], problems: [] };
|
|
89
|
+
const records = [];
|
|
90
|
+
const problems = [];
|
|
91
|
+
for (const name of readFileNames(directory)) {
|
|
92
|
+
const path = join(directory, name);
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
95
|
+
const record = parseInterviewRecord(parsed);
|
|
96
|
+
records.push(record);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
problems.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
103
|
+
return { records, problems };
|
|
104
|
+
}
|
|
105
|
+
export function unfinishedInterviews(projectDir) {
|
|
106
|
+
return readInterviewRecords(projectDir).records.filter((record) => record.status === "in_progress" || record.status === "needs_review" || record.status === "incompatible");
|
|
107
|
+
}
|
|
108
|
+
export function findInterviewRecord(projectDir, id) {
|
|
109
|
+
return readInterviewRecords(projectDir).records.find((record) => record.id === id || record.id.startsWith(id));
|
|
110
|
+
}
|
|
111
|
+
export function discardInterview(projectDir, id) {
|
|
112
|
+
const record = findInterviewRecord(projectDir, id);
|
|
113
|
+
if (!record)
|
|
114
|
+
return false;
|
|
115
|
+
rmSync(interviewRecordPath(projectDir, record.id));
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/** Remove only completed records past retention; malformed/incompatible files stay for explicit discard. */
|
|
119
|
+
export function pruneCompletedInterviews(projectDir, now = new Date()) {
|
|
120
|
+
const removed = [];
|
|
121
|
+
for (const record of readInterviewRecords(projectDir).records) {
|
|
122
|
+
if (record.status !== "completed" || !record.completedAt)
|
|
123
|
+
continue;
|
|
124
|
+
if (now.getTime() - new Date(record.completedAt).getTime() <= COMPLETE_RETENTION_MS)
|
|
125
|
+
continue;
|
|
126
|
+
rmSync(interviewRecordPath(projectDir, record.id));
|
|
127
|
+
removed.push(record.id);
|
|
128
|
+
}
|
|
129
|
+
return removed;
|
|
130
|
+
}
|
|
131
|
+
export function fingerprintOutputs(projectDir, paths) {
|
|
132
|
+
if (!projectDir)
|
|
133
|
+
return paths.map((path) => ({ path, sha256: null }));
|
|
134
|
+
return paths.map((path) => ({ path, sha256: fingerprintFile(projectDir, path) }));
|
|
135
|
+
}
|
|
136
|
+
export function outputsChanged(projectDir, fingerprints) {
|
|
137
|
+
return fingerprints
|
|
138
|
+
.filter((fingerprint) => fingerprintFile(projectDir, fingerprint.path) !== fingerprint.sha256)
|
|
139
|
+
.map((fingerprint) => fingerprint.path);
|
|
140
|
+
}
|
|
141
|
+
export function redactFailure(error) {
|
|
142
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
143
|
+
return raw
|
|
144
|
+
.replace(/((?:api[_-]?key|token|password|secret)\s*[=:])\s*[^\s,;]+/gi, "$1<redacted>")
|
|
145
|
+
.replace(/\b(?:sk|rk|ghp|github_pat)_[A-Za-z0-9_-]+\b/g, "<redacted>")
|
|
146
|
+
.replace(/\s+/g, " ")
|
|
147
|
+
.slice(0, MAX_FAILURE_LENGTH);
|
|
148
|
+
}
|
|
149
|
+
function interviewRecordPath(projectDir, id) {
|
|
150
|
+
return join(interviewDirectory(projectDir), `${id}.json`);
|
|
151
|
+
}
|
|
152
|
+
function readFileNames(directory) {
|
|
153
|
+
return readdirSync(directory).filter((name) => name.endsWith(".json"));
|
|
154
|
+
}
|
|
155
|
+
function fingerprintFile(projectDir, path) {
|
|
156
|
+
const root = resolve(projectDir);
|
|
157
|
+
const absolute = resolve(root, path);
|
|
158
|
+
if (relative(root, absolute).startsWith(".."))
|
|
159
|
+
return null;
|
|
160
|
+
if (!existsSync(absolute))
|
|
161
|
+
return null;
|
|
162
|
+
return createHash("sha256").update(readFileSync(absolute)).digest("hex");
|
|
163
|
+
}
|
|
164
|
+
function parseInterviewRecord(value) {
|
|
165
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
166
|
+
throw new Error("invalid interview record");
|
|
167
|
+
const record = value;
|
|
168
|
+
if (record.version !== INTERVIEW_STATE_VERSION) {
|
|
169
|
+
// Preserve it on disk. The caller can show it and require an explicit discard.
|
|
170
|
+
if (typeof record.id === "string" && typeof record.workflow === "string") {
|
|
171
|
+
return { ...record, status: "incompatible" };
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`unsupported interview record version: ${String(record.version)}`);
|
|
174
|
+
}
|
|
175
|
+
validateRecord(record);
|
|
176
|
+
return record;
|
|
177
|
+
}
|
|
178
|
+
function validateRecord(record) {
|
|
179
|
+
if (record.version !== INTERVIEW_STATE_VERSION)
|
|
180
|
+
throw new Error("unsupported interview record version");
|
|
181
|
+
if (!record.id || !record.workflow || !record.checkpoint || !record.createdAt || !record.updatedAt) {
|
|
182
|
+
throw new Error("invalid interview record");
|
|
183
|
+
}
|
|
184
|
+
}
|
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. */
|
package/dist/runtimeAuth.js
CHANGED
|
@@ -25,7 +25,7 @@ export function checkRuntimeReady(projectDir, runtime) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
else {
|
|
28
|
-
execFileSync("codex", ["exec", "-C", projectDir, "Return exactly OK"], {
|
|
28
|
+
execFileSync("codex", ["exec", "--skip-git-repo-check", "-C", projectDir, "Return exactly OK"], {
|
|
29
29
|
cwd: projectDir,
|
|
30
30
|
encoding: "utf8",
|
|
31
31
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -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;
|
|
@@ -92,3 +93,4 @@ export declare function cmdImplementationQueue(projectDir: string): import("./qu
|
|
|
92
93
|
export declare function cmdArchive(projectDir: string, _opts: {
|
|
93
94
|
olderThanDays?: number;
|
|
94
95
|
}): void;
|
|
96
|
+
export declare function cmdReview(projectDir: string, opts?: ReviewCommandOptions): ReviewCommandResult;
|
package/dist/tickets/commands.js
CHANGED
|
@@ -8,6 +8,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);
|
|
@@ -78,6 +79,14 @@ export function cmdInit(projectDir, opts) {
|
|
|
78
79
|
};
|
|
79
80
|
writeFileSync(join(ticketsDir, "config.yaml"), stringify(config, { lineWidth: 80 }), "utf8");
|
|
80
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");
|
|
81
90
|
writeFileSync(join(ticketsDir, "tracker-rules.md"), renderTrackerRules(progressDoc), "utf8");
|
|
82
91
|
const ticketSchema = {
|
|
83
92
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -100,6 +109,20 @@ export function cmdInit(projectDir, opts) {
|
|
|
100
109
|
likely_files: { type: "array", items: { type: "string" } },
|
|
101
110
|
rollback: { type: ["string", "null"] },
|
|
102
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
|
+
},
|
|
103
126
|
},
|
|
104
127
|
};
|
|
105
128
|
writeFileSync(join(ticketsDir, "schema", "tickets.schema.json"), JSON.stringify(ticketSchema, null, 2), "utf8");
|
|
@@ -453,3 +476,7 @@ export function cmdArchive(projectDir, _opts) {
|
|
|
453
476
|
renderAfterUpdate(ctx);
|
|
454
477
|
});
|
|
455
478
|
}
|
|
479
|
+
// ── review recommendations ──────────────────────────────────────────────────
|
|
480
|
+
export function cmdReview(projectDir, opts = {}) {
|
|
481
|
+
return cmdReviewRecommendations(projectDir, opts);
|
|
482
|
+
}
|
|
@@ -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
|
+
};
|