ai-foreman 1.7.12 → 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 (73) 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 +7 -1
  13. package/dist/branch/resume.js +11 -1
  14. package/dist/branch/runner.d.ts +11 -3
  15. package/dist/branch/runner.js +114 -60
  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 +289 -53
  25. package/dist/cli/status.js +13 -2
  26. package/dist/cli/tickets.js +26 -8
  27. package/dist/config.d.ts +2 -0
  28. package/dist/config.js +23 -0
  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 +16 -6
  33. package/dist/foreman.js +70 -82
  34. package/dist/handoffs.d.ts +5 -1
  35. package/dist/handoffs.js +4 -1
  36. package/dist/index.js +6 -0
  37. package/dist/log.d.ts +2 -2
  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/observability.d.ts +248 -0
  44. package/dist/observability.js +1018 -0
  45. package/dist/planChanges.d.ts +4 -0
  46. package/dist/planChanges.js +16 -0
  47. package/dist/projectDiagnostics.d.ts +27 -0
  48. package/dist/projectDiagnostics.js +407 -0
  49. package/dist/qaRecovery.d.ts +140 -0
  50. package/dist/qaRecovery.js +595 -0
  51. package/dist/qaReview.d.ts +128 -14
  52. package/dist/qaReview.js +974 -57
  53. package/dist/qaSnapshot.d.ts +31 -5
  54. package/dist/qaSnapshot.js +209 -134
  55. package/dist/recoveryPolicy.d.ts +37 -0
  56. package/dist/recoveryPolicy.js +251 -0
  57. package/dist/roles.js +1 -0
  58. package/dist/supervisor.d.ts +40 -0
  59. package/dist/supervisor.js +102 -0
  60. package/dist/ticketHistoryReader.d.ts +19 -0
  61. package/dist/ticketHistoryReader.js +58 -0
  62. package/dist/tickets/details.d.ts +8 -0
  63. package/dist/tickets/details.js +59 -11
  64. package/dist/tickets/output.d.ts +5 -0
  65. package/dist/tickets/output.js +73 -0
  66. package/dist/tickets/stateDb.d.ts +1 -0
  67. package/dist/tickets/stateDb.js +5 -0
  68. package/dist/workflowDb.d.ts +31 -2
  69. package/dist/workflowDb.js +192 -23
  70. package/dist/workflowReader.d.ts +42 -0
  71. package/dist/workflowReader.js +171 -0
  72. package/foreman.yaml +9 -0
  73. package/package.json +51 -3
package/README.md CHANGED
@@ -148,6 +148,21 @@ With the saved `current` strategy, Foreman works in the active branch while the
148
148
 
149
149
  The current terminal status includes the role/provider, activity, truthful context state, successful compaction count, and handoff generation. At the Builder threshold, Foreman settles the in-flight action, verifies native compaction plus a fresh provider usage sample, and resumes the frozen action. The configured maximum defaults to ten successful compactions per location-scoped provider session; the next crossing uses a validated fresh handoff instead of exceeding it. Disposable QA snapshots never reuse a prior QA conversation in a new `/tmp/rafi-qa-*` worktree; they transfer cumulative state through an accepted handoff.
150
150
 
151
+ Failed QA reviews use the versioned `RAFI_QA_FAILURE_REPORT_START` / `RAFI_QA_FAILURE_REPORT_END` JSON contract. Foreman validates the report before Builder remediation and preserves reports by digest. A malformed failing report receives two correction-only turns, a compacted pair, then five turns in a validated fresh successor. Recovery packets are owner-only, locally Git-excluded, and stored under `.foreman/qa-report-recovery/`; the successor must acknowledge the copied packet and reviewed-state digests before reporting.
152
+
153
+ ### `ai-foreman manager`
154
+
155
+ Manager is a project-wide, read-only diagnostic conversation over every retained build run. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
156
+
157
+ Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
158
+
159
+ When the initial bounded packet omits evidence needed for a question, Manager can request catalog rows, aggregates, comparisons, or details through a host-validated read-only lookup. Each question is limited to two rounds, six operations per round, five detailed runs per operation, and fifty catalog rows per list operation. Questions, answers, and lookup envelopes are not stored as project evidence.
160
+
161
+ ```bash
162
+ ai-foreman manager ./my-project
163
+ ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
164
+ ```
165
+
151
166
  ### GitHub PR Failure Recovery
152
167
 
153
168
  When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
@@ -63,7 +63,11 @@ export declare class ActivityReporter {
63
63
  export declare function currentActivity(): ActivityReporter | undefined;
64
64
  export declare function withActivityContext<T>(command: string, operation: () => Promise<T>, options?: ActivityReporterOptions): Promise<T>;
65
65
  export declare function withActivityPhase<T>(label: string, operation: () => Promise<T>): Promise<T>;
66
- export declare function pauseActivityForInput<T>(operation: () => Promise<T>): Promise<T>;
66
+ export interface ActivityInputPauseMetadata {
67
+ label?: string;
68
+ context?: Record<string, unknown>;
69
+ }
70
+ export declare function pauseActivityForInput<T>(operation: () => Promise<T>, _metadata?: ActivityInputPauseMetadata): Promise<T>;
67
71
  export declare function reportBuilderEvent(event: BuilderEvent): void;
68
72
  /** Async event queue that also feeds the active CLI reporter without consuming the stream. */
69
73
  export declare class BuilderEventQueue extends AsyncQueue<BuilderEvent> {
package/dist/activity.js CHANGED
@@ -225,7 +225,7 @@ export async function withActivityPhase(label, operation) {
225
225
  end?.();
226
226
  }
227
227
  }
228
- export async function pauseActivityForInput(operation) {
228
+ export async function pauseActivityForInput(operation, _metadata = {}) {
229
229
  const resume = currentActivity()?.pause();
230
230
  try {
231
231
  return await operation();
@@ -62,6 +62,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
62
62
  private nativeCompactions;
63
63
  private nativeCompactionSequence;
64
64
  private cumulativeUsage;
65
+ private priorTurnUsage;
66
+ private activeProviderTurnId?;
67
+ private activeProviderTurnSpanId?;
68
+ private readonly toolSpans;
65
69
  private pending?;
66
70
  private terminalResult?;
67
71
  private streamEnded;
@@ -74,11 +78,14 @@ export declare class ClaudeAdapter implements BuilderAdapter {
74
78
  sendTurn(text: string): Promise<TurnResult>;
75
79
  private sendTurnInternal;
76
80
  private captureStderr;
81
+ private observationContext;
82
+ private observeToolResults;
77
83
  sessionId(): string | undefined;
78
84
  sessionRef(): ProviderSessionRefV1 | undefined;
79
85
  adoptSessionRef(ref: ProviderSessionRefV1): void;
80
86
  validateSession(): Promise<SessionAvailabilityV1>;
81
87
  compact(): Promise<CompactResult>;
88
+ private compactInternal;
82
89
  prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
83
90
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
84
91
  drainNativeCompactions(): NativeCompaction[];
@@ -1,3 +1,4 @@
1
+ import { createHash, randomUUID } from "node:crypto";
1
2
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
2
3
  export async function requireClaudeSDK() {
3
4
  try {
@@ -159,6 +160,10 @@ export class ClaudeAdapter {
159
160
  nativeCompactions = [];
160
161
  nativeCompactionSequence = 0;
161
162
  cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
163
+ priorTurnUsage = {};
164
+ activeProviderTurnId;
165
+ activeProviderTurnSpanId;
166
+ toolSpans = new Map();
162
167
  pending;
163
168
  terminalResult;
164
169
  streamEnded = false;
@@ -275,14 +280,28 @@ export class ClaudeAdapter {
275
280
  }
276
281
  for (const block of msg.message.content) {
277
282
  if (block.type === "text" && block.text) {
278
- this.eventQueue.push({ kind: "text", text: block.text });
283
+ this.eventQueue.push({ kind: "text", text: block.text, byteCount: Buffer.byteLength(block.text), digest: createHash("sha256").update(block.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
284
+ this.opts.observer?.signal(true);
279
285
  }
280
286
  else if (block.type === "tool_use") {
287
+ const callId = block.id;
281
288
  this.eventQueue.push({
282
289
  kind: "tool",
283
290
  name: block.name,
284
291
  input: block.input,
292
+ inputCompleteness: "complete",
293
+ lifecycle: "started",
294
+ callId,
295
+ completionKnown: false,
296
+ providerTurnId: this.activeProviderTurnId,
297
+ eventId: randomUUID(),
298
+ observedAt: new Date().toISOString(),
285
299
  });
300
+ if (this.opts.observer) {
301
+ const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name: block.name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: block.input } });
302
+ this.toolSpans.set(callId, spanId);
303
+ this.opts.observer.signal(true);
304
+ }
286
305
  }
287
306
  }
288
307
  }
@@ -309,10 +328,24 @@ export class ClaudeAdapter {
309
328
  this.structuredError = msg.error;
310
329
  this.apiErrorStatus = msg.error_status;
311
330
  this.turnSignals.push(`API retry ${msg.attempt}/${msg.max_retries}: ${msg.error}${msg.error_status === null ? "" : ` (HTTP ${msg.error_status})`}`);
312
- this.eventQueue.push(claudeApiRetryEvent(msg));
331
+ const retryEvent = { ...claudeApiRetryEvent(msg), retryId: randomUUID(), providerTurnId: this.activeProviderTurnId, observedAt: new Date().toISOString() };
332
+ this.eventQueue.push(retryEvent);
333
+ if (this.opts.observer) {
334
+ const attributes = { provider: "claude", attempt: msg.attempt, maximum: msg.max_retries, reportedDelayMs: msg.retry_delay_ms, reasonDigest: createHash("sha256").update(msg.error).digest("hex") };
335
+ const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryEvent.retryId, kind: "retry", name: "Claude provider retry", providerTurnId: this.activeProviderTurnId, attributes });
336
+ this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryEvent.retryId, spanId: retrySpanId, severity: "warning", attributes });
337
+ this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
338
+ }
313
339
  }
314
340
  else if (msg.type === "tool_progress") {
315
341
  this.eventQueue.push({ kind: "activity", provider: "claude", state: "running tool", detail: `${msg.tool_name} (${Math.floor(msg.elapsed_time_seconds)}s)`, transient: true });
342
+ const progress = msg;
343
+ const callId = typeof progress.tool_use_id === "string" ? progress.tool_use_id : typeof progress.toolUseID === "string" ? progress.toolUseID : undefined;
344
+ this.eventQueue.push({ kind: "tool", name: msg.tool_name, input: {}, lifecycle: "progress", callId, durationMs: Math.max(0, msg.elapsed_time_seconds * 1000), completionKnown: false, eventId: randomUUID(), observedAt: new Date().toISOString() });
345
+ this.opts.observer?.signal(false);
346
+ }
347
+ else if (msg.type === "user") {
348
+ this.observeToolResults(msg);
316
349
  }
317
350
  else if (msg.type === "system") {
318
351
  const system = msg;
@@ -364,13 +397,20 @@ export class ClaudeAdapter {
364
397
  costUsd: msg.total_cost_usd,
365
398
  costAuthoritative: Number.isFinite(msg.total_cost_usd),
366
399
  failure,
400
+ turnId: this.pending?.turnId ?? this.activeProviderTurnId,
401
+ hostInstruction: this.pending?.instruction,
402
+ providerInstruction: this.pending?.instruction,
403
+ rawResponse: text,
404
+ cleanedResponse: failure ? formatClaudeFailure(failure) : text,
405
+ providerMetadata: { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef },
367
406
  };
368
407
  const rawResult = msg;
369
408
  const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
370
409
  result.inputTokens = mergedUsage.inputTokens;
371
410
  result.outputTokens = mergedUsage.outputTokens;
411
+ result.usage = { scope: "session-cumulative", inputTokens: mergedUsage.sample.inputTokens, outputTokens: mergedUsage.sample.outputTokens, totalTokens: mergedUsage.sample.totalTokens, costUsd: mergedUsage.sample.authoritativeCostUsd };
372
412
  this.cumulativeUsage = mergedUsage.sample;
373
- this.eventQueue.push({ kind: "turn-complete", result });
413
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
374
414
  this.settlePending(result, false);
375
415
  this.turnSignals = [];
376
416
  this.structuredError = undefined;
@@ -378,8 +418,43 @@ export class ClaudeAdapter {
378
418
  this.stderrChunks.length = 0;
379
419
  }
380
420
  }
381
- sendTurn(text) {
382
- return withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
421
+ async sendTurn(text) {
422
+ const turnId = randomUUID();
423
+ this.activeProviderTurnId = turnId;
424
+ const observer = this.opts.observer;
425
+ if (!observer) {
426
+ try {
427
+ return await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
428
+ }
429
+ finally {
430
+ this.activeProviderTurnId = undefined;
431
+ }
432
+ }
433
+ const context = this.observationContext();
434
+ const spanId = observer.store.startSpan(context, { spanId: turnId, kind: "provider_turn", name: `Claude ${activityPhase(this.opts.runtimePhase)}`, providerTurnId: turnId, attributes: { provider: "claude" } });
435
+ this.activeProviderTurnSpanId = spanId;
436
+ observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "claude", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
437
+ try {
438
+ const result = await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
439
+ const deltaInput = nonNegativeDelta(result.usage?.inputTokens, this.priorTurnUsage.inputTokens);
440
+ const deltaOutput = nonNegativeDelta(result.usage?.outputTokens, this.priorTurnUsage.outputTokens);
441
+ const deltaCost = nonNegativeDelta(result.usage?.costUsd, this.priorTurnUsage.costUsd);
442
+ this.priorTurnUsage = { inputTokens: result.usage?.inputTokens, outputTokens: result.usage?.outputTokens, costUsd: result.usage?.costUsd };
443
+ observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { cumulativeUsage: result.usage, turnDelta: { inputTokens: deltaInput, outputTokens: deltaOutput, costUsd: deltaCost } } });
444
+ return result;
445
+ }
446
+ catch (error) {
447
+ observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
448
+ throw error;
449
+ }
450
+ finally {
451
+ for (const [callId, toolSpanId] of this.toolSpans) {
452
+ observer.store.finishSpan(toolSpanId, { outcome: "unknown", completionKnown: false, attributes: { callId } });
453
+ this.toolSpans.delete(callId);
454
+ }
455
+ this.activeProviderTurnId = undefined;
456
+ this.activeProviderTurnSpanId = undefined;
457
+ }
383
458
  }
384
459
  sendTurnInternal(text) {
385
460
  if (this.closed)
@@ -394,7 +469,7 @@ export class ClaudeAdapter {
394
469
  this.turnSignals = [];
395
470
  this.structuredError = undefined;
396
471
  this.apiErrorStatus = undefined;
397
- this.pending = { resolve, reject };
472
+ this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
398
473
  this.inbox.push({
399
474
  type: "user",
400
475
  message: { role: "user", content: text },
@@ -407,6 +482,37 @@ export class ClaudeAdapter {
407
482
  while (this.stderrChunks.join("").length > 8 * 1024)
408
483
  this.stderrChunks.shift();
409
484
  }
485
+ observationContext() {
486
+ const observer = this.opts.observer;
487
+ const inherited = observer.context();
488
+ return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
489
+ stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
490
+ ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
491
+ parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
492
+ }
493
+ observeToolResults(message) {
494
+ const body = message.message;
495
+ if (!Array.isArray(body?.content))
496
+ return;
497
+ for (const block of body.content) {
498
+ if (block.type !== "tool_result")
499
+ continue;
500
+ const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
501
+ if (!callId)
502
+ continue;
503
+ const spanId = this.toolSpans.get(callId);
504
+ const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
505
+ const sanitized = text.replace(/\b(?:token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]");
506
+ const failed = block.is_error === true;
507
+ const outputDigest = createHash("sha256").update(text).digest("hex");
508
+ this.eventQueue.push({ kind: "tool", name: "tool_result", input: {}, inputCompleteness: "unavailable", lifecycle: "completed", callId, status: failed ? "failed" : "completed", outputSummary: sanitized.slice(0, 1000), outputDigest, outputCompleteness: block.is_truncated === true ? "truncated" : "complete", rawOutputBytes: Buffer.byteLength(text), completionKnown: true, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
509
+ if (spanId && this.opts.observer) {
510
+ this.opts.observer.store.finishSpan(spanId, { outcome: failed ? "failed" : "completed", completionKnown: true, attributes: { callId, outputSummary: sanitized.slice(0, 1000), outputDigest } });
511
+ this.toolSpans.delete(callId);
512
+ }
513
+ this.opts.observer?.signal(true);
514
+ }
515
+ }
410
516
  sessionId() {
411
517
  return this._sessionId;
412
518
  }
@@ -430,6 +536,11 @@ export class ClaudeAdapter {
430
536
  });
431
537
  }
432
538
  async compact() {
539
+ return this.opts.observer
540
+ ? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
541
+ : this.compactInternal();
542
+ }
543
+ async compactInternal() {
433
544
  this.compactResult = undefined;
434
545
  this.manualCompactionInFlight = true;
435
546
  let result;
@@ -553,8 +664,16 @@ export class ClaudeAdapter {
553
664
  if (!pending)
554
665
  return;
555
666
  this.pending = undefined;
667
+ if (!result.turnId) {
668
+ result.turnId = pending.turnId;
669
+ result.hostInstruction = pending.instruction;
670
+ result.providerInstruction = pending.instruction;
671
+ result.rawResponse = result.text;
672
+ result.cleanedResponse = result.text;
673
+ result.providerMetadata = { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef };
674
+ }
556
675
  if (emit)
557
- this.eventQueue.push({ kind: "turn-complete", result });
676
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
558
677
  pending.resolve(result);
559
678
  }
560
679
  streamFailureResult(message, sessionUnavailable) {
@@ -575,6 +694,11 @@ export class ClaudeAdapter {
575
694
  }
576
695
  }
577
696
  function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
697
+ function nonNegativeDelta(current, prior) {
698
+ if (current === undefined)
699
+ return undefined;
700
+ return prior === undefined ? current : Math.max(0, current - prior);
701
+ }
578
702
  function validThreshold(value) {
579
703
  if (!Number.isInteger(value) || value < 1 || value > 99)
580
704
  throw new Error("automatic compaction threshold must be an integer from 1 to 99");
@@ -595,6 +719,8 @@ function activityPhase(phase) {
595
719
  return "reviewing with QA";
596
720
  if (phase === "uninstaller")
597
721
  return "planning uninstall";
722
+ if (phase === "manager")
723
+ return "analyzing diagnostics";
598
724
  return "building";
599
725
  }
600
726
  function formatClaudeFailure(failure) {
@@ -35,6 +35,9 @@ export declare class CodexAdapter implements BuilderAdapter {
35
35
  private nativeCompactionSequence;
36
36
  private manualCompactionInFlight;
37
37
  private observedToolCalls;
38
+ private activeProviderTurnId?;
39
+ private activeProviderTurnSpanId?;
40
+ private readonly toolSpans;
38
41
  constructor(opts: BuilderAdapterOptions);
39
42
  buildInstruction(instruction: string): string;
40
43
  buildAppServerArgs(): string[];
@@ -43,6 +46,7 @@ export declare class CodexAdapter implements BuilderAdapter {
43
46
  sendTurn(instruction: string): Promise<TurnResult>;
44
47
  private sendTurnInternal;
45
48
  compact(): Promise<CompactResult>;
49
+ private compactInternal;
46
50
  prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
47
51
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
48
52
  drainNativeCompactions(): NativeCompaction[];
@@ -63,6 +67,7 @@ export declare class CodexAdapter implements BuilderAdapter {
63
67
  private request;
64
68
  private write;
65
69
  private handle;
70
+ private observationContext;
66
71
  private waitFor;
67
72
  private disconnect;
68
73
  private restartForAutoCompaction;
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { createInterface } from "node:readline";
@@ -51,6 +52,9 @@ export class CodexAdapter {
51
52
  nativeCompactionSequence = 0;
52
53
  manualCompactionInFlight = false;
53
54
  observedToolCalls = 0;
55
+ activeProviderTurnId;
56
+ activeProviderTurnSpanId;
57
+ toolSpans = new Map();
54
58
  constructor(opts) {
55
59
  this.opts = opts;
56
60
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
@@ -82,13 +86,45 @@ export class CodexAdapter {
82
86
  return args;
83
87
  }
84
88
  async sendTurn(instruction) {
85
- return withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
89
+ const turnId = randomUUID();
90
+ this.activeProviderTurnId = turnId;
91
+ const observer = this.opts.observer;
92
+ if (!observer) {
93
+ try {
94
+ return await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
95
+ }
96
+ finally {
97
+ this.activeProviderTurnId = undefined;
98
+ }
99
+ }
100
+ const context = this.observationContext();
101
+ const spanId = observer.store.startSpan(context, { spanId: turnId, kind: "provider_turn", name: `Codex ${activityPhase(this.opts.runtimePhase)}`, providerTurnId: turnId, attributes: { provider: "codex" } });
102
+ this.activeProviderTurnSpanId = spanId;
103
+ observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "codex", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
104
+ try {
105
+ const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
106
+ observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { usage: result.usage } });
107
+ return result;
108
+ }
109
+ catch (error) {
110
+ observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
111
+ throw error;
112
+ }
113
+ finally {
114
+ for (const [callId, toolSpanId] of this.toolSpans) {
115
+ observer.store.finishSpan(toolSpanId, { outcome: "unknown", completionKnown: false });
116
+ this.toolSpans.delete(callId);
117
+ }
118
+ this.activeProviderTurnId = undefined;
119
+ this.activeProviderTurnSpanId = undefined;
120
+ }
86
121
  }
87
122
  async sendTurnInternal(instruction) {
88
123
  if (this.closed)
89
124
  throw new Error("builder is closed");
90
125
  this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
91
126
  let turnStartDispatched = false;
127
+ const providerInstruction = this.buildInstruction(instruction);
92
128
  try {
93
129
  await this.ensureThread();
94
130
  this.activeText = [];
@@ -97,7 +133,7 @@ export class CodexAdapter {
97
133
  turnStartDispatched = true;
98
134
  await this.request("turn/start", {
99
135
  threadId: this._sessionId,
100
- input: [{ type: "text", text: this.buildInstruction(instruction), text_elements: [] }],
136
+ input: [{ type: "text", text: providerInstruction, text_elements: [] }],
101
137
  cwd: this.opts.cwd,
102
138
  model: this.opts.model ?? null,
103
139
  effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
@@ -110,16 +146,27 @@ export class CodexAdapter {
110
146
  const result = {
111
147
  text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
112
148
  isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
149
+ turnId: this.activeProviderTurnId, hostInstruction: instruction, providerInstruction,
150
+ rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
151
+ providerMetadata: { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef },
113
152
  ...(this.lastTurnTokens ?? {}),
114
153
  };
115
- this.eventQueue.push({ kind: "turn-complete", result });
154
+ result.usage = { scope: "turn-delta", inputTokens: result.inputTokens, outputTokens: result.outputTokens,
155
+ ...(result.inputTokens !== undefined || result.outputTokens !== undefined ? { totalTokens: (result.inputTokens ?? 0) + (result.outputTokens ?? 0) } : {}) };
156
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
116
157
  return result;
117
158
  }
118
159
  catch (error) {
119
160
  if (error instanceof SessionUnavailableError) {
120
161
  const result = sessionUnavailableResult(error);
121
162
  this.eventQueue.push({ kind: "error", message: result.text });
122
- this.eventQueue.push({ kind: "turn-complete", result });
163
+ result.turnId = this.activeProviderTurnId;
164
+ result.hostInstruction = instruction;
165
+ result.providerInstruction = providerInstruction;
166
+ result.rawResponse = result.text;
167
+ result.cleanedResponse = result.text;
168
+ result.providerMetadata = { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef };
169
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
123
170
  return result;
124
171
  }
125
172
  const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
@@ -134,11 +181,22 @@ export class CodexAdapter {
134
181
  }))
135
182
  : { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false };
136
183
  this.eventQueue.push({ kind: "error", message: text });
137
- this.eventQueue.push({ kind: "turn-complete", result });
184
+ result.turnId = this.activeProviderTurnId;
185
+ result.hostInstruction = instruction;
186
+ result.providerInstruction = providerInstruction;
187
+ result.rawResponse = result.text;
188
+ result.cleanedResponse = result.text;
189
+ result.providerMetadata = { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef };
190
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
138
191
  return result;
139
192
  }
140
193
  }
141
194
  async compact() {
195
+ return this.opts.observer
196
+ ? this.opts.observer.span("compaction", "Codex context compaction", () => this.compactInternal())
197
+ : this.compactInternal();
198
+ }
199
+ async compactInternal() {
142
200
  try {
143
201
  await this.ensureThread();
144
202
  this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
@@ -418,20 +476,43 @@ export class CodexAdapter {
418
476
  const params = message.params ?? {};
419
477
  if (message.method === "item/started") {
420
478
  const item = params.item;
421
- if (isCodexToolItem(item))
479
+ if (isCodexToolItem(item)) {
422
480
  this.observedToolCalls += 1;
481
+ const callId = codexItemId(item) ?? randomUUID();
482
+ const name = codexToolName(item);
483
+ this.eventQueue.push({ kind: "tool", name, input: exactToolInput(item), inputCompleteness: "complete", lifecycle: "started", callId, completionKnown: false, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
484
+ if (this.opts.observer) {
485
+ const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: boundedToolInput(item) } });
486
+ this.toolSpans.set(callId, spanId);
487
+ this.opts.observer.signal(true);
488
+ }
489
+ }
423
490
  const activity = codexItemActivity(item);
424
491
  if (activity)
425
492
  this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
426
493
  }
427
494
  else if (message.method === "item/completed") {
428
495
  const item = params.item;
496
+ if (isCodexToolItem(item)) {
497
+ const callId = codexItemId(item);
498
+ const spanId = callId ? this.toolSpans.get(callId) : undefined;
499
+ const status = typeof item?.status === "string" ? item.status : undefined;
500
+ const exitCode = finiteNumber(item?.exitCode ?? item?.exit_code);
501
+ const durationMs = finiteNumber(item?.durationMs ?? item?.duration_ms);
502
+ const output = codexOutputSummary(item);
503
+ this.eventQueue.push({ kind: "tool", name: codexToolName(item), input: exactToolInput(item), inputCompleteness: "complete", lifecycle: "completed", callId, status, durationMs, exitCode, outputSummary: output?.summary, outputDigest: output?.digest, outputCompleteness: output?.completeness ?? "unavailable", rawOutputBytes: output?.bytes, completionKnown: true, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
504
+ if (spanId && this.opts.observer) {
505
+ this.opts.observer.store.finishSpan(spanId, { outcome: status ?? (exitCode === undefined || exitCode === 0 ? "completed" : "failed"), completionKnown: true, attributes: { callId, providerDurationMs: durationMs, exitCode, outputSummary: output?.summary, outputDigest: output?.digest } });
506
+ this.toolSpans.delete(callId);
507
+ }
508
+ this.opts.observer?.signal(true);
509
+ }
429
510
  if (item?.type === "agentMessage" && typeof item.text === "string") {
430
511
  this.activeText.push(item.text);
431
- this.eventQueue.push({ kind: "text", text: item.text });
512
+ this.eventQueue.push({ kind: "text", text: item.text, byteCount: Buffer.byteLength(item.text), digest: createHash("sha256").update(item.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
513
+ this.opts.observer?.signal(true);
432
514
  }
433
- else if (item?.type === "commandExecution") {
434
- this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
515
+ else if (item?.type === "commandExecution") { /* tool completion was emitted above */
435
516
  }
436
517
  else if (item?.type === "contextCompaction" && !this.manualCompactionInFlight) {
437
518
  // Completion is the provider's authoritative confirmation. Token-usage
@@ -485,8 +566,18 @@ export class CodexAdapter {
485
566
  else if (message.method === "error") {
486
567
  const error = params.error;
487
568
  const reason = String(error?.message ?? params.message ?? "Codex app-server error");
488
- if (params.willRetry === true)
489
- this.eventQueue.push({ kind: "retry", provider: "codex", reason, managedBy: "provider" });
569
+ if (params.willRetry === true) {
570
+ const retryId = randomUUID();
571
+ this.eventQueue.push(this.opts.observer
572
+ ? { kind: "retry", provider: "codex", reason, managedBy: "provider", retryId, providerTurnId: this.activeProviderTurnId, eventId: retryId, observedAt: new Date().toISOString() }
573
+ : { kind: "retry", provider: "codex", reason, managedBy: "provider" });
574
+ if (this.opts.observer) {
575
+ const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryId, kind: "retry", name: "Codex provider retry", providerTurnId: this.activeProviderTurnId,
576
+ attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
577
+ this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryId, spanId: retrySpanId, severity: "warning", attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
578
+ this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
579
+ }
580
+ }
490
581
  else
491
582
  this.eventQueue.push({ kind: "error", message: reason });
492
583
  }
@@ -496,6 +587,14 @@ export class CodexAdapter {
496
587
  waiter.predicate(params) ? waiter.resolve(params) : remaining.push(waiter);
497
588
  this.notificationWaiters.set(message.method, remaining);
498
589
  }
590
+ observationContext() {
591
+ const observer = this.opts.observer;
592
+ const inherited = observer.context();
593
+ return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
594
+ stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
595
+ ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
596
+ parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
597
+ }
499
598
  waitFor(method, predicate, timeoutMs) {
500
599
  return new Promise((resolve, reject) => {
501
600
  let timeout;
@@ -571,6 +670,8 @@ function activityPhase(phase) {
571
670
  return "reviewing with QA";
572
671
  if (phase === "uninstaller")
573
672
  return "planning uninstall";
673
+ if (phase === "manager")
674
+ return "analyzing diagnostics";
574
675
  return "building";
575
676
  }
576
677
  function codexItemActivity(item, completed = false) {
@@ -596,6 +697,15 @@ function codexItemActivity(item, completed = false) {
596
697
  function isCodexToolItem(item) {
597
698
  return item?.type === "commandExecution" || item?.type === "mcpToolCall" || item?.type === "dynamicToolCall" || item?.type === "webSearch" || item?.type === "fileChange";
598
699
  }
700
+ function codexItemId(item) { const value = item?.id ?? item?.itemId ?? item?.callId; return typeof value === "string" && value ? value : undefined; }
701
+ function codexToolName(item) { return String(item?.tool ?? item?.name ?? item?.type ?? "tool"); }
702
+ function exactToolInput(item) { if (!item)
703
+ return {}; return Object.fromEntries(Object.entries(item).filter(([key]) => ["command", "tool", "name", "type", "path", "query", "arguments", "input"].includes(key))); }
704
+ function boundedToolInput(item) { if (!item)
705
+ return {}; return Object.fromEntries(Object.entries(exactToolInput(item)).map(([key, value]) => [key, typeof value === "string" ? value.slice(0, 1000) : value])); }
706
+ function codexOutputSummary(item) { const value = item?.aggregatedOutput ?? item?.output ?? item?.stdout; if (typeof value !== "string" || !value)
707
+ return undefined; const sanitized = value.replace(/\b(?:token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]"); const providerTruncated = item?.outputTruncated === true || item?.truncated === true; return { summary: sanitized.slice(0, 1000), digest: createHash("sha256").update(value).digest("hex"), completeness: providerTruncated ? "truncated" : "complete", bytes: Buffer.byteLength(value) }; }
708
+ function finiteNumber(value) { const result = Number(value); return Number.isFinite(result) ? result : undefined; }
599
709
  function loadSkillMarkdown(cwd, skill) {
600
710
  const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
601
711
  if (existsSync(projectPath))
@@ -1,6 +1,7 @@
1
1
  import type { AgentRuntime } from "../runtimeAuth.js";
2
2
  import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, TurnResult } from "./types.js";
3
3
  import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
4
+ import type { RunObserver } from "../observability.js";
4
5
  export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
5
6
  export interface RecoveringAdapterOptions {
6
7
  initial: BuilderAdapter;
@@ -12,6 +13,7 @@ export interface RecoveringAdapterOptions {
12
13
  choose?: (result: TurnResult, runtime: AgentRuntime, otherRuntime: AgentRuntime) => Promise<TurnRecoveryChoice>;
13
14
  onRuntimeChange?: (runtime: AgentRuntime) => void;
14
15
  onSessionRef?: (ref: ProviderSessionRefV1) => void;
16
+ observer?: RunObserver;
15
17
  }
16
18
  /** Adds interactive recovery around structured runtime turn failures. */
17
19
  export declare class RecoveringAdapter implements BuilderAdapter {
@@ -1,6 +1,7 @@
1
1
  import { isCancel, log, select } from "@clack/prompts";
2
2
  import { AsyncQueue } from "../util/asyncQueue.js";
3
3
  import { pauseActivityForInput, reportBuilderEvent } from "../activity.js";
4
+ import { randomUUID } from "node:crypto";
4
5
  /** Adds interactive recovery around structured runtime turn failures. */
5
6
  export class RecoveringAdapter {
6
7
  opts;
@@ -39,7 +40,9 @@ export class RecoveringAdapter {
39
40
  const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
40
41
  const choice = this.opts.choose
41
42
  ? await this.opts.choose(result, this.runtime, otherRuntime)
42
- : await pauseActivityForInput(() => promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch));
43
+ : await (this.opts.observer
44
+ ? this.opts.observer.span("user_wait", "runtime recovery choice", () => pauseActivityForInput(() => promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch)))
45
+ : pauseActivityForInput(() => promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch)));
43
46
  if (choice === "cancel")
44
47
  return result;
45
48
  if (choice === "switch" && !this.opts.allowSwitch)
@@ -49,6 +52,13 @@ export class RecoveringAdapter {
49
52
  : { kind: "activity", provider: otherRuntime, state: `switching to ${otherRuntime}`, detail: "starting a fresh provider session" };
50
53
  this.eventQueue.push(recoveryEvent);
51
54
  reportBuilderEvent(recoveryEvent);
55
+ if (choice === "retry" && this.opts.observer) {
56
+ const retryId = randomUUID();
57
+ const context = this.opts.observer.context();
58
+ const spanId = this.opts.observer.store.startSpan(context, { spanId: retryId, kind: "retry", name: "Rafi recovery retry", attributes: { provider: this.runtime, managedBy: "rafi" } });
59
+ this.opts.observer.store.recordEvent(context, "retry", { eventId: retryId, spanId, severity: "warning", attributes: { provider: this.runtime, managedBy: "rafi" } });
60
+ this.opts.observer.store.finishSpan(spanId, { outcome: "scheduled" });
61
+ }
52
62
  const nextRuntime = choice === "switch" ? otherRuntime : this.runtime;
53
63
  const resumeSessionId = choice === "retry" ? this.adapter.sessionId() : undefined;
54
64
  const resumeSessionRef = choice === "retry" ? this.adapter.sessionRef?.() : undefined;