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
package/dist/statusReporter.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContextSample, SessionUsageSample } from "rafi-spec";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./adapters/types.js";
|
|
2
3
|
export interface AgentStatusSnapshot {
|
|
3
|
-
role:
|
|
4
|
-
provider:
|
|
4
|
+
role: "builder" | "qa";
|
|
5
|
+
provider: "claude" | "codex";
|
|
5
6
|
model: string;
|
|
6
7
|
reasoning: string;
|
|
7
8
|
fast: boolean;
|
|
@@ -11,12 +12,22 @@ export interface AgentStatusSnapshot {
|
|
|
11
12
|
total: number;
|
|
12
13
|
phase: string;
|
|
13
14
|
qaCycle?: number;
|
|
15
|
+
/** Compatibility projection. New consumers should use contextSample. */
|
|
14
16
|
context?: {
|
|
15
17
|
used: number;
|
|
16
18
|
maximum?: number;
|
|
17
19
|
percentage?: number;
|
|
18
20
|
};
|
|
21
|
+
contextSample: ContextSample;
|
|
22
|
+
sessionUsage?: SessionUsageSample;
|
|
19
23
|
sessionTransition: string;
|
|
24
|
+
lifecycleState?: string;
|
|
25
|
+
configuredCeilingPercent?: number;
|
|
26
|
+
installedNativeTokenLimit?: number;
|
|
27
|
+
installedNativePercent?: number;
|
|
28
|
+
compactionCount?: number;
|
|
29
|
+
compactMaximum?: number;
|
|
30
|
+
settingsRevision?: number;
|
|
20
31
|
at: string;
|
|
21
32
|
}
|
|
22
33
|
export interface StatusClock {
|
|
@@ -24,17 +35,82 @@ export interface StatusClock {
|
|
|
24
35
|
setInterval(callback: () => void, milliseconds: number): unknown;
|
|
25
36
|
clearInterval(handle: unknown): void;
|
|
26
37
|
}
|
|
27
|
-
|
|
38
|
+
export interface AgentStatusReporterInput {
|
|
39
|
+
runId: string;
|
|
40
|
+
role: "builder" | "qa";
|
|
41
|
+
provider: "claude" | "codex";
|
|
42
|
+
model: string;
|
|
43
|
+
reasoning: string;
|
|
44
|
+
fast: boolean;
|
|
45
|
+
ticket?: string;
|
|
46
|
+
stack?: string;
|
|
47
|
+
step: number;
|
|
48
|
+
total: number;
|
|
49
|
+
phase: string;
|
|
50
|
+
qaCycle?: number;
|
|
51
|
+
sessionTransition: string;
|
|
52
|
+
settingsRevision: number;
|
|
53
|
+
displaySessionCost: boolean;
|
|
54
|
+
adapter: BuilderAdapter | (() => BuilderAdapter);
|
|
55
|
+
compactionCount?: number | (() => number);
|
|
56
|
+
handoffGeneration?: number | (() => number);
|
|
57
|
+
/** Shared enforcement snapshot. When supplied, status never polls occupancy independently. */
|
|
58
|
+
contextSnapshot?: () => {
|
|
59
|
+
role: "builder" | "qa";
|
|
60
|
+
provider: "claude" | "codex";
|
|
61
|
+
model: string;
|
|
62
|
+
lifecycleState: string;
|
|
63
|
+
contextSample: ContextSample;
|
|
64
|
+
configuredCeilingPercent: number;
|
|
65
|
+
installedNativeTokenLimit?: number;
|
|
66
|
+
installedNativePercent?: number;
|
|
67
|
+
compactionCount: number;
|
|
68
|
+
compactMaximum: number;
|
|
69
|
+
settingsRevision: number;
|
|
70
|
+
} | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A non-blocking ten-second reporter. It never invents occupancy or cost: an
|
|
74
|
+
* absent provider sample is rendered as measuring, stale, or unavailable.
|
|
75
|
+
*/
|
|
28
76
|
export declare class AgentStatusReporter {
|
|
29
77
|
private readonly input;
|
|
30
78
|
private readonly emit;
|
|
31
79
|
private readonly clock;
|
|
32
80
|
private readonly intervalMs;
|
|
33
81
|
private handle?;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
private lastGoodContext?;
|
|
83
|
+
private readonly lastGoodByRole;
|
|
84
|
+
constructor(input: AgentStatusReporterInput, emit: (line: string, snapshot: AgentStatusSnapshot) => void, clock?: StatusClock, intervalMs?: number);
|
|
37
85
|
start(): void;
|
|
38
86
|
tick(): Promise<void>;
|
|
39
|
-
stop(): void;
|
|
87
|
+
stop(finalPhase?: string): void;
|
|
88
|
+
updateState(patch: Partial<Omit<AgentStatusReporterInput, "runId">>): void;
|
|
89
|
+
private emitSnapshot;
|
|
90
|
+
private measuringSample;
|
|
91
|
+
private contextSample;
|
|
92
|
+
private adapter;
|
|
93
|
+
}
|
|
94
|
+
/** Switches the shared live status to a role for the lifetime of its adapter. */
|
|
95
|
+
export declare class RoleStatusAdapter implements BuilderAdapter {
|
|
96
|
+
private readonly adapter;
|
|
97
|
+
private readonly onActive;
|
|
98
|
+
private readonly onClose;
|
|
99
|
+
readonly agent: "claude" | "codex";
|
|
100
|
+
constructor(adapter: BuilderAdapter, onActive: (adapter: BuilderAdapter) => void, onClose: () => void);
|
|
101
|
+
sendTurn(text: string): Promise<TurnResult>;
|
|
102
|
+
sessionId(): string | undefined;
|
|
103
|
+
sessionRef(): import("rafi-spec").ProviderSessionRefV1 | undefined;
|
|
104
|
+
adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
|
|
105
|
+
validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
|
|
106
|
+
compact(): Promise<CompactResult>;
|
|
107
|
+
prepareContextManagement(policy: import("./adapters/types.js").ContextManagementPolicy): Promise<import("./adapters/types.js").PreparedContextManagement>;
|
|
108
|
+
updateContextManagement(policy: import("./adapters/types.js").ContextManagementPolicy): Promise<import("./adapters/types.js").PreparedContextManagement>;
|
|
109
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<import("./adapters/types.js").InterruptResult>;
|
|
110
|
+
installManagedTurnDispatcher(dispatcher: import("./adapters/types.js").ManagedTurnDispatcher): void;
|
|
111
|
+
contextUsage(): Promise<ContextUsage | undefined>;
|
|
112
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
113
|
+
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
114
|
+
events(): AsyncIterable<BuilderEvent>;
|
|
115
|
+
close(): Promise<void>;
|
|
40
116
|
}
|
package/dist/statusReporter.js
CHANGED
|
@@ -1,31 +1,287 @@
|
|
|
1
|
+
import { providerSessionKey } from "./sessionIdentity.js";
|
|
1
2
|
const systemClock = {
|
|
2
3
|
now: () => new Date(),
|
|
3
|
-
setInterval: (callback, milliseconds) => {
|
|
4
|
+
setInterval: (callback, milliseconds) => {
|
|
5
|
+
const handle = setInterval(callback, milliseconds);
|
|
6
|
+
handle.unref();
|
|
7
|
+
return handle;
|
|
8
|
+
},
|
|
4
9
|
clearInterval: (handle) => clearInterval(handle),
|
|
5
10
|
};
|
|
6
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* A non-blocking ten-second reporter. It never invents occupancy or cost: an
|
|
13
|
+
* absent provider sample is rendered as measuring, stale, or unavailable.
|
|
14
|
+
*/
|
|
7
15
|
export class AgentStatusReporter {
|
|
8
16
|
input;
|
|
9
17
|
emit;
|
|
10
18
|
clock;
|
|
11
19
|
intervalMs;
|
|
12
20
|
handle;
|
|
13
|
-
|
|
21
|
+
lastGoodContext;
|
|
22
|
+
lastGoodByRole = new Map();
|
|
23
|
+
constructor(input, emit, clock = systemClock, intervalMs = 10_000) {
|
|
14
24
|
this.input = input;
|
|
15
25
|
this.emit = emit;
|
|
16
26
|
this.clock = clock;
|
|
17
27
|
this.intervalMs = intervalMs;
|
|
18
28
|
}
|
|
19
|
-
start() {
|
|
20
|
-
|
|
29
|
+
start() {
|
|
30
|
+
if (this.handle !== undefined)
|
|
31
|
+
return;
|
|
32
|
+
this.emitSnapshot(this.measuringSample(), undefined);
|
|
33
|
+
this.handle = this.clock.setInterval(() => void this.tick(), this.intervalMs);
|
|
34
|
+
}
|
|
21
35
|
async tick() {
|
|
22
|
-
const adapter =
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const adapter = this.adapter();
|
|
37
|
+
const now = this.clock.now();
|
|
38
|
+
let contextSample;
|
|
39
|
+
const hasCoordinator = this.input.contextSnapshot !== undefined;
|
|
40
|
+
const coordinated = this.input.contextSnapshot?.();
|
|
41
|
+
if (coordinated && coordinated.role === this.input.role) {
|
|
42
|
+
contextSample = coordinated.contextSample;
|
|
43
|
+
this.input.provider = coordinated.provider;
|
|
44
|
+
this.input.model = coordinated.model;
|
|
45
|
+
this.input.settingsRevision = coordinated.settingsRevision;
|
|
46
|
+
this.input.compactionCount = coordinated.compactionCount;
|
|
47
|
+
if (contextSample.freshness === "fresh") {
|
|
48
|
+
this.lastGoodContext = contextSample;
|
|
49
|
+
this.lastGoodByRole.set(this.input.role, contextSample);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (!hasCoordinator) {
|
|
53
|
+
let observed;
|
|
54
|
+
try {
|
|
55
|
+
observed = await adapter.contextUsage?.();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
observed = undefined;
|
|
59
|
+
}
|
|
60
|
+
if (observed && Number.isFinite(observed.used))
|
|
61
|
+
contextSample = this.contextSample(observed, now);
|
|
62
|
+
else if (this.lastGoodContext)
|
|
63
|
+
contextSample = { ...this.lastGoodContext, freshness: "stale" };
|
|
64
|
+
else
|
|
65
|
+
contextSample = { ...this.measuringSample(now), freshness: "unavailable" };
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const prior = this.lastGoodByRole.get(this.input.role);
|
|
69
|
+
contextSample = prior
|
|
70
|
+
? { ...prior, freshness: "stale" }
|
|
71
|
+
: { ...this.measuringSample(now), freshness: "unavailable" };
|
|
72
|
+
}
|
|
73
|
+
if (contextSample.freshness === "fresh") {
|
|
74
|
+
this.lastGoodContext = contextSample;
|
|
75
|
+
this.lastGoodByRole.set(this.input.role, contextSample);
|
|
76
|
+
}
|
|
77
|
+
let sessionUsage;
|
|
78
|
+
if (this.input.displaySessionCost) {
|
|
79
|
+
try {
|
|
80
|
+
const usage = await adapter.sessionUsage?.();
|
|
81
|
+
const ref = adapter.sessionRef?.();
|
|
82
|
+
sessionUsage = usage ? {
|
|
83
|
+
version: 1,
|
|
84
|
+
runId: this.input.runId,
|
|
85
|
+
role: this.input.role,
|
|
86
|
+
provider: this.input.provider,
|
|
87
|
+
providerSessionId: adapter.sessionId(),
|
|
88
|
+
...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}),
|
|
89
|
+
observedAt: usage.observedAt,
|
|
90
|
+
source: usage.source,
|
|
91
|
+
cumulativeInputTokens: usage.inputTokens,
|
|
92
|
+
cumulativeOutputTokens: usage.outputTokens,
|
|
93
|
+
cumulativeTotalTokens: usage.totalTokens,
|
|
94
|
+
authoritativeCostUsd: usage.authoritativeCostUsd,
|
|
95
|
+
} : unavailableUsage(this.input, adapter, now);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
sessionUsage = unavailableUsage(this.input, adapter, now);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.emitSnapshot(contextSample, sessionUsage, now);
|
|
102
|
+
}
|
|
103
|
+
stop(finalPhase = "completed") {
|
|
104
|
+
if (this.handle === undefined)
|
|
105
|
+
return;
|
|
106
|
+
this.clock.clearInterval(this.handle);
|
|
107
|
+
this.handle = undefined;
|
|
108
|
+
this.input.phase = finalPhase;
|
|
109
|
+
this.emitSnapshot(this.lastGoodContext ?? { ...this.measuringSample(), freshness: "unavailable" }, undefined);
|
|
110
|
+
}
|
|
111
|
+
updateState(patch) {
|
|
112
|
+
const priorRole = this.input.role;
|
|
113
|
+
const priorAdapter = this.adapter();
|
|
114
|
+
const priorSessionId = priorAdapter.sessionId();
|
|
115
|
+
if (this.lastGoodContext)
|
|
116
|
+
this.lastGoodByRole.set(priorRole, this.lastGoodContext);
|
|
117
|
+
Object.assign(this.input, patch);
|
|
118
|
+
if (priorRole !== this.input.role
|
|
119
|
+
|| priorAdapter !== this.adapter()
|
|
120
|
+
|| priorSessionId !== this.adapter().sessionId()
|
|
121
|
+
|| patch.sessionTransition !== undefined)
|
|
122
|
+
this.lastGoodContext = this.lastGoodByRole.get(this.input.role);
|
|
123
|
+
this.emitSnapshot(this.measuringSample(), undefined);
|
|
124
|
+
}
|
|
125
|
+
emitSnapshot(contextSample, sessionUsage, now = this.clock.now()) {
|
|
126
|
+
const coordinated = this.input.contextSnapshot?.();
|
|
127
|
+
const { adapter: _adapter, displaySessionCost: _display, compactionCount: _compactions, handoffGeneration: _handoffs, contextSnapshot: _contextSnapshot, ...base } = this.input;
|
|
128
|
+
const context = contextSample.used === undefined ? undefined : {
|
|
129
|
+
used: contextSample.used,
|
|
130
|
+
maximum: contextSample.maximum,
|
|
131
|
+
percentage: contextSample.percentage,
|
|
132
|
+
};
|
|
133
|
+
const snapshot = {
|
|
134
|
+
...base,
|
|
135
|
+
...(context ? { context } : {}),
|
|
136
|
+
contextSample,
|
|
137
|
+
...(sessionUsage ? { sessionUsage } : {}),
|
|
138
|
+
...(coordinated && coordinated.role === this.input.role ? {
|
|
139
|
+
lifecycleState: coordinated.lifecycleState,
|
|
140
|
+
configuredCeilingPercent: coordinated.configuredCeilingPercent,
|
|
141
|
+
installedNativeTokenLimit: coordinated.installedNativeTokenLimit,
|
|
142
|
+
installedNativePercent: coordinated.installedNativePercent,
|
|
143
|
+
compactionCount: coordinated.compactionCount,
|
|
144
|
+
compactMaximum: coordinated.compactMaximum,
|
|
145
|
+
settingsRevision: coordinated.settingsRevision,
|
|
146
|
+
} : {
|
|
147
|
+
compactionCount: contextSample.compactionCount,
|
|
148
|
+
settingsRevision: contextSample.settingsRevision,
|
|
149
|
+
}),
|
|
150
|
+
at: now.toISOString(),
|
|
151
|
+
};
|
|
152
|
+
const line = [
|
|
153
|
+
`${snapshot.role} ${snapshot.provider}/${snapshot.model}`,
|
|
154
|
+
`activity=${snapshot.phase}`,
|
|
155
|
+
formatContext(contextSample),
|
|
156
|
+
...(snapshot.configuredCeilingPercent === undefined ? [] : [`ceiling=${snapshot.configuredCeilingPercent}%`]),
|
|
157
|
+
...(snapshot.installedNativePercent === undefined ? [] : [`native=${snapshot.installedNativePercent.toFixed(1)}%${snapshot.installedNativeTokenLimit === undefined ? "" : `/${snapshot.installedNativeTokenLimit}`}`]),
|
|
158
|
+
...(snapshot.lifecycleState ? [`state=${formatLifecycle(snapshot.lifecycleState)}`] : []),
|
|
159
|
+
`compactions=${snapshot.compactionCount ?? contextSample.compactionCount}${snapshot.compactMaximum === undefined ? "" : `/${snapshot.compactMaximum}`}`,
|
|
160
|
+
`revision=${snapshot.settingsRevision ?? contextSample.settingsRevision}`,
|
|
161
|
+
`handoff=${contextSample.handoffGeneration}`,
|
|
162
|
+
`session=${snapshot.sessionTransition}`,
|
|
163
|
+
...(this.input.displaySessionCost ? [formatSessionUsage(sessionUsage)] : []),
|
|
164
|
+
].join("; ");
|
|
165
|
+
this.emit(`[${snapshot.at}] ${line}`, snapshot);
|
|
166
|
+
}
|
|
167
|
+
measuringSample(now = this.clock.now()) {
|
|
168
|
+
const ref = this.adapter().sessionRef?.();
|
|
169
|
+
return {
|
|
170
|
+
version: 1,
|
|
171
|
+
runId: this.input.runId,
|
|
172
|
+
role: this.input.role,
|
|
173
|
+
provider: this.input.provider,
|
|
174
|
+
providerSessionId: this.adapter().sessionId(),
|
|
175
|
+
...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}),
|
|
176
|
+
model: this.input.model,
|
|
177
|
+
observedAt: now.toISOString(),
|
|
178
|
+
source: "provider-query",
|
|
179
|
+
freshness: "measuring",
|
|
180
|
+
settingsRevision: this.input.settingsRevision,
|
|
181
|
+
compactionCount: numeric(this.input.compactionCount),
|
|
182
|
+
handoffGeneration: numeric(this.input.handoffGeneration),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
contextSample(usage, now) {
|
|
186
|
+
const ref = this.adapter().sessionRef?.();
|
|
187
|
+
return {
|
|
188
|
+
...this.measuringSample(now),
|
|
189
|
+
providerSessionId: this.adapter().sessionId(),
|
|
190
|
+
...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}),
|
|
191
|
+
observedAt: usage.observedAt ?? now.toISOString(),
|
|
192
|
+
source: usage.source ?? "provider-query",
|
|
193
|
+
freshness: "fresh",
|
|
194
|
+
used: usage.used,
|
|
195
|
+
maximum: usage.maximum,
|
|
196
|
+
percentage: usage.percentage ?? (usage.maximum ? (usage.used / usage.maximum) * 100 : undefined),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
adapter() {
|
|
200
|
+
return typeof this.input.adapter === "function" ? this.input.adapter() : this.input.adapter;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Switches the shared live status to a role for the lifetime of its adapter. */
|
|
204
|
+
export class RoleStatusAdapter {
|
|
205
|
+
adapter;
|
|
206
|
+
onActive;
|
|
207
|
+
onClose;
|
|
208
|
+
agent;
|
|
209
|
+
constructor(adapter, onActive, onClose) {
|
|
210
|
+
this.adapter = adapter;
|
|
211
|
+
this.onActive = onActive;
|
|
212
|
+
this.onClose = onClose;
|
|
213
|
+
this.agent = adapter.agent;
|
|
214
|
+
}
|
|
215
|
+
async sendTurn(text) { this.onActive(this.adapter); return this.adapter.sendTurn(text); }
|
|
216
|
+
sessionId() { return this.adapter.sessionId(); }
|
|
217
|
+
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
218
|
+
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
219
|
+
validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
220
|
+
compact() { this.onActive(this.adapter); return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
221
|
+
prepareContextManagement(policy) { this.onActive(this.adapter); if (!this.adapter.prepareContextManagement)
|
|
222
|
+
return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
|
|
223
|
+
updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
|
|
224
|
+
return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
|
|
225
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
|
|
226
|
+
installManagedTurnDispatcher(dispatcher) { this.adapter.installManagedTurnDispatcher?.(dispatcher); }
|
|
227
|
+
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
228
|
+
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
229
|
+
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
230
|
+
events() { return this.adapter.events(); }
|
|
231
|
+
async close() { try {
|
|
232
|
+
await this.adapter.close();
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
this.onClose();
|
|
236
|
+
} }
|
|
237
|
+
}
|
|
238
|
+
function unavailableUsage(input, adapter, now) {
|
|
239
|
+
const ref = adapter.sessionRef?.();
|
|
240
|
+
return {
|
|
241
|
+
version: 1,
|
|
242
|
+
runId: input.runId,
|
|
243
|
+
role: input.role,
|
|
244
|
+
provider: input.provider,
|
|
245
|
+
providerSessionId: adapter.sessionId(),
|
|
246
|
+
...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}),
|
|
247
|
+
observedAt: now.toISOString(),
|
|
248
|
+
source: "unavailable",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function numeric(value) {
|
|
252
|
+
const resolved = typeof value === "function" ? value() : value;
|
|
253
|
+
return Number.isSafeInteger(resolved) && (resolved ?? -1) >= 0 ? resolved : 0;
|
|
254
|
+
}
|
|
255
|
+
function formatContext(sample) {
|
|
256
|
+
if (sample.freshness === "measuring")
|
|
257
|
+
return "context measuring…";
|
|
258
|
+
if (sample.freshness === "unavailable")
|
|
259
|
+
return "context unavailable";
|
|
260
|
+
const occupancy = sample.percentage === undefined
|
|
261
|
+
? `${sample.used ?? "unknown"}/${sample.maximum ?? "unknown"}`
|
|
262
|
+
: `${sample.percentage.toFixed(1)}% (${sample.used ?? "unknown"}/${sample.maximum ?? "unknown"})`;
|
|
263
|
+
return `context ${occupancy}${sample.freshness === "stale" ? " stale" : ""}`;
|
|
264
|
+
}
|
|
265
|
+
function formatLifecycle(state) {
|
|
266
|
+
if (state === "initializing")
|
|
267
|
+
return "measuring";
|
|
268
|
+
if (state === "threshold_pending")
|
|
269
|
+
return "pending";
|
|
270
|
+
if (state === "native_compacting" || state === "host_compacting")
|
|
271
|
+
return "compacting";
|
|
272
|
+
if (state === "compacted_unverified")
|
|
273
|
+
return "verifying";
|
|
274
|
+
if (state === "handoff_required" || state === "resuming")
|
|
275
|
+
return "handing off";
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
278
|
+
function formatSessionUsage(sample) {
|
|
279
|
+
if (!sample || sample.source === "unavailable")
|
|
280
|
+
return "session usage unavailable";
|
|
281
|
+
if (sample.authoritativeCostUsd !== undefined)
|
|
282
|
+
return `session cost $${sample.authoritativeCostUsd.toFixed(4)} (provider)`;
|
|
283
|
+
if (sample.cumulativeTotalTokens !== undefined)
|
|
284
|
+
return `session tokens ${sample.cumulativeTotalTokens}`;
|
|
285
|
+
const total = (sample.cumulativeInputTokens ?? 0) + (sample.cumulativeOutputTokens ?? 0);
|
|
286
|
+
return total > 0 ? `session tokens ${total}` : "session usage unavailable";
|
|
31
287
|
}
|
package/dist/ticketPlanning.js
CHANGED
|
@@ -203,6 +203,7 @@ export function applyApprovedTicketPlan(projectDir, proposal, opts) {
|
|
|
203
203
|
try {
|
|
204
204
|
const now = new Date().toISOString();
|
|
205
205
|
db.transaction(() => {
|
|
206
|
+
db.ensureSyntheticLegacyGroup(existing, opts.now ?? new Date());
|
|
206
207
|
const existingNext = [...db.getAllStates().values()].filter((state) => state.status === "next").map((state) => state.ticket_id);
|
|
207
208
|
if (proposal.next.replace_existing)
|
|
208
209
|
for (const id of existingNext)
|
|
@@ -227,6 +228,10 @@ export function applyApprovedTicketPlan(projectDir, proposal, opts) {
|
|
|
227
228
|
throw new Error(`future work item ${disposition.id} does not exist`);
|
|
228
229
|
db.updateFutureWorkDisposition(disposition.id, disposition.disposition);
|
|
229
230
|
}
|
|
231
|
+
if (proposal.additions.length) {
|
|
232
|
+
const operationDigest = createHash("sha256").update(JSON.stringify(proposal.additions)).digest("hex");
|
|
233
|
+
db.createTicketGroup({ origin: "ticket-plan", operationId: `ticket-plan:${operationDigest}`, createdAt: now, members: proposal.additions.map((ticket) => ({ ticketId: ticket.id, definition: ticket, validatedAt: now })) });
|
|
234
|
+
}
|
|
230
235
|
});
|
|
231
236
|
const states = db.getAllStates();
|
|
232
237
|
renderAndWrite({ config, projectDir: root, ticketDefs: staged, states, db });
|
|
@@ -234,6 +239,9 @@ export function applyApprovedTicketPlan(projectDir, proposal, opts) {
|
|
|
234
239
|
const errors = validation.filter((issue) => issue.severity === "error");
|
|
235
240
|
if (errors.length)
|
|
236
241
|
throw new Error(`post-apply tracker validation failed:\n${errors.map((issue) => `- ${issue.message}`).join("\n")}`);
|
|
242
|
+
for (const ticket of staged)
|
|
243
|
+
if (db.getTicketGroupForTicket(ticket.id))
|
|
244
|
+
db.updateTicketDefinitionSnapshot(ticket.id, ticket, now);
|
|
237
245
|
}
|
|
238
246
|
finally {
|
|
239
247
|
db.close();
|
package/dist/ticketPopulation.js
CHANGED
|
@@ -2,9 +2,9 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { stringify } from "yaml";
|
|
5
|
-
import { WorkflowDb } from "./workflowDb.js";
|
|
5
|
+
import { WorkflowDb, WORKFLOW_DB_FILE } from "./workflowDb.js";
|
|
6
6
|
import { cmdRender, cmdValidate } from "./tickets/commands.js";
|
|
7
|
-
import { validateTicketDefs } from "./tickets/ticketLoader.js";
|
|
7
|
+
import { loadTickets, validateTicketDefs } from "./tickets/ticketLoader.js";
|
|
8
8
|
import { normalizeDeliveryConfig, validateDeliveryConfig } from "./tickets/delivery.js";
|
|
9
9
|
import { loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
|
|
10
10
|
import { StateDb } from "./tickets/stateDb.js";
|
|
@@ -106,6 +106,12 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
106
106
|
const root = resolve(projectDir);
|
|
107
107
|
const config = loadTicketsConfig(root);
|
|
108
108
|
const paths = resolveTicketPaths(config, root);
|
|
109
|
+
// Finish any older two-phase publication before resolving a new batch. This
|
|
110
|
+
// makes direct API use as safe as the CLI pre-action hook.
|
|
111
|
+
recoverTicketPublications(root);
|
|
112
|
+
const previousTickets = loadTickets(paths.tickets);
|
|
113
|
+
const previousIds = new Set(previousTickets.map((ticket) => ticket.id));
|
|
114
|
+
const createdTickets = materialized.tickets.filter((ticket) => !previousIds.has(ticket.id));
|
|
109
115
|
const workflow = new WorkflowDb(root);
|
|
110
116
|
let actualRunId = runId;
|
|
111
117
|
let lease;
|
|
@@ -113,12 +119,17 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
113
119
|
if (!actualRunId)
|
|
114
120
|
actualRunId = workflow.createRun({ kind: "ticket-populate", originalWork: { tickets: materialized.tickets.map((ticket) => ticket.id) }, state: {} }, now).runId;
|
|
115
121
|
lease = workflow.acquireLease(actualRunId, undefined, now);
|
|
122
|
+
const operationId = `ticket-populate-publication:${actualRunId}`;
|
|
116
123
|
const deliveryPath = join(root, ".tickets", "delivery.yaml");
|
|
117
124
|
const stage = join(root, ".rafi", "staging", randomUUID());
|
|
118
125
|
mkdirSync(stage, { recursive: true });
|
|
119
126
|
const stagedTickets = join(stage, "tickets.yaml");
|
|
120
127
|
const stagedDelivery = join(stage, "delivery.yaml");
|
|
121
|
-
const tx = workflow.beginPublication(actualRunId, {
|
|
128
|
+
const tx = workflow.beginPublication(actualRunId, {
|
|
129
|
+
operation: "ticket-populate", operationId,
|
|
130
|
+
files: [{ staged: stagedTickets, target: paths.tickets }, { staged: stagedDelivery, target: deliveryPath }],
|
|
131
|
+
stage, managedTicketIds: materialized.tickets.map((ticket) => ticket.id),
|
|
132
|
+
}, {
|
|
122
133
|
[paths.tickets]: fileDigest(paths.tickets), [deliveryPath]: fileDigest(deliveryPath),
|
|
123
134
|
}, now);
|
|
124
135
|
fsyncWrite(stagedTickets, stringify({ tickets: materialized.tickets }, { lineWidth: 120 }));
|
|
@@ -128,6 +139,7 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
128
139
|
try {
|
|
129
140
|
db.transaction(() => {
|
|
130
141
|
const at = now.toISOString();
|
|
142
|
+
db.ensureSyntheticLegacyGroup(previousTickets, now);
|
|
131
143
|
for (const ticket of materialized.tickets)
|
|
132
144
|
if (!db.getState(ticket.id))
|
|
133
145
|
db.upsertState(ticket.id, { status: "planned", updated_by: "rafi tickets populate" }, at);
|
|
@@ -138,6 +150,16 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
138
150
|
db.upsertState(id, { status: "obsolete", completed_at: at, updated_by: "rafi tickets populate" }, at);
|
|
139
151
|
db.insertEvent({ timestamp: at, actor: "rafi tickets populate", ticket_id: id, event_type: "obsolete", old_status: prior?.status ?? "planned", new_status: "obsolete", summary: "Removed by approved plan revision", validation: null, evidence: null, payload_json: JSON.stringify({ runId: actualRunId }) });
|
|
140
152
|
}
|
|
153
|
+
if (createdTickets.length)
|
|
154
|
+
db.createTicketGroup({ origin: "ticket-populate", operationId: `ticket-populate:${actualRunId}`, createdAt: at, members: createdTickets.map((ticket) => ({ ticketId: ticket.id, definition: ticket, validatedAt: at })) });
|
|
155
|
+
db.recordOperationReceipt({
|
|
156
|
+
operation_id: operationId,
|
|
157
|
+
operation_type: "ticket-populate-publication",
|
|
158
|
+
ticket_id: null,
|
|
159
|
+
run_id: actualRunId,
|
|
160
|
+
completed_at: at,
|
|
161
|
+
payload_json: JSON.stringify({ transactionId: tx.transactionId, tickets: materialized.tickets.map((ticket) => ticket.id) }),
|
|
162
|
+
});
|
|
141
163
|
});
|
|
142
164
|
}
|
|
143
165
|
finally {
|
|
@@ -151,6 +173,15 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
151
173
|
const validation = cmdValidate(root);
|
|
152
174
|
if (!validation.clean)
|
|
153
175
|
throw new Error(`published ticket population failed validation: ${validation.issues.map((issue) => issue.message).join("; ")}`);
|
|
176
|
+
const snapshotDb = new StateDb(paths.stateDb);
|
|
177
|
+
try {
|
|
178
|
+
for (const ticket of materialized.tickets)
|
|
179
|
+
if (snapshotDb.getTicketGroupForTicket(ticket.id))
|
|
180
|
+
snapshotDb.updateTicketDefinitionSnapshot(ticket.id, ticket, now.toISOString());
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
snapshotDb.close();
|
|
184
|
+
}
|
|
154
185
|
workflow.updatePublication(tx.transactionId, "committed", now);
|
|
155
186
|
workflow.transition(actualRunId, { status: "completed", checkpoint: "population-committed", remainingWork: {}, state: { transactionId: tx.transactionId } }, now);
|
|
156
187
|
rmSync(stage, { recursive: true, force: true });
|
|
@@ -164,25 +195,70 @@ export function applyTicketPopulation(projectDir, materialized, runId, now = new
|
|
|
164
195
|
}
|
|
165
196
|
/** Startup recovery deterministically finishes publications after tracker commit. */
|
|
166
197
|
export function recoverTicketPublications(projectDir) {
|
|
198
|
+
if (!existsSync(join(resolve(projectDir), WORKFLOW_DB_FILE)))
|
|
199
|
+
return [];
|
|
167
200
|
const workflow = new WorkflowDb(projectDir);
|
|
168
201
|
const recovered = [];
|
|
169
202
|
try {
|
|
170
203
|
for (const tx of workflow.incompletePublications()) {
|
|
171
204
|
const intent = tx.intent;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
const run = workflow.getRun(tx.runId);
|
|
206
|
+
const isLegacyPopulation = !intent.operation && Array.isArray(intent.files) && run?.kind === "ticket-populate";
|
|
207
|
+
if (intent.operation !== "ticket-populate" && !isLegacyPopulation)
|
|
208
|
+
continue;
|
|
209
|
+
if (!intent.files || !intent.stage)
|
|
210
|
+
throw new Error(`ticket population ${tx.transactionId} has an invalid durable publication intent`);
|
|
211
|
+
const root = resolve(projectDir);
|
|
212
|
+
const config = loadTicketsConfig(root);
|
|
213
|
+
const paths = resolveTicketPaths(config, root);
|
|
214
|
+
let trackerCommitted = ["tracker_committed", "published"].includes(tx.status);
|
|
215
|
+
if (!trackerCommitted && intent.operationId) {
|
|
216
|
+
const receiptDb = new StateDb(paths.stateDb);
|
|
217
|
+
try {
|
|
218
|
+
trackerCommitted = Boolean(receiptDb.getOperationReceipt(intent.operationId));
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
receiptDb.close();
|
|
222
|
+
}
|
|
180
223
|
}
|
|
181
|
-
|
|
224
|
+
if (!trackerCommitted) {
|
|
182
225
|
rmSync(intent.stage, { recursive: true, force: true });
|
|
183
226
|
workflow.updatePublication(tx.transactionId, "rolled_back");
|
|
227
|
+
if (run && run.status !== "failed")
|
|
228
|
+
workflow.transition(run.runId, { status: "failed", checkpoint: "population-rolled-back-before-tracker-commit", state: { ...run.state, recoveredPublication: tx.transactionId } });
|
|
184
229
|
recovered.push(tx.transactionId);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (tx.status !== "published") {
|
|
233
|
+
for (const file of intent.files) {
|
|
234
|
+
if (!existsSync(file.staged))
|
|
235
|
+
throw new Error(`ticket population ${tx.transactionId} cannot recover because staged file is missing: ${file.staged}`);
|
|
236
|
+
publishStaged(file.staged, file.target);
|
|
237
|
+
}
|
|
238
|
+
workflow.updatePublication(tx.transactionId, "published");
|
|
239
|
+
}
|
|
240
|
+
cmdRender(root);
|
|
241
|
+
const validation = cmdValidate(root);
|
|
242
|
+
if (!validation.clean)
|
|
243
|
+
throw new Error(`recovered ticket population failed validation: ${validation.issues.map((issue) => issue.message).join("; ")}`);
|
|
244
|
+
const definitions = loadTickets(paths.tickets);
|
|
245
|
+
const managedIds = new Set(intent.managedTicketIds ?? definitions.map((ticket) => ticket.id));
|
|
246
|
+
const snapshotDb = new StateDb(paths.stateDb);
|
|
247
|
+
try {
|
|
248
|
+
snapshotDb.transaction(() => {
|
|
249
|
+
for (const ticket of definitions)
|
|
250
|
+
if (managedIds.has(ticket.id) && snapshotDb.getTicketGroupForTicket(ticket.id))
|
|
251
|
+
snapshotDb.updateTicketDefinitionSnapshot(ticket.id, ticket);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
snapshotDb.close();
|
|
185
256
|
}
|
|
257
|
+
workflow.updatePublication(tx.transactionId, "committed");
|
|
258
|
+
if (run && run.status !== "completed")
|
|
259
|
+
workflow.transition(run.runId, { status: "completed", checkpoint: "population-recovered", remainingWork: {}, state: { ...run.state, recoveredPublication: tx.transactionId } });
|
|
260
|
+
recovered.push(tx.transactionId);
|
|
261
|
+
rmSync(intent.stage, { recursive: true, force: true });
|
|
186
262
|
}
|
|
187
263
|
return recovered;
|
|
188
264
|
}
|
package/dist/tickets/commands.js
CHANGED
|
@@ -385,19 +385,27 @@ export function cmdAcceptFutureWork(projectDir, futureWorkId, opts) {
|
|
|
385
385
|
rollback: null,
|
|
386
386
|
notes: fw.rationale ?? null,
|
|
387
387
|
};
|
|
388
|
-
ctx.db.
|
|
389
|
-
ctx.db.updateFutureWorkDisposition(futureWorkId, "accepted");
|
|
390
|
-
logEvent(ctx.db, {
|
|
391
|
-
timestamp: now,
|
|
392
|
-
actor: opts.actor ?? null,
|
|
393
|
-
ticketId: opts.ticketId,
|
|
394
|
-
eventType: "accept-future-work",
|
|
395
|
-
oldStatus: null,
|
|
396
|
-
newStatus: "planned",
|
|
397
|
-
summary: `Accepted future work ${futureWorkId} as ticket ${opts.ticketId}`,
|
|
398
|
-
});
|
|
399
|
-
});
|
|
388
|
+
ctx.db.ensureSyntheticLegacyGroup(ctx.tickets);
|
|
400
389
|
saveTickets(ctx.paths.tickets, [...ctx.tickets, newTicket]);
|
|
390
|
+
try {
|
|
391
|
+
ctx.db.transaction(() => {
|
|
392
|
+
ctx.db.updateFutureWorkDisposition(futureWorkId, "accepted");
|
|
393
|
+
logEvent(ctx.db, {
|
|
394
|
+
timestamp: now,
|
|
395
|
+
actor: opts.actor ?? null,
|
|
396
|
+
ticketId: opts.ticketId,
|
|
397
|
+
eventType: "accept-future-work",
|
|
398
|
+
oldStatus: null,
|
|
399
|
+
newStatus: "planned",
|
|
400
|
+
summary: `Accepted future work ${futureWorkId} as ticket ${opts.ticketId}`,
|
|
401
|
+
});
|
|
402
|
+
ctx.db.createTicketGroup({ origin: "future-work", operationId: `future-work:${futureWorkId}`, members: [{ ticketId: newTicket.id, definition: newTicket, validatedAt: now }] });
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
saveTickets(ctx.paths.tickets, ctx.tickets);
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
401
409
|
// Re-open context to pick up new ticket list
|
|
402
410
|
const newCtx = openContext(projectDir);
|
|
403
411
|
try {
|