ai-foreman 1.7.11 → 1.7.13
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 +15 -0
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +133 -7
- package/dist/adapters/codex.d.ts +5 -0
- package/dist/adapters/codex.js +121 -11
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +11 -1
- package/dist/adapters/types.d.ts +47 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +8 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +131 -67
- package/dist/buildRuns.d.ts +6 -2
- package/dist/buildRuns.js +76 -32
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +446 -142
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +27 -9
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -1
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +26 -6
- package/dist/foreman.js +147 -104
- package/dist/handoffs.d.ts +35 -3
- package/dist/handoffs.js +155 -17
- package/dist/index.js +6 -0
- package/dist/log.d.ts +3 -3
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaRecovery.d.ts +140 -0
- package/dist/qaRecovery.js +595 -0
- package/dist/qaReview.d.ts +133 -14
- package/dist/qaReview.js +984 -59
- package/dist/qaSnapshot.d.ts +31 -5
- package/dist/qaSnapshot.js +209 -134
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +31 -2
- package/dist/workflowDb.js +192 -23
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +13 -0
- package/package.json +51 -3
package/dist/cli/status.js
CHANGED
|
@@ -3,7 +3,8 @@ import { resolve, join } from "node:path";
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { findResumableBranchSessions, formatBranchContinueCommand } from "../branch/resume.js";
|
|
5
5
|
import { recoverableBuildRuns } from "../buildRuns.js";
|
|
6
|
-
import {
|
|
6
|
+
import { WORKFLOW_DB_FILE } from "../workflowDb.js";
|
|
7
|
+
import { WorkflowReader } from "../workflowReader.js";
|
|
7
8
|
import { formatWorkflowIssue } from "../outcomes.js";
|
|
8
9
|
function fail(message) {
|
|
9
10
|
console.error(`foreman: ${message}`);
|
|
@@ -21,7 +22,7 @@ export function runStatus(project) {
|
|
|
21
22
|
const dir = join(projectDir, ".foreman");
|
|
22
23
|
const recoverable = recoverableBuildRuns(projectDir);
|
|
23
24
|
const workflowPath = join(projectDir, WORKFLOW_DB_FILE);
|
|
24
|
-
const workflow = existsSync(workflowPath) ? new
|
|
25
|
+
const workflow = existsSync(workflowPath) ? new WorkflowReader(projectDir, workflowPath) : undefined;
|
|
25
26
|
const activeWorkflows = workflow?.activeRuns() ?? [];
|
|
26
27
|
if (!existsSync(dir) && activeWorkflows.length === 0) {
|
|
27
28
|
workflow?.close();
|
|
@@ -101,6 +102,16 @@ function printWorkflowIssues(workflow, runs) {
|
|
|
101
102
|
console.log(` ${run.runId} ${run.kind} ${run.status} checkpoint=${run.checkpoint}`);
|
|
102
103
|
for (const value of workflow.issues(run.runId))
|
|
103
104
|
console.log(` ${formatWorkflowIssue(value).replace(/\n/g, " | ")}`);
|
|
105
|
+
if (workflow instanceof WorkflowReader) {
|
|
106
|
+
const supervisor = workflow.supervisorState(run.runId);
|
|
107
|
+
if (supervisor)
|
|
108
|
+
console.log(` supervisor=${supervisor.status} generation=${supervisor.generation} worker=${supervisor.workerPid ?? "none"} restarts=${supervisor.runRestarts}`);
|
|
109
|
+
const attempts = workflow.recoveryAttempts(run.runId);
|
|
110
|
+
if (attempts.length)
|
|
111
|
+
console.log(` recovery attempts=${attempts.length}; latest=${attempts.at(-1)?.action}:${attempts.at(-1)?.outcome}`);
|
|
112
|
+
for (const decision of workflow.pendingHumanDecisions(run.runId))
|
|
113
|
+
console.log(` pending decision ${decision.decisionId}: ${decision.choices.map((choice) => choice.id).join(" | ")}`);
|
|
114
|
+
}
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
function printRecoverable(projectDir, runs) {
|
package/dist/cli/tickets.js
CHANGED
|
@@ -20,7 +20,8 @@ import { checkpointInterview, completeInterview, createInterviewRecord, failInte
|
|
|
20
20
|
import { applyTicketPopulation, authorizeTicketRetirements, extractTicketPopulationProposal, materializeTicketPopulation, recoverTicketPublications, TICKET_POPULATION_PROPOSAL_END, TICKET_POPULATION_PROPOSAL_START } from "../ticketPopulation.js";
|
|
21
21
|
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
22
22
|
import { resolveTicketPaths } from "../tickets/config.js";
|
|
23
|
-
import { formatTicketDetails, getTicketDetails } from "../tickets/details.js";
|
|
23
|
+
import { formatAllTicketDetails, formatTicketDetails, getAllTicketDetails, getTicketDetails } from "../tickets/details.js";
|
|
24
|
+
import { appendTicketOutput } from "../tickets/output.js";
|
|
24
25
|
import { applyResolvedTicketReset, previewTicketReset, recoverTicketResetPublications, resetTickets, resolveTicketResetSelection, TicketResetDependencyConflictError } from "../tickets/reset.js";
|
|
25
26
|
import { validateBranchPrefix } from "../branch/prefix.js";
|
|
26
27
|
import { listTicketGroups, previewTicketGroupRepair, repairTicketGroups } from "../tickets/groups.js";
|
|
@@ -1026,7 +1027,7 @@ export async function cmdPopulateCli(opts) {
|
|
|
1026
1027
|
}));
|
|
1027
1028
|
builder = roleBuilder.builder;
|
|
1028
1029
|
viewer = printEvents(builder.events());
|
|
1029
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, dir);
|
|
1030
|
+
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, dir);
|
|
1030
1031
|
console.log(`foreman tickets: populating tickets with ${roleBuilder.runtime}`);
|
|
1031
1032
|
console.log(`foreman tickets: project ${dir}`);
|
|
1032
1033
|
console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
|
|
@@ -1375,18 +1376,35 @@ export function buildTicketsCommand(options = {}) {
|
|
|
1375
1376
|
});
|
|
1376
1377
|
// ── update ──────────────────────────────────────────────────────────────────
|
|
1377
1378
|
tickets
|
|
1378
|
-
.command("show
|
|
1379
|
-
.description("Show
|
|
1379
|
+
.command("show [ticketId]")
|
|
1380
|
+
.description("Show one or all complete canonical tickets, including state, validation, delivery, and history.")
|
|
1380
1381
|
.option("-p, --project <dir>", "project directory (default: cwd)")
|
|
1382
|
+
.option("--all", "show every canonical ticket, including terminal tickets")
|
|
1381
1383
|
.option("--json", "write the complete stable record as JSON")
|
|
1384
|
+
.option("--output <file>", "append the rendered output to a file")
|
|
1382
1385
|
.action((ticketId, opts) => {
|
|
1383
1386
|
try {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1387
|
+
if (ticketId && opts.all)
|
|
1388
|
+
throw new Error("choose either a ticket ID or --all, not both");
|
|
1389
|
+
if (!ticketId && !opts.all) {
|
|
1390
|
+
throw new Error("select one ticket: `rafi tickets show T123` or `rafi tickets show --all`");
|
|
1391
|
+
}
|
|
1392
|
+
const projectDir = cwd(opts);
|
|
1393
|
+
const details = opts.all ? getAllTicketDetails(projectDir) : getTicketDetails(projectDir, ticketId);
|
|
1394
|
+
const lines = opts.all ? formatAllTicketDetails(details) : formatTicketDetails(details);
|
|
1395
|
+
const payload = opts.json ? `${JSON.stringify(details, null, 2)}\n` : `${lines.join("\n")}\n`;
|
|
1396
|
+
if (opts.output) {
|
|
1397
|
+
const result = appendTicketOutput(projectDir, resolve(String(opts.output)), payload);
|
|
1398
|
+
const count = opts.all ? details.ticket_count : 1;
|
|
1399
|
+
console.log(`rafi tickets show: ${result.disposition} ${count} ${count === 1 ? "ticket" : "tickets"} as ${opts.json ? "json" : "text"} to ${result.path}`);
|
|
1400
|
+
}
|
|
1401
|
+
else if (opts.json) {
|
|
1402
|
+
process.stdout.write(payload);
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
for (const line of lines)
|
|
1389
1406
|
console.log(line);
|
|
1407
|
+
}
|
|
1390
1408
|
}
|
|
1391
1409
|
catch (err) {
|
|
1392
1410
|
fail(String(err instanceof Error ? err.message : err));
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ObservabilityConfig } from "./observability.js";
|
|
1
2
|
export interface PermissionConfig {
|
|
2
3
|
/** Bash command prefixes that are auto-approved when every segment matches. */
|
|
3
4
|
allowBash: string[];
|
|
@@ -13,6 +14,8 @@ export interface PermissionConfig {
|
|
|
13
14
|
export interface NotificationsConfig {
|
|
14
15
|
/** Fire a desktop notification when the builder needs user input. Default: false. */
|
|
15
16
|
enabled: boolean;
|
|
17
|
+
/** Ring the terminal bell when user attention is required or a run finishes. Default: true. */
|
|
18
|
+
terminal_bell: boolean;
|
|
16
19
|
}
|
|
17
20
|
export interface QaConfig {
|
|
18
21
|
/** Run a QA review pass after each completed ticket or step. Default: true. */
|
|
@@ -22,6 +25,7 @@ export interface ForemanConfig {
|
|
|
22
25
|
permissions: PermissionConfig;
|
|
23
26
|
notifications: NotificationsConfig;
|
|
24
27
|
qa: QaConfig;
|
|
28
|
+
observability: ObservabilityConfig;
|
|
25
29
|
}
|
|
26
30
|
/** Built-in defaults; foreman.yaml overrides any field present. */
|
|
27
31
|
export declare const DEFAULT_CONFIG: ForemanConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { parse } from "yaml";
|
|
3
|
+
import { DEFAULT_OBSERVABILITY_CONFIG } from "./observability.js";
|
|
3
4
|
/** Built-in defaults; foreman.yaml overrides any field present. */
|
|
4
5
|
export const DEFAULT_CONFIG = {
|
|
5
6
|
permissions: {
|
|
@@ -65,8 +66,9 @@ export const DEFAULT_CONFIG = {
|
|
|
65
66
|
],
|
|
66
67
|
escalateTools: ["WebFetch", "WebSearch"],
|
|
67
68
|
},
|
|
68
|
-
notifications: { enabled: false },
|
|
69
|
+
notifications: { enabled: false, terminal_bell: true },
|
|
69
70
|
qa: { enabled: true },
|
|
71
|
+
observability: DEFAULT_OBSERVABILITY_CONFIG,
|
|
70
72
|
};
|
|
71
73
|
/** Load foreman.yaml if present and deep-merge it over the defaults. */
|
|
72
74
|
export function loadConfig(path = "foreman.yaml") {
|
|
@@ -81,6 +83,7 @@ export function loadConfig(path = "foreman.yaml") {
|
|
|
81
83
|
...(raw.notifications ?? {}),
|
|
82
84
|
},
|
|
83
85
|
qa: { ...DEFAULT_CONFIG.qa, ...(raw.qa ?? {}) },
|
|
86
|
+
observability: { ...DEFAULT_CONFIG.observability, ...(raw.observability ?? {}) },
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
function validateConfig(raw, path) {
|
|
@@ -104,10 +107,34 @@ function validateConfig(raw, path) {
|
|
|
104
107
|
}
|
|
105
108
|
if (cfg.notifications !== undefined) {
|
|
106
109
|
validateBooleanObject(cfg.notifications, "notifications", path);
|
|
110
|
+
const terminalBell = cfg.notifications.terminal_bell;
|
|
111
|
+
if (terminalBell !== undefined && typeof terminalBell !== "boolean") {
|
|
112
|
+
throw new Error(`${path}: notifications.terminal_bell must be a boolean`);
|
|
113
|
+
}
|
|
107
114
|
}
|
|
108
115
|
if (cfg.qa !== undefined) {
|
|
109
116
|
validateBooleanObject(cfg.qa, "qa", path);
|
|
110
117
|
}
|
|
118
|
+
if (cfg.observability !== undefined)
|
|
119
|
+
validateObservability(cfg.observability, path);
|
|
120
|
+
}
|
|
121
|
+
function validateObservability(value, path) {
|
|
122
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
123
|
+
throw new Error(`${path}: observability must be an object`);
|
|
124
|
+
const input = value;
|
|
125
|
+
const keys = ["enabled", "sample_interval_seconds", "detail_retention_days", "log_retention_days", "detail_soft_limit_mb", "detail_hard_limit_mb", "log_limit_mb"];
|
|
126
|
+
for (const key of Object.keys(input))
|
|
127
|
+
if (!keys.includes(key))
|
|
128
|
+
throw new Error(`${path}: unknown observability setting ${key}`);
|
|
129
|
+
if (input.enabled !== undefined && typeof input.enabled !== "boolean")
|
|
130
|
+
throw new Error(`${path}: observability.enabled must be a boolean`);
|
|
131
|
+
for (const key of keys.filter((key) => key !== "enabled")) {
|
|
132
|
+
if (input[key] !== undefined && (!Number.isInteger(input[key]) || Number(input[key]) <= 0))
|
|
133
|
+
throw new Error(`${path}: observability.${key} must be a positive integer`);
|
|
134
|
+
}
|
|
135
|
+
const merged = { ...DEFAULT_OBSERVABILITY_CONFIG, ...input };
|
|
136
|
+
if (merged.detail_soft_limit_mb >= merged.detail_hard_limit_mb)
|
|
137
|
+
throw new Error(`${path}: observability.detail_soft_limit_mb must be less than detail_hard_limit_mb`);
|
|
111
138
|
}
|
|
112
139
|
function validateBooleanObject(value, name, path) {
|
|
113
140
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
package/dist/continuity.js
CHANGED
|
@@ -138,7 +138,8 @@ export class ContinuityAdapter {
|
|
|
138
138
|
finally {
|
|
139
139
|
db.close();
|
|
140
140
|
}
|
|
141
|
-
const
|
|
141
|
+
const providerInstruction = `${instruction}\n\n${continuityInstruction()}`;
|
|
142
|
+
const original = await this.adapter.sendTurn(providerInstruction);
|
|
142
143
|
const activeSession = this.adapter.sessionRef?.() ?? this.adapter.sessionId();
|
|
143
144
|
if (activeSession) {
|
|
144
145
|
const leaseDb = new WorkflowDb(this.options.projectDir);
|
|
@@ -165,7 +166,8 @@ export class ContinuityAdapter {
|
|
|
165
166
|
instruction,
|
|
166
167
|
].join("\n\n"));
|
|
167
168
|
}
|
|
168
|
-
return { ...original, text: parsed.cleanText
|
|
169
|
+
return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
|
|
170
|
+
rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
|
|
169
171
|
}
|
|
170
172
|
const repair = await this.adapter.sendTurn([
|
|
171
173
|
"Continuity protocol repair only. Do not run tools, repeat work, or change files.",
|
|
@@ -177,7 +179,8 @@ export class ContinuityAdapter {
|
|
|
177
179
|
if (repaired.delta) {
|
|
178
180
|
this.publish(repaired.delta, "turn_completed_after_repair", original);
|
|
179
181
|
this.moveRecoveryLeaseAfterCheckpoint();
|
|
180
|
-
return { ...original, text: parsed.cleanText
|
|
182
|
+
return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
|
|
183
|
+
rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
|
|
181
184
|
}
|
|
182
185
|
const invalidDb = new WorkflowDb(this.options.projectDir);
|
|
183
186
|
try {
|
|
@@ -205,7 +208,8 @@ export class ContinuityAdapter {
|
|
|
205
208
|
throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "validated recovery callback did not return a genuinely fresh successor");
|
|
206
209
|
}
|
|
207
210
|
await this.adoptValidatedSuccessor(successor);
|
|
208
|
-
return { ...original, text: parsed.cleanText
|
|
211
|
+
return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
|
|
212
|
+
rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
|
|
209
213
|
}
|
|
210
214
|
if (!this.options.createSuccessor)
|
|
211
215
|
throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "continuity repair failed in the original session and no validated successor is configured");
|
|
@@ -225,7 +229,8 @@ export class ContinuityAdapter {
|
|
|
225
229
|
finally {
|
|
226
230
|
leaseDb.close();
|
|
227
231
|
}
|
|
228
|
-
return { ...original, text: parsed.cleanText
|
|
232
|
+
return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
|
|
233
|
+
rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
|
|
229
234
|
}
|
|
230
235
|
sessionId() { return this.adapter.sessionId(); }
|
|
231
236
|
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
@@ -327,12 +332,12 @@ export class ContinuityAdapter {
|
|
|
327
332
|
try {
|
|
328
333
|
for await (const event of source.events()) {
|
|
329
334
|
this.queue.push(event);
|
|
330
|
-
|
|
335
|
+
// Continuity stores only recovery-relevant transitions. High-rate tool
|
|
336
|
+
// and context telemetry belongs exclusively to observability.
|
|
337
|
+
if (event.kind === "session-transition") {
|
|
331
338
|
const db = new WorkflowDb(this.options.projectDir);
|
|
332
339
|
try {
|
|
333
|
-
const payload = event
|
|
334
|
-
? { name: event.name, inputDigest: sha(JSON.stringify(event.input ?? null)) }
|
|
335
|
-
: event;
|
|
340
|
+
const payload = event;
|
|
336
341
|
db.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: `provider_${event.kind.replace(/-/g, "_")}`, payload, authoritativeStateRevision: this.revision() });
|
|
337
342
|
}
|
|
338
343
|
finally {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BuildRunRecordV2, ManagerDiagnosticReportV1 } from "rafi-spec";
|
|
2
|
+
export type ExternalDiagnosticMode = "auto" | "on" | "off";
|
|
3
|
+
export interface DiagnosticCommandResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr?: string;
|
|
7
|
+
timedOut?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type DiagnosticCommandRunner = (command: string, args: readonly string[], options: {
|
|
10
|
+
cwd: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
}) => DiagnosticCommandResult;
|
|
13
|
+
export interface CollectManagerDiagnosticsOptions {
|
|
14
|
+
runId?: string;
|
|
15
|
+
question?: string;
|
|
16
|
+
external?: ExternalDiagnosticMode;
|
|
17
|
+
now?: Date;
|
|
18
|
+
commandRunner?: DiagnosticCommandRunner;
|
|
19
|
+
maxDetailSpans?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function selectDiagnosticRun(projectDir: string, requestedRunId?: string): BuildRunRecordV2;
|
|
22
|
+
/** Collects and calculates a canonical report. The provider receives this report, never raw database rows. */
|
|
23
|
+
export declare function collectManagerDiagnostics(projectDir: string, options?: CollectManagerDiagnosticsOptions): ManagerDiagnosticReportV1;
|
|
24
|
+
export declare function unionDuration(intervals: ReadonlyArray<readonly [number, number]>): number;
|
|
25
|
+
export declare function ticketSizeBucketForRun(projectDir: string, ticketIds: readonly string[]): string | undefined;
|