ai-foreman 1.4.0 → 1.6.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/adapters/codex.js +1 -0
- package/dist/agentRun.d.ts +4 -2
- package/dist/agentRun.js +32 -9
- package/dist/branch/git.d.ts +3 -0
- package/dist/branch/git.js +31 -5
- package/dist/branch/github.d.ts +2 -2
- package/dist/branch/github.js +2 -2
- package/dist/branch/gitlab.d.ts +3 -2
- package/dist/branch/gitlab.js +10 -4
- package/dist/branch/planner.d.ts +3 -1
- package/dist/branch/planner.js +24 -2
- package/dist/branch/runner.d.ts +10 -1
- package/dist/branch/runner.js +41 -13
- package/dist/branch/types.d.ts +4 -0
- package/dist/buildRuns.d.ts +35 -0
- package/dist/buildRuns.js +149 -0
- package/dist/cli/runtimeAuthPrompt.d.ts +1 -1
- package/dist/cli/runtimeAuthPrompt.js +2 -2
- package/dist/cli/start.js +153 -16
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +154 -57
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +46 -4
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +90 -0
- package/dist/interviews.js +251 -0
- package/dist/log.d.ts +1 -1
- package/dist/roles.d.ts +11 -0
- package/dist/roles.js +8 -0
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +6 -25
- package/dist/runtimeReadiness.d.ts +14 -0
- package/dist/runtimeReadiness.js +122 -0
- package/dist/ticketPlanning.d.ts +83 -0
- package/dist/ticketPlanning.js +287 -0
- package/dist/tickets/commands.js +16 -0
- package/dist/tickets/delivery.d.ts +38 -0
- package/dist/tickets/delivery.js +152 -0
- package/dist/tickets/importer.js +4 -0
- package/dist/tickets/renderMarkdown.js +9 -0
- package/dist/tickets/setupConfig.d.ts +14 -0
- package/dist/tickets/setupConfig.js +59 -2
- package/dist/tickets/sourceFetch.d.ts +24 -0
- package/dist/tickets/sourceFetch.js +246 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +24 -0
- package/dist/tickets/ticketLoader.js +18 -0
- package/dist/tickets/ticketSchema.d.ts +93 -0
- package/dist/tickets/ticketSchema.js +17 -0
- package/dist/tickets/validate.js +12 -0
- package/package.json +33 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export declare const INTERVIEW_STATE_VERSION = 2;
|
|
2
|
+
export declare const INTERVIEW_DIRECTORY = ".rafi/interviews";
|
|
3
|
+
export type InterviewWorkflow = "create" | "plan" | "tickets-plan" | "tickets-setup-init" | "tickets-setup-update";
|
|
4
|
+
export type InterviewStatus = "in_progress" | "paused" | "needs_review" | "completed" | "incompatible";
|
|
5
|
+
export type InterviewStageStatus = "not_offered" | "offered" | "accepted" | "running" | "completed" | "skipped" | "paused" | "failed" | "cancelled";
|
|
6
|
+
export interface InterviewRuntimeState {
|
|
7
|
+
runtime?: "claude" | "codex";
|
|
8
|
+
model?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
/** True when a session id could not be resumed and a fresh session is needed. */
|
|
11
|
+
continuityLost?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface InterviewOutputFingerprint {
|
|
14
|
+
path: string;
|
|
15
|
+
sha256: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface InterviewFailure {
|
|
18
|
+
checkpoint: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
at: string;
|
|
21
|
+
}
|
|
22
|
+
export interface InterviewRecord {
|
|
23
|
+
version: number;
|
|
24
|
+
id: string;
|
|
25
|
+
journeyId: string;
|
|
26
|
+
parentId?: string;
|
|
27
|
+
childIds: string[];
|
|
28
|
+
workflow: InterviewWorkflow;
|
|
29
|
+
status: InterviewStatus;
|
|
30
|
+
/** Arguments/context required to re-enter the runner. No environment secrets. */
|
|
31
|
+
invocation: Record<string, unknown>;
|
|
32
|
+
answers: Record<string, unknown>;
|
|
33
|
+
/** V2 name for compact persisted answers; `answers` remains as a compatibility alias. */
|
|
34
|
+
decisions: Record<string, unknown>;
|
|
35
|
+
checkpoint: string;
|
|
36
|
+
stages: Record<string, {
|
|
37
|
+
status: InterviewStageStatus;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
}>;
|
|
40
|
+
planningMode?: "standard" | "exhaustive";
|
|
41
|
+
runtime: InterviewRuntimeState;
|
|
42
|
+
runtimeAttempts: Array<{
|
|
43
|
+
runtime: "claude" | "codex";
|
|
44
|
+
at: string;
|
|
45
|
+
outcome: "ready" | "failed" | "cancelled";
|
|
46
|
+
category?: string;
|
|
47
|
+
}>;
|
|
48
|
+
sessionIds: Record<string, string>;
|
|
49
|
+
continuityLost: boolean;
|
|
50
|
+
outputs: InterviewOutputFingerprint[];
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
completedAt?: string;
|
|
54
|
+
failure?: InterviewFailure;
|
|
55
|
+
}
|
|
56
|
+
export interface InterviewRecordProblem {
|
|
57
|
+
path: string;
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
export interface InterviewReadResult {
|
|
61
|
+
records: InterviewRecord[];
|
|
62
|
+
problems: InterviewRecordProblem[];
|
|
63
|
+
}
|
|
64
|
+
export declare function interviewDirectory(projectDir: string): string;
|
|
65
|
+
export declare function createInterviewRecord(input: {
|
|
66
|
+
workflow: InterviewWorkflow;
|
|
67
|
+
invocation: Record<string, unknown>;
|
|
68
|
+
answers?: Record<string, unknown>;
|
|
69
|
+
checkpoint: string;
|
|
70
|
+
outputs?: string[];
|
|
71
|
+
now?: Date;
|
|
72
|
+
parentId?: string;
|
|
73
|
+
journeyId?: string;
|
|
74
|
+
planningMode?: "standard" | "exhaustive";
|
|
75
|
+
}): InterviewRecord;
|
|
76
|
+
/** Ensure ignored local state exists without modifying unrelated gitignore text. */
|
|
77
|
+
export declare function ensureInterviewGitignore(projectDir: string): void;
|
|
78
|
+
export declare function saveInterviewRecord(projectDir: string, record: InterviewRecord, now?: Date): InterviewRecord;
|
|
79
|
+
export declare function checkpointInterview(projectDir: string, record: InterviewRecord, update: Partial<Pick<InterviewRecord, "answers" | "decisions" | "checkpoint" | "runtime" | "outputs" | "invocation" | "status" | "stages" | "planningMode" | "runtimeAttempts" | "sessionIds" | "continuityLost" | "childIds">>): InterviewRecord;
|
|
80
|
+
export declare function failInterview(projectDir: string, record: InterviewRecord, checkpoint: string, error: unknown): InterviewRecord;
|
|
81
|
+
export declare function completeInterview(projectDir: string, record: InterviewRecord, now?: Date): InterviewRecord;
|
|
82
|
+
export declare function readInterviewRecords(projectDir: string): InterviewReadResult;
|
|
83
|
+
export declare function unfinishedInterviews(projectDir: string): InterviewRecord[];
|
|
84
|
+
export declare function findInterviewRecord(projectDir: string, id: string): InterviewRecord | undefined;
|
|
85
|
+
export declare function discardInterview(projectDir: string, id: string): boolean;
|
|
86
|
+
/** Remove only completed records past retention; malformed/incompatible files stay for explicit discard. */
|
|
87
|
+
export declare function pruneCompletedInterviews(projectDir: string, now?: Date): string[];
|
|
88
|
+
export declare function fingerprintOutputs(projectDir: string, paths: string[]): InterviewOutputFingerprint[];
|
|
89
|
+
export declare function outputsChanged(projectDir: string, fingerprints: InterviewOutputFingerprint[]): string[];
|
|
90
|
+
export declare function redactFailure(error: unknown): string;
|
|
@@ -0,0 +1,251 @@
|
|
|
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 = 2;
|
|
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
|
+
journeyId: input.journeyId ?? randomUUID(),
|
|
23
|
+
parentId: input.parentId,
|
|
24
|
+
childIds: [],
|
|
25
|
+
workflow: input.workflow,
|
|
26
|
+
status: "in_progress",
|
|
27
|
+
invocation: { ...input.invocation },
|
|
28
|
+
answers: { ...(input.answers ?? {}) },
|
|
29
|
+
decisions: { ...(input.answers ?? {}) },
|
|
30
|
+
checkpoint: input.checkpoint,
|
|
31
|
+
stages: { [input.checkpoint]: { status: "running", updatedAt: stamp } },
|
|
32
|
+
planningMode: input.planningMode,
|
|
33
|
+
runtime: {},
|
|
34
|
+
runtimeAttempts: [],
|
|
35
|
+
sessionIds: {},
|
|
36
|
+
continuityLost: false,
|
|
37
|
+
outputs: fingerprintOutputs(String(input.invocation.projectDir ?? ""), input.outputs ?? []),
|
|
38
|
+
createdAt: stamp,
|
|
39
|
+
updatedAt: stamp,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Ensure ignored local state exists without modifying unrelated gitignore text. */
|
|
43
|
+
export function ensureInterviewGitignore(projectDir) {
|
|
44
|
+
const root = resolve(projectDir);
|
|
45
|
+
const path = join(root, ".gitignore");
|
|
46
|
+
const line = ".rafi/interviews/";
|
|
47
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
48
|
+
if (current.split(/\r?\n/).some((entry) => entry.trim() === line))
|
|
49
|
+
return;
|
|
50
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
51
|
+
writeFileSync(path, `${current}${prefix}${line}\n`, "utf8");
|
|
52
|
+
}
|
|
53
|
+
export function saveInterviewRecord(projectDir, record, now = new Date()) {
|
|
54
|
+
validateRecord(record);
|
|
55
|
+
const directory = interviewDirectory(projectDir);
|
|
56
|
+
mkdirSync(directory, { recursive: true });
|
|
57
|
+
ensureInterviewGitignore(projectDir);
|
|
58
|
+
const next = { ...record, updatedAt: now.toISOString() };
|
|
59
|
+
const path = interviewRecordPath(projectDir, next.id);
|
|
60
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
61
|
+
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
62
|
+
renameSync(temp, path);
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
export function checkpointInterview(projectDir, record, update) {
|
|
66
|
+
const nextAnswers = update.answers ?? update.decisions ?? record.answers;
|
|
67
|
+
const stamp = new Date().toISOString();
|
|
68
|
+
const stages = update.stages ?? (update.checkpoint && update.checkpoint !== record.checkpoint
|
|
69
|
+
? {
|
|
70
|
+
...record.stages,
|
|
71
|
+
[record.checkpoint]: { status: stageCompletionStatus(record.status), updatedAt: stamp },
|
|
72
|
+
[update.checkpoint]: { status: "running", updatedAt: stamp },
|
|
73
|
+
}
|
|
74
|
+
: record.stages);
|
|
75
|
+
const next = {
|
|
76
|
+
...record,
|
|
77
|
+
...update,
|
|
78
|
+
answers: { ...nextAnswers },
|
|
79
|
+
decisions: { ...nextAnswers },
|
|
80
|
+
stages,
|
|
81
|
+
runtime: update.runtime ? { ...update.runtime } : record.runtime,
|
|
82
|
+
sessionIds: update.runtime?.sessionId
|
|
83
|
+
? { ...record.sessionIds, [update.runtime.runtime ?? "agent"]: update.runtime.sessionId }
|
|
84
|
+
: update.sessionIds ? { ...update.sessionIds } : record.sessionIds,
|
|
85
|
+
continuityLost: update.runtime?.continuityLost ?? update.continuityLost ?? record.continuityLost,
|
|
86
|
+
invocation: update.invocation ? { ...update.invocation } : record.invocation,
|
|
87
|
+
outputs: update.outputs ? update.outputs.map((output) => ({ ...output })) : record.outputs,
|
|
88
|
+
};
|
|
89
|
+
return saveInterviewRecord(projectDir, next);
|
|
90
|
+
}
|
|
91
|
+
export function failInterview(projectDir, record, checkpoint, error) {
|
|
92
|
+
const at = new Date().toISOString();
|
|
93
|
+
return saveInterviewRecord(projectDir, {
|
|
94
|
+
...record,
|
|
95
|
+
status: record.status === "needs_review" ? "needs_review" : "in_progress",
|
|
96
|
+
checkpoint,
|
|
97
|
+
failure: { checkpoint, at, summary: redactFailure(error) },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function completeInterview(projectDir, record, now = new Date()) {
|
|
101
|
+
const stamp = now.toISOString();
|
|
102
|
+
return saveInterviewRecord(projectDir, {
|
|
103
|
+
...record,
|
|
104
|
+
status: "completed",
|
|
105
|
+
completedAt: stamp,
|
|
106
|
+
stages: { ...record.stages, [record.checkpoint]: { status: "completed", updatedAt: stamp } },
|
|
107
|
+
failure: undefined,
|
|
108
|
+
}, now);
|
|
109
|
+
}
|
|
110
|
+
export function readInterviewRecords(projectDir) {
|
|
111
|
+
const directory = interviewDirectory(projectDir);
|
|
112
|
+
if (!existsSync(directory))
|
|
113
|
+
return { records: [], problems: [] };
|
|
114
|
+
const records = [];
|
|
115
|
+
const problems = [];
|
|
116
|
+
for (const name of readFileNames(directory)) {
|
|
117
|
+
const path = join(directory, name);
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
const record = parseInterviewRecord(parsed);
|
|
121
|
+
records.push(record);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
problems.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
128
|
+
return { records, problems };
|
|
129
|
+
}
|
|
130
|
+
export function unfinishedInterviews(projectDir) {
|
|
131
|
+
return readInterviewRecords(projectDir).records.filter((record) => record.status === "in_progress" || record.status === "paused" || record.status === "needs_review" || record.status === "incompatible");
|
|
132
|
+
}
|
|
133
|
+
export function findInterviewRecord(projectDir, id) {
|
|
134
|
+
return readInterviewRecords(projectDir).records.find((record) => record.id === id || record.id.startsWith(id));
|
|
135
|
+
}
|
|
136
|
+
export function discardInterview(projectDir, id) {
|
|
137
|
+
const record = findInterviewRecord(projectDir, id);
|
|
138
|
+
if (!record)
|
|
139
|
+
return false;
|
|
140
|
+
rmSync(interviewRecordPath(projectDir, record.id));
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/** Remove only completed records past retention; malformed/incompatible files stay for explicit discard. */
|
|
144
|
+
export function pruneCompletedInterviews(projectDir, now = new Date()) {
|
|
145
|
+
const removed = [];
|
|
146
|
+
for (const record of readInterviewRecords(projectDir).records) {
|
|
147
|
+
if (record.status !== "completed" || !record.completedAt)
|
|
148
|
+
continue;
|
|
149
|
+
if (now.getTime() - new Date(record.completedAt).getTime() <= COMPLETE_RETENTION_MS)
|
|
150
|
+
continue;
|
|
151
|
+
rmSync(interviewRecordPath(projectDir, record.id));
|
|
152
|
+
removed.push(record.id);
|
|
153
|
+
}
|
|
154
|
+
return removed;
|
|
155
|
+
}
|
|
156
|
+
export function fingerprintOutputs(projectDir, paths) {
|
|
157
|
+
if (!projectDir)
|
|
158
|
+
return paths.map((path) => ({ path, sha256: null }));
|
|
159
|
+
return paths.map((path) => ({ path, sha256: fingerprintFile(projectDir, path) }));
|
|
160
|
+
}
|
|
161
|
+
export function outputsChanged(projectDir, fingerprints) {
|
|
162
|
+
return fingerprints
|
|
163
|
+
.filter((fingerprint) => fingerprintFile(projectDir, fingerprint.path) !== fingerprint.sha256)
|
|
164
|
+
.map((fingerprint) => fingerprint.path);
|
|
165
|
+
}
|
|
166
|
+
export function redactFailure(error) {
|
|
167
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
168
|
+
return raw
|
|
169
|
+
.replace(/((?:api[_-]?key|token|password|secret)\s*[=:])\s*[^\s,;]+/gi, "$1<redacted>")
|
|
170
|
+
.replace(/\b(?:sk|rk|ghp|github_pat)_[A-Za-z0-9_-]+\b/g, "<redacted>")
|
|
171
|
+
.replace(/\s+/g, " ")
|
|
172
|
+
.slice(0, MAX_FAILURE_LENGTH);
|
|
173
|
+
}
|
|
174
|
+
function interviewRecordPath(projectDir, id) {
|
|
175
|
+
return join(interviewDirectory(projectDir), `${id}.json`);
|
|
176
|
+
}
|
|
177
|
+
function readFileNames(directory) {
|
|
178
|
+
return readdirSync(directory).filter((name) => name.endsWith(".json"));
|
|
179
|
+
}
|
|
180
|
+
function fingerprintFile(projectDir, path) {
|
|
181
|
+
const root = resolve(projectDir);
|
|
182
|
+
const absolute = resolve(root, path);
|
|
183
|
+
if (relative(root, absolute).startsWith(".."))
|
|
184
|
+
return null;
|
|
185
|
+
if (!existsSync(absolute))
|
|
186
|
+
return null;
|
|
187
|
+
return createHash("sha256").update(readFileSync(absolute)).digest("hex");
|
|
188
|
+
}
|
|
189
|
+
function parseInterviewRecord(value) {
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
191
|
+
throw new Error("invalid interview record");
|
|
192
|
+
const record = value;
|
|
193
|
+
if (record.version === 1)
|
|
194
|
+
return migrateV1Record(value);
|
|
195
|
+
if (record.version !== INTERVIEW_STATE_VERSION) {
|
|
196
|
+
// Preserve it on disk. The caller can show it and require an explicit discard.
|
|
197
|
+
if (typeof record.id === "string" && typeof record.workflow === "string") {
|
|
198
|
+
return { ...record, status: "incompatible" };
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`unsupported interview record version: ${String(record.version)}`);
|
|
201
|
+
}
|
|
202
|
+
validateRecord(record);
|
|
203
|
+
return record;
|
|
204
|
+
}
|
|
205
|
+
function migrateV1Record(value) {
|
|
206
|
+
const legacy = value;
|
|
207
|
+
if (!legacy.id || !legacy.workflow || !legacy.checkpoint || !legacy.createdAt || !legacy.updatedAt) {
|
|
208
|
+
throw new Error("invalid V1 interview record");
|
|
209
|
+
}
|
|
210
|
+
const outputs = Array.isArray(legacy.outputs) ? legacy.outputs : [];
|
|
211
|
+
const evidenceIsComplete = outputs.length > 0 && outputs.every((item) => item && typeof item.path === "string" && typeof item.sha256 === "string");
|
|
212
|
+
const answers = legacy.answers && typeof legacy.answers === "object" ? legacy.answers : {};
|
|
213
|
+
const runtime = legacy.runtime && typeof legacy.runtime === "object" ? legacy.runtime : {};
|
|
214
|
+
const stamp = legacy.updatedAt;
|
|
215
|
+
return {
|
|
216
|
+
version: 2,
|
|
217
|
+
id: legacy.id,
|
|
218
|
+
journeyId: legacy.id,
|
|
219
|
+
childIds: [],
|
|
220
|
+
workflow: legacy.workflow,
|
|
221
|
+
status: legacy.status === "completed" ? "completed" : evidenceIsComplete ? "paused" : "needs_review",
|
|
222
|
+
invocation: legacy.invocation && typeof legacy.invocation === "object" ? legacy.invocation : {},
|
|
223
|
+
answers,
|
|
224
|
+
decisions: { ...answers },
|
|
225
|
+
checkpoint: legacy.checkpoint,
|
|
226
|
+
stages: { [legacy.checkpoint]: { status: evidenceIsComplete ? "paused" : "failed", updatedAt: stamp } },
|
|
227
|
+
runtime,
|
|
228
|
+
runtimeAttempts: [],
|
|
229
|
+
sessionIds: runtime.sessionId ? { [runtime.runtime ?? "agent"]: runtime.sessionId } : {},
|
|
230
|
+
continuityLost: Boolean(runtime.continuityLost || !runtime.sessionId),
|
|
231
|
+
outputs,
|
|
232
|
+
createdAt: legacy.createdAt,
|
|
233
|
+
updatedAt: legacy.updatedAt,
|
|
234
|
+
completedAt: legacy.completedAt,
|
|
235
|
+
failure: legacy.failure,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function stageCompletionStatus(status) {
|
|
239
|
+
if (status === "paused" || status === "needs_review")
|
|
240
|
+
return "paused";
|
|
241
|
+
if (status === "completed")
|
|
242
|
+
return "completed";
|
|
243
|
+
return "completed";
|
|
244
|
+
}
|
|
245
|
+
function validateRecord(record) {
|
|
246
|
+
if (record.version !== INTERVIEW_STATE_VERSION)
|
|
247
|
+
throw new Error("unsupported interview record version");
|
|
248
|
+
if (!record.id || !record.journeyId || !record.workflow || !record.checkpoint || !record.createdAt || !record.updatedAt) {
|
|
249
|
+
throw new Error("invalid interview record");
|
|
250
|
+
}
|
|
251
|
+
}
|
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" | "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";
|
|
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" | "uninstall-proposal" | "qa" | "qa-fix" | "qa-protected-files-changed" | "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/roles.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import type { ConfigurableAgentRole } from "rafi-spec";
|
|
2
|
+
export interface AgentRoleRegistration {
|
|
3
|
+
id: ConfigurableAgentRole;
|
|
4
|
+
label: string;
|
|
5
|
+
consumers: string[];
|
|
6
|
+
configurable: boolean;
|
|
7
|
+
manifest: string;
|
|
8
|
+
commands: string[];
|
|
9
|
+
}
|
|
10
|
+
/** One registry drives compilation, settings UI, and independently launched roles. */
|
|
11
|
+
export declare const AGENT_ROLE_REGISTRY: readonly AgentRoleRegistration[];
|
|
1
12
|
export interface RoleBundle {
|
|
2
13
|
system: string;
|
|
3
14
|
skills: string[];
|
package/dist/roles.js
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { getAgent } from "special-agents";
|
|
14
|
+
/** One registry drives compilation, settings UI, and independently launched roles. */
|
|
15
|
+
export const AGENT_ROLE_REGISTRY = [
|
|
16
|
+
{ id: "planner", label: "Planner", consumers: ["initial planning", "ticket planning"], configurable: true, manifest: "planner", commands: ["rafi create", "rafi plan", "rafi tickets plan"] },
|
|
17
|
+
{ id: "builder", label: "Builder", consumers: ["implementation"], configurable: true, manifest: "builder", commands: ["rafi start", "rafi build:resume"] },
|
|
18
|
+
{ id: "qa", label: "QA (separate review session)", consumers: ["run-wide independent review"], configurable: true, manifest: "qa", commands: ["rafi start", "rafi build:resume"] },
|
|
19
|
+
{ id: "ticket-maker", label: "Ticket maker", consumers: ["ticket population"], configurable: true, manifest: "ticket-maker", commands: ["rafi tickets populate"] },
|
|
20
|
+
{ id: "uninstaller", label: "Uninstaller", consumers: ["non-empty uninstall instructions only"], configurable: true, manifest: "uninstaller", commands: ["rafi uninstall"] },
|
|
21
|
+
];
|
|
14
22
|
function libraryGetter(role) {
|
|
15
23
|
const a = getAgent(role);
|
|
16
24
|
return { system: a.system, skills: a.skills, model: a.model, effort: a.effort };
|
package/dist/runtimeAuth.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class RuntimeAuthError extends Error {
|
|
|
15
15
|
readonly authLikely: boolean;
|
|
16
16
|
constructor(opts: RuntimeAuthErrorOptions);
|
|
17
17
|
}
|
|
18
|
-
export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime): void
|
|
18
|
+
export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime): Promise<void>;
|
|
19
19
|
export declare function normalizeRuntimeErrorText(runtime: AgentRuntime, text: string, exitCode?: number | null, context?: string): string;
|
|
20
20
|
export declare function isRuntimeAuthFailure(output: string): boolean;
|
|
21
21
|
export declare function runtimeCommandLabel(runtime: AgentRuntime): string;
|
package/dist/runtimeAuth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatRuntimeProbeFailure, probeRuntime } from "./runtimeReadiness.js";
|
|
2
2
|
export class RuntimeAuthError extends Error {
|
|
3
3
|
runtime;
|
|
4
4
|
exitCode;
|
|
@@ -15,34 +15,15 @@ export class RuntimeAuthError extends Error {
|
|
|
15
15
|
this.authLikely = isRuntimeAuthFailure(`${this.stderr}\n${this.stdout}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export function checkRuntimeReady(projectDir, runtime) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
execFileSync("claude", ["-p", "Return exactly OK"], {
|
|
22
|
-
cwd: projectDir,
|
|
23
|
-
encoding: "utf8",
|
|
24
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
execFileSync("codex", ["exec", "-C", projectDir, "Return exactly OK"], {
|
|
29
|
-
cwd: projectDir,
|
|
30
|
-
encoding: "utf8",
|
|
31
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
const failure = err;
|
|
18
|
+
export async function checkRuntimeReady(projectDir, runtime) {
|
|
19
|
+
const result = await probeRuntime(projectDir, runtime);
|
|
20
|
+
if (!result.ok)
|
|
37
21
|
throw new RuntimeAuthError({
|
|
38
22
|
runtime,
|
|
39
23
|
context: "readiness check",
|
|
40
|
-
exitCode:
|
|
41
|
-
|
|
42
|
-
stderr: outputToString(failure.stderr),
|
|
43
|
-
cause: err,
|
|
24
|
+
exitCode: result.exitCode,
|
|
25
|
+
stderr: formatRuntimeProbeFailure(result),
|
|
44
26
|
});
|
|
45
|
-
}
|
|
46
27
|
}
|
|
47
28
|
export function normalizeRuntimeErrorText(runtime, text, exitCode, context = "builder turn") {
|
|
48
29
|
if (!isRuntimeAuthFailure(text))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RuntimeProbeCategory, RuntimeProbePhase, RuntimeProbeResult } from "rafi-spec";
|
|
2
|
+
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
3
|
+
export declare const RUNTIME_PROBE_TIMEOUT_MS = 30000;
|
|
4
|
+
export declare const RUNTIME_DIAGNOSTIC_LIMIT: number;
|
|
5
|
+
export interface ProbeRuntimeOptions {
|
|
6
|
+
phase?: RuntimeProbePhase;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
maxDiagnosticsBytes?: number;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}
|
|
11
|
+
export declare function probeRuntime(cwd: string, runtime: AgentRuntime, opts?: ProbeRuntimeOptions): Promise<RuntimeProbeResult>;
|
|
12
|
+
export declare function classifyRuntimeFailure(text: string, phase?: RuntimeProbePhase): RuntimeProbeCategory;
|
|
13
|
+
export declare function sanitizeDiagnostics(text: string, maxBytes?: number): string;
|
|
14
|
+
export declare function formatRuntimeProbeFailure(result: RuntimeProbeResult): string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const RUNTIME_PROBE_TIMEOUT_MS = 30_000;
|
|
3
|
+
export const RUNTIME_DIAGNOSTIC_LIMIT = 8 * 1024;
|
|
4
|
+
const RELEVANT_ENV = /^(ANTHROPIC|CLAUDE|CODEX|OPENAI|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i;
|
|
5
|
+
export async function probeRuntime(cwd, runtime, opts = {}) {
|
|
6
|
+
const executable = runtime === "claude" ? "claude" : "codex";
|
|
7
|
+
const args = runtime === "claude"
|
|
8
|
+
? ["-p", "Return exactly OK"]
|
|
9
|
+
: ["exec", "--skip-git-repo-check", "-C", cwd, "Return exactly OK"];
|
|
10
|
+
const phase = opts.phase ?? "readiness";
|
|
11
|
+
const env = opts.env ?? process.env;
|
|
12
|
+
const limit = opts.maxDiagnosticsBytes ?? RUNTIME_DIAGNOSTIC_LIMIT;
|
|
13
|
+
const timeoutMs = opts.timeoutMs ?? RUNTIME_PROBE_TIMEOUT_MS;
|
|
14
|
+
return new Promise((resolveResult) => {
|
|
15
|
+
let output = Buffer.alloc(0);
|
|
16
|
+
let timedOut = false;
|
|
17
|
+
let settled = false;
|
|
18
|
+
const spawnOpts = { cwd, env, stdio: ["ignore", "pipe", "pipe"] };
|
|
19
|
+
const child = spawn(executable, args, spawnOpts);
|
|
20
|
+
const append = (chunk) => {
|
|
21
|
+
if (output.length >= limit)
|
|
22
|
+
return;
|
|
23
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
+
output = Buffer.concat([output, bytes.subarray(0, Math.max(0, limit - output.length))]);
|
|
25
|
+
};
|
|
26
|
+
child.stdout?.on("data", append);
|
|
27
|
+
child.stderr?.on("data", append);
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
timedOut = true;
|
|
30
|
+
child.kill("SIGTERM");
|
|
31
|
+
setTimeout(() => child.kill("SIGKILL"), 1_000).unref();
|
|
32
|
+
}, timeoutMs);
|
|
33
|
+
timer.unref();
|
|
34
|
+
const finish = (exitCode, signal, spawnError) => {
|
|
35
|
+
if (settled)
|
|
36
|
+
return;
|
|
37
|
+
settled = true;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
const diagnostics = sanitizeDiagnostics(spawnError?.message
|
|
40
|
+
? `${spawnError.message}\n${output.toString("utf8")}`
|
|
41
|
+
: output.toString("utf8"), limit);
|
|
42
|
+
const category = timedOut
|
|
43
|
+
? "timeout"
|
|
44
|
+
: spawnError?.code === "ENOENT"
|
|
45
|
+
? "missing-executable"
|
|
46
|
+
: exitCode === 0
|
|
47
|
+
? "ready"
|
|
48
|
+
: classifyRuntimeFailure(diagnostics, phase);
|
|
49
|
+
resolveResult({
|
|
50
|
+
ok: category === "ready",
|
|
51
|
+
runtime,
|
|
52
|
+
phase,
|
|
53
|
+
category,
|
|
54
|
+
executable,
|
|
55
|
+
cwd,
|
|
56
|
+
timedOut,
|
|
57
|
+
exitCode,
|
|
58
|
+
signal,
|
|
59
|
+
diagnostics,
|
|
60
|
+
environmentNames: Object.keys(env).filter((name) => RELEVANT_ENV.test(name)).sort(),
|
|
61
|
+
recoveryChoices: category === "ready" ? [] : ["retry", "switch", "cancel"],
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
child.once("error", (error) => finish(null, null, error));
|
|
65
|
+
child.once("close", (code, signal) => finish(code, signal));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function classifyRuntimeFailure(text, phase = "readiness") {
|
|
69
|
+
const value = text.toLowerCase();
|
|
70
|
+
if (/not logged in|login required|unauthenticated|invalid authentication|expired.{0,20}token|token.{0,20}expired|\b401\b/.test(value))
|
|
71
|
+
return "authentication";
|
|
72
|
+
if (/forbidden|not authorized|unauthorized|entitlement|permission denied|\b403\b/.test(value))
|
|
73
|
+
return "authorization";
|
|
74
|
+
if (/rate.?limit|too many requests|\b429\b|quota exceeded/.test(value))
|
|
75
|
+
return "rate-limit";
|
|
76
|
+
if (/enotfound|econnreset|econnrefused|network|dns|socket hang up|timed? out|tls|certificate/.test(value))
|
|
77
|
+
return "network";
|
|
78
|
+
if (/cannot find module|module not found|sdk/.test(value))
|
|
79
|
+
return "sdk-load";
|
|
80
|
+
if (/malformed|invalid json|protocol|unexpected token/.test(value))
|
|
81
|
+
return "malformed-protocol";
|
|
82
|
+
if (/stream|agent turn|agent error/.test(value))
|
|
83
|
+
return "agent-stream";
|
|
84
|
+
if (/config|configuration|invalid model|unsupported model/.test(value))
|
|
85
|
+
return "configuration";
|
|
86
|
+
if (phase === "compiler-update")
|
|
87
|
+
return "compiler-update";
|
|
88
|
+
if (phase === "capability-discovery")
|
|
89
|
+
return "capability-discovery";
|
|
90
|
+
return "unknown";
|
|
91
|
+
}
|
|
92
|
+
export function sanitizeDiagnostics(text, maxBytes = RUNTIME_DIAGNOSTIC_LIMIT) {
|
|
93
|
+
const clean = text
|
|
94
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
95
|
+
.replace(/((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret)\s*[=:]\s*)(?:bearer\s+)?[^\s,;]+/gi, "$1<redacted>")
|
|
96
|
+
.replace(/\b(?:sk|rk|ghp|github_pat)_[A-Za-z0-9_-]+\b/g, "<redacted>")
|
|
97
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi, "Bearer <redacted>")
|
|
98
|
+
.replace(/\s+$/g, "")
|
|
99
|
+
.trim();
|
|
100
|
+
const bytes = Buffer.from(clean);
|
|
101
|
+
if (bytes.length <= maxBytes)
|
|
102
|
+
return clean;
|
|
103
|
+
return `${bytes.subarray(0, Math.max(0, maxBytes - 18)).toString("utf8")}\n... truncated ...`;
|
|
104
|
+
}
|
|
105
|
+
export function formatRuntimeProbeFailure(result) {
|
|
106
|
+
if (result.ok)
|
|
107
|
+
return `${result.executable} is ready.`;
|
|
108
|
+
const lines = [
|
|
109
|
+
`${result.executable} failed during ${result.phase} (${result.category}${result.timedOut ? ", timed out" : result.exitCode === null ? "" : `, exit ${result.exitCode}`}).`,
|
|
110
|
+
];
|
|
111
|
+
if (result.category === "authentication") {
|
|
112
|
+
lines.push(result.runtime === "claude"
|
|
113
|
+
? "Authenticate with `claude auth login --claudeai`, then retry."
|
|
114
|
+
: "Authenticate with `codex login`, then retry.");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
lines.push("Review the diagnostic below, then retry, switch to a verified provider, or cancel.");
|
|
118
|
+
}
|
|
119
|
+
if (result.diagnostics)
|
|
120
|
+
lines.push("", result.diagnostics);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
2
|
+
import { type TicketStatus } from "./tickets/stateDb.js";
|
|
3
|
+
import { type DeliveryConfig } from "./tickets/delivery.js";
|
|
4
|
+
import { type TicketBuildSetupConfig } from "./tickets/setupConfig.js";
|
|
5
|
+
export declare const PROPOSAL_START = "RAFI_PROPOSAL_START";
|
|
6
|
+
export declare const PROPOSAL_END = "RAFI_PROPOSAL_END";
|
|
7
|
+
export interface TicketEdit {
|
|
8
|
+
id: string;
|
|
9
|
+
patch: Partial<Omit<TicketDef, "id">>;
|
|
10
|
+
}
|
|
11
|
+
export interface TicketSupersession {
|
|
12
|
+
replaced: string[];
|
|
13
|
+
replacements: string[];
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TicketStateChange {
|
|
17
|
+
id: string;
|
|
18
|
+
status: TicketStatus;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SourceReconciliation {
|
|
22
|
+
source: string;
|
|
23
|
+
item: string;
|
|
24
|
+
disposition: "mapped" | "split" | "combined" | "deferred" | "excluded";
|
|
25
|
+
ticket_ids: string[];
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface FutureWorkDisposition {
|
|
29
|
+
id: number;
|
|
30
|
+
disposition: "accepted" | "rejected" | "merged" | "queued";
|
|
31
|
+
}
|
|
32
|
+
export interface TicketPlanProposal {
|
|
33
|
+
version: 1;
|
|
34
|
+
title: string;
|
|
35
|
+
markdown: string;
|
|
36
|
+
additions: TicketDef[];
|
|
37
|
+
edits: TicketEdit[];
|
|
38
|
+
supersessions: TicketSupersession[];
|
|
39
|
+
state_changes: TicketStateChange[];
|
|
40
|
+
source_reconciliation: SourceReconciliation[];
|
|
41
|
+
delivery?: DeliveryConfig | null;
|
|
42
|
+
build_defaults?: Partial<TicketBuildSetupConfig> | null;
|
|
43
|
+
future_work: FutureWorkDisposition[];
|
|
44
|
+
next: {
|
|
45
|
+
ticket_ids: string[];
|
|
46
|
+
replace_existing: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface PlanningFingerprint {
|
|
50
|
+
path: string;
|
|
51
|
+
sha256: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface TicketPlanningContext {
|
|
54
|
+
tickets: TicketDef[];
|
|
55
|
+
states: Array<{
|
|
56
|
+
ticket_id: string;
|
|
57
|
+
status: TicketStatus;
|
|
58
|
+
}>;
|
|
59
|
+
futureWork: Array<{
|
|
60
|
+
id?: number;
|
|
61
|
+
summary: string;
|
|
62
|
+
disposition: string;
|
|
63
|
+
rationale: string | null;
|
|
64
|
+
}>;
|
|
65
|
+
existingNext: string[];
|
|
66
|
+
}
|
|
67
|
+
export declare function readTicketPlanningContext(projectDir: string): TicketPlanningContext;
|
|
68
|
+
export declare function ticketPlanningFingerprint(projectDir: string): PlanningFingerprint[];
|
|
69
|
+
export declare function planningFingerprintChanges(projectDir: string, expected: PlanningFingerprint[]): string[];
|
|
70
|
+
export declare function extractTicketPlanProposal(output: string, existingTickets?: TicketDef[]): TicketPlanProposal;
|
|
71
|
+
export declare function validateTicketPlanProposal(value: unknown, existingTickets?: TicketDef[]): string[];
|
|
72
|
+
export interface ApplyTicketPlanOptions {
|
|
73
|
+
expectedFingerprint: PlanningFingerprint[];
|
|
74
|
+
docsRoot: string;
|
|
75
|
+
now?: Date;
|
|
76
|
+
}
|
|
77
|
+
export interface AppliedTicketPlan {
|
|
78
|
+
added: string[];
|
|
79
|
+
edited: string[];
|
|
80
|
+
artifacts: string[];
|
|
81
|
+
backupDir: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function applyApprovedTicketPlan(projectDir: string, proposal: TicketPlanProposal, opts: ApplyTicketPlanOptions): AppliedTicketPlan;
|