ai-foreman 1.7.6 → 1.7.8

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 (69) hide show
  1. package/README.md +26 -1
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +31 -6
  4. package/dist/adapters/claude.d.ts +61 -6
  5. package/dist/adapters/claude.js +544 -36
  6. package/dist/adapters/codex.d.ts +28 -1
  7. package/dist/adapters/codex.js +398 -27
  8. package/dist/adapters/recovering.d.ts +13 -2
  9. package/dist/adapters/recovering.js +32 -4
  10. package/dist/adapters/sessionFailure.d.ts +18 -0
  11. package/dist/adapters/sessionFailure.js +51 -0
  12. package/dist/adapters/types.d.ts +117 -9
  13. package/dist/agentRun.d.ts +4 -1
  14. package/dist/agentRun.js +96 -9
  15. package/dist/branch/currentGuard.d.ts +38 -0
  16. package/dist/branch/currentGuard.js +71 -0
  17. package/dist/branch/planner.d.ts +1 -1
  18. package/dist/branch/planner.js +2 -7
  19. package/dist/branch/prefix.d.ts +8 -0
  20. package/dist/branch/prefix.js +26 -0
  21. package/dist/branch/resume.d.ts +2 -0
  22. package/dist/branch/resume.js +8 -0
  23. package/dist/branch/runner.d.ts +14 -2
  24. package/dist/branch/runner.js +77 -17
  25. package/dist/buildRuns.d.ts +13 -4
  26. package/dist/buildRuns.js +85 -19
  27. package/dist/cli/handoffs.d.ts +2 -0
  28. package/dist/cli/handoffs.js +74 -0
  29. package/dist/cli/start.js +1079 -43
  30. package/dist/cli/tickets.js +199 -3
  31. package/dist/continuity.d.ts +92 -0
  32. package/dist/continuity.js +415 -0
  33. package/dist/foreman.d.ts +5 -1
  34. package/dist/foreman.js +41 -6
  35. package/dist/handoffs.d.ts +69 -0
  36. package/dist/handoffs.js +546 -0
  37. package/dist/log.d.ts +1 -1
  38. package/dist/permissions/policy.d.ts +5 -1
  39. package/dist/permissions/policy.js +48 -1
  40. package/dist/qaReview.d.ts +6 -6
  41. package/dist/qaReview.js +24 -14
  42. package/dist/sessionAvailability.d.ts +15 -0
  43. package/dist/sessionAvailability.js +57 -0
  44. package/dist/sessionIdentity.d.ts +33 -0
  45. package/dist/sessionIdentity.js +132 -0
  46. package/dist/sessionLifecycle.d.ts +198 -5
  47. package/dist/sessionLifecycle.js +1373 -20
  48. package/dist/statusReporter.d.ts +84 -8
  49. package/dist/statusReporter.js +270 -14
  50. package/dist/ticketPlanning.js +8 -0
  51. package/dist/ticketPopulation.js +88 -12
  52. package/dist/tickets/commands.js +20 -12
  53. package/dist/tickets/groupReset.d.ts +25 -0
  54. package/dist/tickets/groupReset.js +279 -0
  55. package/dist/tickets/groups.d.ts +36 -0
  56. package/dist/tickets/groups.js +68 -0
  57. package/dist/tickets/importer.js +27 -3
  58. package/dist/tickets/recommendations.js +31 -7
  59. package/dist/tickets/reset.d.ts +1 -0
  60. package/dist/tickets/reset.js +1 -0
  61. package/dist/tickets/setupConfig.d.ts +1 -0
  62. package/dist/tickets/setupConfig.js +10 -2
  63. package/dist/tickets/stateDb.d.ts +34 -0
  64. package/dist/tickets/stateDb.js +207 -0
  65. package/dist/tickets/ticketLoader.js +5 -2
  66. package/dist/tickets/validate.js +4 -0
  67. package/dist/workflowDb.d.ts +145 -3
  68. package/dist/workflowDb.js +571 -10
  69. package/package.json +32 -4
package/README.md CHANGED
@@ -105,7 +105,7 @@ What each part does:
105
105
  | `-y, --yes` | flag | off | Skips preflight confirmation. |
106
106
  | `--no-qa` | flag | QA on | Disables per-ticket QA review. |
107
107
  | `--continue` | flag | off | Resumes the most recent logged session. |
108
- | `-r, --resume <sessionId>` | session ID from `.foreman/` logs | none | Resumes a specific Claude/Codex session. |
108
+ | `-r, --resume <sessionId>` | session ID with a stored scoped binding | none | Resumes only after provider, canonical worktree, and workspace identity validation. |
109
109
  | `--branch-per-ticket` | flag | off | Runs each selected structured ticket in its own git worktree and branch. Requires initialized `.tickets/`. |
110
110
  | `--create-pr` | flag | off | Pushes each successful ticket branch and creates a GitHub PR. Implies `--branch-per-ticket`. |
111
111
  | `--pr-ready` | flag | draft PRs | Creates ready-for-review PRs instead of draft PRs when used with `--create-pr`. |
@@ -114,6 +114,10 @@ What each part does:
114
114
  | `--base <ref>` | `main` / `origin/main` / `HEAD` | current branch or `HEAD` | Base ref for root ticket branches. |
115
115
  | `--branch-prefix <prefix>` | `feature` / `rafi` / another valid Git prefix | `feature` | Prefix for generated ticket branches. Explicit prefixes are preserved verbatim. |
116
116
  | `--max-branch-depth <n>` | positive integer | `5` | Maximum selected branch/PR stack depth (root PR is depth 1). |
117
+ | `--show-session-cost` / `--hide-session-cost` | flags | saved role preferences | Show authoritative provider cost or trustworthy cumulative tokens for Builder and QA; does not change context occupancy display. |
118
+ | `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `50` | Initial Builder threshold override. A later explicit Builder-threshold revision wins for the active run. |
119
+
120
+ Builder and QA context ceilings are configured independently (default `50%`, maximum `10` successful compactions per scoped provider session). Claude and Codex install the ceiling as a native in-turn auto-compaction limit before real work begins; Rafi verifies the post-compaction occupancy and counts provider-native and host-requested compactions together. An ineffective or failed compact receives one bounded retry, followed by a validated cumulative handoff. Status output reports the configured and installed ceilings, lifecycle state, scoped count/maximum, and settings revision without substituting cumulative session tokens for active context occupancy.
117
121
 
118
122
  Auto-detected tracker files:
119
123
 
@@ -124,6 +128,7 @@ Auto-detected tracker files:
124
128
  Resume rule:
125
129
 
126
130
  - Use either `--continue` or `--resume`.
131
+ - Exact resume requires the original canonical worktree and its recorded identity. A deleted, recreated, cross-worktree, ambiguous, or unverifiable session is rejected with fresh/handoff recovery guidance.
127
132
  - Do not use both in the same command.
128
133
  - In branch mode, use `--continue --ticket <id>` to continue a ticket with its saved builder session.
129
134
  - For multiple branch tickets, repeat `--ticket` with `--continue`; do not reuse one explicit `--resume <sessionId>` across multiple tickets.
@@ -141,6 +146,10 @@ ai-foreman start ./my-project --steps 1 --branch-per-ticket --continue --ticket
141
146
 
142
147
  Saved `tickets.build` defaults in `rafi-config.yaml` can enable branch-per-ticket and completion behavior automatically. Use `--completion pr|auto-merge|direct-merge|none`, `--no-branch-per-ticket`, `--no-create-pr`, or `--auto-merge-wait` / `--no-auto-merge-wait` to override saved defaults for one run.
143
148
 
149
+ 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.
150
+
151
+ The active terminal line always includes the current 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.
152
+
144
153
  ### GitHub PR Failure Recovery
145
154
 
146
155
  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.
@@ -502,8 +511,14 @@ ai-foreman tickets accept-future-work 1 --project ./my-project --ticket-id T051
502
511
  ai-foreman tickets cancel T001 --project ./my-project --summary "Superseded by T002"
503
512
  ai-foreman tickets reorder T051 --project ./my-project --after T050
504
513
  ai-foreman tickets archive --project ./my-project --older-than-days 30
514
+
515
+ # Inspect immutable creation groups and reset an exact approved group
516
+ ai-foreman tickets groups list --project ./my-project
517
+ ai-foreman tickets reset --group TG-3 --project ./my-project
505
518
  ```
506
519
 
520
+ Ticket creation groups use stable repository-local IDs (`TG-1`, `TG-2`, …); newest-first list position is separate and may change. Missing definitions remain immutable members with a validated recovery snapshot. Non-interactive group restoration requires an explicit deleted-ticket policy, and missing dependency definitions stop the reset atomically.
521
+
507
522
  ## How The Loop Works
508
523
 
509
524
  Before implementation starts:
@@ -514,6 +529,8 @@ Before implementation starts:
514
529
 
515
530
  Every implementation turn must end with exactly one marker.
516
531
 
532
+ Builder and QA turns also publish a bounded cumulative continuity delta. Automatic fresh transitions stage a versioned JSON/Markdown handoff, require `HANDOFF_ACCEPTED` plus a valid successor checkpoint, and only then move the role lease. The all-in-one `rafi` CLI exposes durable handoff inspection and cache/history maintenance commands.
533
+
517
534
  Marker rules:
518
535
 
519
536
  - Put the marker on the final non-empty line.
@@ -592,6 +609,14 @@ pnpm build
592
609
  pnpm dev -- start ../../examples/dummy-project --steps 2
593
610
  ```
594
611
 
612
+ Changes to provider adapters or context lifecycle enforcement also require the opt-in authenticated gate:
613
+
614
+ ```bash
615
+ pnpm test:live-providers
616
+ ```
617
+
618
+ It exercises Claude and Codex as both Builder and QA with a low native ceiling, verifies in-turn compaction and same-session continuation, then verifies a maximum-one boundary interruption and validated fresh handoff. Set `RAFI_LIVE_CONTEXT_THRESHOLD_PERCENT` to an integer from `1` through `10` to override its default `10%` ceiling. Very low Claude ceilings can intentionally fail the capability check when they fall below the SDK's minimum native window. The gate uses locally authenticated provider CLIs and is intentionally skipped by the ordinary test suite.
619
+
595
620
  Package output:
596
621
 
597
622
  - The package publishes an `ai-foreman` binary from `dist/index.js`.
@@ -34,6 +34,7 @@ export declare class ActivityReporter {
34
34
  private lastHeartbeatAt;
35
35
  private lineVisible;
36
36
  private paused;
37
+ private agentStatusLine?;
37
38
  constructor(command: string, options?: ActivityReporterOptions);
38
39
  begin(label: string): () => void;
39
40
  update(state: string, detail?: string, metadata?: {
@@ -44,6 +45,8 @@ export declare class ActivityReporter {
44
45
  pulse(detail?: string): void;
45
46
  note(message: string): void;
46
47
  writePersistent(text: string): void;
48
+ /** Keep role/provider/context state in the single bottom-most TTY line. */
49
+ setAgentStatus(line: string | undefined): void;
47
50
  pause(): () => void;
48
51
  dispose(): void;
49
52
  private startTimer;
package/dist/activity.js CHANGED
@@ -23,6 +23,7 @@ export class ActivityReporter {
23
23
  lastHeartbeatAt = 0;
24
24
  lineVisible = false;
25
25
  paused = 0;
26
+ agentStatusLine;
26
27
  constructor(command, options = {}) {
27
28
  this.command = command;
28
29
  this.output = options.output ?? {
@@ -90,6 +91,20 @@ export class ActivityReporter {
90
91
  this.output.write("\n");
91
92
  this.render(true);
92
93
  }
94
+ /** Keep role/provider/context state in the single bottom-most TTY line. */
95
+ setAgentStatus(line) {
96
+ this.agentStatusLine = line ? clean(line, 500) : undefined;
97
+ if (this.agentStatusLine && !this.timer) {
98
+ this.lastSignalAt = this.now();
99
+ this.lastHeartbeatAt = this.lastSignalAt;
100
+ this.startTimer();
101
+ }
102
+ if (!this.agentStatusLine && this.phases.size === 0) {
103
+ this.idle();
104
+ return;
105
+ }
106
+ this.render(true);
107
+ }
93
108
  pause() {
94
109
  this.paused++;
95
110
  this.clearLine();
@@ -104,6 +119,7 @@ export class ActivityReporter {
104
119
  }
105
120
  dispose() {
106
121
  this.phases.clear();
122
+ this.agentStatusLine = undefined;
107
123
  this.idle();
108
124
  }
109
125
  startTimer() {
@@ -113,6 +129,14 @@ export class ActivityReporter {
113
129
  this.timer.unref();
114
130
  }
115
131
  idle() {
132
+ if (this.agentStatusLine) {
133
+ this.clearLine();
134
+ this.detail = undefined;
135
+ this.provider = undefined;
136
+ this.model = undefined;
137
+ this.render(true);
138
+ return;
139
+ }
116
140
  if (this.timer)
117
141
  clearInterval(this.timer);
118
142
  this.timer = undefined;
@@ -120,27 +144,28 @@ export class ActivityReporter {
120
144
  this.detail = undefined;
121
145
  this.provider = undefined;
122
146
  this.model = undefined;
147
+ this.agentStatusLine = undefined;
123
148
  }
124
149
  currentPhase() {
125
150
  return [...this.phases.values()].at(-1);
126
151
  }
127
152
  render(force) {
128
153
  const phase = this.currentPhase();
129
- if (!phase || this.paused > 0)
154
+ if ((!phase && !this.agentStatusLine) || this.paused > 0)
130
155
  return;
131
156
  const now = this.now();
132
- const elapsed = now - phase.startedAt;
157
+ const elapsed = phase ? now - phase.startedAt : now - this.commandStartedAt;
133
158
  if (!force && elapsed < this.displayDelayMs)
134
159
  return;
135
160
  const quietFor = now - this.lastSignalAt;
136
- if (quietFor >= this.quietWarningMs && !this.quietWarningPrinted) {
161
+ if (phase && quietFor >= this.quietWarningMs && !this.quietWarningPrinted) {
137
162
  this.quietWarningPrinted = true;
138
163
  this.note(`rafi: ${this.provider ?? "provider"} has been quiet for ${formatDuration(quietFor)}; RAFI is still responsive and will keep waiting`);
139
164
  return;
140
165
  }
141
166
  const provider = [this.provider, this.model].filter(Boolean).join("/");
142
167
  const quiet = quietFor >= this.quietWarningMs ? `provider quiet ${formatDuration(quietFor)}; RAFI is responsive` : this.detail;
143
- const body = [phase.label, provider || undefined, quiet].filter(Boolean).join(" — ");
168
+ const body = [phase?.label, provider || undefined, phase ? quiet : undefined, this.agentStatusLine].filter(Boolean).join(" — ");
144
169
  if (this.output.isTTY) {
145
170
  const line = `${FRAMES[this.frame++ % FRAMES.length]} RAFI working: ${body} (${formatDuration(now - this.commandStartedAt)})`;
146
171
  this.output.write(`\r\x1b[2K${line}`);
@@ -211,8 +236,8 @@ export function reportBuilderEvent(event) {
211
236
  reporter.update("processing agent response");
212
237
  else if (event.kind === "session-transition")
213
238
  reporter.update(`session ${event.transition}`, event.detail);
214
- else if (event.kind === "context-usage")
215
- reporter.pulse(event.percentage === undefined ? undefined : `context ${event.percentage.toFixed(0)}%`);
239
+ else if (event.kind === "context-compaction")
240
+ reporter.update(event.phase === "started" ? "compacting context" : event.phase === "succeeded" ? "context compacted" : "context compaction failed", `${event.origin} ${event.providerEventId}`, { provider: event.provider });
216
241
  else if (event.kind === "turn-complete")
217
242
  reporter.update(event.result.isError ? "agent turn failed" : "agent turn complete");
218
243
  else if (event.kind === "error")
@@ -1,14 +1,34 @@
1
- import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
1
+ import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
2
2
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
3
3
  export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
4
- import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, PermissionDecision, ProviderSettingSwitch, TurnResult } from "./types.js";
4
+ import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, PermissionDecision, PreparedContextManagement, ProviderSettingSwitch, ProviderSessionUsage, TurnResult } from "./types.js";
5
+ import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
5
6
  /**
6
7
  * Pure function: build the `options` object passed to `query()`.
7
8
  * Extracted so tests can assert on the shape without making a live SDK call.
8
9
  * `canUseTool` is omitted here — it's a closure that the constructor adds.
9
10
  */
10
11
  export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">): Record<string, unknown>;
12
+ export declare function probeClaudeSession(ref: ProviderSessionRefV1, input: {
13
+ cwd: string;
14
+ configRoot?: string;
15
+ workspaceIdentity?: string;
16
+ role?: ProviderSessionRefV1["role"];
17
+ stream?: string;
18
+ ticketId?: string;
19
+ deliveryUnitId?: string;
20
+ getSessionInfo?: (sessionId: string, options?: {
21
+ dir?: string;
22
+ }) => Promise<SDKSessionInfo | undefined>;
23
+ now?: Date;
24
+ }): Promise<SessionAvailabilityV1>;
11
25
  export declare function permissionDecisionToClaudeResult(decision: PermissionDecision, toolUseID?: string): PermissionResult;
26
+ /** Project one cumulative SDK result without inventing or double-counting counters. */
27
+ export declare function mergeClaudeProviderSessionUsage(prior: ProviderSessionUsage, rawResult: Record<string, unknown>, observedAt?: string): {
28
+ sample: ProviderSessionUsage;
29
+ inputTokens?: number;
30
+ outputTokens?: number;
31
+ };
12
32
  export declare function claudeApiRetryEvent(message: {
13
33
  error: string;
14
34
  attempt: number;
@@ -23,21 +43,42 @@ export declare function claudeApiRetryEvent(message: {
23
43
  export declare class ClaudeAdapter implements BuilderAdapter {
24
44
  private readonly opts;
25
45
  readonly agent: "claude";
26
- private readonly inbox;
46
+ private inbox;
27
47
  private readonly eventQueue;
28
- private readonly query;
29
- private readonly abort;
30
- private readonly pumpDone;
48
+ private query;
49
+ private abort;
50
+ private pumpDone;
51
+ private readonly queryFactory;
52
+ private transportGeneration;
53
+ private transportObservedSessionId?;
31
54
  private _sessionId?;
55
+ private _sessionRef?;
32
56
  private readonly stderrChunks;
33
57
  private turnSignals;
34
58
  private structuredError?;
35
59
  private apiErrorStatus?;
36
60
  private compactResult?;
61
+ private contextPolicy?;
62
+ private contextSequence;
63
+ private compactionSequence;
64
+ private activeCompaction?;
65
+ private manualCompactionPending;
66
+ private boundaryInterrupt?;
67
+ private compactionTerminalObserved;
68
+ private lastContextUsage?;
69
+ /** Full provider context capacity, before Rafi narrows Claude's auto-compact window. */
70
+ private modelContextWindow?;
71
+ /** SDK-owned space between autoCompactWindow and the actual used-token trigger. */
72
+ private nativeAutoCompactReserve?;
73
+ private contextPrepared;
74
+ private cumulativeUsage;
37
75
  private pending?;
76
+ private terminalResult?;
77
+ private streamEnded;
38
78
  private closed;
39
79
  static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
40
80
  private constructor();
81
+ private openQuery;
41
82
  /** Background loop: consume the SDK message stream until it ends. */
42
83
  private pump;
43
84
  private handle;
@@ -45,9 +86,23 @@ export declare class ClaudeAdapter implements BuilderAdapter {
45
86
  private sendTurnInternal;
46
87
  private captureStderr;
47
88
  sessionId(): string | undefined;
89
+ sessionRef(): ProviderSessionRefV1 | undefined;
90
+ adoptSessionRef(ref: ProviderSessionRefV1): void;
91
+ validateSession(): Promise<SessionAvailabilityV1>;
48
92
  compact(): Promise<CompactResult>;
49
93
  contextUsage(): Promise<ContextUsage | undefined>;
94
+ prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
95
+ updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
96
+ interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
97
+ sessionUsage(): Promise<ProviderSessionUsage | undefined>;
50
98
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
51
99
  events(): AsyncIterable<BuilderEvent>;
52
100
  close(): Promise<void>;
101
+ private observeSession;
102
+ private makeContextUsage;
103
+ private configureContextManagement;
104
+ private restartTransport;
105
+ private completeCompaction;
106
+ private settlePending;
107
+ private streamFailureResult;
53
108
  }