steroids-cli 0.8.33 → 0.8.35

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 (109) hide show
  1. package/dist/commands/loop-phases.d.ts +6 -2
  2. package/dist/commands/loop-phases.d.ts.map +1 -1
  3. package/dist/commands/loop-phases.js +42 -2
  4. package/dist/commands/loop-phases.js.map +1 -1
  5. package/dist/commands/merge.d.ts.map +1 -1
  6. package/dist/commands/merge.js +13 -1
  7. package/dist/commands/merge.js.map +1 -1
  8. package/dist/commands/runners-parallel.d.ts +1 -0
  9. package/dist/commands/runners-parallel.d.ts.map +1 -1
  10. package/dist/commands/runners-parallel.js +93 -6
  11. package/dist/commands/runners-parallel.js.map +1 -1
  12. package/dist/commands/workspaces.d.ts.map +1 -1
  13. package/dist/commands/workspaces.js +38 -4
  14. package/dist/commands/workspaces.js.map +1 -1
  15. package/dist/config/loader.d.ts +9 -0
  16. package/dist/config/loader.d.ts.map +1 -1
  17. package/dist/config/loader.js +12 -3
  18. package/dist/config/loader.js.map +1 -1
  19. package/dist/config/schema.d.ts.map +1 -1
  20. package/dist/config/schema.js +48 -3
  21. package/dist/config/schema.js.map +1 -1
  22. package/dist/database/queries.d.ts.map +1 -1
  23. package/dist/database/queries.js +15 -1
  24. package/dist/database/queries.js.map +1 -1
  25. package/dist/database/schema.d.ts +2 -2
  26. package/dist/database/schema.d.ts.map +1 -1
  27. package/dist/database/schema.js +9 -0
  28. package/dist/database/schema.js.map +1 -1
  29. package/dist/orchestrator/coder.js +1 -1
  30. package/dist/orchestrator/coder.js.map +1 -1
  31. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  32. package/dist/orchestrator/coordinator.js +5 -3
  33. package/dist/orchestrator/coordinator.js.map +1 -1
  34. package/dist/orchestrator/fallback-handler.d.ts +2 -1
  35. package/dist/orchestrator/fallback-handler.d.ts.map +1 -1
  36. package/dist/orchestrator/fallback-handler.js +36 -20
  37. package/dist/orchestrator/fallback-handler.js.map +1 -1
  38. package/dist/orchestrator/post-reviewer.d.ts.map +1 -1
  39. package/dist/orchestrator/post-reviewer.js +18 -23
  40. package/dist/orchestrator/post-reviewer.js.map +1 -1
  41. package/dist/orchestrator/reviewer.d.ts +1 -1
  42. package/dist/orchestrator/reviewer.d.ts.map +1 -1
  43. package/dist/orchestrator/reviewer.js +19 -16
  44. package/dist/orchestrator/reviewer.js.map +1 -1
  45. package/dist/parallel/clone.d.ts +13 -0
  46. package/dist/parallel/clone.d.ts.map +1 -1
  47. package/dist/parallel/clone.js +40 -0
  48. package/dist/parallel/clone.js.map +1 -1
  49. package/dist/parallel/merge-conflict-attempts.d.ts +8 -0
  50. package/dist/parallel/merge-conflict-attempts.d.ts.map +1 -0
  51. package/dist/parallel/merge-conflict-attempts.js +68 -0
  52. package/dist/parallel/merge-conflict-attempts.js.map +1 -0
  53. package/dist/parallel/merge-conflict-invoke.d.ts +2 -0
  54. package/dist/parallel/merge-conflict-invoke.d.ts.map +1 -0
  55. package/dist/parallel/merge-conflict-invoke.js +41 -0
  56. package/dist/parallel/merge-conflict-invoke.js.map +1 -0
  57. package/dist/parallel/merge-conflict-prompts.d.ts +23 -0
  58. package/dist/parallel/merge-conflict-prompts.d.ts.map +1 -0
  59. package/dist/parallel/merge-conflict-prompts.js +40 -0
  60. package/dist/parallel/merge-conflict-prompts.js.map +1 -0
  61. package/dist/parallel/merge-conflict-task.d.ts +3 -0
  62. package/dist/parallel/merge-conflict-task.d.ts.map +1 -0
  63. package/dist/parallel/merge-conflict-task.js +59 -0
  64. package/dist/parallel/merge-conflict-task.js.map +1 -0
  65. package/dist/parallel/merge-conflict.d.ts +7 -7
  66. package/dist/parallel/merge-conflict.d.ts.map +1 -1
  67. package/dist/parallel/merge-conflict.js +91 -135
  68. package/dist/parallel/merge-conflict.js.map +1 -1
  69. package/dist/parallel/merge-errors.d.ts +2 -0
  70. package/dist/parallel/merge-errors.d.ts.map +1 -1
  71. package/dist/parallel/merge-errors.js +2 -0
  72. package/dist/parallel/merge-errors.js.map +1 -1
  73. package/dist/parallel/merge-git.d.ts.map +1 -1
  74. package/dist/parallel/merge-git.js +4 -1
  75. package/dist/parallel/merge-git.js.map +1 -1
  76. package/dist/parallel/merge-lock.d.ts +4 -2
  77. package/dist/parallel/merge-lock.d.ts.map +1 -1
  78. package/dist/parallel/merge-lock.js +51 -18
  79. package/dist/parallel/merge-lock.js.map +1 -1
  80. package/dist/parallel/merge-progress.d.ts +3 -1
  81. package/dist/parallel/merge-progress.d.ts.map +1 -1
  82. package/dist/parallel/merge-progress.js +9 -4
  83. package/dist/parallel/merge-progress.js.map +1 -1
  84. package/dist/parallel/merge.d.ts +4 -0
  85. package/dist/parallel/merge.d.ts.map +1 -1
  86. package/dist/parallel/merge.js +222 -17
  87. package/dist/parallel/merge.js.map +1 -1
  88. package/dist/prompts/prompt-helpers.d.ts +1 -1
  89. package/dist/prompts/prompt-helpers.d.ts.map +1 -1
  90. package/dist/prompts/prompt-helpers.js +10 -7
  91. package/dist/prompts/prompt-helpers.js.map +1 -1
  92. package/dist/prompts/reviewer.d.ts.map +1 -1
  93. package/dist/prompts/reviewer.js +48 -21
  94. package/dist/prompts/reviewer.js.map +1 -1
  95. package/dist/runners/global-db.d.ts +32 -0
  96. package/dist/runners/global-db.d.ts.map +1 -1
  97. package/dist/runners/global-db.js +403 -2
  98. package/dist/runners/global-db.js.map +1 -1
  99. package/dist/runners/orchestrator-loop.d.ts.map +1 -1
  100. package/dist/runners/orchestrator-loop.js +50 -3
  101. package/dist/runners/orchestrator-loop.js.map +1 -1
  102. package/dist/runners/wakeup.d.ts +1 -0
  103. package/dist/runners/wakeup.d.ts.map +1 -1
  104. package/dist/runners/wakeup.js +363 -29
  105. package/dist/runners/wakeup.js.map +1 -1
  106. package/migrations/012_add_merge_lock_epoch.sql +15 -0
  107. package/migrations/013_add_merge_progress_applied_commit_sha.sql +15 -0
  108. package/migrations/manifest.json +17 -1
  109. package/package.json +1 -1
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_CONFLICT_ATTEMPTS = void 0;
4
+ exports.recordConflictAttempt = recordConflictAttempt;
5
+ exports.clearConflictAttemptState = clearConflictAttemptState;
6
+ const global_db_js_1 = require("../runners/global-db.js");
7
+ const merge_errors_js_1 = require("./merge-errors.js");
8
+ exports.MAX_CONFLICT_ATTEMPTS = 5;
9
+ const MAX_CONFLICT_BACKOFF_MINUTES = 30;
10
+ function recordConflictAttempt(sessionId, workstreamId) {
11
+ const { db, close } = (0, global_db_js_1.openGlobalDatabase)();
12
+ try {
13
+ const row = db
14
+ .prepare(`SELECT conflict_attempts
15
+ FROM workstreams
16
+ WHERE session_id = ?
17
+ AND id = ?
18
+ LIMIT 1`)
19
+ .get(sessionId, workstreamId);
20
+ if (!row) {
21
+ throw new merge_errors_js_1.ParallelMergeError('Parallel workstream row not found while recording conflict attempt', 'LEASE_ROW_MISSING');
22
+ }
23
+ const attempts = (row.conflict_attempts ?? 0) + 1;
24
+ if (attempts >= exports.MAX_CONFLICT_ATTEMPTS) {
25
+ db.prepare(`UPDATE workstreams
26
+ SET conflict_attempts = ?,
27
+ status = 'failed',
28
+ next_retry_at = NULL,
29
+ last_reconcile_action = 'blocked_conflict',
30
+ last_reconciled_at = datetime('now')
31
+ WHERE session_id = ?
32
+ AND id = ?`).run(attempts, sessionId, workstreamId);
33
+ db.prepare(`UPDATE parallel_sessions
34
+ SET status = 'blocked_conflict',
35
+ completed_at = NULL
36
+ WHERE id = ?`).run(sessionId);
37
+ return { attempts, blocked: true, backoffMinutes: null };
38
+ }
39
+ const backoffMinutes = Math.min(2 ** Math.max(0, attempts - 1), MAX_CONFLICT_BACKOFF_MINUTES);
40
+ db.prepare(`UPDATE workstreams
41
+ SET conflict_attempts = ?,
42
+ next_retry_at = datetime('now', ?),
43
+ last_reconcile_action = 'conflict_retry',
44
+ last_reconciled_at = datetime('now')
45
+ WHERE session_id = ?
46
+ AND id = ?`).run(attempts, `+${backoffMinutes} minutes`, sessionId, workstreamId);
47
+ return { attempts, blocked: false, backoffMinutes };
48
+ }
49
+ finally {
50
+ close();
51
+ }
52
+ }
53
+ function clearConflictAttemptState(sessionId, workstreamId) {
54
+ const { db, close } = (0, global_db_js_1.openGlobalDatabase)();
55
+ try {
56
+ db.prepare(`UPDATE workstreams
57
+ SET conflict_attempts = 0,
58
+ next_retry_at = NULL,
59
+ last_reconcile_action = 'conflict_resolved',
60
+ last_reconciled_at = datetime('now')
61
+ WHERE session_id = ?
62
+ AND id = ?`).run(sessionId, workstreamId);
63
+ }
64
+ finally {
65
+ close();
66
+ }
67
+ }
68
+ //# sourceMappingURL=merge-conflict-attempts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-attempts.js","sourceRoot":"","sources":["../../src/parallel/merge-conflict-attempts.ts"],"names":[],"mappings":";;;AAMA,sDA6DC;AAED,8DAeC;AApFD,0DAA6D;AAC7D,uDAAuD;AAE1C,QAAA,qBAAqB,GAAG,CAAC,CAAC;AACvC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAExC,SAAgB,qBAAqB,CACnC,SAAiB,EACjB,YAAoB;IAEpB,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,IAAA,iCAAkB,GAAE,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE;aACX,OAAO,CACN;;;;iBAIS,CACV;aACA,GAAG,CAAC,SAAS,EAAE,YAAY,CAA8C,CAAC;QAE7E,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,oCAAkB,CAC1B,oEAAoE,EACpE,mBAAmB,CACpB,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,iBAAiB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,QAAQ,IAAI,6BAAqB,EAAE,CAAC;YACtC,EAAE,CAAC,OAAO,CACR;;;;;;;sBAOc,CACf,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;YAEzC,EAAE,CAAC,OAAO,CACR;;;sBAGc,CACf,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAEjB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;QAC3D,CAAC;QAED,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAC;QAC9F,EAAE,CAAC,OAAO,CACR;;;;;;oBAMc,CACf,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,cAAc,UAAU,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;QAEvE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IACtD,CAAC;YAAS,CAAC;QACT,KAAK,EAAE,CAAC;IACV,CAAC;AACH,CAAC;AAED,SAAgB,yBAAyB,CAAC,SAAiB,EAAE,YAAoB;IAC/E,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,IAAA,iCAAkB,GAAE,CAAC;IAC3C,IAAI,CAAC;QACH,EAAE,CAAC,OAAO,CACR;;;;;;oBAMc,CACf,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,KAAK,EAAE,CAAC;IACV,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function invokeMergeConflictModel(role: 'coder' | 'reviewer', projectPath: string, taskId: string | undefined, prompt: string): Promise<string>;
2
+ //# sourceMappingURL=merge-conflict-invoke.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-invoke.d.ts","sourceRoot":"","sources":["../../src/parallel/merge-conflict-invoke.ts"],"names":[],"mappings":"AAKA,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,OAAO,GAAG,UAAU,EAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAiDjB"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.invokeMergeConflictModel = invokeMergeConflictModel;
4
+ const loader_js_1 = require("../config/loader.js");
5
+ const invocation_logger_js_1 = require("../providers/invocation-logger.js");
6
+ const registry_js_1 = require("../providers/registry.js");
7
+ const merge_errors_js_1 = require("./merge-errors.js");
8
+ async function invokeMergeConflictModel(role, projectPath, taskId, prompt) {
9
+ const config = (0, loader_js_1.loadConfig)(projectPath);
10
+ const modelConfig = role === 'coder' ? config.ai?.coder : config.ai?.reviewer;
11
+ if (!modelConfig?.provider || !modelConfig?.model) {
12
+ throw new merge_errors_js_1.ParallelMergeError(`Missing AI ${role} configuration. Configure via config.ai.${role}.`, 'AI_CONFIG_MISSING');
13
+ }
14
+ const providerName = modelConfig.provider;
15
+ const model = modelConfig.model;
16
+ const registry = (0, registry_js_1.getProviderRegistry)();
17
+ const provider = registry.get(providerName);
18
+ const result = await (0, invocation_logger_js_1.logInvocation)(prompt, (ctx) => provider.invoke(prompt, {
19
+ model,
20
+ timeout: 60 * 60 * 1000,
21
+ cwd: projectPath,
22
+ role,
23
+ streamOutput: false,
24
+ onActivity: ctx?.onActivity,
25
+ }), {
26
+ role,
27
+ provider: providerName,
28
+ model,
29
+ taskId,
30
+ projectPath,
31
+ });
32
+ if (!result.success) {
33
+ const details = result.stderr || result.stdout || 'model returned non-zero exit code';
34
+ throw new merge_errors_js_1.ParallelMergeError(`${role.toUpperCase()} invocation failed during merge conflict handling: ${details}`, 'AI_INVOCATION_FAILED');
35
+ }
36
+ if (result.timedOut) {
37
+ throw new merge_errors_js_1.ParallelMergeError(`${role} invocation timed out`, 'AI_INVOKE_TIMEOUT');
38
+ }
39
+ return result.stdout;
40
+ }
41
+ //# sourceMappingURL=merge-conflict-invoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-invoke.js","sourceRoot":"","sources":["../../src/parallel/merge-conflict-invoke.ts"],"names":[],"mappings":";;AAKA,4DAsDC;AA3DD,mDAAiD;AACjD,4EAAkE;AAClE,0DAA+D;AAC/D,uDAAuD;AAEhD,KAAK,UAAU,wBAAwB,CAC5C,IAA0B,EAC1B,WAAmB,EACnB,MAA0B,EAC1B,MAAc;IAEd,MAAM,MAAM,GAAG,IAAA,sBAAU,EAAC,WAAW,CAAC,CAAC;IACvC,MAAM,WAAW,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC;IAE9E,IAAI,CAAC,WAAW,EAAE,QAAQ,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC;QAClD,MAAM,IAAI,oCAAkB,CAC1B,cAAc,IAAI,2CAA2C,IAAI,GAAG,EACpE,mBAAmB,CACpB,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,CAAC;IAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC;IAEhC,MAAM,QAAQ,GAAG,IAAA,iCAAmB,GAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,IAAA,oCAAa,EAChC,MAAM,EACN,CAAC,GAAG,EAAE,EAAE,CACN,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;QACtB,KAAK;QACL,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;QACvB,GAAG,EAAE,WAAW;QAChB,IAAI;QACJ,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,GAAG,EAAE,UAAU;KAC5B,CAAC,EACJ;QACE,IAAI;QACJ,QAAQ,EAAE,YAAY;QACtB,KAAK;QACL,MAAM;QACN,WAAW;KACZ,CACF,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,IAAI,mCAAmC,CAAC;QACtF,MAAM,IAAI,oCAAkB,CAC1B,GAAG,IAAI,CAAC,WAAW,EAAE,sDAAsD,OAAO,EAAE,EACpF,sBAAsB,CACvB,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,MAAM,IAAI,oCAAkB,CAAC,GAAG,IAAI,uBAAuB,EAAE,mBAAmB,CAAC,CAAC;IACpF,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC"}
@@ -0,0 +1,23 @@
1
+ export interface ParseReviewDecisionResult {
2
+ decision: 'approve' | 'reject';
3
+ notes: string;
4
+ }
5
+ export declare function parseReviewDecision(raw: string): ParseReviewDecisionResult;
6
+ export declare function createPromptForConflictCoder(options: {
7
+ workstreamId: string;
8
+ shortSha: string;
9
+ branchName: string;
10
+ commitMessage: string;
11
+ conflictedFiles: string[];
12
+ conflictPatch: string;
13
+ rejectionNotes?: string;
14
+ }): string;
15
+ export declare function createPromptForConflictReviewer(options: {
16
+ workstreamId: string;
17
+ shortSha: string;
18
+ branchName: string;
19
+ commitMessage: string;
20
+ stagedDiff: string;
21
+ stagedFiles: string[];
22
+ }): string;
23
+ //# sourceMappingURL=merge-conflict-prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-prompts.d.ts","sourceRoot":"","sources":["../../src/parallel/merge-conflict-prompts.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,yBAAyB,CAwB1E;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAMT;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,GAAG,MAAM,CAMT"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseReviewDecision = parseReviewDecision;
4
+ exports.createPromptForConflictCoder = createPromptForConflictCoder;
5
+ exports.createPromptForConflictReviewer = createPromptForConflictReviewer;
6
+ function parseReviewDecision(raw) {
7
+ const trimmed = raw.trim();
8
+ const upper = trimmed.toUpperCase();
9
+ const hasApprove = upper.includes('APPROVE');
10
+ const hasReject = upper.includes('REJECT');
11
+ if (hasApprove && !hasReject) {
12
+ return {
13
+ decision: 'approve',
14
+ notes: trimmed || 'APPROVED by merge-conflict reviewer',
15
+ };
16
+ }
17
+ if (hasReject) {
18
+ return {
19
+ decision: 'reject',
20
+ notes: trimmed || 'Please review and correct conflict resolution',
21
+ };
22
+ }
23
+ return {
24
+ decision: 'reject',
25
+ notes: trimmed || 'Decision was not clear. Please provide explicit APPROVE/REJECT.',
26
+ };
27
+ }
28
+ function createPromptForConflictCoder(options) {
29
+ const notesSection = options.rejectionNotes
30
+ ? `\n\nLatest review note from the resolver:\n${options.rejectionNotes}\n`
31
+ : '';
32
+ return `You are resolving a merge conflict for a cherry-pick during parallel merge.\n\n## Conflict context\nWorkstream: ${options.workstreamId}\nBranch: ${options.branchName}\nCommit: ${options.shortSha}\nCommit Message:\n${options.commitMessage}\n\nConflicted files:\n${options.conflictedFiles.map((file) => `- ${file}`).join('\n')}\n\nIntended patch:\n${options.conflictPatch}\n\nRules:\n1) Edit conflicted files to a correct resolution.\n2) Remove ALL conflict markers (<<<<<<, =======, >>>>>>) in resolved files.\n3) Stage only the resolved files using git add.\n4) Do NOT commit.\n5) Be surgical; change only files required for this commit.\n${notesSection}\n\nRespond with a short confirmation when done.`;
33
+ }
34
+ function createPromptForConflictReviewer(options) {
35
+ const files = options.stagedFiles.length > 0
36
+ ? options.stagedFiles.map((file) => `- ${file}`).join('\n')
37
+ : 'No files staged yet';
38
+ return `You are reviewing a staged resolution for a cherry-pick conflict in parallel merge.\n\nWorkstream: ${options.workstreamId}\nBranch: ${options.branchName}\nCommit: ${options.shortSha}\nOriginal message: ${options.commitMessage}\n\nCurrent staged diff to be committed by cherry-pick --continue:\n${options.stagedDiff || '(empty diff)'}\n\nFiles staged:\n${files}\n\nDecision rules:\n- Reply with APPROVE if the resolution is correct.\n- Reply with REJECT and actionable notes if any conflict marker remains or logic is incorrect.\n\nFormat:\nAPPROVE - <optional note> or\nREJECT - <checklist itemized note>`;
39
+ }
40
+ //# sourceMappingURL=merge-conflict-prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-prompts.js","sourceRoot":"","sources":["../../src/parallel/merge-conflict-prompts.ts"],"names":[],"mappings":";;AAKA,kDAwBC;AAED,oEAcC;AAED,0EAaC;AAvDD,SAAgB,mBAAmB,CAAC,GAAW;IAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAE3C,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE,CAAC;QAC7B,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,KAAK,EAAE,OAAO,IAAI,qCAAqC;SACxD,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,OAAO,IAAI,+CAA+C;SAClE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,OAAO,IAAI,iEAAiE;KACpF,CAAC;AACJ,CAAC;AAED,SAAgB,4BAA4B,CAAC,OAQ5C;IACC,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc;QACzC,CAAC,CAAC,8CAA8C,OAAO,CAAC,cAAc,IAAI;QAC1E,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO,mHAAmH,OAAO,CAAC,YAAY,aAAa,OAAO,CAAC,UAAU,aAAa,OAAO,CAAC,QAAQ,sBAAsB,OAAO,CAAC,aAAa,0BAA0B,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,wBAAwB,OAAO,CAAC,aAAa,gRAAgR,YAAY,kDAAkD,CAAC;AAC1sB,CAAC;AAED,SAAgB,+BAA+B,CAAC,OAO/C;IACC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;QAC1C,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3D,CAAC,CAAC,qBAAqB,CAAC;IAE1B,OAAO,sGAAsG,OAAO,CAAC,YAAY,aAAa,OAAO,CAAC,UAAU,aAAa,OAAO,CAAC,QAAQ,uBAAuB,OAAO,CAAC,aAAa,uEAAuE,OAAO,CAAC,UAAU,IAAI,cAAc,sBAAsB,KAAK,sPAAsP,CAAC;AACxmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type Database from 'better-sqlite3';
2
+ export declare function ensureMergeConflictTask(db: Database.Database, workstreamId: string, shortSha: string, branchName: string, commitMessage: string, conflictedFiles: string[], conflictPatch: string, forceNew?: boolean): string;
3
+ //# sourceMappingURL=merge-conflict-task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-task.d.ts","sourceRoot":"","sources":["../../src/parallel/merge-conflict-task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAqC3C,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EAAE,EACzB,aAAa,EAAE,MAAM,EACrB,QAAQ,UAAQ,GACf,MAAM,CA2CR"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureMergeConflictTask = ensureMergeConflictTask;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const queries_js_1 = require("../database/queries.js");
6
+ function buildMergeConflictSectionName() {
7
+ return 'merge-conflicts';
8
+ }
9
+ function getNowISOString() {
10
+ return new Date().toISOString();
11
+ }
12
+ function createMergeConflictSection(db) {
13
+ const sectionName = buildMergeConflictSectionName();
14
+ const existing = db
15
+ .prepare('SELECT id FROM sections WHERE name = ? LIMIT 1')
16
+ .get(sectionName);
17
+ if (existing) {
18
+ return existing.id;
19
+ }
20
+ const maxPosRow = db
21
+ .prepare('SELECT MAX(position) as maxPos FROM sections')
22
+ .get();
23
+ const position = (maxPosRow?.maxPos ?? -1) + 1;
24
+ const sectionId = (0, node_crypto_1.createHash)('sha1').update(sectionName + position).digest('hex');
25
+ db.prepare(`INSERT INTO sections (id, name, position, priority, skipped, created_at)
26
+ VALUES (?, ?, ?, ?, ?, ?)`).run(sectionId, sectionName, position, 80, 0, getNowISOString());
27
+ return sectionId;
28
+ }
29
+ function ensureMergeConflictTask(db, workstreamId, shortSha, branchName, commitMessage, conflictedFiles, conflictPatch, forceNew = false) {
30
+ const sectionId = createMergeConflictSection(db);
31
+ if (!forceNew) {
32
+ const existing = db
33
+ .prepare(`SELECT t.id
34
+ FROM tasks t
35
+ INNER JOIN sections s ON s.id = t.section_id
36
+ WHERE s.name = ? AND t.title LIKE ?
37
+ ORDER BY t.created_at DESC
38
+ LIMIT 1`)
39
+ .get(buildMergeConflictSectionName(), `Merge conflict: cherry-pick ${shortSha}%`);
40
+ if (existing?.id) {
41
+ return existing.id;
42
+ }
43
+ }
44
+ const title = `Merge conflict: cherry-pick ${shortSha} from ${branchName}`;
45
+ const created = (0, queries_js_1.createTask)(db, title, {
46
+ sectionId,
47
+ sourceFile: `merge-conflict (${workstreamId})`,
48
+ filePath: conflictedFiles.join(', '),
49
+ status: 'pending',
50
+ fileContentHash: conflictPatch.substring(0, 2048),
51
+ fileCommitSha: commitMessage,
52
+ });
53
+ (0, queries_js_1.addAuditEntry)(db, created.id, 'null', 'pending', 'merge', {
54
+ actorType: 'orchestrator',
55
+ notes: `Generated from conflict while cherry-picking ${shortSha} from ${branchName}:\n${commitMessage}`,
56
+ });
57
+ return created.id;
58
+ }
59
+ //# sourceMappingURL=merge-conflict-task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-conflict-task.js","sourceRoot":"","sources":["../../src/parallel/merge-conflict-task.ts"],"names":[],"mappings":";;AAqCA,0DAoDC;AAxFD,6CAAyC;AACzC,uDAAmE;AAEnE,SAAS,6BAA6B;IACpC,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,0BAA0B,CAAC,EAAqB;IACvD,MAAM,WAAW,GAAG,6BAA6B,EAAE,CAAC;IACpD,MAAM,QAAQ,GAAG,EAAE;SAChB,OAAO,CAAC,gDAAgD,CAAC;SACzD,GAAG,CAAC,WAAW,CAA+B,CAAC;IAElD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,EAAE,CAAC;IACrB,CAAC;IAED,MAAM,SAAS,GAAG,EAAE;SACjB,OAAO,CAAC,8CAA8C,CAAC;SACvD,GAAG,EAA+B,CAAC;IAEtC,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAA,wBAAU,EAAC,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAElF,EAAE,CAAC,OAAO,CACR;+BAC2B,CAC5B,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC;IAElE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAgB,uBAAuB,CACrC,EAAqB,EACrB,YAAoB,EACpB,QAAgB,EAChB,UAAkB,EAClB,aAAqB,EACrB,eAAyB,EACzB,aAAqB,EACrB,QAAQ,GAAG,KAAK;IAEhB,MAAM,SAAS,GAAG,0BAA0B,CAAC,EAAE,CAAC,CAAC;IAEjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,QAAQ,GAAG,EAAE;aAChB,OAAO,CACN;;;;;iBAKS,CACV;aACA,GAAG,CAAC,6BAA6B,EAAE,EAAE,+BAA+B,QAAQ,GAAG,CAA+B,CAAC;QAElH,IAAI,QAAQ,EAAE,EAAE,EAAE,CAAC;YACjB,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,+BAA+B,QAAQ,SAAS,UAAU,EAAE,CAAC;IAC3E,MAAM,OAAO,GAAG,IAAA,uBAAU,EAAC,EAAE,EAAE,KAAK,EAAE;QACpC,SAAS;QACT,UAAU,EAAE,mBAAmB,YAAY,GAAG;QAC9C,QAAQ,EAAE,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QACpC,MAAM,EAAE,SAAS;QACjB,eAAe,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC;QACjD,aAAa,EAAE,aAAa;KAC7B,CAAC,CAAC;IAEH,IAAA,0BAAa,EACX,EAAE,EACF,OAAO,CAAC,EAAE,EACV,MAAM,EACN,SAAS,EACT,OAAO,EACP;QACE,SAAS,EAAE,cAAc;QACzB,KAAK,EAAE,gDAAgD,QAAQ,SAAS,UAAU,MAAM,aAAa,EAAE;KACxG,CACF,CAAC;IAEF,OAAO,OAAO,CAAC,EAAE,CAAC;AACpB,CAAC"}
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Merge conflict resolution helpers.
2
+ * Merge conflict resolution orchestrator.
3
3
  */
4
4
  import type Database from 'better-sqlite3';
5
- interface ParseReviewDecisionResult {
6
- decision: 'approve' | 'reject';
7
- notes: string;
8
- }
5
+ export { parseReviewDecision } from './merge-conflict-prompts.js';
9
6
  export interface ConflictRunOptions {
10
7
  db: Database.Database;
11
8
  projectPath: string;
12
9
  sessionId: string;
13
10
  workstreamId: string;
11
+ runnerId: string;
12
+ mergeLockHeartbeat?: {
13
+ lockEpoch: number;
14
+ timeoutMinutes: number;
15
+ };
14
16
  branchName: string;
15
17
  position: number;
16
18
  commitSha: string;
17
19
  existingTaskId?: string;
18
20
  }
19
- export declare function parseReviewDecision(raw: string): ParseReviewDecisionResult;
20
21
  export declare function runConflictResolutionCycle(options: ConflictRunOptions): Promise<'continued' | 'skipped'>;
21
- export {};
22
22
  //# sourceMappingURL=merge-conflict.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-conflict.d.ts","sourceRoot":"","sources":["../../src/parallel/merge-conflict.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AA2B3C,UAAU,yBAAyB;IACjC,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,yBAAyB,CAwB1E;AAoLD,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6J9G"}
1
+ {"version":3,"file":"merge-conflict.d.ts","sourceRoot":"","sources":["../../src/parallel/merge-conflict.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAkC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwGD,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAyN9G"}
@@ -1,145 +1,85 @@
1
1
  "use strict";
2
2
  /**
3
- * Merge conflict resolution helpers.
3
+ * Merge conflict resolution orchestrator.
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.parseReviewDecision = parseReviewDecision;
6
+ exports.parseReviewDecision = void 0;
7
7
  exports.runConflictResolutionCycle = runConflictResolutionCycle;
8
- const node_crypto_1 = require("node:crypto");
9
- const loader_js_1 = require("../config/loader.js");
10
8
  const queries_js_1 = require("../database/queries.js");
11
- const invocation_logger_js_1 = require("../providers/invocation-logger.js");
12
- const registry_js_1 = require("../providers/registry.js");
13
9
  const merge_git_js_1 = require("./merge-git.js");
14
10
  const merge_errors_js_1 = require("./merge-errors.js");
15
11
  const merge_progress_js_1 = require("./merge-progress.js");
16
- function parseReviewDecision(raw) {
17
- const trimmed = raw.trim();
18
- const upper = trimmed.toUpperCase();
19
- const hasApprove = upper.includes('APPROVE');
20
- const hasReject = upper.includes('REJECT');
21
- if (hasApprove && !hasReject) {
22
- return {
23
- decision: 'approve',
24
- notes: trimmed || 'APPROVED by merge-conflict reviewer',
25
- };
26
- }
27
- if (hasReject) {
28
- return {
29
- decision: 'reject',
30
- notes: trimmed || 'Please review and correct conflict resolution',
31
- };
32
- }
33
- return {
34
- decision: 'reject',
35
- notes: trimmed || 'Decision was not clear. Please provide explicit APPROVE/REJECT.',
36
- };
37
- }
38
- function buildMergeConflictSectionName() {
39
- return 'merge-conflicts';
40
- }
41
- function getNowISOString() {
42
- return new Date().toISOString();
43
- }
44
- function createMergeConflictSection(db) {
45
- const sectionName = buildMergeConflictSectionName();
46
- const existing = db
47
- .prepare('SELECT id FROM sections WHERE name = ? LIMIT 1')
48
- .get(sectionName);
49
- if (existing) {
50
- return existing.id;
51
- }
52
- const maxPosRow = db
53
- .prepare('SELECT MAX(position) as maxPos FROM sections')
54
- .get();
55
- const position = (maxPosRow?.maxPos ?? -1) + 1;
56
- const sectionId = (0, node_crypto_1.createHash)('sha1').update(sectionName + position).digest('hex');
57
- db.prepare(`INSERT INTO sections (id, name, position, priority, skipped, created_at)
58
- VALUES (?, ?, ?, ?, ?, ?)`).run(sectionId, sectionName, position, 80, 0, getNowISOString());
59
- return sectionId;
60
- }
61
- function ensureMergeConflictTask(db, workstreamId, shortSha, branchName, commitMessage, conflictedFiles, conflictPatch, forceNew = false) {
62
- const sectionId = createMergeConflictSection(db);
63
- if (!forceNew) {
64
- const existing = db
65
- .prepare(`SELECT t.id
66
- FROM tasks t
67
- INNER JOIN sections s ON s.id = t.section_id
68
- WHERE s.name = ? AND t.title LIKE ?
69
- ORDER BY t.created_at DESC
12
+ const global_db_js_1 = require("../runners/global-db.js");
13
+ const merge_conflict_attempts_js_1 = require("./merge-conflict-attempts.js");
14
+ const merge_conflict_invoke_js_1 = require("./merge-conflict-invoke.js");
15
+ const merge_conflict_prompts_js_1 = require("./merge-conflict-prompts.js");
16
+ const merge_conflict_task_js_1 = require("./merge-conflict-task.js");
17
+ const merge_lock_js_1 = require("./merge-lock.js");
18
+ var merge_conflict_prompts_js_2 = require("./merge-conflict-prompts.js");
19
+ Object.defineProperty(exports, "parseReviewDecision", { enumerable: true, get: function () { return merge_conflict_prompts_js_2.parseReviewDecision; } });
20
+ function refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId) {
21
+ const { db, close } = (0, global_db_js_1.openGlobalDatabase)();
22
+ try {
23
+ const row = db
24
+ .prepare(`SELECT id, claim_generation, runner_id
25
+ FROM workstreams
26
+ WHERE session_id = ?
27
+ AND id = ?
28
+ AND clone_path = ?
29
+ AND status IN ('running', 'completed')
70
30
  LIMIT 1`)
71
- .get(buildMergeConflictSectionName(), `Merge conflict: cherry-pick ${shortSha}%`);
72
- if (existing?.id) {
73
- return existing.id;
31
+ .get(sessionId, workstreamId, projectPath);
32
+ if (!row) {
33
+ throw new merge_errors_js_1.ParallelMergeError('Parallel workstream lease row not found during conflict resolution', 'LEASE_ROW_MISSING');
34
+ }
35
+ if (row.runner_id !== null && row.runner_id !== runnerId) {
36
+ throw new merge_errors_js_1.ParallelMergeError(`Parallel workstream lease owned by ${row.runner_id}, not ${runnerId}`, 'LEASE_FENCE_FAILED');
37
+ }
38
+ const update = db
39
+ .prepare(`UPDATE workstreams
40
+ SET runner_id = ?,
41
+ lease_expires_at = datetime('now', '+120 seconds')
42
+ WHERE id = ?
43
+ AND status IN ('running', 'completed')
44
+ AND claim_generation = ?
45
+ AND (runner_id IS NULL OR runner_id = ?)`)
46
+ .run(runnerId, row.id, row.claim_generation, runnerId);
47
+ if (update.changes !== 1) {
48
+ throw new merge_errors_js_1.ParallelMergeError('Parallel workstream lease fence check failed during conflict resolution', 'LEASE_FENCE_FAILED');
74
49
  }
75
50
  }
76
- const title = `Merge conflict: cherry-pick ${shortSha} from ${branchName}`;
77
- const created = (0, queries_js_1.createTask)(db, title, {
78
- sectionId,
79
- sourceFile: `merge-conflict (${workstreamId})`,
80
- filePath: conflictedFiles.join(', '),
81
- status: 'pending',
82
- fileContentHash: conflictPatch.substring(0, 2048),
83
- fileCommitSha: commitMessage,
84
- });
85
- (0, queries_js_1.addAuditEntry)(db, created.id, 'null', 'pending', 'merge', {
86
- actorType: 'orchestrator',
87
- notes: `Generated from conflict while cherry-picking ${shortSha} from ${branchName}:\n${commitMessage}`,
88
- });
89
- return created.id;
90
- }
91
- function createPromptForConflictCoder(options) {
92
- const notesSection = options.rejectionNotes
93
- ? `\n\nLatest review note from the resolver:\n${options.rejectionNotes}\n`
94
- : '';
95
- return `You are resolving a merge conflict for a cherry-pick during parallel merge.\n\n## Conflict context\nWorkstream: ${options.workstreamId}\nBranch: ${options.branchName}\nCommit: ${options.shortSha}\nCommit Message:\n${options.commitMessage}\n\nConflicted files:\n${options.conflictedFiles.map((file) => `- ${file}`).join('\n')}\n\nIntended patch:\n${options.conflictPatch}\n\nRules:\n1) Edit conflicted files to a correct resolution.\n2) Remove ALL conflict markers (<<<<<<, =======, >>>>>>) in resolved files.\n3) Stage only the resolved files using git add.\n4) Do NOT commit.\n5) Be surgical; change only files required for this commit.\n${notesSection}\n\nRespond with a short confirmation when done.`;
96
- }
97
- function createPromptForConflictReviewer(options) {
98
- const files = options.stagedFiles.length > 0
99
- ? options.stagedFiles.map((file) => `- ${file}`).join('\n')
100
- : 'No files staged yet';
101
- return `You are reviewing a staged resolution for a cherry-pick conflict in parallel merge.\n\nWorkstream: ${options.workstreamId}\nBranch: ${options.branchName}\nCommit: ${options.shortSha}\nOriginal message: ${options.commitMessage}\n\nCurrent staged diff to be committed by cherry-pick --continue:\n${options.stagedDiff || '(empty diff)'}\n\nFiles staged:\n${files}\n\nDecision rules:\n- Reply with APPROVE if the resolution is correct.\n- Reply with REJECT and actionable notes if any conflict marker remains or logic is incorrect.\n\nFormat:\nAPPROVE - <optional note> or\nREJECT - <checklist itemized note>`;
102
- }
103
- async function invokeModel(role, projectPath, taskId, prompt) {
104
- const config = (0, loader_js_1.loadConfig)(projectPath);
105
- const modelConfig = role === 'coder' ? config.ai?.coder : config.ai?.reviewer;
106
- if (!modelConfig?.provider || !modelConfig?.model) {
107
- throw new merge_errors_js_1.ParallelMergeError(`Missing AI ${role} configuration. Configure via config.ai.${role}.`, 'AI_CONFIG_MISSING');
51
+ finally {
52
+ close();
108
53
  }
109
- const providerName = modelConfig.provider;
110
- const model = modelConfig.model;
111
- const registry = (0, registry_js_1.getProviderRegistry)();
112
- const provider = registry.get(providerName);
113
- const result = await (0, invocation_logger_js_1.logInvocation)(prompt, (ctx) => provider.invoke(prompt, {
114
- model,
115
- timeout: 60 * 60 * 1000,
116
- cwd: projectPath,
117
- role,
118
- streamOutput: false,
119
- onActivity: ctx?.onActivity,
120
- }), {
121
- role,
122
- provider: providerName,
123
- model,
124
- taskId,
125
- projectPath,
54
+ }
55
+ function delay(ms) {
56
+ return new Promise((resolve) => {
57
+ setTimeout(resolve, ms);
126
58
  });
127
- if (!result.success) {
128
- const details = result.stderr || result.stdout || 'model returned non-zero exit code';
129
- throw new merge_errors_js_1.ParallelMergeError(`${role.toUpperCase()} invocation failed during merge conflict handling: ${details}`, 'AI_INVOCATION_FAILED');
59
+ }
60
+ async function waitForConflictRetryWindow(db, sessionId, workstreamId, projectPath, runnerId, backoffMinutes, mergeLockHeartbeat) {
61
+ if (backoffMinutes <= 0) {
62
+ return;
130
63
  }
131
- if (result.timedOut) {
132
- throw new merge_errors_js_1.ParallelMergeError(`${role} invocation timed out`, 'AI_INVOKE_TIMEOUT');
64
+ let remainingMs = backoffMinutes * 60_000;
65
+ const heartbeatSliceMs = 30_000;
66
+ while (remainingMs > 0) {
67
+ const waitMs = Math.min(heartbeatSliceMs, remainingMs);
68
+ await delay(waitMs);
69
+ remainingMs -= waitMs;
70
+ refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId);
71
+ if (mergeLockHeartbeat) {
72
+ (0, merge_lock_js_1.refreshMergeLock)(db, sessionId, runnerId, mergeLockHeartbeat.timeoutMinutes, mergeLockHeartbeat.lockEpoch);
73
+ }
133
74
  }
134
- return result.stdout;
135
75
  }
136
76
  async function runConflictResolutionCycle(options) {
137
- const { db, projectPath, sessionId, workstreamId, branchName, position, commitSha, existingTaskId, } = options;
138
- const shortSha = options.commitSha.slice(0, 7);
77
+ const { db, projectPath, sessionId, workstreamId, runnerId, mergeLockHeartbeat, branchName, position, commitSha, existingTaskId, } = options;
78
+ const shortSha = commitSha.slice(0, 7);
139
79
  const conflictedFiles = (0, merge_git_js_1.getConflictedFiles)(projectPath);
140
80
  const conflictPatch = (0, merge_git_js_1.getCommitPatch)(projectPath, commitSha);
141
81
  const commitMessage = (0, merge_git_js_1.getCommitMessage)(projectPath, commitSha);
142
- const taskId = ensureMergeConflictTask(db, workstreamId, shortSha, branchName, commitMessage, conflictedFiles, conflictPatch, !existingTaskId);
82
+ const taskId = (0, merge_conflict_task_js_1.ensureMergeConflictTask)(db, workstreamId, shortSha, branchName, commitMessage, conflictedFiles, conflictPatch, !existingTaskId);
143
83
  let conflictTaskId = existingTaskId ?? taskId;
144
84
  if (conflictTaskId !== taskId) {
145
85
  conflictTaskId = taskId;
@@ -149,17 +89,32 @@ async function runConflictResolutionCycle(options) {
149
89
  if (!currentConflictTask) {
150
90
  throw new merge_errors_js_1.ParallelMergeError('Created merge-conflict task not found', 'TASK_MISSING');
151
91
  }
92
+ refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId);
152
93
  if (currentConflictTask.status === 'completed') {
153
- (0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstreamId, position, commitSha, 'applied', conflictTaskId);
94
+ const appliedCommitSha = (0, merge_git_js_1.runGitCommand)(projectPath, ['rev-parse', 'HEAD'], { allowFailure: true }).trim();
95
+ (0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstreamId, position, commitSha, 'applied', conflictTaskId, appliedCommitSha || null);
96
+ (0, merge_conflict_attempts_js_1.clearConflictAttemptState)(sessionId, workstreamId);
154
97
  return 'continued';
155
98
  }
156
99
  (0, queries_js_1.updateTaskStatus)(db, currentConflictTask.id, 'in_progress', 'merge-conflict-orchestrator');
157
100
  while (true) {
101
+ const conflictAttempt = (0, merge_conflict_attempts_js_1.recordConflictAttempt)(sessionId, workstreamId);
102
+ if (conflictAttempt.blocked) {
103
+ (0, queries_js_1.rejectTask)(db, currentConflictTask.id, 'merge-conflict-reviewer', `Conflict resolution attempt limit reached (${merge_conflict_attempts_js_1.MAX_CONFLICT_ATTEMPTS}). Session moved to blocked_conflict.`);
104
+ throw new merge_errors_js_1.ParallelMergeError(`Conflict resolution exceeded ${merge_conflict_attempts_js_1.MAX_CONFLICT_ATTEMPTS} attempts for ${shortSha}`, 'CONFLICT_ATTEMPT_LIMIT');
105
+ }
106
+ refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId);
107
+ if (conflictAttempt.attempts > 1 && (conflictAttempt.backoffMinutes ?? 0) > 0) {
108
+ (0, queries_js_1.updateTaskStatus)(db, currentConflictTask.id, 'in_progress', 'merge-conflict-orchestrator', `Waiting ${conflictAttempt.backoffMinutes} minute(s) before retry ${conflictAttempt.attempts}.`);
109
+ await waitForConflictRetryWindow(db, sessionId, workstreamId, projectPath, runnerId, conflictAttempt.backoffMinutes ?? 0, mergeLockHeartbeat);
110
+ }
158
111
  const existingTask = (0, queries_js_1.getTask)(db, currentConflictTask.id);
159
- const lastNotes = existingTask?.rejection_count
160
- ? `After ${existingTask.rejection_count} rejection(s).`
161
- : undefined;
162
- const coderPrompt = createPromptForConflictCoder({
112
+ const noteParts = [`Attempt ${conflictAttempt.attempts}/${merge_conflict_attempts_js_1.MAX_CONFLICT_ATTEMPTS}.`];
113
+ if (existingTask?.rejection_count) {
114
+ noteParts.push(`After ${existingTask.rejection_count} rejection(s).`);
115
+ }
116
+ const lastNotes = noteParts.join(' ');
117
+ const coderPrompt = (0, merge_conflict_prompts_js_1.createPromptForConflictCoder)({
163
118
  workstreamId,
164
119
  shortSha,
165
120
  branchName,
@@ -168,7 +123,7 @@ async function runConflictResolutionCycle(options) {
168
123
  conflictPatch,
169
124
  rejectionNotes: lastNotes,
170
125
  });
171
- await invokeModel('coder', projectPath, currentConflictTask.id, coderPrompt);
126
+ await (0, merge_conflict_invoke_js_1.invokeMergeConflictModel)('coder', projectPath, currentConflictTask.id, coderPrompt);
172
127
  const remaining = (0, merge_git_js_1.getConflictedFiles)(projectPath);
173
128
  if (remaining.length > 0) {
174
129
  (0, queries_js_1.updateTaskStatus)(db, currentConflictTask.id, 'in_progress', 'merge-conflict-orchestrator', `Conflict markers still present: ${remaining.join(', ')}`);
@@ -181,7 +136,7 @@ async function runConflictResolutionCycle(options) {
181
136
  continue;
182
137
  }
183
138
  (0, queries_js_1.updateTaskStatus)(db, currentConflictTask.id, 'review', 'merge-conflict-orchestrator');
184
- const reviewerPrompt = createPromptForConflictReviewer({
139
+ const reviewerPrompt = (0, merge_conflict_prompts_js_1.createPromptForConflictReviewer)({
185
140
  workstreamId,
186
141
  shortSha,
187
142
  branchName,
@@ -189,14 +144,10 @@ async function runConflictResolutionCycle(options) {
189
144
  stagedDiff,
190
145
  stagedFiles,
191
146
  });
192
- const decisionText = await invokeModel('reviewer', projectPath, currentConflictTask.id, reviewerPrompt);
193
- const decision = parseReviewDecision(decisionText);
147
+ const decisionText = await (0, merge_conflict_invoke_js_1.invokeMergeConflictModel)('reviewer', projectPath, currentConflictTask.id, reviewerPrompt);
148
+ const decision = (0, merge_conflict_prompts_js_1.parseReviewDecision)(decisionText);
194
149
  if (decision.decision === 'reject') {
195
150
  (0, queries_js_1.rejectTask)(db, currentConflictTask.id, 'merge-conflict-reviewer', decision.notes);
196
- if ((0, merge_git_js_1.cleanTreeHasConflicts)(projectPath)) {
197
- continue;
198
- }
199
- // If no explicit conflict markers remain but reviewer still rejects, keep iterating.
200
151
  continue;
201
152
  }
202
153
  if ((0, merge_git_js_1.hasUnmergedFiles)(projectPath)) {
@@ -207,17 +158,22 @@ async function runConflictResolutionCycle(options) {
207
158
  throw new merge_errors_js_1.ParallelMergeError('Cherry-pick no longer in progress while resolving conflict', 'CHERRY_PICK_CONTEXT_LOST');
208
159
  }
209
160
  try {
161
+ refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId);
210
162
  (0, merge_git_js_1.runGitCommand)(projectPath, ['-c', 'core.editor=true', 'cherry-pick', '--continue']);
211
163
  (0, queries_js_1.approveTask)(db, currentConflictTask.id, 'merge-conflict-reviewer', decision.notes);
212
- (0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstreamId, position, commitSha, 'applied', currentConflictTask.id);
164
+ const appliedCommitSha = (0, merge_git_js_1.runGitCommand)(projectPath, ['rev-parse', 'HEAD'], { allowFailure: true }).trim();
165
+ (0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstreamId, position, commitSha, 'applied', currentConflictTask.id, appliedCommitSha || null);
166
+ (0, merge_conflict_attempts_js_1.clearConflictAttemptState)(sessionId, workstreamId);
213
167
  return 'continued';
214
168
  }
215
169
  catch (error) {
216
170
  const message = error instanceof Error ? error.message : String(error);
217
171
  if (/nothing to commit|previous cherry-pick is empty/i.test(message)) {
172
+ refreshMergeConflictLease(sessionId, workstreamId, projectPath, runnerId);
218
173
  (0, merge_git_js_1.runGitCommand)(projectPath, ['cherry-pick', '--skip']);
219
174
  (0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstreamId, position, commitSha, 'skipped', currentConflictTask.id);
220
175
  (0, queries_js_1.updateTaskStatus)(db, currentConflictTask.id, 'completed', 'merge-conflict-reviewer', 'Cherry-pick is now empty after resolution; skipped this commit.');
176
+ (0, merge_conflict_attempts_js_1.clearConflictAttemptState)(sessionId, workstreamId);
221
177
  return 'skipped';
222
178
  }
223
179
  throw error;