ai-foreman 1.6.2 → 1.7.1

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 (55) hide show
  1. package/README.md +2 -2
  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/sources/sourceRegistry.d.ts +49 -0
  33. package/dist/sources/sourceRegistry.js +577 -0
  34. package/dist/statusReporter.d.ts +40 -0
  35. package/dist/statusReporter.js +31 -0
  36. package/dist/ticketPopulation.d.ts +54 -0
  37. package/dist/ticketPopulation.js +209 -0
  38. package/dist/tickets/blockers.d.ts +1 -1
  39. package/dist/tickets/blockers.js +2 -0
  40. package/dist/tickets/commands.d.ts +1 -0
  41. package/dist/tickets/commands.js +31 -1
  42. package/dist/tickets/delivery.d.ts +36 -0
  43. package/dist/tickets/delivery.js +204 -4
  44. package/dist/tickets/queue.d.ts +3 -0
  45. package/dist/tickets/queue.js +50 -1
  46. package/dist/tickets/setupConfig.js +30 -2
  47. package/dist/tickets/sourceFetch.js +6 -1
  48. package/dist/tickets/stateDb.d.ts +2 -1
  49. package/dist/tickets/stateDb.js +24 -1
  50. package/dist/tickets/ticketSchema.d.ts +53 -0
  51. package/dist/tickets/ticketSchema.js +11 -0
  52. package/dist/tickets/validate.js +1 -1
  53. package/dist/workflowDb.d.ts +113 -0
  54. package/dist/workflowDb.js +261 -0
  55. package/package.json +20 -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,49 @@
1
+ import type { ProjectSourceEntry, ProjectSourceLocator, ProjectSourceType, SourceRegistryConfig, SourceSnapshotStorage, TicketSetupSource } from "rafi-spec";
2
+ export declare const SOURCE_REQUEST_START = "RAFI_SOURCE_REQUEST_START";
3
+ export declare const SOURCE_REQUEST_END = "RAFI_SOURCE_REQUEST_END";
4
+ export interface StructuredSourceRequest {
5
+ type?: ProjectSourceType;
6
+ description?: string;
7
+ label?: string;
8
+ locator?: ProjectSourceLocator;
9
+ }
10
+ export interface LoadedSourceRegistry {
11
+ registry: SourceRegistryConfig;
12
+ configured: boolean;
13
+ migrated: boolean;
14
+ warnings: string[];
15
+ }
16
+ export interface CaptureResult {
17
+ registry: SourceRegistryConfig;
18
+ entries: ProjectSourceEntry[];
19
+ snapshots: string[];
20
+ pending: string[];
21
+ }
22
+ export declare function emptySourceRegistry(storage?: SourceSnapshotStorage): SourceRegistryConfig;
23
+ /** Read the shared registry and merge legacy source fields in memory. The next source-aware save persists migration. */
24
+ export declare function loadSourceRegistry(projectDir: string): LoadedSourceRegistry;
25
+ /** Persist the registry while preserving ticket populate/build settings and removing only superseded legacy fields. */
26
+ export declare function saveSourceRegistry(projectDir: string, registry: SourceRegistryConfig): void;
27
+ export declare function addPendingSourceDescription(registry: SourceRegistryConfig, description: string, now?: Date): SourceRegistryConfig;
28
+ export declare function setSourceStorage(registry: SourceRegistryConfig, storage: SourceSnapshotStorage): SourceRegistryConfig;
29
+ export declare function deactivateSource(registry: SourceRegistryConfig, id: string): SourceRegistryConfig;
30
+ /** Remove only capture files introduced by a staged registry; existing history is never touched. */
31
+ export declare function discardStagedSourceCaptures(projectDir: string, original: SourceRegistryConfig, staged: SourceRegistryConfig): void;
32
+ /** Compatibility bridge for legacy ticket setup callers. Existing entries and versions are retained. */
33
+ export declare function appendLegacyTicketSources(registry: SourceRegistryConfig, sources: TicketSetupSource[]): SourceRegistryConfig;
34
+ export declare function extractSourceRequests(output: string): StructuredSourceRequest[];
35
+ /** Deterministically recognize only an entire obvious locator; all other language stays pending for agent interpretation. */
36
+ export declare function sourceRequestFromAnswer(answer: string, projectDir: string): StructuredSourceRequest;
37
+ export declare function registerSourceRequests(projectDir: string, registry: SourceRegistryConfig, requests: StructuredSourceRequest[], opts?: {
38
+ capture?: boolean;
39
+ storage?: SourceSnapshotStorage;
40
+ }): Promise<CaptureResult>;
41
+ export declare function refreshSourceRegistry(projectDir: string, registry: SourceRegistryConfig, ids?: string[]): Promise<CaptureResult>;
42
+ export declare function sourceContext(registry: SourceRegistryConfig, options?: {
43
+ metadataOnly?: Set<string>;
44
+ pinned?: Map<string, Set<string>>;
45
+ }): Array<Record<string, unknown>>;
46
+ export declare function validateSourceVersionRef(registry: SourceRegistryConfig, ref: {
47
+ source_id: string;
48
+ fingerprint: string;
49
+ }, projectDir?: string): string | undefined;