ai-foreman 1.7.12 → 1.7.14

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 (91) hide show
  1. package/README.md +47 -2
  2. package/dist/activity.d.ts +5 -1
  3. package/dist/activity.js +1 -1
  4. package/dist/adapters/claude.d.ts +12 -0
  5. package/dist/adapters/claude.js +255 -23
  6. package/dist/adapters/codex.d.ts +7 -0
  7. package/dist/adapters/codex.js +149 -17
  8. package/dist/adapters/recovering.d.ts +4 -0
  9. package/dist/adapters/recovering.js +17 -1
  10. package/dist/adapters/types.d.ts +69 -1
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +28 -4
  13. package/dist/branch/currentGuard.d.ts +2 -0
  14. package/dist/branch/currentGuard.js +9 -0
  15. package/dist/branch/finalization.d.ts +22 -0
  16. package/dist/branch/finalization.js +120 -0
  17. package/dist/branch/git.js +7 -2
  18. package/dist/branch/resume.js +11 -1
  19. package/dist/branch/runner.d.ts +12 -3
  20. package/dist/branch/runner.js +381 -125
  21. package/dist/buildRuns.d.ts +7 -2
  22. package/dist/buildRuns.js +109 -35
  23. package/dist/cli/doctor.d.ts +7 -0
  24. package/dist/cli/doctor.js +84 -1
  25. package/dist/cli/manager.d.ts +17 -0
  26. package/dist/cli/manager.js +135 -0
  27. package/dist/cli/recovery.d.ts +4 -0
  28. package/dist/cli/recovery.js +80 -0
  29. package/dist/cli/start.js +543 -79
  30. package/dist/cli/state.d.ts +4 -0
  31. package/dist/cli/state.js +69 -0
  32. package/dist/cli/status.js +13 -2
  33. package/dist/cli/tickets.js +26 -8
  34. package/dist/config.d.ts +2 -0
  35. package/dist/config.js +23 -0
  36. package/dist/continuity.d.ts +4 -0
  37. package/dist/continuity.js +38 -10
  38. package/dist/diagnostics.d.ts +25 -0
  39. package/dist/diagnostics.js +428 -0
  40. package/dist/foreman.d.ts +34 -6
  41. package/dist/foreman.js +164 -90
  42. package/dist/handoffs.d.ts +10 -3
  43. package/dist/handoffs.js +94 -10
  44. package/dist/index.js +8 -0
  45. package/dist/log.d.ts +2 -2
  46. package/dist/log.js +4 -1
  47. package/dist/managerPacket.d.ts +25 -0
  48. package/dist/managerPacket.js +83 -0
  49. package/dist/markers.d.ts +1 -1
  50. package/dist/markers.js +4 -1
  51. package/dist/observability.d.ts +248 -0
  52. package/dist/observability.js +1018 -0
  53. package/dist/planChanges.d.ts +4 -0
  54. package/dist/planChanges.js +16 -0
  55. package/dist/projectDiagnostics.d.ts +27 -0
  56. package/dist/projectDiagnostics.js +407 -0
  57. package/dist/qaFailureDelivery.d.ts +100 -0
  58. package/dist/qaFailureDelivery.js +570 -0
  59. package/dist/qaProtocolV2.d.ts +233 -0
  60. package/dist/qaProtocolV2.js +114 -0
  61. package/dist/qaRecovery.d.ts +152 -0
  62. package/dist/qaRecovery.js +989 -0
  63. package/dist/qaReview.d.ts +153 -14
  64. package/dist/qaReview.js +1747 -71
  65. package/dist/qaRuntime.d.ts +14 -0
  66. package/dist/qaRuntime.js +44 -0
  67. package/dist/qaSnapshot.d.ts +65 -5
  68. package/dist/qaSnapshot.js +473 -130
  69. package/dist/recoveryPolicy.d.ts +37 -0
  70. package/dist/recoveryPolicy.js +251 -0
  71. package/dist/roles.js +1 -0
  72. package/dist/stateTransfer.d.ts +57 -0
  73. package/dist/stateTransfer.js +644 -0
  74. package/dist/statusReporter.d.ts +2 -0
  75. package/dist/statusReporter.js +6 -0
  76. package/dist/supervisor.d.ts +40 -0
  77. package/dist/supervisor.js +102 -0
  78. package/dist/ticketHistoryReader.d.ts +19 -0
  79. package/dist/ticketHistoryReader.js +58 -0
  80. package/dist/tickets/details.d.ts +8 -0
  81. package/dist/tickets/details.js +59 -11
  82. package/dist/tickets/output.d.ts +5 -0
  83. package/dist/tickets/output.js +73 -0
  84. package/dist/tickets/stateDb.d.ts +1 -0
  85. package/dist/tickets/stateDb.js +5 -0
  86. package/dist/workflowDb.d.ts +275 -3
  87. package/dist/workflowDb.js +839 -29
  88. package/dist/workflowReader.d.ts +42 -0
  89. package/dist/workflowReader.js +171 -0
  90. package/foreman.yaml +9 -0
  91. package/package.json +63 -3
@@ -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" });
@@ -190,6 +248,8 @@ export class CodexAdapter {
190
248
  // setup turn, then restart the app server with the provider-native ceiling
191
249
  // before any Builder or QA work is sent.
192
250
  if (!this.usage?.maximum) {
251
+ if (this.opts.allowAutoCompactionSetupTurn === false)
252
+ return;
193
253
  const toolsBefore = this.observedToolCalls;
194
254
  const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
195
255
  if (setup.isError)
@@ -214,6 +274,9 @@ export class CodexAdapter {
214
274
  };
215
275
  return this.preparedAutoCompactionPolicy;
216
276
  }
277
+ requiresAutoCompactionSetupTurn() {
278
+ return this.opts.autoCompactThresholdPercent !== undefined && !this.autoCompactionPrepared && !this.usage?.maximum;
279
+ }
217
280
  autoCompactionPolicy() { return this.preparedAutoCompactionPolicy; }
218
281
  drainNativeCompactions() {
219
282
  const pending = this.nativeCompactions;
@@ -242,6 +305,14 @@ export class CodexAdapter {
242
305
  }
243
306
  sessionId() { return this._sessionId; }
244
307
  sessionRef() { return this._sessionRef; }
308
+ async prepareSession() {
309
+ await this.ensureThread();
310
+ if (!this._sessionRef)
311
+ throw new Error("Codex did not expose a scoped thread identity during session preparation");
312
+ if (!this._sessionRef.validatedAt)
313
+ this._sessionRef = { ...this._sessionRef, validatedAt: new Date().toISOString() };
314
+ return this._sessionRef;
315
+ }
245
316
  adoptSessionRef(ref) {
246
317
  if (ref.provider !== "codex" || ref.sessionId !== this._sessionId)
247
318
  throw new Error("cannot adopt a session reference for a different Codex thread");
@@ -337,20 +408,22 @@ export class CodexAdapter {
337
408
  availability,
338
409
  });
339
410
  }
340
- this._sessionId = returnedSessionId;
341
- if (!this._sessionId)
411
+ if (!returnedSessionId?.trim())
342
412
  throw new Error(`${method} did not return a thread ID`);
343
413
  const providerCwd = typeof thread?.cwd === "string" ? canonicalSessionPath(thread.cwd) : undefined;
344
- if (this._sessionRef && providerCwd && providerCwd !== canonicalSessionPath(this._sessionRef.cwd)) {
345
- const availability = { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "cwd-mismatch", observedCwd: providerCwd, detail: `Codex thread cwd ${providerCwd} does not match ${this._sessionRef.cwd}`, sessionRef: this._sessionRef };
414
+ const expectedCwd = canonicalSessionPath(this._sessionRef?.cwd ?? this.opts.cwd);
415
+ if ((providerCwd && providerCwd !== expectedCwd) || (!providerCwd && this.opts.sessionRole === "qa")) {
416
+ const availability = { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "cwd-mismatch", observedCwd: providerCwd, detail: `Codex thread cwd ${providerCwd ?? "was not returned"} does not match ${expectedCwd}`, sessionRef: this._sessionRef };
346
417
  this.sessionAvailability = availability;
347
418
  throw new SessionUnavailableError({ runtime: "codex", phase: "attach", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: availability.detail, availability });
348
419
  }
420
+ this._sessionId = returnedSessionId;
349
421
  if (!this._sessionRef) {
350
422
  this._sessionRef = createProviderSessionRef({
351
423
  provider: "codex", sessionId: this._sessionId, cwd: providerCwd ?? this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
352
424
  role: this.opts.sessionRole, stream: this.opts.sessionStream, generation: this.opts.sessionGeneration,
353
425
  workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
426
+ validatedAt: new Date().toISOString(),
354
427
  });
355
428
  }
356
429
  else {
@@ -418,20 +491,47 @@ export class CodexAdapter {
418
491
  const params = message.params ?? {};
419
492
  if (message.method === "item/started") {
420
493
  const item = params.item;
421
- if (isCodexToolItem(item))
494
+ if (item)
495
+ this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "started", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
496
+ if (isCodexToolItem(item)) {
422
497
  this.observedToolCalls += 1;
498
+ const callId = codexItemId(item) ?? randomUUID();
499
+ const name = codexToolName(item);
500
+ 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() });
501
+ if (this.opts.observer) {
502
+ const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: boundedToolInput(item) } });
503
+ this.toolSpans.set(callId, spanId);
504
+ this.opts.observer.signal(true);
505
+ }
506
+ }
423
507
  const activity = codexItemActivity(item);
424
508
  if (activity)
425
509
  this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
426
510
  }
427
511
  else if (message.method === "item/completed") {
428
512
  const item = params.item;
513
+ if (item)
514
+ this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "completed", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
515
+ if (isCodexToolItem(item)) {
516
+ const callId = codexItemId(item);
517
+ const spanId = callId ? this.toolSpans.get(callId) : undefined;
518
+ const status = typeof item?.status === "string" ? item.status : undefined;
519
+ const exitCode = finiteNumber(item?.exitCode ?? item?.exit_code);
520
+ const durationMs = finiteNumber(item?.durationMs ?? item?.duration_ms);
521
+ const output = codexOutputSummary(item);
522
+ 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() });
523
+ if (spanId && this.opts.observer) {
524
+ 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 } });
525
+ this.toolSpans.delete(callId);
526
+ }
527
+ this.opts.observer?.signal(true);
528
+ }
429
529
  if (item?.type === "agentMessage" && typeof item.text === "string") {
430
530
  this.activeText.push(item.text);
431
- this.eventQueue.push({ kind: "text", text: item.text });
531
+ 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() });
532
+ this.opts.observer?.signal(true);
432
533
  }
433
- else if (item?.type === "commandExecution") {
434
- this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
534
+ else if (item?.type === "commandExecution") { /* tool completion was emitted above */
435
535
  }
436
536
  else if (item?.type === "contextCompaction" && !this.manualCompactionInFlight) {
437
537
  // Completion is the provider's authoritative confirmation. Token-usage
@@ -485,8 +585,18 @@ export class CodexAdapter {
485
585
  else if (message.method === "error") {
486
586
  const error = params.error;
487
587
  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" });
588
+ if (params.willRetry === true) {
589
+ const retryId = randomUUID();
590
+ this.eventQueue.push(this.opts.observer
591
+ ? { kind: "retry", provider: "codex", reason, managedBy: "provider", retryId, providerTurnId: this.activeProviderTurnId, eventId: retryId, observedAt: new Date().toISOString() }
592
+ : { kind: "retry", provider: "codex", reason, managedBy: "provider" });
593
+ if (this.opts.observer) {
594
+ const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryId, kind: "retry", name: "Codex provider retry", providerTurnId: this.activeProviderTurnId,
595
+ attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
596
+ this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryId, spanId: retrySpanId, severity: "warning", attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
597
+ this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
598
+ }
599
+ }
490
600
  else
491
601
  this.eventQueue.push({ kind: "error", message: reason });
492
602
  }
@@ -496,6 +606,14 @@ export class CodexAdapter {
496
606
  waiter.predicate(params) ? waiter.resolve(params) : remaining.push(waiter);
497
607
  this.notificationWaiters.set(message.method, remaining);
498
608
  }
609
+ observationContext() {
610
+ const observer = this.opts.observer;
611
+ const inherited = observer.context();
612
+ return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
613
+ stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
614
+ ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
615
+ parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
616
+ }
499
617
  waitFor(method, predicate, timeoutMs) {
500
618
  return new Promise((resolve, reject) => {
501
619
  let timeout;
@@ -544,6 +662,9 @@ export class CodexAdapter {
544
662
  child.kill("SIGTERM");
545
663
  }
546
664
  buildSkillsAppendix() {
665
+ if (this.opts.preloadedSkillContent?.length) {
666
+ return ["# Preloaded Skills", "Use the following skills for this run.", ...this.opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n");
667
+ }
547
668
  if (!this.opts.skills?.length)
548
669
  return undefined;
549
670
  const blocks = this.opts.skills.map((skill) => loadSkillMarkdown(this.opts.cwd, skill)).filter((block) => Boolean(block));
@@ -571,6 +692,8 @@ function activityPhase(phase) {
571
692
  return "reviewing with QA";
572
693
  if (phase === "uninstaller")
573
694
  return "planning uninstall";
695
+ if (phase === "manager")
696
+ return "analyzing diagnostics";
574
697
  return "building";
575
698
  }
576
699
  function codexItemActivity(item, completed = false) {
@@ -596,9 +719,18 @@ function codexItemActivity(item, completed = false) {
596
719
  function isCodexToolItem(item) {
597
720
  return item?.type === "commandExecution" || item?.type === "mcpToolCall" || item?.type === "dynamicToolCall" || item?.type === "webSearch" || item?.type === "fileChange";
598
721
  }
722
+ function codexItemId(item) { const value = item?.id ?? item?.itemId ?? item?.callId; return typeof value === "string" && value ? value : undefined; }
723
+ function codexToolName(item) { return String(item?.tool ?? item?.name ?? item?.type ?? "tool"); }
724
+ function exactProviderItem(item) { return structuredClone(item); }
725
+ function exactToolInput(item) { return item ? exactProviderItem(item) : {}; }
726
+ function boundedToolInput(item) { if (!item)
727
+ return {}; return Object.fromEntries(Object.entries(exactToolInput(item)).map(([key, value]) => [key, typeof value === "string" ? value.slice(0, 1000) : value])); }
728
+ function codexOutputSummary(item) { const value = item?.aggregatedOutput ?? item?.output ?? item?.stdout; if (typeof value !== "string" || !value)
729
+ 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) }; }
730
+ function finiteNumber(value) { const result = Number(value); return Number.isFinite(result) ? result : undefined; }
599
731
  function loadSkillMarkdown(cwd, skill) {
600
- const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
601
- if (existsSync(projectPath))
732
+ const projectPath = [join(cwd, ".codex", "skills", skill, "SKILL.md"), join(cwd, ".agents", "skills", skill, "SKILL.md")].find(existsSync);
733
+ if (projectPath)
602
734
  return `## ${skill}\n${readFileSync(projectPath, "utf8").trim()}`;
603
735
  try {
604
736
  const bundled = loadSkill(skill);
@@ -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 {
@@ -26,10 +28,12 @@ export declare class RecoveringAdapter implements BuilderAdapter {
26
28
  sendTurn(text: string): Promise<TurnResult>;
27
29
  sessionId(): string | undefined;
28
30
  sessionRef(): ProviderSessionRefV1 | undefined;
31
+ prepareSession(): Promise<ProviderSessionRefV1>;
29
32
  adoptSessionRef(ref: ProviderSessionRefV1): void;
30
33
  validateSession(): Promise<SessionAvailabilityV1>;
31
34
  compact(): Promise<CompactResult>;
32
35
  prepareAutoCompaction(thresholdPercent?: number): Promise<NativeAutoCompactionPolicy | void>;
36
+ requiresAutoCompactionSetupTurn(): boolean;
33
37
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
34
38
  drainNativeCompactions(): import("./types.js").NativeCompaction[];
35
39
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
@@ -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;
@@ -62,12 +72,18 @@ export class RecoveringAdapter {
62
72
  return this.adapter.sessionId();
63
73
  }
64
74
  sessionRef() { return this.adapter.sessionRef?.(); }
75
+ prepareSession() {
76
+ if (!this.adapter.prepareSession)
77
+ return Promise.reject(new Error("wrapped adapter cannot establish a provider session without a work turn"));
78
+ return this.adapter.prepareSession();
79
+ }
65
80
  adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
66
81
  validateSession() {
67
82
  return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "wrapped adapter does not expose scoped session validation" });
68
83
  }
69
84
  compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
70
85
  prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
86
+ requiresAutoCompactionSetupTurn() { return this.adapter.requiresAutoCompactionSetupTurn?.() ?? false; }
71
87
  autoCompactionPolicy() { return this.adapter.autoCompactionPolicy?.(); }
72
88
  drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
73
89
  restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
@@ -2,6 +2,7 @@
2
2
  * Agent-agnostic interface over a coding agent ("builder").
3
3
  */
4
4
  import type { ConfigurableAgentRole, ProviderSessionRefV1, RuntimeProbeCategory, RuntimeProbePhase, SessionAvailabilityV1 } from "rafi-spec";
5
+ import type { RunObserver } from "../observability.js";
5
6
  /** A permission request surfaced by a builder before it runs a tool. */
6
7
  export interface PermissionRequest {
7
8
  toolName: string;
@@ -58,16 +59,61 @@ export interface TurnResult {
58
59
  costAuthoritative?: boolean;
59
60
  inputTokens?: number;
60
61
  outputTokens?: number;
62
+ /** Explicit counter semantics. Compatibility counters above must not be summed without this scope. */
63
+ usage?: {
64
+ scope: "turn-delta" | "session-cumulative";
65
+ inputTokens?: number;
66
+ outputTokens?: number;
67
+ totalTokens?: number;
68
+ costUsd?: number;
69
+ };
61
70
  failure?: RuntimeFailure;
71
+ /** Stable host/provider correlation ID for this turn. */
72
+ turnId?: string;
73
+ /** Exact instruction requested by the immediate host caller. */
74
+ hostInstruction?: string;
75
+ /** Exact instruction dispatched to the provider after all wrappers. */
76
+ providerInstruction?: string;
77
+ /** Exact provider response before continuity or contract cleanup. */
78
+ rawResponse?: string;
79
+ /** Effective response consumed by the host contract parser. */
80
+ cleanedResponse?: string;
81
+ providerMetadata?: {
82
+ provider: "claude" | "codex";
83
+ sessionId?: string;
84
+ sessionRef?: ProviderSessionRefV1;
85
+ };
62
86
  }
63
87
  /** Observability events emitted while a builder works. */
64
- export type BuilderEvent = {
88
+ export type BuilderEvent = ({
65
89
  kind: "text";
66
90
  text: string;
91
+ byteCount?: number;
92
+ digest?: string;
67
93
  } | {
68
94
  kind: "tool";
69
95
  name: string;
70
96
  input: unknown;
97
+ inputCompleteness?: "complete" | "truncated" | "unavailable";
98
+ lifecycle?: "started" | "progress" | "completed";
99
+ callId?: string;
100
+ status?: string;
101
+ durationMs?: number;
102
+ exitCode?: number;
103
+ outputSummary?: string;
104
+ outputDigest?: string;
105
+ outputCompleteness?: "complete" | "truncated" | "unavailable";
106
+ rawOutputBytes?: number;
107
+ completionKnown?: boolean;
108
+ providerTurnId?: string;
109
+ } | {
110
+ kind: "provider-item";
111
+ provider: "claude" | "codex";
112
+ lifecycle: "started" | "completed";
113
+ itemType: string;
114
+ payload: Record<string, unknown>;
115
+ payloadCompleteness: "complete";
116
+ providerTurnId?: string;
71
117
  } | {
72
118
  kind: "activity";
73
119
  state: string;
@@ -83,9 +129,12 @@ export type BuilderEvent = {
83
129
  maximum?: number;
84
130
  delayMs?: number;
85
131
  managedBy: "provider" | "rafi";
132
+ retryId?: string;
133
+ providerTurnId?: string;
86
134
  } | {
87
135
  kind: "turn-complete";
88
136
  result: TurnResult;
137
+ turnId?: string;
89
138
  } | {
90
139
  kind: "session-transition";
91
140
  transition: "started" | "resumed" | "compacting" | "compacted" | "fresh-fallback";
@@ -100,6 +149,9 @@ export type BuilderEvent = {
100
149
  } | {
101
150
  kind: "error";
102
151
  message: string;
152
+ }) & {
153
+ eventId?: string;
154
+ observedAt?: string;
103
155
  };
104
156
  export interface ContextUsage {
105
157
  used: number;
@@ -183,8 +235,17 @@ export interface BuilderAdapterOptions {
183
235
  systemPromptAppend?: string;
184
236
  /** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
185
237
  skills?: string[];
238
+ /** Exact host-frozen skill text injected into the provider instruction. */
239
+ preloadedSkillContent?: Array<{
240
+ name: string;
241
+ content: string;
242
+ }>;
186
243
  /** Provider-native context ceiling, as a percentage of that provider's model window. */
187
244
  autoCompactThresholdPercent?: number;
245
+ /** QA forbids an unjournaled model turn merely to discover the context window. */
246
+ allowAutoCompactionSetupTurn?: boolean;
247
+ /** Raw-adapter observability; wrapper adapters must not persist re-emitted events. */
248
+ observer?: RunObserver;
188
249
  }
189
250
  export interface BuilderAdapter {
190
251
  readonly agent: "claude" | "codex";
@@ -194,6 +255,11 @@ export interface BuilderAdapter {
194
255
  sessionId(): string | undefined;
195
256
  /** Current location-scoped session reference, once known. */
196
257
  sessionRef?(): ProviderSessionRefV1 | undefined;
258
+ /**
259
+ * Establish and observe the exact provider session without dispatching a
260
+ * role/work turn. Durable QA calls this before binding its first turn.
261
+ */
262
+ prepareSession?(): Promise<ProviderSessionRefV1>;
197
263
  /** Host-only metadata promotion after a validated handoff is accepted. */
198
264
  adoptSessionRef?(ref: ProviderSessionRefV1): void;
199
265
  /** Attach/probe a requested exact session without starting a provider turn. */
@@ -206,6 +272,8 @@ export interface BuilderAdapter {
206
272
  * tool-heavy first turn is protected too.
207
273
  */
208
274
  prepareAutoCompaction?(thresholdPercent?: number): Promise<NativeAutoCompactionPolicy | void>;
275
+ /** True only when a provider needs one observable, tool-free turn to learn its context window. */
276
+ requiresAutoCompactionSetupTurn?(): boolean;
209
277
  /** The last provider-native automatic-compaction policy verified on this transport. */
210
278
  autoCompactionPolicy?(): NativeAutoCompactionPolicy | undefined;
211
279
  /** Consume provider-native compactions observed since the prior drain. */
@@ -22,6 +22,8 @@ export interface RoleBuilderOptions {
22
22
  permissionConfig?: PermissionConfig;
23
23
  extraSkills?: string[];
24
24
  sandboxMode?: BuilderAdapterOptions["sandboxMode"];
25
+ /** Disable recovery-session persistence for isolated, non-recoverable roles such as Manager. */
26
+ persistSessionBindings?: boolean;
25
27
  resumeSessionId?: string;
26
28
  /** Location-scoped exact session. Raw IDs are resolved through durable project bindings or treated as legacy candidates. */
27
29
  resumeSessionRef?: ProviderSessionRefV1;
package/dist/agentRun.js CHANGED
@@ -47,6 +47,22 @@ export function readOnlyPermissionConfig() {
47
47
  "git show --stat",
48
48
  "git ls-files",
49
49
  "git grep",
50
+ "npm test",
51
+ "npm run test",
52
+ "npm run typecheck",
53
+ "npm run lint",
54
+ "npm run check",
55
+ "pnpm test",
56
+ "pnpm run test",
57
+ "pnpm typecheck",
58
+ "pnpm lint",
59
+ "pnpm --filter ",
60
+ "yarn test",
61
+ "pytest",
62
+ "python -m pytest",
63
+ "python3 -m pytest",
64
+ "cargo test",
65
+ "go test",
50
66
  ],
51
67
  escalateBash: [
52
68
  ...DEFAULT_CONFIG.permissions.escalateBash,
@@ -60,10 +76,12 @@ export function readOnlyPermissionConfig() {
60
76
  "git restore",
61
77
  "git stash",
62
78
  "npm install",
63
- "npm run",
64
79
  "npm ci",
65
- "pnpm ",
66
- "yarn ",
80
+ "pnpm install",
81
+ "pnpm add",
82
+ "pnpm remove",
83
+ "yarn add",
84
+ "yarn install",
67
85
  "mkdir ",
68
86
  "touch ",
69
87
  "mv ",
@@ -199,6 +217,8 @@ export async function createRoleBuilder(opts) {
199
217
  };
200
218
  const initial = await makeAdapter(runtime, opts.resumeSessionId, requestedResumeRef);
201
219
  const persistSessionRef = (ref) => {
220
+ if (opts.persistSessionBindings === false)
221
+ return;
202
222
  const bindingDb = new WorkflowDb(opts.projectDir);
203
223
  try {
204
224
  bindingDb.recordProviderSessionBinding(ref);
@@ -240,6 +260,8 @@ function phaseForRole(role) {
240
260
  return "qa";
241
261
  if (role === "uninstaller")
242
262
  return "uninstaller";
263
+ if (role === "manager")
264
+ return "manager";
243
265
  return "builder";
244
266
  }
245
267
  export function readRoleDefaultsForExecution(projectDir, role) {
@@ -253,6 +275,8 @@ export function readRoleDefaultsForExecution(projectDir, role) {
253
275
  : undefined;
254
276
  if (!saved)
255
277
  return undefined;
278
+ if (role === "manager")
279
+ return saved;
256
280
  if (isFullyInitializedTracker(projectDir))
257
281
  return saved;
258
282
  const { make: _pendingMake, ...active } = saved;
@@ -333,7 +357,7 @@ export async function runRoleInstruction(opts) {
333
357
  }
334
358
  }
335
359
  function configurableRole(role) {
336
- return ["builder", "qa", "planner", "ticket-maker", "uninstaller"].includes(role)
360
+ return ["builder", "qa", "planner", "ticket-maker", "uninstaller", "manager"].includes(role)
337
361
  ? role
338
362
  : "builder";
339
363
  }
@@ -23,10 +23,12 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
23
23
  sendTurn(text: string): Promise<TurnResult>;
24
24
  sessionId(): string | undefined;
25
25
  sessionRef(): ProviderSessionRefV1 | undefined;
26
+ prepareSession(): Promise<ProviderSessionRefV1>;
26
27
  adoptSessionRef(ref: ProviderSessionRefV1): void;
27
28
  validateSession(): Promise<SessionAvailabilityV1>;
28
29
  compact(): Promise<CompactResult>;
29
30
  prepareAutoCompaction(thresholdPercent?: number): Promise<NativeAutoCompactionPolicy | void>;
31
+ requiresAutoCompactionSetupTurn(): boolean;
30
32
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
31
33
  drainNativeCompactions(): import("../adapters/types.js").NativeCompaction[];
32
34
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
@@ -45,10 +45,19 @@ export class CurrentWorkflowGuardAdapter {
45
45
  }
46
46
  sessionId() { return this.adapter.sessionId(); }
47
47
  sessionRef() { return this.adapter.sessionRef?.(); }
48
+ async prepareSession() {
49
+ assertCurrentWorkflowIdentity(this.cwd, this.expected);
50
+ if (!this.adapter.prepareSession)
51
+ throw new Error("wrapped adapter cannot establish a provider session without a work turn");
52
+ const ref = await this.adapter.prepareSession();
53
+ assertCurrentWorkflowIdentity(this.cwd, this.expected);
54
+ return ref;
55
+ }
48
56
  adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
49
57
  validateSession() { assertCurrentWorkflowIdentity(this.cwd, this.expected); return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
50
58
  async compact() { assertCurrentWorkflowIdentity(this.cwd, this.expected); const result = await (this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" })); assertCurrentWorkflowIdentity(this.cwd, this.expected); return result; }
51
59
  prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
60
+ requiresAutoCompactionSetupTurn() { return this.adapter.requiresAutoCompactionSetupTurn?.() ?? false; }
52
61
  autoCompactionPolicy() { return this.adapter.autoCompactionPolicy?.(); }
53
62
  drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
54
63
  restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }