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.
- package/README.md +26 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +31 -6
- package/dist/adapters/claude.d.ts +61 -6
- package/dist/adapters/claude.js +544 -36
- package/dist/adapters/codex.d.ts +28 -1
- package/dist/adapters/codex.js +398 -27
- package/dist/adapters/recovering.d.ts +13 -2
- package/dist/adapters/recovering.js +32 -4
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +117 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +38 -0
- package/dist/branch/currentGuard.js +71 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1079 -43
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +92 -0
- package/dist/continuity.js +415 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +546 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +24 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +198 -5
- package/dist/sessionLifecycle.js +1373 -20
- package/dist/statusReporter.d.ts +84 -8
- package/dist/statusReporter.js +270 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +145 -3
- package/dist/workflowDb.js +571 -10
- package/package.json +32 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
-
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, TurnResult } from "./types.js";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
3
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
4
|
export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
|
|
4
5
|
export interface RecoveringAdapterOptions {
|
|
5
6
|
initial: BuilderAdapter;
|
|
@@ -7,9 +8,10 @@ export interface RecoveringAdapterOptions {
|
|
|
7
8
|
enabled: boolean;
|
|
8
9
|
allowSwitch: boolean;
|
|
9
10
|
label: string;
|
|
10
|
-
recreate: (runtime: AgentRuntime, resumeSessionId?: string) => Promise<BuilderAdapter>;
|
|
11
|
+
recreate: (runtime: AgentRuntime, resumeSessionId?: string, resumeSessionRef?: ProviderSessionRefV1) => Promise<BuilderAdapter>;
|
|
11
12
|
choose?: (result: TurnResult, runtime: AgentRuntime, otherRuntime: AgentRuntime) => Promise<TurnRecoveryChoice>;
|
|
12
13
|
onRuntimeChange?: (runtime: AgentRuntime) => void;
|
|
14
|
+
onSessionRef?: (ref: ProviderSessionRefV1) => void;
|
|
13
15
|
}
|
|
14
16
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
15
17
|
export declare class RecoveringAdapter implements BuilderAdapter {
|
|
@@ -19,12 +21,21 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
19
21
|
private readonly eventQueue;
|
|
20
22
|
private eventPump;
|
|
21
23
|
private closed;
|
|
24
|
+
private turnDispatcher?;
|
|
22
25
|
constructor(opts: RecoveringAdapterOptions);
|
|
23
26
|
get agent(): "claude" | "codex";
|
|
24
27
|
sendTurn(text: string): Promise<TurnResult>;
|
|
25
28
|
sessionId(): string | undefined;
|
|
29
|
+
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
30
|
+
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
31
|
+
validateSession(): Promise<SessionAvailabilityV1>;
|
|
26
32
|
compact(): Promise<CompactResult>;
|
|
33
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
34
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
35
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
36
|
+
installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
|
|
27
37
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
38
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
28
39
|
events(): AsyncIterable<BuilderEvent>;
|
|
29
40
|
close(): Promise<void>;
|
|
30
41
|
private replaceAdapter;
|
|
@@ -9,6 +9,7 @@ export class RecoveringAdapter {
|
|
|
9
9
|
eventQueue = new AsyncQueue();
|
|
10
10
|
eventPump;
|
|
11
11
|
closed = false;
|
|
12
|
+
turnDispatcher;
|
|
12
13
|
constructor(opts) {
|
|
13
14
|
this.opts = opts;
|
|
14
15
|
this.adapter = opts.initial;
|
|
@@ -20,9 +21,23 @@ export class RecoveringAdapter {
|
|
|
20
21
|
}
|
|
21
22
|
async sendTurn(text) {
|
|
22
23
|
while (true) {
|
|
23
|
-
const
|
|
24
|
+
const invoke = () => this.adapter.sendTurn(text);
|
|
25
|
+
const result = this.turnDispatcher ? await this.turnDispatcher(text, invoke) : await invoke();
|
|
26
|
+
const sessionRef = this.adapter.sessionRef?.();
|
|
27
|
+
if (sessionRef && this.opts.onSessionRef) {
|
|
28
|
+
try {
|
|
29
|
+
this.opts.onSessionRef(sessionRef);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
this.eventQueue.push({ kind: "error", message: `failed to persist scoped provider session binding: ${error instanceof Error ? error.message : String(error)}` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
if (!this.opts.enabled || !result.isError || !result.failure)
|
|
25
36
|
return result;
|
|
37
|
+
// The host cannot know whether a missing exact session received this
|
|
38
|
+
// instruction. Never replay it or silently switch providers.
|
|
39
|
+
if (result.failure.category === "session-unavailable")
|
|
40
|
+
return result;
|
|
26
41
|
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
27
42
|
const choice = this.opts.choose
|
|
28
43
|
? await this.opts.choose(result, this.runtime, otherRuntime)
|
|
@@ -38,7 +53,8 @@ export class RecoveringAdapter {
|
|
|
38
53
|
reportBuilderEvent(recoveryEvent);
|
|
39
54
|
const nextRuntime = choice === "switch" ? otherRuntime : this.runtime;
|
|
40
55
|
const resumeSessionId = choice === "retry" ? this.adapter.sessionId() : undefined;
|
|
41
|
-
|
|
56
|
+
const resumeSessionRef = choice === "retry" ? this.adapter.sessionRef?.() : undefined;
|
|
57
|
+
await this.replaceAdapter(nextRuntime, resumeSessionId, resumeSessionRef);
|
|
42
58
|
if (choice === "switch") {
|
|
43
59
|
log.info(`Using ${nextRuntime} for the rest of this run. This is a fresh provider session; conversational continuity was not transferred.`);
|
|
44
60
|
}
|
|
@@ -47,8 +63,20 @@ export class RecoveringAdapter {
|
|
|
47
63
|
sessionId() {
|
|
48
64
|
return this.adapter.sessionId();
|
|
49
65
|
}
|
|
66
|
+
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
67
|
+
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
68
|
+
validateSession() {
|
|
69
|
+
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" });
|
|
70
|
+
}
|
|
50
71
|
compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
72
|
+
prepareContextManagement(policy) { if (!this.adapter.prepareContextManagement)
|
|
73
|
+
return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
|
|
74
|
+
updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
|
|
75
|
+
return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
|
|
76
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
|
|
77
|
+
installManagedTurnDispatcher(dispatcher) { this.turnDispatcher = dispatcher; }
|
|
51
78
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
79
|
+
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
52
80
|
events() {
|
|
53
81
|
return this.eventQueue;
|
|
54
82
|
}
|
|
@@ -60,10 +88,10 @@ export class RecoveringAdapter {
|
|
|
60
88
|
await this.eventPump.catch(() => { });
|
|
61
89
|
this.eventQueue.close();
|
|
62
90
|
}
|
|
63
|
-
async replaceAdapter(runtime, resumeSessionId) {
|
|
91
|
+
async replaceAdapter(runtime, resumeSessionId, resumeSessionRef) {
|
|
64
92
|
await this.adapter.close().catch(() => { });
|
|
65
93
|
await this.eventPump.catch(() => { });
|
|
66
|
-
this.adapter = await this.opts.recreate(runtime, resumeSessionId);
|
|
94
|
+
this.adapter = await this.opts.recreate(runtime, resumeSessionId, resumeSessionRef);
|
|
67
95
|
this.runtime = runtime;
|
|
68
96
|
this.opts.onRuntimeChange?.(runtime);
|
|
69
97
|
this.eventPump = this.forwardEvents(this.adapter);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionAvailabilityV1 } from "rafi-spec";
|
|
2
|
+
import type { RuntimeFailure, SessionFailurePhase, TurnDispatchState, TurnResult } from "./types.js";
|
|
3
|
+
export declare class SessionUnavailableError extends Error {
|
|
4
|
+
readonly failure: RuntimeFailure;
|
|
5
|
+
constructor(input: {
|
|
6
|
+
runtime: "claude" | "codex";
|
|
7
|
+
phase: SessionFailurePhase;
|
|
8
|
+
dispatchState: TurnDispatchState;
|
|
9
|
+
executable: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
diagnostics: string;
|
|
12
|
+
availability?: SessionAvailabilityV1;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function sessionUnavailableResult(error: SessionUnavailableError): TurnResult;
|
|
17
|
+
export declare function isSessionUnavailableFailure(value: unknown): boolean;
|
|
18
|
+
export declare function sessionUnavailableErrorFromFailure(failure: RuntimeFailure): SessionUnavailableError;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class SessionUnavailableError extends Error {
|
|
2
|
+
failure;
|
|
3
|
+
constructor(input) {
|
|
4
|
+
super(input.diagnostics, input.cause === undefined ? undefined : { cause: input.cause });
|
|
5
|
+
this.name = "SessionUnavailableError";
|
|
6
|
+
this.failure = {
|
|
7
|
+
runtime: input.runtime,
|
|
8
|
+
phase: input.phase,
|
|
9
|
+
category: "session-unavailable",
|
|
10
|
+
executable: input.executable,
|
|
11
|
+
cwd: input.cwd,
|
|
12
|
+
diagnostics: input.diagnostics,
|
|
13
|
+
dispatchState: input.dispatchState,
|
|
14
|
+
...(input.availability ? { availability: input.availability } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function sessionUnavailableResult(error) {
|
|
19
|
+
return {
|
|
20
|
+
text: error.message,
|
|
21
|
+
isError: true,
|
|
22
|
+
numTurns: 0,
|
|
23
|
+
costUsd: 0,
|
|
24
|
+
costAuthoritative: false,
|
|
25
|
+
failure: error.failure,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function isSessionUnavailableFailure(value) {
|
|
29
|
+
if (value instanceof SessionUnavailableError)
|
|
30
|
+
return true;
|
|
31
|
+
if (!value || typeof value !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const failure = "failure" in value ? value.failure : value;
|
|
34
|
+
return failure?.category === "session-unavailable";
|
|
35
|
+
}
|
|
36
|
+
export function sessionUnavailableErrorFromFailure(failure) {
|
|
37
|
+
if (failure.category !== "session-unavailable")
|
|
38
|
+
throw new Error("runtime failure is not a session-unavailable failure");
|
|
39
|
+
const phase = failure.phase === "preflight" || failure.phase === "attach" || failure.phase === "turn"
|
|
40
|
+
? failure.phase
|
|
41
|
+
: "turn";
|
|
42
|
+
return new SessionUnavailableError({
|
|
43
|
+
runtime: failure.runtime,
|
|
44
|
+
phase,
|
|
45
|
+
dispatchState: failure.dispatchState ?? "unknown",
|
|
46
|
+
executable: failure.executable,
|
|
47
|
+
cwd: failure.cwd,
|
|
48
|
+
diagnostics: failure.diagnostics,
|
|
49
|
+
availability: failure.availability,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-agnostic interface over a coding agent ("builder").
|
|
3
3
|
*/
|
|
4
|
-
import type { RuntimeProbeCategory, RuntimeProbePhase } from "rafi-spec";
|
|
4
|
+
import type { ConfigurableAgentRole, ProviderSessionRefV1, RuntimeProbeCategory, RuntimeProbePhase, SessionAvailabilityV1 } from "rafi-spec";
|
|
5
5
|
/** A permission request surfaced by a builder before it runs a tool. */
|
|
6
6
|
export interface PermissionRequest {
|
|
7
7
|
toolName: string;
|
|
@@ -36,21 +36,78 @@ export type PermissionDecision = {
|
|
|
36
36
|
/** Decides each permission request. Supplied by the foreman, called by the adapter. */
|
|
37
37
|
export type PermissionHandler = (req: PermissionRequest) => Promise<PermissionDecision>;
|
|
38
38
|
/** Result of a single completed turn. */
|
|
39
|
+
export type SessionFailurePhase = "preflight" | "attach" | "turn";
|
|
40
|
+
export type TurnDispatchState = "not-sent" | "unknown";
|
|
41
|
+
export interface RuntimeFailure {
|
|
42
|
+
runtime: "claude" | "codex";
|
|
43
|
+
phase: RuntimeProbePhase | SessionFailurePhase;
|
|
44
|
+
category: RuntimeProbeCategory;
|
|
45
|
+
executable: string;
|
|
46
|
+
cwd: string;
|
|
47
|
+
diagnostics: string;
|
|
48
|
+
dispatchState?: TurnDispatchState;
|
|
49
|
+
availability?: SessionAvailabilityV1;
|
|
50
|
+
}
|
|
39
51
|
export interface TurnResult {
|
|
40
52
|
/** Final assistant message text — where the STEP_STATUS marker lives. */
|
|
41
53
|
text: string;
|
|
42
54
|
isError: boolean;
|
|
43
55
|
numTurns: number;
|
|
44
56
|
costUsd: number;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
/** True only when the provider supplied this value; never inferred by Rafi. */
|
|
58
|
+
costAuthoritative?: boolean;
|
|
59
|
+
inputTokens?: number;
|
|
60
|
+
outputTokens?: number;
|
|
61
|
+
failure?: RuntimeFailure;
|
|
62
|
+
/** A provider turn stopped at a context-compaction boundary, not a failure. */
|
|
63
|
+
interrupted?: {
|
|
64
|
+
reason: "compaction-boundary";
|
|
65
|
+
providerEventId?: string;
|
|
52
66
|
};
|
|
53
67
|
}
|
|
68
|
+
export type ContextCompactionOrigin = "provider-auto" | "rafi-manual" | "recovery" | "boundary";
|
|
69
|
+
export type ContextCompactionPhase = "started" | "succeeded" | "failed";
|
|
70
|
+
/** Native context policy installed before a Builder/QA role may do real work. */
|
|
71
|
+
export interface ContextManagementPolicy {
|
|
72
|
+
role: "builder" | "qa";
|
|
73
|
+
configuredThresholdPercent: number;
|
|
74
|
+
compactMaximum: number;
|
|
75
|
+
settingsRevision: number;
|
|
76
|
+
model: string;
|
|
77
|
+
/** Continue a provider-monotonic sequence after process recovery. */
|
|
78
|
+
providerSequenceStart?: number;
|
|
79
|
+
/** A persisted, model-matched maximum may avoid a discovery turn. */
|
|
80
|
+
knownModelContextWindow?: number;
|
|
81
|
+
/** Native compaction stays enabled until the host vetoes a boundary. */
|
|
82
|
+
nativeCompactionEnabled?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface PreparedContextManagement {
|
|
85
|
+
modelContextWindow: number;
|
|
86
|
+
configuredTokenLimit: number;
|
|
87
|
+
installedNativeTokenLimit: number;
|
|
88
|
+
installedNativePercent: number;
|
|
89
|
+
sample?: ContextUsage;
|
|
90
|
+
}
|
|
91
|
+
export interface ContextCompactionEvent {
|
|
92
|
+
kind: "context-compaction";
|
|
93
|
+
phase: ContextCompactionPhase;
|
|
94
|
+
origin: ContextCompactionOrigin;
|
|
95
|
+
providerEventId: string;
|
|
96
|
+
providerSequence: number;
|
|
97
|
+
provider: "claude" | "codex";
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
sessionRef?: ProviderSessionRefV1;
|
|
100
|
+
observedAt: string;
|
|
101
|
+
postCompactSample?: ContextUsage;
|
|
102
|
+
reason?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface InterruptResult {
|
|
105
|
+
ok: boolean;
|
|
106
|
+
error?: string;
|
|
107
|
+
providerEventId?: string;
|
|
108
|
+
}
|
|
109
|
+
/** Host-owned gate used by wrappers that need additional provider turns. */
|
|
110
|
+
export type ManagedTurnDispatcher = (text: string, invoke: () => Promise<TurnResult>) => Promise<TurnResult>;
|
|
54
111
|
/** Observability events emitted while a builder works. */
|
|
55
112
|
export type BuilderEvent = {
|
|
56
113
|
kind: "text";
|
|
@@ -86,7 +143,12 @@ export type BuilderEvent = {
|
|
|
86
143
|
used: number;
|
|
87
144
|
maximum?: number;
|
|
88
145
|
percentage?: number;
|
|
89
|
-
|
|
146
|
+
observedAt?: string;
|
|
147
|
+
source?: "provider-event" | "provider-query" | "post-compact";
|
|
148
|
+
sequence?: number;
|
|
149
|
+
sessionId?: string;
|
|
150
|
+
model?: string;
|
|
151
|
+
} | ContextCompactionEvent | {
|
|
90
152
|
kind: "error";
|
|
91
153
|
message: string;
|
|
92
154
|
};
|
|
@@ -94,10 +156,26 @@ export interface ContextUsage {
|
|
|
94
156
|
used: number;
|
|
95
157
|
maximum?: number;
|
|
96
158
|
percentage?: number;
|
|
159
|
+
observedAt?: string;
|
|
160
|
+
source?: "provider-event" | "provider-query" | "post-compact";
|
|
161
|
+
/** Monotonic within one scoped provider session. */
|
|
162
|
+
sequence?: number;
|
|
163
|
+
sessionId?: string;
|
|
164
|
+
model?: string;
|
|
165
|
+
}
|
|
166
|
+
export interface ProviderSessionUsage {
|
|
167
|
+
inputTokens?: number;
|
|
168
|
+
outputTokens?: number;
|
|
169
|
+
totalTokens?: number;
|
|
170
|
+
authoritativeCostUsd?: number;
|
|
171
|
+
observedAt: string;
|
|
172
|
+
source: "provider" | "turn-aggregate";
|
|
97
173
|
}
|
|
98
174
|
export interface CompactResult {
|
|
99
175
|
ok: boolean;
|
|
100
176
|
error?: string;
|
|
177
|
+
/** Preserve exact-session loss so lifecycle policy never treats it as an ordinary retryable compaction failure. */
|
|
178
|
+
failure?: RuntimeFailure;
|
|
101
179
|
}
|
|
102
180
|
export interface ProviderSettingSwitch {
|
|
103
181
|
model?: string;
|
|
@@ -115,7 +193,19 @@ export interface BuilderAdapterOptions {
|
|
|
115
193
|
/** Permission decision callback. */
|
|
116
194
|
permission: PermissionHandler;
|
|
117
195
|
/** Resume a prior session instead of starting fresh. */
|
|
196
|
+
/** @deprecated Foreman-controlled recovery must use resumeSessionRef. */
|
|
118
197
|
resumeSessionId?: string;
|
|
198
|
+
/** Location-scoped provider conversation to validate before exact resume. */
|
|
199
|
+
resumeSessionRef?: ProviderSessionRefV1;
|
|
200
|
+
/** Canonical Rafi configuration/recovery root. Defaults to cwd for compatibility callers. */
|
|
201
|
+
configRoot?: string;
|
|
202
|
+
/** Metadata used when the provider first reveals a fresh session identity. */
|
|
203
|
+
sessionRole?: ConfigurableAgentRole;
|
|
204
|
+
sessionStream?: string;
|
|
205
|
+
sessionGeneration?: number;
|
|
206
|
+
workspaceIdentity?: string;
|
|
207
|
+
ticketId?: string;
|
|
208
|
+
deliveryUnitId?: string;
|
|
119
209
|
/** Override the model; omit for the agent's default. */
|
|
120
210
|
model?: string;
|
|
121
211
|
/** Reasoning effort level. Claude also accepts "max"; Codex supports up to "xhigh". */
|
|
@@ -128,6 +218,8 @@ export interface BuilderAdapterOptions {
|
|
|
128
218
|
systemPromptAppend?: string;
|
|
129
219
|
/** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
|
|
130
220
|
skills?: string[];
|
|
221
|
+
/** Builder/QA native policy used even for setup and handoff-acceptance turns. */
|
|
222
|
+
contextManagementPolicy?: ContextManagementPolicy;
|
|
131
223
|
}
|
|
132
224
|
export interface BuilderAdapter {
|
|
133
225
|
readonly agent: "claude" | "codex";
|
|
@@ -135,10 +227,26 @@ export interface BuilderAdapter {
|
|
|
135
227
|
sendTurn(text: string): Promise<TurnResult>;
|
|
136
228
|
/** Current session id, once known — used for resume. */
|
|
137
229
|
sessionId(): string | undefined;
|
|
230
|
+
/** Current location-scoped session reference, once known. */
|
|
231
|
+
sessionRef?(): ProviderSessionRefV1 | undefined;
|
|
232
|
+
/** Host-only metadata promotion after a validated handoff is accepted. */
|
|
233
|
+
adoptSessionRef?(ref: ProviderSessionRefV1): void;
|
|
234
|
+
/** Attach/probe a requested exact session without starting a provider turn. */
|
|
235
|
+
validateSession?(): Promise<SessionAvailabilityV1>;
|
|
138
236
|
/** Provider-native compaction on the exact live conversation. */
|
|
139
237
|
compact?(): Promise<CompactResult>;
|
|
238
|
+
/** Initialize the session and install native in-turn context controls. */
|
|
239
|
+
prepareContextManagement?(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
240
|
+
/** Safely reconfigure native in-turn controls on the same session. */
|
|
241
|
+
updateContextManagement?(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
242
|
+
/** Stop only at a provider-observed compaction boundary. */
|
|
243
|
+
interruptTurnAtCompactionBoundary?(providerEventId?: string): Promise<InterruptResult>;
|
|
244
|
+
/** Route wrapper-owned repair/retry/follow-up turns through the role gate. */
|
|
245
|
+
installManagedTurnDispatcher?(dispatcher: ManagedTurnDispatcher): void;
|
|
140
246
|
/** Truthful provider context occupancy, when exposed by the provider. */
|
|
141
247
|
contextUsage?(): Promise<ContextUsage | undefined>;
|
|
248
|
+
/** Provider/session cumulative totals; distinct from live context occupancy. */
|
|
249
|
+
sessionUsage?(): Promise<ProviderSessionUsage | undefined>;
|
|
142
250
|
/** Attempt a provider-supported in-conversation model/reasoning transition. */
|
|
143
251
|
switchSettings?(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
144
252
|
/** Stream of observability events. Iterate to drive a live view. */
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } f
|
|
|
5
5
|
import type { AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
6
6
|
import { type RoleBundle } from "./roles.js";
|
|
7
7
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
8
|
-
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
8
|
+
import type { AgentRoleDefaultsV1, ProviderSessionRefV1 } from "rafi-spec";
|
|
9
9
|
export type { EffortLevel } from "./adapters/types.js";
|
|
10
10
|
export declare const VALID_EFFORT: readonly EffortLevel[];
|
|
11
11
|
export interface RoleBuilderOptions {
|
|
@@ -23,6 +23,8 @@ export interface RoleBuilderOptions {
|
|
|
23
23
|
extraSkills?: string[];
|
|
24
24
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
25
25
|
resumeSessionId?: string;
|
|
26
|
+
/** Location-scoped exact session. Raw IDs are resolved through durable project bindings or treated as legacy candidates. */
|
|
27
|
+
resumeSessionRef?: ProviderSessionRefV1;
|
|
26
28
|
/** Observe successfully answered provider-native questions. */
|
|
27
29
|
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
28
30
|
/** Observe any provider-native question attempt, including denied prompts. */
|
|
@@ -51,6 +53,7 @@ export interface RoleInstructionRunResult {
|
|
|
51
53
|
model?: string;
|
|
52
54
|
effort?: EffortLevel;
|
|
53
55
|
sessionId?: string;
|
|
56
|
+
sessionRef?: ProviderSessionRefV1;
|
|
54
57
|
logPath: string;
|
|
55
58
|
roleBundle: RoleBundle;
|
|
56
59
|
skills: string[];
|
package/dist/agentRun.js
CHANGED
|
@@ -6,12 +6,15 @@ import { PermissionPolicy } from "./permissions/policy.js";
|
|
|
6
6
|
import { ClaudeAdapter } from "./adapters/claude.js";
|
|
7
7
|
import { CodexAdapter } from "./adapters/codex.js";
|
|
8
8
|
import { RecoveringAdapter } from "./adapters/recovering.js";
|
|
9
|
+
import { SessionUnavailableError } from "./adapters/sessionFailure.js";
|
|
9
10
|
import { Foreman, createPermissionHandler } from "./foreman.js";
|
|
10
11
|
import { printEvents } from "./cli/events.js";
|
|
11
12
|
import { loadRoleBundle } from "./roles.js";
|
|
12
13
|
import { ensureRuntimeReadyForCommand } from "./cli/runtimeAuthPrompt.js";
|
|
13
14
|
import { resolveAgentForProject } from "./cli/runtimeSelection.js";
|
|
14
15
|
import { parse as parseYaml } from "yaml";
|
|
16
|
+
import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "./sessionIdentity.js";
|
|
17
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
15
18
|
export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
16
19
|
export function assertEffortLevel(effort) {
|
|
17
20
|
if (effort && !VALID_EFFORT.includes(effort)) {
|
|
@@ -87,10 +90,29 @@ export async function createRoleBuilder(opts) {
|
|
|
87
90
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
88
91
|
}
|
|
89
92
|
const saved = readRoleDefaultsForExecution(opts.projectDir, opts.role);
|
|
93
|
+
if (opts.resumeSessionRef && opts.resumeSessionId && opts.resumeSessionRef.sessionId !== opts.resumeSessionId) {
|
|
94
|
+
throw new Error("resume session ID does not match the supplied location-scoped reference");
|
|
95
|
+
}
|
|
96
|
+
let requestedResumeRef = opts.resumeSessionRef;
|
|
97
|
+
if (!requestedResumeRef && opts.resumeSessionId) {
|
|
98
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
99
|
+
try {
|
|
100
|
+
requestedResumeRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(opts.resumeSessionId), opts.resumeSessionId);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
bindingDb.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
91
107
|
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
92
108
|
assertEffortLevel(effectiveEffort);
|
|
93
|
-
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? saved?.make);
|
|
109
|
+
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? requestedResumeRef?.provider ?? saved?.make);
|
|
110
|
+
if (requestedResumeRef && requestedResumeRef.provider !== runtime) {
|
|
111
|
+
throw new Error(`session ${requestedResumeRef.sessionId} belongs to ${requestedResumeRef.provider}, but ${runtime} was selected`);
|
|
112
|
+
}
|
|
113
|
+
if (requestedResumeRef && requestedResumeRef.role !== configurableRole(opts.role)) {
|
|
114
|
+
throw new Error(`session ${requestedResumeRef.sessionId} belongs to role ${requestedResumeRef.role}, not ${configurableRole(opts.role)}`);
|
|
115
|
+
}
|
|
94
116
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
95
117
|
const log = opts.log ?? new Log(makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-")));
|
|
96
118
|
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
|
|
@@ -99,7 +121,7 @@ export async function createRoleBuilder(opts) {
|
|
|
99
121
|
const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
|
|
100
122
|
label: opts.label,
|
|
101
123
|
yes: Boolean(opts.yes),
|
|
102
|
-
allowSwitch: opts.allowSwitch,
|
|
124
|
+
allowSwitch: requestedResumeRef || opts.resumeSessionId ? false : opts.allowSwitch,
|
|
103
125
|
model: initialModel,
|
|
104
126
|
});
|
|
105
127
|
runtime = ready.runtime;
|
|
@@ -108,13 +130,42 @@ export async function createRoleBuilder(opts) {
|
|
|
108
130
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
109
131
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
110
132
|
const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
111
|
-
const makeAdapter = async (nextRuntime, resumeSessionId) => {
|
|
133
|
+
const makeAdapter = async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
134
|
+
let scopedRef = resumeSessionRef;
|
|
135
|
+
if (!scopedRef && resumeSessionId) {
|
|
136
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
137
|
+
try {
|
|
138
|
+
scopedRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(resumeSessionId), resumeSessionId);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
bindingDb.close();
|
|
142
|
+
}
|
|
143
|
+
scopedRef ??= createProviderSessionRef({
|
|
144
|
+
provider: nextRuntime,
|
|
145
|
+
sessionId: resumeSessionId,
|
|
146
|
+
role: configurableRole(opts.role),
|
|
147
|
+
stream: opts.role,
|
|
148
|
+
generation: 0,
|
|
149
|
+
cwd: opts.projectDir,
|
|
150
|
+
configRoot: opts.projectDir,
|
|
151
|
+
workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
|
|
152
|
+
source: "legacy-inferred",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (scopedRef && scopedRef.provider !== nextRuntime) {
|
|
156
|
+
throw new Error(`session ${scopedRef.sessionId} belongs to ${scopedRef.provider}, but ${nextRuntime} was selected`);
|
|
157
|
+
}
|
|
112
158
|
const adapterOpts = {
|
|
113
159
|
cwd: opts.projectDir,
|
|
160
|
+
configRoot: opts.projectDir,
|
|
114
161
|
runtimeExecutable,
|
|
115
162
|
runtimePhase: phaseForRole(opts.role),
|
|
116
163
|
model,
|
|
117
|
-
|
|
164
|
+
...(scopedRef ? { resumeSessionRef: scopedRef } : {}),
|
|
165
|
+
sessionRole: configurableRole(opts.role),
|
|
166
|
+
sessionStream: opts.role,
|
|
167
|
+
sessionGeneration: scopedRef?.generation ?? 0,
|
|
168
|
+
workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
|
|
118
169
|
permission: createPermissionHandler(policy, log, {
|
|
119
170
|
interactive,
|
|
120
171
|
onAnsweredQuestion: opts.onAnsweredQuestion,
|
|
@@ -126,18 +177,46 @@ export async function createRoleBuilder(opts) {
|
|
|
126
177
|
systemPromptAppend: roleBundle.system || undefined,
|
|
127
178
|
skills: skills.length > 0 ? skills : undefined,
|
|
128
179
|
};
|
|
129
|
-
|
|
180
|
+
const adapter = nextRuntime === "codex"
|
|
130
181
|
? new CodexAdapter(adapterOpts)
|
|
131
182
|
: await ClaudeAdapter.create(adapterOpts);
|
|
183
|
+
if (scopedRef && adapter.agent === "codex") {
|
|
184
|
+
const availability = await adapter.validateSession();
|
|
185
|
+
if (availability.status !== "available") {
|
|
186
|
+
await adapter.close().catch(() => { });
|
|
187
|
+
throw new SessionUnavailableError({
|
|
188
|
+
runtime: "codex",
|
|
189
|
+
phase: "preflight",
|
|
190
|
+
dispatchState: "not-sent",
|
|
191
|
+
executable: runtimeExecutable,
|
|
192
|
+
cwd: opts.projectDir,
|
|
193
|
+
diagnostics: availability.detail ?? `Codex session ${scopedRef.sessionId} is ${availability.status}`,
|
|
194
|
+
availability,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return adapter;
|
|
132
199
|
};
|
|
133
|
-
const initial = await makeAdapter(runtime, opts.resumeSessionId);
|
|
200
|
+
const initial = await makeAdapter(runtime, opts.resumeSessionId, requestedResumeRef);
|
|
201
|
+
const persistSessionRef = (ref) => {
|
|
202
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
203
|
+
try {
|
|
204
|
+
bindingDb.recordProviderSessionBinding(ref);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
bindingDb.close();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const initialRef = initial.sessionRef?.();
|
|
211
|
+
if (initialRef)
|
|
212
|
+
persistSessionRef(initialRef);
|
|
134
213
|
const builder = new RecoveringAdapter({
|
|
135
214
|
initial,
|
|
136
215
|
runtime,
|
|
137
216
|
label: opts.label,
|
|
138
217
|
enabled: interactive,
|
|
139
|
-
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId,
|
|
140
|
-
recreate: async (nextRuntime, resumeSessionId) => {
|
|
218
|
+
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId && !requestedResumeRef,
|
|
219
|
+
recreate: async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
141
220
|
const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
|
|
142
221
|
label: opts.label,
|
|
143
222
|
allowSwitch: false,
|
|
@@ -146,8 +225,9 @@ export async function createRoleBuilder(opts) {
|
|
|
146
225
|
runtime = nextReady.runtime;
|
|
147
226
|
model = nextReady.model;
|
|
148
227
|
runtimeExecutable = nextReady.executable;
|
|
149
|
-
return makeAdapter(runtime, resumeSessionId);
|
|
228
|
+
return makeAdapter(runtime, resumeSessionId, resumeSessionRef);
|
|
150
229
|
},
|
|
230
|
+
onSessionRef: persistSessionRef,
|
|
151
231
|
});
|
|
152
232
|
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
153
233
|
}
|
|
@@ -225,6 +305,7 @@ export async function runRoleInstruction(opts) {
|
|
|
225
305
|
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
226
306
|
effort,
|
|
227
307
|
sessionId: builder.sessionId(),
|
|
308
|
+
sessionRef: builder.sessionRef?.(),
|
|
228
309
|
statusKind: turn.status.kind,
|
|
229
310
|
summary: turn.status.summary,
|
|
230
311
|
reason: turn.status.reason,
|
|
@@ -238,6 +319,7 @@ export async function runRoleInstruction(opts) {
|
|
|
238
319
|
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
239
320
|
effort,
|
|
240
321
|
sessionId: builder.sessionId(),
|
|
322
|
+
sessionRef: builder.sessionRef?.(),
|
|
241
323
|
logPath,
|
|
242
324
|
roleBundle,
|
|
243
325
|
skills,
|
|
@@ -250,3 +332,8 @@ export async function runRoleInstruction(opts) {
|
|
|
250
332
|
throw err;
|
|
251
333
|
}
|
|
252
334
|
}
|
|
335
|
+
function configurableRole(role) {
|
|
336
|
+
return ["builder", "qa", "planner", "ticket-maker", "uninstaller"].includes(role)
|
|
337
|
+
? role
|
|
338
|
+
: "builder";
|
|
339
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "../adapters/types.js";
|
|
2
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
|
+
export interface CurrentWorkflowIdentity {
|
|
4
|
+
worktree: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class CurrentWorkflowChangedError extends Error {
|
|
8
|
+
readonly expected: CurrentWorkflowIdentity;
|
|
9
|
+
readonly actual: CurrentWorkflowIdentity;
|
|
10
|
+
constructor(expected: CurrentWorkflowIdentity, actual: CurrentWorkflowIdentity);
|
|
11
|
+
}
|
|
12
|
+
export declare function captureCurrentWorkflowIdentity(cwd: string): CurrentWorkflowIdentity;
|
|
13
|
+
export declare function assertCurrentWorkflowIdentity(cwd: string, expected: CurrentWorkflowIdentity): void;
|
|
14
|
+
export declare function currentWorkflowIdentityKey(identity: CurrentWorkflowIdentity): string;
|
|
15
|
+
export declare function captureCurrentWorkflowSessionIdentity(cwd: string): string;
|
|
16
|
+
/** Fence every provider boundary against an unexpected user/provider ref switch. */
|
|
17
|
+
export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
18
|
+
private readonly adapter;
|
|
19
|
+
private readonly cwd;
|
|
20
|
+
private readonly expected;
|
|
21
|
+
readonly agent: BuilderAdapter["agent"];
|
|
22
|
+
constructor(adapter: BuilderAdapter, cwd: string, expected?: CurrentWorkflowIdentity);
|
|
23
|
+
sendTurn(text: string): Promise<TurnResult>;
|
|
24
|
+
sessionId(): string | undefined;
|
|
25
|
+
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
26
|
+
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
27
|
+
validateSession(): Promise<SessionAvailabilityV1>;
|
|
28
|
+
compact(): Promise<CompactResult>;
|
|
29
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
30
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
31
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
32
|
+
installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
|
|
33
|
+
contextUsage(): Promise<ContextUsage | undefined>;
|
|
34
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
35
|
+
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
36
|
+
events(): AsyncIterable<BuilderEvent>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|