ai-foreman 1.7.13 → 1.7.14

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 (47) hide show
  1. package/README.md +33 -3
  2. package/dist/adapters/claude.d.ts +5 -0
  3. package/dist/adapters/claude.js +122 -16
  4. package/dist/adapters/codex.d.ts +2 -0
  5. package/dist/adapters/codex.js +30 -8
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +6 -0
  8. package/dist/adapters/types.d.ts +22 -0
  9. package/dist/agentRun.js +21 -3
  10. package/dist/branch/currentGuard.d.ts +2 -0
  11. package/dist/branch/currentGuard.js +9 -0
  12. package/dist/branch/finalization.d.ts +22 -0
  13. package/dist/branch/finalization.js +120 -0
  14. package/dist/branch/git.js +7 -2
  15. package/dist/branch/runner.d.ts +3 -2
  16. package/dist/branch/runner.js +277 -75
  17. package/dist/buildRuns.d.ts +1 -0
  18. package/dist/buildRuns.js +33 -3
  19. package/dist/cli/start.js +314 -86
  20. package/dist/cli/state.d.ts +4 -0
  21. package/dist/cli/state.js +69 -0
  22. package/dist/continuity.d.ts +4 -0
  23. package/dist/continuity.js +26 -3
  24. package/dist/foreman.d.ts +22 -4
  25. package/dist/foreman.js +105 -19
  26. package/dist/handoffs.d.ts +5 -2
  27. package/dist/handoffs.js +93 -12
  28. package/dist/index.js +2 -0
  29. package/dist/qaFailureDelivery.d.ts +100 -0
  30. package/dist/qaFailureDelivery.js +570 -0
  31. package/dist/qaProtocolV2.d.ts +233 -0
  32. package/dist/qaProtocolV2.js +114 -0
  33. package/dist/qaRecovery.d.ts +15 -3
  34. package/dist/qaRecovery.js +477 -83
  35. package/dist/qaReview.d.ts +42 -17
  36. package/dist/qaReview.js +1216 -457
  37. package/dist/qaRuntime.d.ts +14 -0
  38. package/dist/qaRuntime.js +44 -0
  39. package/dist/qaSnapshot.d.ts +34 -0
  40. package/dist/qaSnapshot.js +302 -34
  41. package/dist/stateTransfer.d.ts +57 -0
  42. package/dist/stateTransfer.js +644 -0
  43. package/dist/statusReporter.d.ts +2 -0
  44. package/dist/statusReporter.js +6 -0
  45. package/dist/workflowDb.d.ts +245 -2
  46. package/dist/workflowDb.js +647 -6
  47. package/package.json +15 -3
@@ -0,0 +1,22 @@
1
+ export interface DirectMergeIntent {
2
+ version: 1;
3
+ ticket: string;
4
+ branch: string;
5
+ base: string;
6
+ sourceCommit: string;
7
+ baseCommit: string;
8
+ expectedTree: string;
9
+ method: "squash" | "merge" | "rebase";
10
+ }
11
+ export declare class DirectMergeSourceChangedError extends Error {
12
+ }
13
+ /** Freeze both inputs and the resulting tree before any merge mutates refs. */
14
+ export declare function prepareDirectMerge(projectDir: string, ticket: string, branch: string, base: string, method: DirectMergeIntent["method"]): DirectMergeIntent;
15
+ export declare function readDirectMergeIntent(value: unknown): DirectMergeIntent;
16
+ /** Recognize a merge that reached Git before its SQLite completion receipt. */
17
+ export declare function reconcileDirectMerge(projectDir: string, intent: DirectMergeIntent): string | undefined;
18
+ /** Refuse to discard changes made while the interrupted recovery was paused. */
19
+ export declare function verifyDirectMergeWorktree(projectDir: string, intent: DirectMergeIntent): string | undefined;
20
+ export declare function removeDirectMergeWorktree(projectDir: string, intent: DirectMergeIntent, worktree: string): void;
21
+ export declare function hasExactStagedDirectMerge(projectDir: string, intent: DirectMergeIntent): boolean;
22
+ export declare function executeDirectMerge(projectDir: string, intent: DirectMergeIntent, message: string): string;
@@ -0,0 +1,120 @@
1
+ import { currentWorktreeBranch, findWorktreeForBranch, hasWorktreeChanges, mergeBranchToLocalBase, runGit } from "./git.js";
2
+ import { captureProspectiveGitTree } from "../qaSnapshot.js";
3
+ export class DirectMergeSourceChangedError extends Error {
4
+ }
5
+ /** Freeze both inputs and the resulting tree before any merge mutates refs. */
6
+ export function prepareDirectMerge(projectDir, ticket, branch, base, method) {
7
+ const sourceCommit = runGit(projectDir, ["rev-parse", branch]).stdout;
8
+ const baseCommit = runGit(projectDir, ["rev-parse", base]).stdout;
9
+ const expectedTree = runGit(projectDir, ["merge-tree", "--write-tree", baseCommit, sourceCommit]).stdout.split("\n")[0];
10
+ if (!/^[a-f0-9]{40,64}$/.test(expectedTree))
11
+ throw new Error("Git did not produce a bound direct-merge tree");
12
+ return { version: 1, ticket, branch, base, sourceCommit, baseCommit, expectedTree, method };
13
+ }
14
+ export function readDirectMergeIntent(value) {
15
+ const intent = value;
16
+ if (!intent || intent.version !== 1 || !intent.ticket || !intent.branch || !intent.base
17
+ || ![intent.sourceCommit, intent.baseCommit, intent.expectedTree].every((item) => typeof item === "string" && /^[a-f0-9]{40,64}$/.test(item))
18
+ || !["squash", "merge", "rebase"].includes(intent.method))
19
+ throw new Error("Direct merge is missing its durable source/base/tree intent");
20
+ return intent;
21
+ }
22
+ /** Recognize a merge that reached Git before its SQLite completion receipt. */
23
+ export function reconcileDirectMerge(projectDir, intent) {
24
+ const base = runGit(projectDir, ["rev-parse", intent.base]).stdout;
25
+ if (base === intent.baseCommit)
26
+ return undefined;
27
+ try {
28
+ runGit(projectDir, ["merge-base", "--is-ancestor", intent.baseCommit, base]);
29
+ }
30
+ catch {
31
+ throw new Error("Direct-merge base was rewritten after its durable intent");
32
+ }
33
+ const commits = runGit(projectDir, ["rev-list", "--first-parent", "--reverse", `${intent.baseCommit}..${base}`]).stdout.split("\n").filter(Boolean);
34
+ for (const commit of commits) {
35
+ if (runGit(projectDir, ["rev-parse", `${commit}^{tree}`]).stdout !== intent.expectedTree)
36
+ continue;
37
+ const parents = runGit(projectDir, ["show", "-s", "--format=%P", commit]).stdout.split(" ");
38
+ if (intent.method === "squash" && parents.length === 1 && parents[0] === intent.baseCommit)
39
+ return commit;
40
+ if (intent.method === "merge" && parents[0] === intent.baseCommit && parents.includes(intent.sourceCommit))
41
+ return commit;
42
+ if (intent.method === "rebase")
43
+ return commit;
44
+ }
45
+ throw new Error("Direct-merge base changed without the intended merge result; reconcile the base before retrying");
46
+ }
47
+ /** Refuse to discard changes made while the interrupted recovery was paused. */
48
+ export function verifyDirectMergeWorktree(projectDir, intent) {
49
+ const worktree = findWorktreeForBranch(projectDir, intent.branch);
50
+ if (worktree && hasWorktreeChanges(worktree))
51
+ throw new DirectMergeSourceChangedError(`Direct-merge worktree has new changes and must be preserved: ${worktree}`);
52
+ let source;
53
+ try {
54
+ source = runGit(projectDir, ["rev-parse", intent.branch]).stdout;
55
+ }
56
+ catch {
57
+ return worktree;
58
+ } // A confirmed merge may already have removed its branch.
59
+ if (source !== intent.sourceCommit) {
60
+ const tree = runGit(projectDir, ["rev-parse", `${source}^{tree}`]).stdout;
61
+ let basedOnFrozenBase = false;
62
+ try {
63
+ runGit(projectDir, ["merge-base", "--is-ancestor", intent.baseCommit, source]);
64
+ basedOnFrozenBase = true;
65
+ }
66
+ catch { /* reject below */ }
67
+ if (intent.method !== "rebase" || tree !== intent.expectedTree || !basedOnFrozenBase)
68
+ throw new DirectMergeSourceChangedError("Direct-merge source branch changed after its durable intent; preserve it for a fresh review");
69
+ }
70
+ return worktree;
71
+ }
72
+ export function removeDirectMergeWorktree(projectDir, intent, worktree) {
73
+ if (currentWorktreeBranch(worktree) !== intent.branch)
74
+ throw new Error("Direct-merge worktree switched branches; preserve it for reconciliation");
75
+ verifyDirectMergeWorktree(projectDir, intent);
76
+ // Let Git reject edits made even after the preceding verification. Never
77
+ // force-remove or recursively delete a finalization worktree on failure.
78
+ runGit(projectDir, ["worktree", "remove", worktree]);
79
+ }
80
+ export function hasExactStagedDirectMerge(projectDir, intent) {
81
+ let recognizedMethod = intent.method === "squash";
82
+ if (intent.method === "merge") {
83
+ try {
84
+ recognizedMethod = runGit(projectDir, ["rev-parse", "--verify", "MERGE_HEAD"]).stdout === intent.sourceCommit;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ return recognizedMethod
91
+ && runGit(projectDir, ["branch", "--show-current"]).stdout === intent.base
92
+ && runGit(projectDir, ["rev-parse", intent.base]).stdout === intent.baseCommit
93
+ && runGit(projectDir, ["write-tree"]).stdout === intent.expectedTree
94
+ && captureProspectiveGitTree(projectDir) === intent.expectedTree;
95
+ }
96
+ export function executeDirectMerge(projectDir, intent, message) {
97
+ const completed = reconcileDirectMerge(projectDir, intent);
98
+ if (completed)
99
+ return completed;
100
+ verifyDirectMergeWorktree(projectDir, intent);
101
+ const indexTree = runGit(projectDir, ["write-tree"]).stdout;
102
+ const baseTree = runGit(projectDir, ["rev-parse", `${intent.baseCommit}^{tree}`]).stdout;
103
+ if (indexTree !== baseTree) {
104
+ if (!hasExactStagedDirectMerge(projectDir, intent)) {
105
+ throw new Error("Direct merge found unrecognized staged changes; preserve the base worktree for reconciliation");
106
+ }
107
+ // Git prepared the merge before the host crashed, but commit did not.
108
+ // Only commit the exact tree frozen in the durable intent.
109
+ runGit(projectDir, ["commit", "-m", message]);
110
+ }
111
+ else {
112
+ // Squash/merge publish the immutable source commit, even if an external
113
+ // actor moves the human-readable branch between verification and Git.
114
+ mergeBranchToLocalBase(projectDir, intent.method === "rebase" ? intent.branch : intent.sourceCommit, intent.base, message, intent.method);
115
+ }
116
+ const result = reconcileDirectMerge(projectDir, intent);
117
+ if (!result)
118
+ throw new Error("Direct merge returned without publishing its intended result");
119
+ return result;
120
+ }
@@ -118,8 +118,13 @@ export function pushBranch(worktreePath, branch) {
118
118
  runGit(worktreePath, ["push", "-u", "origin", branch]);
119
119
  }
120
120
  export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message, method = "squash") {
121
- if (method === "rebase")
122
- runGit(projectDir, ["rebase", baseBranch, branch]);
121
+ if (method === "rebase") {
122
+ const branchWorktree = findWorktreeForBranch(projectDir, branch);
123
+ if (branchWorktree)
124
+ runGit(branchWorktree, ["rebase", baseBranch]);
125
+ else
126
+ runGit(projectDir, ["rebase", baseBranch, branch]);
127
+ }
123
128
  if (currentGitRef(projectDir) !== baseBranch) {
124
129
  runGit(projectDir, ["checkout", baseBranch]);
125
130
  }
@@ -56,7 +56,7 @@ export interface BranchRunnerOptions {
56
56
  recordQaSession?: (session: string | ProviderSessionRefV1, ticketId: string, worktreePath: string) => void;
57
57
  /** Notify the host and stop the branch plan without discarding its worktree. */
58
58
  onSessionUnavailable?: (error: SessionUnavailableError | SessionUnavailableContinuityError) => void;
59
- createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter | QaSessionHandle>;
59
+ createQa?: (cwd: string, sessionId?: string) => Promise<QaSessionHandle>;
60
60
  builderSessionStrategy?: SessionStrategy;
61
61
  qaSessionStrategy?: SessionStrategy;
62
62
  observeBuilder?: (builder: BuilderAdapter) => Promise<void>;
@@ -65,12 +65,13 @@ export interface BranchRunnerOptions {
65
65
  qaNonconvergence?: (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
66
66
  beforeBuilderTurn?: (adapter: BuilderAdapter, frozenAction: string, cwd: string) => Promise<BuilderAdapter>;
67
67
  builderSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
68
- qaSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string, recovery?: QaSessionBoundaryRecovery) => Promise<BuilderAdapter>;
68
+ qaSessionBoundary?: (handle: QaSessionHandle, frozenAction: string, strategy: SessionStrategy, cwd: string, recovery?: QaSessionBoundaryRecovery) => Promise<import("../qaReview.js").QaSessionBoundaryResult>;
69
69
  observer?: RunObserver;
70
70
  qaRuntimeContext?: unknown;
71
71
  qaContinuityManaged?: boolean;
72
72
  qaReportRecovery?: QaReportRecoveryHandler;
73
73
  qaResumedRecovery?: QaRecoveryPacket;
74
+ qaProtocolResumeTicket?: string;
74
75
  }
75
76
  export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<BranchRunSummary[]>;
76
77
  export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {