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.
Files changed (75) hide show
  1. package/README.md +15 -0
  2. package/dist/activity.d.ts +5 -1
  3. package/dist/activity.js +1 -1
  4. package/dist/adapters/claude.d.ts +7 -0
  5. package/dist/adapters/claude.js +133 -7
  6. package/dist/adapters/codex.d.ts +5 -0
  7. package/dist/adapters/codex.js +121 -11
  8. package/dist/adapters/recovering.d.ts +2 -0
  9. package/dist/adapters/recovering.js +11 -1
  10. package/dist/adapters/types.d.ts +47 -1
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +8 -2
  13. package/dist/branch/resume.js +11 -1
  14. package/dist/branch/runner.d.ts +12 -3
  15. package/dist/branch/runner.js +131 -67
  16. package/dist/buildRuns.d.ts +6 -2
  17. package/dist/buildRuns.js +76 -32
  18. package/dist/cli/doctor.d.ts +7 -0
  19. package/dist/cli/doctor.js +84 -1
  20. package/dist/cli/manager.d.ts +17 -0
  21. package/dist/cli/manager.js +135 -0
  22. package/dist/cli/recovery.d.ts +4 -0
  23. package/dist/cli/recovery.js +80 -0
  24. package/dist/cli/start.js +446 -142
  25. package/dist/cli/status.js +13 -2
  26. package/dist/cli/tickets.js +27 -9
  27. package/dist/config.d.ts +4 -0
  28. package/dist/config.js +28 -1
  29. package/dist/continuity.js +14 -9
  30. package/dist/diagnostics.d.ts +25 -0
  31. package/dist/diagnostics.js +428 -0
  32. package/dist/foreman.d.ts +26 -6
  33. package/dist/foreman.js +147 -104
  34. package/dist/handoffs.d.ts +35 -3
  35. package/dist/handoffs.js +155 -17
  36. package/dist/index.js +6 -0
  37. package/dist/log.d.ts +3 -3
  38. package/dist/log.js +4 -1
  39. package/dist/managerPacket.d.ts +25 -0
  40. package/dist/managerPacket.js +83 -0
  41. package/dist/markers.d.ts +1 -1
  42. package/dist/markers.js +4 -1
  43. package/dist/notify.d.ts +4 -0
  44. package/dist/notify.js +15 -0
  45. package/dist/observability.d.ts +248 -0
  46. package/dist/observability.js +1018 -0
  47. package/dist/planChanges.d.ts +4 -0
  48. package/dist/planChanges.js +16 -0
  49. package/dist/projectDiagnostics.d.ts +27 -0
  50. package/dist/projectDiagnostics.js +407 -0
  51. package/dist/qaRecovery.d.ts +140 -0
  52. package/dist/qaRecovery.js +595 -0
  53. package/dist/qaReview.d.ts +133 -14
  54. package/dist/qaReview.js +984 -59
  55. package/dist/qaSnapshot.d.ts +31 -5
  56. package/dist/qaSnapshot.js +209 -134
  57. package/dist/recoveryPolicy.d.ts +37 -0
  58. package/dist/recoveryPolicy.js +251 -0
  59. package/dist/roles.js +1 -0
  60. package/dist/supervisor.d.ts +40 -0
  61. package/dist/supervisor.js +102 -0
  62. package/dist/ticketHistoryReader.d.ts +19 -0
  63. package/dist/ticketHistoryReader.js +58 -0
  64. package/dist/tickets/details.d.ts +8 -0
  65. package/dist/tickets/details.js +59 -11
  66. package/dist/tickets/output.d.ts +5 -0
  67. package/dist/tickets/output.js +73 -0
  68. package/dist/tickets/stateDb.d.ts +1 -0
  69. package/dist/tickets/stateDb.js +5 -0
  70. package/dist/workflowDb.d.ts +31 -2
  71. package/dist/workflowDb.js +192 -23
  72. package/dist/workflowReader.d.ts +42 -0
  73. package/dist/workflowReader.js +171 -0
  74. package/foreman.yaml +13 -0
  75. package/package.json +51 -3
@@ -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 { WorkflowDb, WORKFLOW_DB_FILE } from "../workflowDb.js";
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 WorkflowDb(projectDir, workflowPath) : undefined;
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) {
@@ -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 <ticketId>")
1379
- .description("Show the complete canonical ticket, active state, validation, delivery, and history.")
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
- const details = getTicketDetails(cwd(opts), ticketId);
1385
- if (opts.json)
1386
- process.stdout.write(`${JSON.stringify(details, null, 2)}\n`);
1387
- else
1388
- for (const line of formatTicketDetails(details))
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)) {
@@ -138,7 +138,8 @@ export class ContinuityAdapter {
138
138
  finally {
139
139
  db.close();
140
140
  }
141
- const original = await this.adapter.sendTurn(`${instruction}\n\n${continuityInstruction()}`);
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
- if (event.kind === "tool" || event.kind === "session-transition" || event.kind === "context-usage") {
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.kind === "tool"
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;