ai-foreman 1.7.18 → 1.7.20

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 (99) hide show
  1. package/README.md +33 -8
  2. package/dist/activity.d.ts +11 -1
  3. package/dist/activity.js +70 -15
  4. package/dist/adapters/claude.d.ts +11 -1
  5. package/dist/adapters/claude.js +122 -18
  6. package/dist/adapters/codex.d.ts +13 -1
  7. package/dist/adapters/codex.js +148 -31
  8. package/dist/adapters/recovering.d.ts +3 -1
  9. package/dist/adapters/recovering.js +12 -4
  10. package/dist/adapters/types.d.ts +27 -1
  11. package/dist/agentRun.js +2 -0
  12. package/dist/branch/currentGuard.d.ts +3 -1
  13. package/dist/branch/currentGuard.js +11 -2
  14. package/dist/branch/resume.js +1 -1
  15. package/dist/branch/runner.d.ts +2 -0
  16. package/dist/branch/runner.js +92 -12
  17. package/dist/buildAdmission.d.ts +61 -0
  18. package/dist/buildAdmission.js +284 -0
  19. package/dist/buildApproval.d.ts +11 -0
  20. package/dist/buildApproval.js +143 -0
  21. package/dist/buildLaunchGate.d.ts +1 -0
  22. package/dist/buildLaunchGate.js +36 -0
  23. package/dist/buildRuns.d.ts +18 -1
  24. package/dist/buildRuns.js +261 -83
  25. package/dist/cli/recovery.js +9 -13
  26. package/dist/cli/runtimeAuthPrompt.d.ts +12 -0
  27. package/dist/cli/runtimeAuthPrompt.js +55 -6
  28. package/dist/cli/start.d.ts +24 -1
  29. package/dist/cli/start.js +2026 -1808
  30. package/dist/cli/tickets.d.ts +11 -0
  31. package/dist/cli/tickets.js +382 -145
  32. package/dist/continuity.d.ts +12 -2
  33. package/dist/continuity.js +131 -12
  34. package/dist/diagnostics.js +35 -1
  35. package/dist/foreman.d.ts +10 -3
  36. package/dist/foreman.js +207 -46
  37. package/dist/handoffs.js +121 -79
  38. package/dist/humanDecision.d.ts +18 -0
  39. package/dist/humanDecision.js +44 -0
  40. package/dist/index.js +4 -2
  41. package/dist/log.d.ts +1 -1
  42. package/dist/managerPacket.js +1 -0
  43. package/dist/observability.js +2 -1
  44. package/dist/processIdentity.d.ts +21 -1
  45. package/dist/processIdentity.js +80 -5
  46. package/dist/providerQuestions.d.ts +5 -0
  47. package/dist/providerQuestions.js +24 -3
  48. package/dist/qaDeliveryJournal.d.ts +70 -0
  49. package/dist/qaDeliveryJournal.js +1 -0
  50. package/dist/qaFailureDelivery.d.ts +26 -10
  51. package/dist/qaFailureDelivery.js +399 -412
  52. package/dist/qaHandbackHistory.d.ts +4 -0
  53. package/dist/qaHandbackHistory.js +30 -0
  54. package/dist/qaHandbackMigration.d.ts +9 -0
  55. package/dist/qaHandbackMigration.js +147 -0
  56. package/dist/qaPrerequisites.d.ts +26 -0
  57. package/dist/qaPrerequisites.js +61 -0
  58. package/dist/qaProtocolV2.d.ts +8 -0
  59. package/dist/qaProtocolV2.js +3 -3
  60. package/dist/qaReview.d.ts +8 -0
  61. package/dist/qaReview.js +113 -27
  62. package/dist/questionTrace.d.ts +31 -0
  63. package/dist/questionTrace.js +85 -0
  64. package/dist/readOnlyProposal.d.ts +8 -0
  65. package/dist/readOnlyProposal.js +41 -0
  66. package/dist/readinessCleanup.d.ts +19 -0
  67. package/dist/readinessCleanup.js +115 -0
  68. package/dist/readinessGate.d.ts +1 -0
  69. package/dist/readinessGate.js +82 -0
  70. package/dist/recoveryGuidance.d.ts +8 -0
  71. package/dist/recoveryGuidance.js +25 -0
  72. package/dist/recoveryPolicy.js +14 -3
  73. package/dist/runtimeAuth.d.ts +2 -1
  74. package/dist/runtimeAuth.js +2 -2
  75. package/dist/runtimeReadiness.d.ts +25 -1
  76. package/dist/runtimeReadiness.js +256 -14
  77. package/dist/sessionLifecycle.d.ts +1 -0
  78. package/dist/sessionLifecycle.js +53 -3
  79. package/dist/sources/sourceRegistry.d.ts +7 -0
  80. package/dist/sources/sourceRegistry.js +14 -0
  81. package/dist/stateTransfer.js +37 -7
  82. package/dist/statusReporter.d.ts +3 -1
  83. package/dist/statusReporter.js +8 -2
  84. package/dist/supervisedStart.d.ts +18 -0
  85. package/dist/supervisedStart.js +382 -0
  86. package/dist/supervisor.d.ts +2 -0
  87. package/dist/supervisor.js +70 -24
  88. package/dist/ticketPopulation.js +1 -0
  89. package/dist/tickets/setupConfig.d.ts +8 -0
  90. package/dist/tickets/setupConfig.js +38 -4
  91. package/dist/util/deadline.d.ts +13 -0
  92. package/dist/util/deadline.js +60 -0
  93. package/dist/windowsProbeJob.d.ts +12 -0
  94. package/dist/windowsProbeJob.js +144 -0
  95. package/dist/workflowDb.d.ts +125 -12
  96. package/dist/workflowDb.js +810 -72
  97. package/dist/workflowReader.d.ts +17 -2
  98. package/dist/workflowReader.js +118 -1
  99. package/package.json +8 -4
package/README.md CHANGED
@@ -15,9 +15,9 @@ Loop Claude Code or Codex through tickets.
15
15
  - **Rich ticket system** — tickets carry acceptance criteria, required tests, dependencies, priority, size, and risk level. The agent reads the full context before starting each step.
16
16
  - **Future work tracking** — when the builder discovers out-of-scope work during a run, `ai-foreman tickets discover` captures it without derailing the current ticket. Discovered items live in a separate inbox until you promote them.
17
17
  - **QA that actually gates** — QA runs after every completed ticket and checks code quality, test passing, and regression protection. The ticket only closes after QA passes. QA turns do not count against `--steps`.
18
- - **Visible long-running work** — semantic activity rows identify provider, phase, tools, retries, and quiet periods; cursor-capable terminals update changing counters in place, while record-oriented terminals coalesce them.
18
+ - **Visible long-running work** — semantic activity rows identify provider, phase, tools, retries, and quiet periods; safe record output coalesces spinner- and time-only changes.
19
19
 
20
- Provider-managed retries are reported immediately as durable lines. A quiet-provider warning appears after 60 seconds without a signal, but Foreman does not add retries, abort the provider, or change provider retry limits. Non-TTY output receives a heartbeat every 30 seconds while work remains pending. TTY rendering defaults to automatic detection: Codex, CI, and `TERM=dumb` use newline records and suppress spinner- or number-only repeats; other terminals redraw those changes in place and retain a row when the textual status changes. Use `RAFI_ACTIVITY_RENDER_MODE=auto|cursor|records` to override detection.
20
+ Provider-managed retries are reported immediately as durable lines. A quiet-provider warning appears after 60 seconds without a signal, but Foreman does not add retries, abort the provider, or change provider retry limits. Non-TTY output emits the initial and meaningful status changes without ANSI redraws. Automatic TTY output also uses newline records and suppresses spinner-, elapsed-time-, and number-only repeats, making it safe for transcript hosts. Set `RAFI_ACTIVITY_RENDER_MODE=cursor` to explicitly enable animated in-place redraws in a known real terminal; `records` explicitly forces safe records, while `auto` selects the safe default.
21
21
 
22
22
  ## Install
23
23
 
@@ -77,13 +77,14 @@ What each part does:
77
77
 
78
78
  - `./my-project`: the repo Foreman will work on.
79
79
  - `tickets init`: creates the local `.tickets/` tracker.
80
- - `tickets populate`: asks the ticket-maker role to convert project planning material.
80
+ - `tickets populate`: generates tickets from an approved structured Rafi plan, with optional supporting context.
81
81
  - Converted output uses Foreman's schema.
82
82
  - `start`: drives Codex through the next tickets one step at a time.
83
83
 
84
- `tickets populate` can convert:
84
+ In `approved_plan` mode, `tickets populate` requires a validated `<docs.root>/rafi-plan.json`. Supporting documents and URLs add detail but cannot replace its slices. `external_import` is a separate mode for importing configured Linear or Jira tickets.
85
+
86
+ Supporting context can include:
85
87
 
86
- - a managed Rafi plan at `<docs.root>/rafi-plan.md`
87
88
  - plans
88
89
  - TODOs
89
90
  - roadmap docs
@@ -161,11 +162,23 @@ Saved `tickets.build` defaults in `rafi-config.yaml` can enable branch-per-ticke
161
162
 
162
163
  With the saved `current` strategy, Foreman works in the active branch while the user owns Git. It may edit, test, run QA, and update tracker/recovery state, but all branch/worktree, commit, push, merge, rebase, and review lifecycle commands are fenced. An unexpected active ref or worktree change pauses the run. Explicit isolated flags are reported as run overrides.
163
164
 
165
+ Build stall recovery uses current context occupancy, not lifetime token usage. The `compact` session strategy checks occupancy at ordinary boundaries and compacts only when the threshold requires it; `fresh` still creates an intentional new session. Compaction normally has a 120-second deadline, extending to at most 180 seconds only after correlated progress. Late completion is attributed to the original session; unresolved attempts remain quarantined across restart. Unknown completion pauses recovery rather than replaying implementation in a replacement session.
166
+
167
+ Unchanged, source-bound plan and ticket approvals are reused. Material ticket, plan, delivery, or workflow changes require a new decision, including with `--yes`. Legacy approval evidence that cannot prove unchanged scope requires review. `--steps` counts completed tickets or steps; it is neither a wall-clock limit nor a model-turn budget.
168
+
169
+ Questions are persisted before prompting. Redirected or detached runs return a recoverable needs-input outcome (exit 2). Use `rafi build:attach . --run <id>` to see decisions, `rafi build:decide . --run <id> --decision <id> --choice <id>` to answer, and `rafi resume` to select a build and continue. Custom answers additionally use `--choice custom --answer "text"`. Independent eligible tickets continue around a ticket blocker; dependent and unsafe shared-delivery work stays deferred. Answered ticket questions are reconsidered at safe boundaries.
170
+
171
+ CLI starts run under a durable parent supervisor by default. `--detach` writes output under `.rafi/logs/supervisor-<run>.log`; `--no-supervisor` retains direct execution. A preparation-only worker crash may restart within frozen budgets after proving the worker and registered readiness children are stopped. A worker crash with unresolved provider work pauses for reconciliation: it does not silently repeat a possibly completed action. `build:stop` signals only a verified supervisor process incarnation.
172
+
173
+ Runtime deadlines can be overridden in `autonomy.runtime_deadlines` using positive integer milliseconds: `preparation_ms` (120000), `rpc_ms` (60000), `compaction_ms` (120000), `shutdown_ms` (10000), and `turn_ms` (3600000). They are frozen with the run policy. Transport-idle and hard active-turn deadlines have distinct meanings; neither grants permission to repeat uncertain side effects. Attached-process elapsed time includes waiting and is not provider compute time. Activity warns after five minutes without a completed tool or agent turn; intentional human waits are excluded, and the warning alone does not cancel work. Final paused output lists the durable question, its answer command, and the short `rafi resume` recovery command.
174
+
164
175
  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.
165
176
 
166
177
  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, namespaces finding IDs by run, ticket, and review, and durably indexes the exact report, remediation response, and bounded fix summary by digest. A malformed failing report receives two correction-only turns in its original session, then a validated fresh successor performs one complete source-bound review followed by up to two correction-only turns. Any later operator-requested fresh successor performs one complete source-bound review followed by five usable correction-only turns.
167
178
 
168
- Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review to a full frozen source digest: commit, porcelain status, staged and unstaged bytes, and untracked content. Every fresh attempt captures and compares that complete state. Drift appends the new state and forces a complete review; correction-only reconstruction is never applied to changed source. Packet revisions and the authoritative WorkflowDb recovery head advance together. A successor must have a location-scoped identity, accept a handoff containing the complete resource inventory (purpose, byte count, digest, and recovery requirement), and acknowledge the copied packet before reporting. Paused recovery resumes with `rafi build:resume <project> --run <run-id> --ticket <ticket-id> --fresh-with-handoff`.
179
+ A completed QA turn with missing/invalid continuity metadata or a malformed response can trigger one automatic complete fresh review per ticket. That allowance is durable across restarts and shared across protocol failures. The old session and snapshot close before the new review starts. Exhausted same-session report correction and malformed resumed report/acknowledgement responses use this route, rather than requiring an immediate manual resume. Earlier invalid responses cannot authorize Builder work or a QA pass. Provider execution failures, uncertain dispatch, invalid acceptance receipts, and modified recovery evidence still pause. Repeated failures exhaust the allowance and preserve recoverable state; independent eligible tickets can continue.
180
+
181
+ Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review to a full frozen source digest: commit, porcelain status, staged and unstaged bytes, and untracked content. Every fresh attempt captures and compares that complete state. Drift appends the new state and forces a complete review; correction-only reconstruction is never applied to changed source. Packet revisions and the authoritative WorkflowDb recovery head advance together. A successor must have a location-scoped identity, accept a handoff containing the complete resource inventory (purpose, byte count, digest, and recovery requirement), and acknowledge the copied packet before reporting. Paused recovery is available through `rafi resume` or `rafi build:resume` in the project directory. The picker resolves the ticket, current QA revision, and validated QA handoff internally; explicit recovery flags still reject stale revisions.
169
182
 
170
183
  ### `ai-foreman manager`
171
184
 
@@ -214,11 +227,11 @@ gh repo view <host>/<owner>/<repo>
214
227
  | `-a, --agent <agent>` | `claude` / `codex` | single `rafi-config.yaml` target, otherwise `claude` | Builder agent. Explicit `--agent` always wins. |
215
228
  | `-m, --model <model>` | `gpt-5.5` / any supported agent model | agent default | Overrides the builder model. |
216
229
  | `--effort <level>` | Claude Code: `low` / `medium` / `high` / `xhigh`<br>Codex: `low` / `medium` / `high` / `xhigh` | agent default | Reasoning level. |
217
- | `--sources <paths...>` | `docs/tickets.md docs/plans/**` | Configured Rafi `<docs.root>/rafi-plan.md`, then ticket docs root, then `docs/rafi-plan.md`, otherwise scans | Optional files, folders, or globs for the agent to check first. Any reasonable planning format is OK. |
230
+ | `--sources <paths...>` | `docs/tickets.md docs/plans/**` | saved context, subject to `source_handling` | Supplemental files, folders, URLs, or globs. Passing an approved `rafi-plan.json` remains supported; ordinary documents cannot replace the approved plan. |
218
231
  | `--fast` | flag | off | Lower latency. |
219
232
  | `-y, --yes` | flag | off | Skips confirmation before builder edits ticket files. |
220
233
 
221
- Use `ai-foreman tickets setup:init` or `setup:update` to append local, Linear, or Jira Cloud sources to the project-wide registry in `rafi-config.yaml`. Linear imports use `LINEAR_API_KEY`; Jira Cloud imports use `JIRA_EMAIL` and `JIRA_API_TOKEN`. Credentials are never persisted, and mirrored tickets retain source provenance.
234
+ Use `ai-foreman tickets setup:init` or `setup:update` to manage supporting local/URL context and external Linear/Jira connections in the project-wide registry in `rafi-config.yaml`. Linear imports use `LINEAR_API_KEY`; Jira Cloud imports use `JIRA_EMAIL` and `JIRA_API_TOKEN`. Credentials are never persisted, and mirrored tickets retain source provenance.
222
235
 
223
236
  Use `ai-foreman tickets review` to accept, defer, or dismiss pending split/combine/duplicate recommendations rendered in the progress doc.
224
237
 
@@ -670,3 +683,15 @@ Package output:
670
683
  - **`special-agents`** — library (rules + skills + agents + composition)
671
684
  - **`ai-foreman`** — this runtime
672
685
  - **`@rafi-ai/cli`** — CLI for `rafi create` and `rafi compile`
686
+
687
+ ### Recovery ownership upgrades
688
+
689
+ Stop existing workers before upgrading. The recovery database now records preparation admission, single-use launches, retry lineage, owned readiness processes and monotonic lease generations. Older binaries must not write an upgraded database; preserve these records when troubleshooting rather than deleting locks. Unknown process identity, incomplete child-launch evidence or an unresolved provider dispatch requires reconciliation. A stale heartbeat alone does not authorize another worker. Read-only resume inspection supports older storage without migrating it.
690
+
691
+ Ownership schema version 2 adds bounded child registration/acknowledgement, explicit child-workflow ownership, and separate readiness/cleanup outcomes. Build readiness requires the original admission and owning project even when the command executes in an external worktree. Both the current build and competing starts wait for verified probe cleanup. Runtime snapshot, dispatch, continuity and session writes retain original authority; releasing a lease does not make those records writable by an arbitrary caller. Completed, cancelled and superseded builds cannot be reactivated by saving a snapshot.
692
+
693
+ Ownership schema version 3 gates provider readiness behind registered helper authorization and records cleanup separately. Cleanup is reachable through both short resume commands even when its owning build is terminal or has no valid build snapshot. Restricted cleanup connections can update only their selected readiness records. Migration waits for local legacy execution to become safely quiescent; persistent compatibility guards then reject already-open older writers as well as new older opens. Uncertain remote provider records survive the upgrade unchanged so their owning recovery flow remains reachable. Ordinary starts remain blocked until those operations are reconciled. A lost acknowledgement cannot revoke an already-authorized helper.
694
+
695
+ Standalone readiness also rejects unverified cleanup. The current process retains its cleanup evidence and checks it before another probe starts; restoring process visibility permits a retry. Standalone commands do not create a build recovery database, so this evidence does not survive process exit.
696
+
697
+ On macOS/Linux, readiness cleanup checks the dedicated process group and inherited probe tags, including tagged descendants that escape the group. This is not a sandbox for descendants that deliberately remove their tag and detach. Unavailable inventory and ambiguous legacy evidence remain unresolved. The Windows readiness implementation targets Windows 10 / Server 2016 APIs, but those minimum versions have not been verified. Native Windows verification remains a release gate. Readiness providers start inside a non-breakaway Job Object with kill-on-close behavior, using built-in Windows PowerShell and `Add-Type`. Standard npm `.cmd`/`.bat` provider shims resolve to direct Node arguments; arbitrary shell shims are rejected. The Windows Job Object applies to readiness probes; it does not add containment for arbitrary Builder commands. Native regression tests are in `.github/workflows/build-recovery.yml`; the implementation plan records actual platform verification.
@@ -11,7 +11,8 @@ export interface ActivityReporterOptions {
11
11
  tickMs?: number;
12
12
  heartbeatMs?: number;
13
13
  quietWarningMs?: number;
14
- /** Override automatic TTY capability detection for tests or embedding hosts. */
14
+ usefulProgressWarningMs?: number;
15
+ /** Override the safe automatic TTY rendering mode for tests or embedding hosts. */
15
16
  ttyMode?: ActivityTtyMode;
16
17
  }
17
18
  export type ActivityTtyMode = "auto" | "cursor" | "records";
@@ -24,6 +25,10 @@ export declare class ActivityReporter {
24
25
  private readonly tickMs;
25
26
  private readonly heartbeatMs;
26
27
  private readonly quietWarningMs;
28
+ private readonly usefulProgressWarningMs;
29
+ private lastUsefulProgressAt;
30
+ private usefulProgressWarningPrinted;
31
+ private pauseStartedAt;
27
32
  private readonly ttyMode;
28
33
  private readonly commandStartedAt;
29
34
  private readonly phases;
@@ -48,6 +53,8 @@ export declare class ActivityReporter {
48
53
  immediate?: boolean;
49
54
  }): void;
50
55
  pulse(detail?: string): void;
56
+ /** A completed tool or turn is distinct from transport/status activity. */
57
+ completedWork(): void;
51
58
  note(message: string): void;
52
59
  writePersistent(text: string): void;
53
60
  /** Keep role/provider/context state attached to the current activity status. */
@@ -71,5 +78,8 @@ export declare function pauseActivityForInput<T>(operation: () => Promise<T>, _m
71
78
  export declare function reportBuilderEvent(event: BuilderEvent): void;
72
79
  /** Async event queue that also feeds the active CLI reporter without consuming the stream. */
73
80
  export declare class BuilderEventQueue extends AsyncQueue<BuilderEvent> {
81
+ private readonly observers;
82
+ /** Synchronous fan-out; never consumes or competes with the owning iterator. */
83
+ observe(listener: (event: BuilderEvent) => void): () => void;
74
84
  push(event: BuilderEvent): void;
75
85
  }
package/dist/activity.js CHANGED
@@ -10,6 +10,10 @@ export class ActivityReporter {
10
10
  tickMs;
11
11
  heartbeatMs;
12
12
  quietWarningMs;
13
+ usefulProgressWarningMs;
14
+ lastUsefulProgressAt = 0;
15
+ usefulProgressWarningPrinted = false;
16
+ pauseStartedAt = 0;
13
17
  ttyMode;
14
18
  commandStartedAt;
15
19
  phases = new Map();
@@ -37,6 +41,7 @@ export class ActivityReporter {
37
41
  this.tickMs = options.tickMs ?? 1_000;
38
42
  this.heartbeatMs = options.heartbeatMs ?? 30_000;
39
43
  this.quietWarningMs = options.quietWarningMs ?? 60_000;
44
+ this.usefulProgressWarningMs = options.usefulProgressWarningMs ?? 300_000;
40
45
  this.ttyMode = resolveTtyMode(options.ttyMode);
41
46
  this.commandStartedAt = this.now();
42
47
  }
@@ -45,6 +50,8 @@ export class ActivityReporter {
45
50
  this.phases.set(phase.id, phase);
46
51
  if (this.phases.size === 1) {
47
52
  this.lastSignalAt = phase.startedAt;
53
+ this.lastUsefulProgressAt = phase.startedAt;
54
+ this.usefulProgressWarningPrinted = false;
48
55
  this.lastHeartbeatAt = phase.startedAt;
49
56
  this.quietWarningPrinted = false;
50
57
  this.startTimer();
@@ -80,6 +87,11 @@ export class ActivityReporter {
80
87
  this.quietWarningPrinted = false;
81
88
  this.render(this.output.isTTY);
82
89
  }
90
+ /** A completed tool or turn is distinct from transport/status activity. */
91
+ completedWork() {
92
+ this.lastUsefulProgressAt = this.now();
93
+ this.usefulProgressWarningPrinted = false;
94
+ }
83
95
  note(message) {
84
96
  this.clearLine();
85
97
  this.output.write(`${clean(message, 500)}\n`);
@@ -109,6 +121,8 @@ export class ActivityReporter {
109
121
  this.render(true);
110
122
  }
111
123
  pause() {
124
+ if (this.paused === 0)
125
+ this.pauseStartedAt = this.now();
112
126
  this.paused++;
113
127
  this.clearLine();
114
128
  let resumed = false;
@@ -117,6 +131,11 @@ export class ActivityReporter {
117
131
  return;
118
132
  resumed = true;
119
133
  this.paused = Math.max(0, this.paused - 1);
134
+ if (this.paused === 0) {
135
+ const duration = this.now() - this.pauseStartedAt;
136
+ this.lastUsefulProgressAt += duration;
137
+ this.lastSignalAt += duration;
138
+ }
120
139
  this.render(true);
121
140
  };
122
141
  }
@@ -161,9 +180,17 @@ export class ActivityReporter {
161
180
  if (!force && elapsed < this.displayDelayMs)
162
181
  return;
163
182
  const quietFor = now - this.lastSignalAt;
183
+ if (phase && now - this.lastUsefulProgressAt >= this.usefulProgressWarningMs && !this.usefulProgressWarningPrinted) {
184
+ this.usefulProgressWarningPrinted = true;
185
+ this.note(`rafi: no completed tool or agent turn observed for ${formatDuration(now - this.lastUsefulProgressAt)}; current phase: ${phase.label}. The operation remains subject to its configured deadline.`);
186
+ return;
187
+ }
164
188
  if (phase && quietFor >= this.quietWarningMs && !this.quietWarningPrinted) {
165
189
  this.quietWarningPrinted = true;
166
- this.note(`rafi: ${this.provider ?? "provider"} has been quiet for ${formatDuration(quietFor)}; RAFI is still responsive and will keep waiting`);
190
+ const status = phase.label === "answer sent; waiting for Claude"
191
+ ? `Claude has not resumed its stream yet after your answer was sent (${formatDuration(quietFor)})`
192
+ : `${this.provider ?? "provider"} has been quiet for ${formatDuration(quietFor)}`;
193
+ this.note(`rafi: ${status}; RAFI is still responsive and will keep waiting`);
167
194
  return;
168
195
  }
169
196
  const provider = [this.provider, this.model].filter(Boolean).join("/");
@@ -253,22 +280,49 @@ export function reportBuilderEvent(event) {
253
280
  reporter.note(`rafi: ${event.provider} ${clean(event.reason, 300)}; retrying${attempt}${delay}`);
254
281
  reporter.update(`retrying ${event.provider}`, event.reason, { provider: event.provider });
255
282
  }
256
- else if (event.kind === "tool")
257
- reporter.update(`running ${event.name}`, briefInput(event.input));
283
+ else if (event.kind === "tool") {
284
+ switch (event.lifecycle) {
285
+ case "progress":
286
+ reporter.pulse();
287
+ break;
288
+ case "completed":
289
+ reporter.completedWork();
290
+ if (isFailedToolCompletion(event))
291
+ reporter.update("tool failed", event.name);
292
+ else
293
+ reporter.pulse();
294
+ break;
295
+ case "started":
296
+ case undefined:
297
+ // Older event producers omitted lifecycle for a tool start.
298
+ reporter.update(`running ${event.name}`, briefInput(event.input));
299
+ break;
300
+ }
301
+ }
258
302
  else if (event.kind === "text")
259
303
  reporter.update("processing agent response");
260
304
  else if (event.kind === "session-transition")
261
305
  reporter.update(`session ${event.transition}`, event.detail);
262
306
  else if (event.kind === "context-usage")
263
307
  reporter.pulse(event.percentage === undefined ? undefined : `context ${event.percentage.toFixed(0)}%`);
264
- else if (event.kind === "turn-complete")
308
+ else if (event.kind === "turn-complete") {
309
+ reporter.completedWork();
265
310
  reporter.update(event.result.isError ? "agent turn failed" : "agent turn complete");
311
+ }
266
312
  else if (event.kind === "error")
267
313
  reporter.update("provider error", event.message);
268
314
  }
269
315
  /** Async event queue that also feeds the active CLI reporter without consuming the stream. */
270
316
  export class BuilderEventQueue extends AsyncQueue {
317
+ observers = new Set();
318
+ /** Synchronous fan-out; never consumes or competes with the owning iterator. */
319
+ observe(listener) {
320
+ this.observers.add(listener);
321
+ return () => { this.observers.delete(listener); };
322
+ }
271
323
  push(event) {
324
+ for (const listener of this.observers)
325
+ listener(event);
272
326
  reportBuilderEvent(event);
273
327
  super.push(event);
274
328
  }
@@ -280,6 +334,12 @@ function briefInput(input) {
280
334
  const detail = value.command ?? value.file_path ?? value.path ?? value.pattern ?? value.query;
281
335
  return detail === undefined ? undefined : clean(String(detail), 100);
282
336
  }
337
+ function isFailedToolCompletion(event) {
338
+ const status = event.status?.toLowerCase();
339
+ return status === "failed"
340
+ || status === "error"
341
+ || (typeof event.exitCode === "number" && Number.isFinite(event.exitCode) && event.exitCode !== 0);
342
+ }
283
343
  function clean(value, maximum) {
284
344
  return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
285
345
  }
@@ -293,23 +353,18 @@ function semanticStatusKey(body) {
293
353
  .trim();
294
354
  }
295
355
  function resolveTtyMode(requested) {
296
- const configured = requested ?? configuredTtyMode(process.env.RAFI_ACTIVITY_RENDER_MODE);
356
+ const configured = requested === "cursor" || requested === "records"
357
+ ? requested
358
+ : configuredTtyMode(process.env.RAFI_ACTIVITY_RENDER_MODE);
297
359
  if (configured === "cursor" || configured === "records")
298
360
  return configured;
299
- return truthyEnvironmentValue(process.env.CODEX_CI)
300
- || truthyEnvironmentValue(process.env.CI)
301
- || process.env.TERM?.toLowerCase() === "dumb"
302
- ? "records"
303
- : "cursor";
361
+ // A TTY-like output can still be an append-only transcript. Cursor redraws
362
+ // are therefore an explicit opt-in rather than an unsafe capability guess.
363
+ return "records";
304
364
  }
305
365
  function configuredTtyMode(value) {
306
366
  return value === "cursor" || value === "records" || value === "auto" ? value : "auto";
307
367
  }
308
- function truthyEnvironmentValue(value) {
309
- if (value === undefined)
310
- return false;
311
- return !["", "0", "false"].includes(value.trim().toLowerCase());
312
- }
313
368
  function formatDuration(milliseconds) {
314
369
  const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
315
370
  if (seconds < 60)
@@ -58,6 +58,8 @@ export declare class ClaudeAdapter implements BuilderAdapter {
58
58
  private structuredError?;
59
59
  private apiErrorStatus?;
60
60
  private compactResult?;
61
+ private manualCompactionDeadline?;
62
+ private compactionPromise?;
61
63
  private autoCompactionPrepared;
62
64
  private preparedAutoCompactThreshold?;
63
65
  private preparedAutoCompactionPolicy?;
@@ -70,6 +72,8 @@ export declare class ClaudeAdapter implements BuilderAdapter {
70
72
  private activeProviderTurnSpanId?;
71
73
  private readonly toolSpans;
72
74
  private pending?;
75
+ private questionTrace?;
76
+ private responseOnlyTurn;
73
77
  private terminalResult?;
74
78
  private streamEnded;
75
79
  private closed;
@@ -78,7 +82,11 @@ export declare class ClaudeAdapter implements BuilderAdapter {
78
82
  /** Background loop: consume the SDK message stream until it ends. */
79
83
  private pump;
80
84
  private handle;
81
- sendTurn(text: string): Promise<TurnResult>;
85
+ sendTurn(text: string, policy?: {
86
+ handback?: boolean;
87
+ responseOnly?: boolean;
88
+ logicalActionId?: string;
89
+ }): Promise<TurnResult>;
82
90
  private sendTurnInternal;
83
91
  private captureStderr;
84
92
  private observationContext;
@@ -91,12 +99,14 @@ export declare class ClaudeAdapter implements BuilderAdapter {
91
99
  compact(): Promise<CompactResult>;
92
100
  private compactInternal;
93
101
  prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
102
+ private prepareAutoCompactionInternal;
94
103
  autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
95
104
  drainNativeCompactions(): NativeCompaction[];
96
105
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
97
106
  contextUsage(): Promise<ContextUsage | undefined>;
98
107
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
99
108
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
109
+ observeEvents(listener: (event: BuilderEvent) => void): () => void;
100
110
  events(): AsyncIterable<BuilderEvent>;
101
111
  close(): Promise<void>;
102
112
  private failSessionIdentity;