ai-foreman 1.6.2 → 1.7.0

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +5 -1
  3. package/dist/adapters/claude.js +41 -0
  4. package/dist/adapters/codex.d.ts +26 -28
  5. package/dist/adapters/codex.js +209 -148
  6. package/dist/adapters/recovering.d.ts +3 -1
  7. package/dist/adapters/recovering.js +2 -0
  8. package/dist/adapters/types.d.ts +29 -0
  9. package/dist/branch/planner.js +1 -1
  10. package/dist/branch/runner.d.ts +6 -0
  11. package/dist/branch/runner.js +149 -16
  12. package/dist/buildRuns.d.ts +9 -2
  13. package/dist/buildRuns.js +110 -7
  14. package/dist/cli/start.d.ts +4 -0
  15. package/dist/cli/start.js +283 -28
  16. package/dist/cli/status.js +25 -2
  17. package/dist/cli/tickets.d.ts +1 -0
  18. package/dist/cli/tickets.js +67 -27
  19. package/dist/foreman.d.ts +18 -3
  20. package/dist/foreman.js +112 -5
  21. package/dist/log.d.ts +1 -1
  22. package/dist/operationRecovery.d.ts +25 -0
  23. package/dist/operationRecovery.js +46 -0
  24. package/dist/outcomes.d.ts +16 -0
  25. package/dist/outcomes.js +27 -0
  26. package/dist/qaReview.d.ts +52 -0
  27. package/dist/qaReview.js +103 -0
  28. package/dist/qaSnapshot.d.ts +19 -0
  29. package/dist/qaSnapshot.js +88 -0
  30. package/dist/sessionLifecycle.d.ts +37 -0
  31. package/dist/sessionLifecycle.js +104 -0
  32. package/dist/statusReporter.d.ts +40 -0
  33. package/dist/statusReporter.js +31 -0
  34. package/dist/ticketPopulation.d.ts +47 -0
  35. package/dist/ticketPopulation.js +205 -0
  36. package/dist/tickets/blockers.d.ts +1 -1
  37. package/dist/tickets/blockers.js +2 -0
  38. package/dist/tickets/commands.d.ts +1 -0
  39. package/dist/tickets/commands.js +30 -1
  40. package/dist/tickets/delivery.d.ts +36 -0
  41. package/dist/tickets/delivery.js +204 -4
  42. package/dist/tickets/queue.d.ts +3 -0
  43. package/dist/tickets/queue.js +50 -1
  44. package/dist/tickets/stateDb.d.ts +2 -1
  45. package/dist/tickets/stateDb.js +24 -1
  46. package/dist/tickets/ticketSchema.d.ts +44 -0
  47. package/dist/tickets/ticketSchema.js +10 -0
  48. package/dist/tickets/validate.js +1 -1
  49. package/dist/workflowDb.d.ts +113 -0
  50. package/dist/workflowDb.js +261 -0
  51. package/package.json +16 -3
@@ -0,0 +1,103 @@
1
+ import { buildQaFixInstruction, buildQaInstruction, parseStepStatus } from "./foreman.js";
2
+ import { createDisposableQaSnapshot } from "./qaSnapshot.js";
3
+ export async function runIsolatedQa(opts) {
4
+ const history = [];
5
+ for (let cycle = 1; cycle <= opts.maxCycles; cycle++) {
6
+ const review = await oneReview(opts, cycle);
7
+ if (review.outcome === "retry-modification") {
8
+ cycle -= 1;
9
+ continue;
10
+ }
11
+ if (review.outcome === "passed")
12
+ return review;
13
+ if (review.outcome !== "failed")
14
+ return review;
15
+ history.push({ cycle, outcome: "qa_fail", detail: review.detail });
16
+ if (cycle === opts.maxCycles)
17
+ break;
18
+ const fix = await opts.fix(review.detail ?? "QA reported unspecified issues");
19
+ if (!fix.ok)
20
+ return { outcome: "blocked", detail: fix.detail ?? "Builder QA fix failed" };
21
+ }
22
+ const detail = `QA could not converge after ${opts.maxCycles} automatic cycle(s); choose retry, pause, waive, or Planner remediation`;
23
+ if (!opts.onNonconvergence)
24
+ return { outcome: "nonconverged", detail };
25
+ const decision = await opts.onNonconvergence({ ticket: opts.ticket, history, builderWorktree: opts.builderWorktree });
26
+ if (decision.action === "pause")
27
+ return { outcome: "nonconverged", detail };
28
+ if (decision.action === "waive")
29
+ return { outcome: "waived", detail: history.at(-1)?.detail ?? detail, summary: "QA explicitly waived by user" };
30
+ const issues = decision.action === "remediate" ? decision.remediation : history.at(-1)?.detail;
31
+ if (!issues)
32
+ return { outcome: "nonconverged", detail: "Planner remediation did not produce approved fix instructions" };
33
+ const fix = await opts.fix(issues);
34
+ if (!fix.ok)
35
+ return { outcome: "blocked", detail: fix.detail ?? "Builder QA fix failed" };
36
+ return runIsolatedQa(opts);
37
+ }
38
+ async function oneReview(opts, cycle) {
39
+ const snapshot = createDisposableQaSnapshot(opts.builderWorktree);
40
+ let qa;
41
+ try {
42
+ const resume = opts.qaStrategy === "compact" && opts.state.modificationViolations === 0 ? opts.state.sessionId : undefined;
43
+ qa = await opts.createQa(snapshot.path, resume);
44
+ if (opts.qaStrategy === "compact" && opts.state.reviews > 0 && resume) {
45
+ const compacted = await compactWithRetry(qa);
46
+ if (!compacted.ok) {
47
+ await qa.close();
48
+ qa = await opts.createQa(snapshot.path);
49
+ opts.state.sessionId = undefined;
50
+ }
51
+ }
52
+ const handoff = [
53
+ buildQaInstruction(), "", "QA handoff:", `Ticket: ${opts.ticket.id} — ${opts.ticket.title}`,
54
+ `Acceptance: ${opts.ticket.acceptance.join("; ")}`, `Builder summary: ${opts.builderSummary}`,
55
+ `Changed files/diff digest: ${snapshot.manifest.diffDigest}`, `Required tests: ${opts.ticket.required_tests.join("; ")}`,
56
+ "Do not change source, configuration, documentation, tickets, or control files. Only ignored cache, coverage, and build output is allowed.",
57
+ ].join("\n");
58
+ const turn = await qa.sendTurn(handoff);
59
+ const status = parseStepStatus(turn.text);
60
+ opts.state.reviews += 1;
61
+ opts.state.sessionId = qa.sessionId();
62
+ const changes = snapshot.qaChanges();
63
+ if (changes.length) {
64
+ opts.state.modificationViolations += 1;
65
+ opts.evidence?.({ cycle, outcome: "qa_file_modification", detail: "QA modified the disposable review copy", qaDiff: changes });
66
+ if (opts.state.modificationViolations === 1)
67
+ return { outcome: "retry-modification" };
68
+ return { outcome: "needs-human", detail: `QA modified files twice: ${changes.join(", ")}` };
69
+ }
70
+ opts.state.modificationViolations = 0;
71
+ if (turn.isError)
72
+ return { outcome: "blocked", detail: turn.text.slice(0, 500) };
73
+ if (status.kind === "qa_pass") {
74
+ opts.evidence?.({ cycle, outcome: "passed", detail: status.summary ?? "qa_pass" });
75
+ return { outcome: "passed", summary: status.summary };
76
+ }
77
+ if (status.kind === "qa_fail") {
78
+ const detail = status.issues ?? "QA reported no issue text";
79
+ opts.evidence?.({ cycle, outcome: "failed", detail });
80
+ return { outcome: "failed", detail };
81
+ }
82
+ return { outcome: "needs-human", detail: status.error ?? `QA returned ${status.kind}` };
83
+ }
84
+ finally {
85
+ await qa?.close().catch(() => { });
86
+ snapshot.remove();
87
+ }
88
+ }
89
+ export async function compactWithRetry(adapter) {
90
+ if (!adapter.compact)
91
+ return { ok: false, error: "native compaction unavailable" };
92
+ let error = "";
93
+ for (let attempt = 0; attempt < 2; attempt++) {
94
+ const result = await adapter.compact();
95
+ if (result.ok)
96
+ return { ok: true };
97
+ error = result.error ?? "compaction failed";
98
+ }
99
+ return { ok: false, error };
100
+ }
101
+ export function buildDurableQaFixHandoff(ticket, issues, worktree, priorResult, diffDigest) {
102
+ return [buildQaFixInstruction(issues), "", `Ticket: ${ticket.id} — ${ticket.title}`, `Acceptance: ${ticket.acceptance.join("; ")}`, `Worktree: ${worktree}`, `Current diff: ${diffDigest}`, `Prior result: ${priorResult}`, `All QA issues: ${issues}`].join("\n");
103
+ }
@@ -0,0 +1,19 @@
1
+ export interface QaChangeManifest {
2
+ diffDigest: string;
3
+ untracked: Array<{
4
+ path: string;
5
+ kind: "file" | "symlink";
6
+ mode: number;
7
+ digest: string;
8
+ }>;
9
+ }
10
+ export interface DisposableQaSnapshot {
11
+ path: string;
12
+ manifest: QaChangeManifest;
13
+ verify(): void;
14
+ qaChanges(): string[];
15
+ remove(): void;
16
+ }
17
+ /** Reproduce the Builder's exact change set in a detached disposable worktree. */
18
+ export declare function createDisposableQaSnapshot(builderWorktree: string): DisposableQaSnapshot;
19
+ export declare function changeManifest(worktree: string): QaChangeManifest;
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ /** Reproduce the Builder's exact change set in a detached disposable worktree. */
7
+ export function createDisposableQaSnapshot(builderWorktree) {
8
+ const source = resolve(builderWorktree);
9
+ const root = execFileSync("git", ["-C", source, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
10
+ const tempRoot = mkdtempSync(join(tmpdir(), "rafi-qa-"));
11
+ const review = join(tempRoot, "review");
12
+ const head = execFileSync("git", ["-C", source, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
13
+ const added = spawnSync("git", ["-C", root, "worktree", "add", "--detach", review, head], { encoding: "utf8" });
14
+ if (added.status !== 0) {
15
+ rmSync(tempRoot, { recursive: true, force: true });
16
+ throw new Error(`cannot create disposable QA worktree: ${added.stderr.trim()}`);
17
+ }
18
+ try {
19
+ const diff = execFileSync("git", ["-C", source, "diff", "--binary", "HEAD"], { encoding: "buffer", maxBuffer: 128 * 1024 * 1024 });
20
+ if (diff.length) {
21
+ const applied = spawnSync("git", ["-C", review, "apply", "--index", "--binary", "--whitespace=nowarn", "-"], { input: diff, encoding: "buffer", maxBuffer: 128 * 1024 * 1024 });
22
+ if (applied.status !== 0)
23
+ throw new Error(`cannot apply Builder diff to QA worktree: ${Buffer.from(applied.stderr).toString().trim()}`);
24
+ }
25
+ for (const path of untrackedPaths(source))
26
+ copyUntracked(source, review, path);
27
+ const manifest = changeManifest(source);
28
+ const snapshot = {
29
+ path: review, manifest,
30
+ verify: () => {
31
+ const actual = changeManifest(review);
32
+ if (JSON.stringify(actual) !== JSON.stringify(manifest))
33
+ throw new Error(`disposable QA snapshot does not match Builder change manifest: expected ${JSON.stringify(manifest)}, got ${JSON.stringify(actual)}`);
34
+ },
35
+ qaChanges: () => manifestDifference(manifest, changeManifest(review)),
36
+ remove: () => {
37
+ spawnSync("git", ["-C", root, "worktree", "remove", "--force", review], { encoding: "utf8" });
38
+ rmSync(tempRoot, { recursive: true, force: true });
39
+ },
40
+ };
41
+ snapshot.verify();
42
+ return snapshot;
43
+ }
44
+ catch (error) {
45
+ spawnSync("git", ["-C", root, "worktree", "remove", "--force", review], { encoding: "utf8" });
46
+ rmSync(tempRoot, { recursive: true, force: true });
47
+ throw error;
48
+ }
49
+ }
50
+ export function changeManifest(worktree) {
51
+ const diff = execFileSync("git", ["-C", worktree, "diff", "--binary", "HEAD"], { encoding: "buffer", maxBuffer: 128 * 1024 * 1024 });
52
+ const untracked = untrackedPaths(worktree).map((path) => {
53
+ const absolute = join(worktree, path);
54
+ const stat = lstatSync(absolute);
55
+ const kind = stat.isSymbolicLink() ? "symlink" : "file";
56
+ const bytes = kind === "symlink" ? Buffer.from(readlinkSync(absolute)) : readFileSync(absolute);
57
+ return { path, kind, mode: stat.mode & 0o7777, digest: createHash("sha256").update(bytes).digest("hex") };
58
+ });
59
+ return { diffDigest: createHash("sha256").update(diff).digest("hex"), untracked };
60
+ }
61
+ function untrackedPaths(worktree) {
62
+ const raw = execFileSync("git", ["-C", worktree, "ls-files", "--others", "--exclude-standard", "-z"], { encoding: "buffer" });
63
+ return raw.toString().split("\0").filter(Boolean).sort();
64
+ }
65
+ function copyUntracked(source, target, path) {
66
+ const from = join(source, path);
67
+ const to = join(target, path);
68
+ mkdirSync(dirname(to), { recursive: true });
69
+ const stat = lstatSync(from);
70
+ if (stat.isSymbolicLink()) {
71
+ if (existsSync(to))
72
+ rmSync(to, { force: true });
73
+ symlinkSync(readlinkSync(from), to);
74
+ }
75
+ else
76
+ cpSync(from, to, { preserveTimestamps: true });
77
+ }
78
+ function manifestDifference(before, after) {
79
+ const changes = [];
80
+ if (before.diffDigest !== after.diffDigest)
81
+ changes.push("tracked diff changed");
82
+ const left = new Map(before.untracked.map((item) => [item.path, item]));
83
+ const right = new Map(after.untracked.map((item) => [item.path, item]));
84
+ for (const path of [...new Set([...left.keys(), ...right.keys()])].sort())
85
+ if (JSON.stringify(left.get(path)) !== JSON.stringify(right.get(path)))
86
+ changes.push(path);
87
+ return changes;
88
+ }
@@ -0,0 +1,37 @@
1
+ import type { ConfigurableAgentRole, ResolvedAgentSettings, WorkflowIssue } from "rafi-spec";
2
+ import type { BuilderAdapter, ContextUsage } from "./adapters/types.js";
3
+ export interface SessionTransition {
4
+ kind: "initial" | "compacted" | "fresh" | "missing" | "compaction-retry" | "compaction-fallback" | "settings-attempt" | "settings-continued" | "settings-fallback";
5
+ role: ConfigurableAgentRole;
6
+ workSession: number;
7
+ sessionId?: string;
8
+ message: string;
9
+ }
10
+ export interface SessionBoundary {
11
+ adapter: BuilderAdapter;
12
+ transition: SessionTransition;
13
+ issue?: WorkflowIssue;
14
+ }
15
+ export interface RoleSessionControllerOptions {
16
+ role: ConfigurableAgentRole;
17
+ settings: ResolvedAgentSettings;
18
+ create: (settings: ResolvedAgentSettings, resumeSessionId?: string) => Promise<BuilderAdapter>;
19
+ report?: (transition: SessionTransition) => void;
20
+ now?: () => Date;
21
+ initialSessionId?: string;
22
+ }
23
+ /** One lifecycle implementation shared by interviews, builds, QA, and recovery. */
24
+ export declare class RoleSessionController {
25
+ private readonly options;
26
+ private adapter?;
27
+ private workSessions;
28
+ constructor(options: RoleSessionControllerOptions);
29
+ next(_durableHandoff: string, nextSettings?: ResolvedAgentSettings): Promise<SessionBoundary>;
30
+ current(): BuilderAdapter | undefined;
31
+ count(): number;
32
+ usage(): Promise<ContextUsage | undefined>;
33
+ close(): Promise<void>;
34
+ private boundary;
35
+ private emit;
36
+ private issue;
37
+ }
@@ -0,0 +1,104 @@
1
+ import { sanitizeDiagnostics } from "./runtimeReadiness.js";
2
+ /** One lifecycle implementation shared by interviews, builds, QA, and recovery. */
3
+ export class RoleSessionController {
4
+ options;
5
+ adapter;
6
+ workSessions = 0;
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async next(_durableHandoff, nextSettings) {
11
+ this.workSessions += 1;
12
+ if (!this.adapter) {
13
+ if (nextSettings)
14
+ this.options.settings = nextSettings;
15
+ this.adapter = await this.options.create(this.options.settings, this.options.initialSessionId);
16
+ return this.boundary("initial", "started a workflow-scoped provider conversation");
17
+ }
18
+ if (nextSettings) {
19
+ const current = this.options.settings;
20
+ if (nextSettings.make !== current.make) {
21
+ await this.adapter.close();
22
+ this.options.settings = nextSettings;
23
+ this.adapter = await this.options.create(nextSettings);
24
+ return this.boundary("settings-fallback", `provider changed from ${current.make} to ${nextSettings.make}; started fresh from the durable handoff`);
25
+ }
26
+ const changed = nextSettings.model !== current.model || nextSettings.reasoning !== current.reasoning || nextSettings.fast !== current.fast;
27
+ this.options.settings = nextSettings;
28
+ if (changed) {
29
+ this.emit("settings-attempt", `attempting same-conversation settings switch to ${nextSettings.model}, reasoning=${nextSettings.reasoning}, fast=${nextSettings.fast}`);
30
+ const switched = this.adapter.switchSettings
31
+ ? await this.adapter.switchSettings({ model: nextSettings.model === "default" ? undefined : nextSettings.model, effort: effort(nextSettings.reasoning), fast: nextSettings.fast })
32
+ : { ok: false, error: "provider adapter does not support an in-conversation settings switch" };
33
+ if (!switched.ok) {
34
+ await this.adapter.close();
35
+ this.adapter = await this.options.create(nextSettings);
36
+ const detail = sanitize(switched.error ?? "settings switch failed");
37
+ const issue = this.issue("session_model_switch_failure", detail, "This work session continued fresh with the current project settings.");
38
+ return this.boundary("settings-fallback", `settings switch failed (${detail}); started fresh and continuity was lost`, issue);
39
+ }
40
+ this.emit("settings-continued", "provider accepted the same-conversation settings switch");
41
+ }
42
+ }
43
+ if (this.options.settings.session_strategy === "fresh") {
44
+ await this.adapter.close();
45
+ this.adapter = await this.options.create(this.options.settings);
46
+ const result = this.boundary("fresh", "started a fresh conversation with a durable handoff");
47
+ return result;
48
+ }
49
+ if (!this.adapter.sessionId()) {
50
+ await this.adapter.close();
51
+ this.adapter = await this.options.create(this.options.settings);
52
+ const issue = this.issue("session_missing", "exact provider session is unavailable", "Continue fresh from the durable handoff.");
53
+ const result = this.boundary("missing", "continuity unavailable; started fresh from the durable handoff", issue);
54
+ return result;
55
+ }
56
+ let firstError = "";
57
+ for (let attempt = 1; attempt <= 2; attempt++) {
58
+ try {
59
+ const compacted = this.adapter.compact
60
+ ? await this.adapter.compact()
61
+ : { ok: false, error: "provider adapter does not expose native compaction" };
62
+ if (compacted.ok)
63
+ return this.boundary("compacted", attempt === 1 ? "compacted and continued the exact conversation" : "compaction retry succeeded; continued the exact conversation");
64
+ firstError = sanitize(compacted.error ?? "provider reported compaction failure");
65
+ }
66
+ catch (error) {
67
+ firstError = sanitize(error);
68
+ }
69
+ if (attempt === 1)
70
+ this.emit("compaction-retry", `compaction failed (${firstError}); retrying once`);
71
+ }
72
+ await this.adapter.close();
73
+ this.adapter = await this.options.create(this.options.settings);
74
+ const issue = this.issue("session_compaction_failure", firstError, "Inspect provider health; this work session continued fresh.");
75
+ const result = this.boundary("compaction-fallback", `compaction failed twice (${firstError}); started fresh and continuity was lost`, issue);
76
+ return result;
77
+ }
78
+ current() { return this.adapter; }
79
+ count() { return this.workSessions; }
80
+ async usage() { return this.adapter?.contextUsage?.(); }
81
+ async close() { await this.adapter?.close(); this.adapter = undefined; }
82
+ boundary(kind, message, issue) {
83
+ const transition = this.emit(kind, message);
84
+ return { adapter: this.adapter, transition, issue };
85
+ }
86
+ emit(kind, message) {
87
+ const transition = { kind, role: this.options.role, workSession: this.workSessions, sessionId: this.adapter?.sessionId(), message };
88
+ this.options.report?.(transition);
89
+ return transition;
90
+ }
91
+ issue(code, detail, action) {
92
+ return {
93
+ code, role: this.options.role, phase: "session-boundary", provider: this.options.settings.make,
94
+ model: this.options.settings.model, detail, human_required: false, recoverable: true,
95
+ suggested_action: action, occurred_at: (this.options.now?.() ?? new Date()).toISOString(),
96
+ };
97
+ }
98
+ }
99
+ function effort(value) {
100
+ return ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
101
+ }
102
+ function sanitize(error) {
103
+ return sanitizeDiagnostics(error instanceof Error ? error.message : String(error)).slice(0, 1000);
104
+ }
@@ -0,0 +1,40 @@
1
+ import type { BuilderAdapter } from "./adapters/types.js";
2
+ export interface AgentStatusSnapshot {
3
+ role: string;
4
+ provider: string;
5
+ model: string;
6
+ reasoning: string;
7
+ fast: boolean;
8
+ ticket?: string;
9
+ stack?: string;
10
+ step: number;
11
+ total: number;
12
+ phase: string;
13
+ qaCycle?: number;
14
+ context?: {
15
+ used: number;
16
+ maximum?: number;
17
+ percentage?: number;
18
+ };
19
+ sessionTransition: string;
20
+ at: string;
21
+ }
22
+ export interface StatusClock {
23
+ now(): Date;
24
+ setInterval(callback: () => void, milliseconds: number): unknown;
25
+ clearInterval(handle: unknown): void;
26
+ }
27
+ /** Non-blocking five-minute status with provider-truthful context occupancy. */
28
+ export declare class AgentStatusReporter {
29
+ private readonly input;
30
+ private readonly emit;
31
+ private readonly clock;
32
+ private readonly intervalMs;
33
+ private handle?;
34
+ constructor(input: Omit<AgentStatusSnapshot, "context" | "at"> & {
35
+ adapter: BuilderAdapter | (() => BuilderAdapter);
36
+ }, emit: (line: string, snapshot: AgentStatusSnapshot) => void, clock?: StatusClock, intervalMs?: number);
37
+ start(): void;
38
+ tick(): Promise<void>;
39
+ stop(): void;
40
+ }
@@ -0,0 +1,31 @@
1
+ const systemClock = {
2
+ now: () => new Date(),
3
+ setInterval: (callback, milliseconds) => { const handle = setInterval(callback, milliseconds); handle.unref(); return handle; },
4
+ clearInterval: (handle) => clearInterval(handle),
5
+ };
6
+ /** Non-blocking five-minute status with provider-truthful context occupancy. */
7
+ export class AgentStatusReporter {
8
+ input;
9
+ emit;
10
+ clock;
11
+ intervalMs;
12
+ handle;
13
+ constructor(input, emit, clock = systemClock, intervalMs = 300_000) {
14
+ this.input = input;
15
+ this.emit = emit;
16
+ this.clock = clock;
17
+ this.intervalMs = intervalMs;
18
+ }
19
+ start() { if (this.handle !== undefined)
20
+ return; this.handle = this.clock.setInterval(() => void this.tick(), this.intervalMs); }
21
+ async tick() {
22
+ const adapter = typeof this.input.adapter === "function" ? this.input.adapter() : this.input.adapter;
23
+ const context = await adapter.contextUsage?.();
24
+ const { adapter: _adapter, ...base } = this.input;
25
+ const snapshot = { ...base, ...(context ? { context } : {}), at: this.clock.now().toISOString() };
26
+ const occupancy = context ? `context: ${context.used}/${context.maximum ?? "unknown"}${context.percentage === undefined ? "" : ` (${context.percentage.toFixed(1)}%)`}` : "context: unavailable";
27
+ this.emit(`[${snapshot.at}] ${snapshot.role} ${snapshot.provider}/${snapshot.model} reasoning=${snapshot.reasoning} fast=${snapshot.fast}; ${snapshot.ticket ? `ticket=${snapshot.ticket}; ` : ""}${snapshot.stack ? `stack=${snapshot.stack}; ` : ""}step=${snapshot.step}/${snapshot.total}; phase=${snapshot.phase}${snapshot.qaCycle ? `; qa_cycle=${snapshot.qaCycle}` : ""}; ${occupancy}; session=${snapshot.sessionTransition}`, snapshot);
28
+ }
29
+ stop() { if (this.handle !== undefined)
30
+ this.clock.clearInterval(this.handle); this.handle = undefined; }
31
+ }
@@ -0,0 +1,47 @@
1
+ import type { StructuredPlanV1 } from "rafi-spec";
2
+ import type { TicketDef } from "./tickets/ticketSchema.js";
3
+ import { type DeliveryConfig } from "./tickets/delivery.js";
4
+ export interface TicketSliceProposal {
5
+ slice_ref: string;
6
+ title: string;
7
+ area: string;
8
+ priority: TicketDef["priority"];
9
+ size: TicketDef["size"];
10
+ risk: TicketDef["risk"];
11
+ summary: string;
12
+ acceptance: string[];
13
+ required_tests: string[];
14
+ likely_files: string[];
15
+ depends_on: string[];
16
+ rollback?: string | null;
17
+ notes?: string | null;
18
+ }
19
+ export interface TicketPopulationProposalV1 {
20
+ version: 1;
21
+ plan_id: string;
22
+ revision: number;
23
+ tickets: TicketSliceProposal[];
24
+ retirements: string[];
25
+ }
26
+ export declare const TICKET_POPULATION_PROPOSAL_START = "RAFI_TICKET_POPULATION_PROPOSAL_START";
27
+ export declare const TICKET_POPULATION_PROPOSAL_END = "RAFI_TICKET_POPULATION_PROPOSAL_END";
28
+ export interface MaterializedTicketPopulation {
29
+ tickets: TicketDef[];
30
+ delivery: DeliveryConfig;
31
+ retirements: string[];
32
+ sliceToTicket: Map<string, string>;
33
+ }
34
+ export declare function extractTicketPopulationProposal(output: string): TicketPopulationProposalV1;
35
+ export declare function validateTicketPopulationProposal(proposal: TicketPopulationProposalV1, plan: StructuredPlanV1, existing: TicketDef[]): string[];
36
+ export declare function materializeTicketPopulation(proposal: TicketPopulationProposalV1, plan: StructuredPlanV1, existing: TicketDef[]): MaterializedTicketPopulation;
37
+ export declare function authorizeTicketRetirements(retirements: string[], input: {
38
+ interactiveConfirmed?: boolean;
39
+ authorizedIds?: string[];
40
+ computerRun?: boolean;
41
+ }): void;
42
+ export declare function applyTicketPopulation(projectDir: string, materialized: MaterializedTicketPopulation, runId?: string, now?: Date): {
43
+ runId: string;
44
+ transactionId: string;
45
+ };
46
+ /** Startup recovery deterministically finishes publications after tracker commit. */
47
+ export declare function recoverTicketPublications(projectDir: string): string[];