contextos-mcp 0.4.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -0
  3. package/bin/mcp.mjs +61 -0
  4. package/bin/swarm.mjs +45 -0
  5. package/dist/agents/aider.d.ts +12 -0
  6. package/dist/agents/aider.js +192 -0
  7. package/dist/agents/aider.js.map +1 -0
  8. package/dist/agents/claude-code.d.ts +9 -0
  9. package/dist/agents/claude-code.js +236 -0
  10. package/dist/agents/claude-code.js.map +1 -0
  11. package/dist/agents/codex.d.ts +14 -0
  12. package/dist/agents/codex.js +203 -0
  13. package/dist/agents/codex.js.map +1 -0
  14. package/dist/agents/direct-llm.d.ts +22 -0
  15. package/dist/agents/direct-llm.js +254 -0
  16. package/dist/agents/direct-llm.js.map +1 -0
  17. package/dist/agents/mock.d.ts +10 -0
  18. package/dist/agents/mock.js +170 -0
  19. package/dist/agents/mock.js.map +1 -0
  20. package/dist/agents/opencode.d.ts +23 -0
  21. package/dist/agents/opencode.js +631 -0
  22. package/dist/agents/opencode.js.map +1 -0
  23. package/dist/agents/provider.d.ts +11 -0
  24. package/dist/agents/provider.js +31 -0
  25. package/dist/agents/provider.js.map +1 -0
  26. package/dist/compression/compressor.d.ts +28 -0
  27. package/dist/compression/compressor.js +268 -0
  28. package/dist/compression/compressor.js.map +1 -0
  29. package/dist/config.d.ts +43 -0
  30. package/dist/config.js +172 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/contextos/loader.d.ts +46 -0
  33. package/dist/contextos/loader.js +120 -0
  34. package/dist/contextos/loader.js.map +1 -0
  35. package/dist/contextos/selector.d.ts +46 -0
  36. package/dist/contextos/selector.js +80 -0
  37. package/dist/contextos/selector.js.map +1 -0
  38. package/dist/core/action-dispatcher.d.ts +15 -0
  39. package/dist/core/action-dispatcher.js +28 -0
  40. package/dist/core/action-dispatcher.js.map +1 -0
  41. package/dist/core/action-schema.d.ts +72 -0
  42. package/dist/core/action-schema.js +86 -0
  43. package/dist/core/action-schema.js.map +1 -0
  44. package/dist/core/repl-interface.d.ts +48 -0
  45. package/dist/core/repl-interface.js +5 -0
  46. package/dist/core/repl-interface.js.map +1 -0
  47. package/dist/core/repl.d.ts +59 -0
  48. package/dist/core/repl.js +353 -0
  49. package/dist/core/repl.js.map +1 -0
  50. package/dist/core/rlm.d.ts +65 -0
  51. package/dist/core/rlm.js +564 -0
  52. package/dist/core/rlm.js.map +1 -0
  53. package/dist/core/runtime.py +470 -0
  54. package/dist/core/types.d.ts +265 -0
  55. package/dist/core/types.js +19 -0
  56. package/dist/core/types.js.map +1 -0
  57. package/dist/env.d.ts +10 -0
  58. package/dist/env.js +75 -0
  59. package/dist/env.js.map +1 -0
  60. package/dist/hooks/runner.d.ts +43 -0
  61. package/dist/hooks/runner.js +173 -0
  62. package/dist/hooks/runner.js.map +1 -0
  63. package/dist/labs/cli.d.ts +15 -0
  64. package/dist/labs/cli.js +295 -0
  65. package/dist/labs/cli.js.map +1 -0
  66. package/dist/labs/episodic-memory.d.ts +132 -0
  67. package/dist/labs/episodic-memory.js +391 -0
  68. package/dist/labs/episodic-memory.js.map +1 -0
  69. package/dist/labs/interactive-swarm.d.ts +20 -0
  70. package/dist/labs/interactive-swarm.js +1173 -0
  71. package/dist/labs/interactive-swarm.js.map +1 -0
  72. package/dist/labs/interactive.d.ts +10 -0
  73. package/dist/labs/interactive.js +2099 -0
  74. package/dist/labs/interactive.js.map +1 -0
  75. package/dist/labs/ts-analyzer.d.ts +74 -0
  76. package/dist/labs/ts-analyzer.js +379 -0
  77. package/dist/labs/ts-analyzer.js.map +1 -0
  78. package/dist/labs/viewer.d.ts +12 -0
  79. package/dist/labs/viewer.js +1284 -0
  80. package/dist/labs/viewer.js.map +1 -0
  81. package/dist/main.d.ts +15 -0
  82. package/dist/main.js +247 -0
  83. package/dist/main.js.map +1 -0
  84. package/dist/mcp/server.d.ts +16 -0
  85. package/dist/mcp/server.js +192775 -0
  86. package/dist/mcp/server.js.map +1 -0
  87. package/dist/mcp/session.d.ts +117 -0
  88. package/dist/mcp/session.js +450 -0
  89. package/dist/mcp/session.js.map +1 -0
  90. package/dist/mcp/state.d.ts +42 -0
  91. package/dist/mcp/state.js +537 -0
  92. package/dist/mcp/state.js.map +1 -0
  93. package/dist/mcp/tools/contextos.d.ts +17 -0
  94. package/dist/mcp/tools/contextos.js +583 -0
  95. package/dist/mcp/tools/contextos.js.map +1 -0
  96. package/dist/mcp/tools.d.ts +15 -0
  97. package/dist/mcp/tools.js +393 -0
  98. package/dist/mcp/tools.js.map +1 -0
  99. package/dist/orchestration/reviewer-gate.d.ts +43 -0
  100. package/dist/orchestration/reviewer-gate.js +280 -0
  101. package/dist/orchestration/reviewer-gate.js.map +1 -0
  102. package/dist/orchestration/reviewer-prompt.d.ts +18 -0
  103. package/dist/orchestration/reviewer-prompt.js +67 -0
  104. package/dist/orchestration/reviewer-prompt.js.map +1 -0
  105. package/dist/orchestration/verification-runner.d.ts +53 -0
  106. package/dist/orchestration/verification-runner.js +352 -0
  107. package/dist/orchestration/verification-runner.js.map +1 -0
  108. package/dist/plugins/adapter.d.ts +11 -0
  109. package/dist/plugins/adapter.js +11 -0
  110. package/dist/plugins/adapter.js.map +1 -0
  111. package/dist/plugins/core/archive.d.ts +9 -0
  112. package/dist/plugins/core/archive.js +128 -0
  113. package/dist/plugins/core/archive.js.map +1 -0
  114. package/dist/plugins/core/digest.d.ts +11 -0
  115. package/dist/plugins/core/digest.js +70 -0
  116. package/dist/plugins/core/digest.js.map +1 -0
  117. package/dist/plugins/core/grants.d.ts +22 -0
  118. package/dist/plugins/core/grants.js +90 -0
  119. package/dist/plugins/core/grants.js.map +1 -0
  120. package/dist/plugins/core/index.d.ts +10 -0
  121. package/dist/plugins/core/index.js +11 -0
  122. package/dist/plugins/core/index.js.map +1 -0
  123. package/dist/plugins/core/pinning.d.ts +15 -0
  124. package/dist/plugins/core/pinning.js +86 -0
  125. package/dist/plugins/core/pinning.js.map +1 -0
  126. package/dist/plugins/core/types.d.ts +105 -0
  127. package/dist/plugins/core/types.js +22 -0
  128. package/dist/plugins/core/types.js.map +1 -0
  129. package/dist/plugins/core/updater.d.ts +16 -0
  130. package/dist/plugins/core/updater.js +64 -0
  131. package/dist/plugins/core/updater.js.map +1 -0
  132. package/dist/plugins/loader.d.ts +65 -0
  133. package/dist/plugins/loader.js +110 -0
  134. package/dist/plugins/loader.js.map +1 -0
  135. package/dist/prompts/orchestrator.d.ts +6 -0
  136. package/dist/prompts/orchestrator.js +114 -0
  137. package/dist/prompts/orchestrator.js.map +1 -0
  138. package/dist/routing/model-resolver.d.ts +27 -0
  139. package/dist/routing/model-resolver.js +265 -0
  140. package/dist/routing/model-resolver.js.map +1 -0
  141. package/dist/routing/model-router.d.ts +130 -0
  142. package/dist/routing/model-router.js +515 -0
  143. package/dist/routing/model-router.js.map +1 -0
  144. package/dist/runtime/attestations.cjs +260 -0
  145. package/dist/runtime/reviewer-pipeline.cjs +319 -0
  146. package/dist/runtime/sandbox.cjs +468 -0
  147. package/dist/runtime/state-machine.cjs +885 -0
  148. package/dist/runtime/thread-store.cjs +175 -0
  149. package/dist/runtime/transactional-git.cjs +315 -0
  150. package/dist/runtime/verification-pipeline.cjs +435 -0
  151. package/dist/sandbox/execution.d.ts +165 -0
  152. package/dist/sandbox/execution.js +497 -0
  153. package/dist/sandbox/execution.js.map +1 -0
  154. package/dist/security/file-policy.d.ts +22 -0
  155. package/dist/security/file-policy.js +53 -0
  156. package/dist/security/file-policy.js.map +1 -0
  157. package/dist/security/repository-boundary.d.ts +21 -0
  158. package/dist/security/repository-boundary.js +119 -0
  159. package/dist/security/repository-boundary.js.map +1 -0
  160. package/dist/security/secret-filter.d.ts +36 -0
  161. package/dist/security/secret-filter.js +149 -0
  162. package/dist/security/secret-filter.js.map +1 -0
  163. package/dist/swarm.d.ts +14 -0
  164. package/dist/swarm.js +624 -0
  165. package/dist/swarm.js.map +1 -0
  166. package/dist/threads/cache.d.ts +64 -0
  167. package/dist/threads/cache.js +241 -0
  168. package/dist/threads/cache.js.map +1 -0
  169. package/dist/threads/manager.d.ts +94 -0
  170. package/dist/threads/manager.js +914 -0
  171. package/dist/threads/manager.js.map +1 -0
  172. package/dist/ui/banner.d.ts +14 -0
  173. package/dist/ui/banner.js +42 -0
  174. package/dist/ui/banner.js.map +1 -0
  175. package/dist/ui/dashboard.d.ts +33 -0
  176. package/dist/ui/dashboard.js +135 -0
  177. package/dist/ui/dashboard.js.map +1 -0
  178. package/dist/ui/index.d.ts +11 -0
  179. package/dist/ui/index.js +12 -0
  180. package/dist/ui/index.js.map +1 -0
  181. package/dist/ui/log.d.ts +39 -0
  182. package/dist/ui/log.js +126 -0
  183. package/dist/ui/log.js.map +1 -0
  184. package/dist/ui/onboarding.d.ts +14 -0
  185. package/dist/ui/onboarding.js +755 -0
  186. package/dist/ui/onboarding.js.map +1 -0
  187. package/dist/ui/run-log.d.ts +50 -0
  188. package/dist/ui/run-log.js +60 -0
  189. package/dist/ui/run-log.js.map +1 -0
  190. package/dist/ui/spinner.d.ts +41 -0
  191. package/dist/ui/spinner.js +172 -0
  192. package/dist/ui/spinner.js.map +1 -0
  193. package/dist/ui/streaming-feed.d.ts +72 -0
  194. package/dist/ui/streaming-feed.js +386 -0
  195. package/dist/ui/streaming-feed.js.map +1 -0
  196. package/dist/ui/summary.d.ts +18 -0
  197. package/dist/ui/summary.js +113 -0
  198. package/dist/ui/summary.js.map +1 -0
  199. package/dist/ui/text-input.d.ts +19 -0
  200. package/dist/ui/text-input.js +266 -0
  201. package/dist/ui/text-input.js.map +1 -0
  202. package/dist/ui/theme.d.ts +63 -0
  203. package/dist/ui/theme.js +97 -0
  204. package/dist/ui/theme.js.map +1 -0
  205. package/dist/utils/command-exists.d.ts +11 -0
  206. package/dist/utils/command-exists.js +41 -0
  207. package/dist/utils/command-exists.js.map +1 -0
  208. package/dist/utils/process-security.d.ts +19 -0
  209. package/dist/utils/process-security.js +90 -0
  210. package/dist/utils/process-security.js.map +1 -0
  211. package/dist/utils/rolling-buffer.d.ts +16 -0
  212. package/dist/utils/rolling-buffer.js +30 -0
  213. package/dist/utils/rolling-buffer.js.map +1 -0
  214. package/dist/utils/safe-spawn.d.ts +13 -0
  215. package/dist/utils/safe-spawn.js +49 -0
  216. package/dist/utils/safe-spawn.js.map +1 -0
  217. package/dist/worktree/manager.d.ts +69 -0
  218. package/dist/worktree/manager.js +593 -0
  219. package/dist/worktree/manager.js.map +1 -0
  220. package/dist/worktree/merge.d.ts +58 -0
  221. package/dist/worktree/merge.js +389 -0
  222. package/dist/worktree/merge.js.map +1 -0
  223. package/package.json +63 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * .agents/runtime/thread-store.cjs
3
+ * ContextOS — Persistent Transactional Thread Store
4
+ *
5
+ * Persists runtime threads to disk with atomic write guarantees,
6
+ * optimistic concurrency checks, and append-only audit event trails.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const fs = require("node:fs");
12
+ const path = require("node:path");
13
+ const crypto = require("node:crypto");
14
+ const { createThreadState, transitionThread } = require("./state-machine.cjs");
15
+
16
+ class ThreadStore {
17
+ /**
18
+ * @param {Object} [options]
19
+ * @param {string} [options.baseDir] - Project root directory
20
+ */
21
+ constructor(options = {}) {
22
+ this.rootDir = path.resolve(options.baseDir || process.cwd());
23
+ this.storeDir = path.join(this.rootDir, ".agents", ".contextos", "threads");
24
+ fs.mkdirSync(this.storeDir, { recursive: true });
25
+ }
26
+
27
+ _getThreadPath(threadId) {
28
+ const safeId = threadId.replace(/[^a-zA-Z0-9_-]/g, "_");
29
+ return path.join(this.storeDir, `${safeId}.cjson`);
30
+ }
31
+
32
+ _getAuditPath(threadId) {
33
+ const safeId = threadId.replace(/[^a-zA-Z0-9_-]/g, "_");
34
+ return path.join(this.storeDir, `${safeId}.audit.cjsonl`);
35
+ }
36
+
37
+ _getLockPath(threadId) {
38
+ const safeId = threadId.replace(/[^a-zA-Z0-9_-]/g, "_");
39
+ return path.join(this.storeDir, `${safeId}.lock`);
40
+ }
41
+
42
+ /**
43
+ * Acquires a lease for a given thread ID.
44
+ * @param {string} threadId
45
+ * @param {string} [instanceId]
46
+ * @returns {import('../../../.agents/transaction-core/ipc-lock').LeaseLock}
47
+ */
48
+ acquireLease(threadId, instanceId) {
49
+ const { LeaseLock } = require("../../../.agents/transaction-core/ipc-lock.js");
50
+ return new LeaseLock({
51
+ lockFilePath: this._getLockPath(threadId),
52
+ instanceId,
53
+ ttlMs: 30000,
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Saves a thread to disk atomically.
59
+ * @param {Object} thread
60
+ * @param {import('../../../.agents/transaction-core/ipc-lock').LeaseLock} [lease] - Optional lease lock to enforce ownership.
61
+ */
62
+ save(thread, lease = null) {
63
+ if (!thread || !thread.id) {
64
+ throw new Error("Cannot save invalid thread object");
65
+ }
66
+
67
+ if (lease) {
68
+ lease.assertValid();
69
+ }
70
+
71
+ const threadPath = this._getThreadPath(thread.id);
72
+ const tempPath = `${threadPath}.${crypto.randomBytes(4).toString("hex")}.tmp`;
73
+
74
+ try {
75
+ fs.writeFileSync(tempPath, JSON.stringify(thread, null, 2), "utf8");
76
+ fs.renameSync(tempPath, threadPath);
77
+ } catch (err) {
78
+ try {
79
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
80
+ } catch {}
81
+ throw err;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Loads a thread from disk.
87
+ *
88
+ * @param {string} threadId
89
+ * @returns {Object|null}
90
+ */
91
+ get(threadId) {
92
+ const threadPath = this._getThreadPath(threadId);
93
+ if (!fs.existsSync(threadPath)) return null;
94
+
95
+ try {
96
+ const content = fs.readFileSync(threadPath, "utf8");
97
+ return Object.freeze(JSON.parse(content));
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Creates, saves, and returns a new thread.
105
+ */
106
+ create(options = {}) {
107
+ const thread = createThreadState(options);
108
+ this.save(thread);
109
+ return thread;
110
+ }
111
+
112
+ /**
113
+ * Atomically transitions a thread with optimistic revision verification and audit logging.
114
+ *
115
+ * @param {string} threadId
116
+ * @param {Object} event - { type, payload, expectedRevision }
117
+ * @returns {Object} Updated ThreadState
118
+ */
119
+ transition(threadId, event = {}) {
120
+ const current = this.get(threadId);
121
+ if (!current) {
122
+ const err = new Error(`Thread not found: "${threadId}"`);
123
+ err.code = "CTX_THREAD_NOT_FOUND";
124
+ throw err;
125
+ }
126
+
127
+ const auditPath = this._getAuditPath(threadId);
128
+ const next = transitionThread(current, event, { auditFilePath: auditPath });
129
+ this.save(next);
130
+ return next;
131
+ }
132
+
133
+ /**
134
+ * Returns audit trail events for a thread.
135
+ *
136
+ * @param {string} threadId
137
+ * @returns {Array<Object>}
138
+ */
139
+ getAuditTrail(threadId) {
140
+ const auditPath = this._getAuditPath(threadId);
141
+ if (!fs.existsSync(auditPath)) return [];
142
+
143
+ try {
144
+ const lines = fs.readFileSync(auditPath, "utf8").split("\n").filter(Boolean);
145
+ return lines.map((line) => JSON.parse(line));
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Lists all persisted threads.
153
+ */
154
+ list() {
155
+ if (!fs.existsSync(this.storeDir)) return [];
156
+
157
+ const files = fs.readdirSync(this.storeDir).filter((f) => f.endsWith(".cjson") && !f.endsWith(".tmp"));
158
+ const threads = [];
159
+
160
+ for (const f of files) {
161
+ try {
162
+ const content = fs.readFileSync(path.join(this.storeDir, f), "utf8");
163
+ threads.push(JSON.parse(content));
164
+ } catch {
165
+ // ignore corrupted files
166
+ }
167
+ }
168
+
169
+ return threads.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
170
+ }
171
+ }
172
+
173
+ module.exports = {
174
+ ThreadStore,
175
+ };
@@ -0,0 +1,315 @@
1
+ /**
2
+ * .agents/runtime/transactional-git.cjs
3
+ * ContextOS — Transactional Git Worktree Merge Pipeline
4
+ *
5
+ * Implements Section 18 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
6
+ * - Mutual exclusion via repository LeaseLock
7
+ * - Fixes target base SHA before applying candidate commits
8
+ * - Isolated integration worktree (ctx-int-<timestamp>-<id>) in temporary directory
9
+ * - All-or-nothing candidate application: aborts whole transaction on conflict
10
+ * - Combined attestation & verification check
11
+ * - Stale base detection: rejects merge if target advanced during verification
12
+ * - Explicit finalization gate with dirty primary checkout protection
13
+ * - Safe cleanup: never deletes active or foreign user branches
14
+ * - Zero external dependencies (pure Node child_process & fs)
15
+ */
16
+
17
+ "use strict";
18
+
19
+ const { execFile } = require("node:child_process");
20
+ const path = require("node:path");
21
+ const _fs = require("node:fs");
22
+ const os = require("node:os");
23
+ const crypto = require("node:crypto");
24
+ const { LeaseLock } = require("../../../.agents/transaction-core/ipc-lock.js");
25
+
26
+ function execGit(args, cwd, env = process.env) {
27
+ return new Promise((resolve, reject) => {
28
+ execFile("git", args, { cwd, maxBuffer: 16 * 1024 * 1024, env }, (err, stdout, stderr) => {
29
+ if (err) {
30
+ const error = new Error(`git ${args[0]} failed: ${stderr || err.message}`);
31
+ error.code = err.code || "CTX_GIT_COMMAND_FAILED";
32
+ error.stderr = stderr ? stderr.toString() : "";
33
+ error.stdout = stdout ? stdout.toString() : "";
34
+ error.args = args;
35
+ reject(error);
36
+ } else {
37
+ resolve({ stdout: stdout ? stdout.toString().trim() : "", stderr: stderr ? stderr.toString().trim() : "" });
38
+ }
39
+ });
40
+ });
41
+ }
42
+
43
+ class TransactionalGitPipeline {
44
+ /**
45
+ * @param {Object} options
46
+ * @param {string} options.repoRoot - Absolute path to repository root
47
+ * @param {string} [options.lockFilePath] - Custom path to lockfile
48
+ * @param {number} [options.leaseTtlMs=30000] - Lease duration in ms
49
+ */
50
+ constructor(options = {}) {
51
+ if (!options.repoRoot) {
52
+ throw new Error("TransactionalGitPipeline requires repoRoot");
53
+ }
54
+
55
+ this.repoRoot = path.resolve(options.repoRoot);
56
+ const lockDir = path.join(this.repoRoot, ".agents", ".contextos", "state");
57
+ this.lockFilePath = options.lockFilePath || path.join(lockDir, "git-operation.lock");
58
+ this.leaseTtlMs = options.leaseTtlMs || 30000;
59
+
60
+ this.lock = new LeaseLock({
61
+ lockFilePath: this.lockFilePath,
62
+ ttlMs: this.leaseTtlMs,
63
+ heartbeatIntervalMs: Math.max(1000, Math.floor(this.leaseTtlMs / 3)),
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Resolves a reference or symbol to a 40-character SHA.
69
+ *
70
+ * @param {string} ref
71
+ * @param {string} [cwd]
72
+ * @returns {Promise<string>}
73
+ */
74
+ async resolveRef(ref, cwd = this.repoRoot) {
75
+ const { stdout } = await execGit(["rev-parse", "--verify", `${ref}^{commit}`], cwd);
76
+ return stdout;
77
+ }
78
+
79
+ /**
80
+ * Checks if the working tree has uncommitted staged or unstaged changes.
81
+ * Internal ContextOS runtime state directories are excluded from dirty checks.
82
+ *
83
+ * @param {string} [cwd]
84
+ * @returns {Promise<boolean>} True if working tree is dirty
85
+ */
86
+ async isWorkingTreeDirty(cwd = this.repoRoot) {
87
+ const { stdout } = await execGit(["status", "--porcelain"], cwd);
88
+ const lines = stdout.split(/\r?\n/).filter((line) => {
89
+ if (!line.trim()) return false;
90
+ const file = line.slice(3).trim();
91
+ if (file === ".agents" || file === ".agents/" || file.startsWith(".agents/") || file.startsWith(".agents\\")) {
92
+ return false;
93
+ }
94
+ return true;
95
+ });
96
+ return lines.length > 0;
97
+ }
98
+
99
+ /**
100
+ * Executes a transactional integration and merge.
101
+ *
102
+ * @param {Object} params
103
+ * @param {string} params.targetRef - Target branch to merge into (e.g. 'main', 'develop')
104
+ * @param {string[]} params.candidateCommits - Ordered array of commit SHAs or branch refs
105
+ * @param {Object} [params.attestations] - Optional map of commitSha -> { status: 'PASS' }
106
+ * @param {boolean} [params.finalize=false] - If true, merges into target branch; if false, prepares integration branch
107
+ * @param {'fast-forward'|'squash'} [params.mode='fast-forward'] - Merge finalization strategy
108
+ * @param {string} [params.squashCommitMessage] - Required if mode is 'squash'
109
+ * @returns {Promise<Object>} Merge transaction outcome
110
+ */
111
+ async executeMerge(params = {}) {
112
+ const {
113
+ targetRef = "main",
114
+ candidateCommits = [],
115
+ attestations = null,
116
+ finalize = false,
117
+ mode = "fast-forward",
118
+ squashCommitMessage = "feat(ctx): merge candidate integration batch",
119
+ } = params;
120
+
121
+ if (!Array.isArray(candidateCommits) || candidateCommits.length === 0) {
122
+ throw new Error("executeMerge requires non-empty candidateCommits array");
123
+ }
124
+
125
+ // 1. Pre-merge Attestation Verification (fail fast before acquiring resources)
126
+ if (attestations) {
127
+ for (const commitRef of candidateCommits) {
128
+ const sha = await this.resolveRef(commitRef);
129
+ const attestation = attestations[sha] || attestations[commitRef];
130
+ if (!attestation || attestation.status !== "PASS") {
131
+ const err = new Error(
132
+ `Candidate commit ${commitRef} (${sha}) lacks evidence-bearing PASS attestation. Merge blocked.`,
133
+ );
134
+ err.code = "CTX_ATTESTATION_UNVERIFIED";
135
+ err.commitSha = sha;
136
+ throw err;
137
+ }
138
+ }
139
+ }
140
+
141
+ // 2. Acquire exclusive repository operation lease
142
+ await this.lock.acquire({ timeoutMs: 15000 });
143
+
144
+ let integrationBranch = null;
145
+ let worktreeDir = null;
146
+ let baseSha = null;
147
+
148
+ try {
149
+ // 3. Fix target base SHA
150
+ baseSha = await this.resolveRef(targetRef);
151
+
152
+ // If finalization is requested, verify primary working tree is clean
153
+ if (finalize) {
154
+ const dirty = await this.isWorkingTreeDirty(this.repoRoot);
155
+ if (dirty) {
156
+ const err = new Error(
157
+ `Primary working tree is dirty. Cannot finalize merge into target "${targetRef}" without clean checkout.`,
158
+ );
159
+ err.code = "CTX_GIT_DIRTY_CHECKOUT";
160
+ throw err;
161
+ }
162
+ }
163
+
164
+ // 4. Create isolated integration worktree off target base SHA
165
+ const runId = crypto.randomBytes(4).toString("hex");
166
+ integrationBranch = `ctx-int-${Date.now()}-${runId}`;
167
+ worktreeDir = path.join(os.tmpdir(), `ctx-worktree-${runId}`);
168
+
169
+ await execGit(["worktree", "add", "-b", integrationBranch, worktreeDir, baseSha], this.repoRoot);
170
+
171
+ // 5. Apply candidate commits inside isolated worktree
172
+ let appliedCount = 0;
173
+ for (const commitRef of candidateCommits) {
174
+ const sha = await this.resolveRef(commitRef);
175
+
176
+ try {
177
+ await execGit(["cherry-pick", sha], worktreeDir);
178
+ appliedCount++;
179
+ } catch (pickErr) {
180
+ // Conflict detected! Abort cherry-pick and clean up entire integration worktree
181
+ try {
182
+ await execGit(["cherry-pick", "--abort"], worktreeDir);
183
+ } catch {}
184
+
185
+ await this._removeWorktreeAndBranch(worktreeDir, integrationBranch);
186
+ worktreeDir = null;
187
+ integrationBranch = null;
188
+
189
+ const err = new Error(
190
+ `Merge conflict while applying candidate commit ${commitRef} (index ${appliedCount}). Entire integration transaction aborted.`,
191
+ );
192
+ err.code = "CTX_GIT_MERGE_CONFLICT";
193
+ err.failedCommit = sha;
194
+ err.appliedCount = appliedCount;
195
+ err.details = pickErr.stderr || pickErr.message;
196
+ throw err;
197
+ }
198
+ }
199
+
200
+ // 6. Get resulting integration head SHA
201
+ const integrationHeadSha = await this.resolveRef(integrationBranch);
202
+
203
+ // 7. Check for target advance (stale base check)
204
+ const currentTargetSha = await this.resolveRef(targetRef);
205
+ if (currentTargetSha !== baseSha) {
206
+ await this._removeWorktreeAndBranch(worktreeDir, integrationBranch);
207
+ worktreeDir = null;
208
+ integrationBranch = null;
209
+
210
+ const err = new Error(
211
+ `Target branch "${targetRef}" advanced concurrently from ${baseSha} to ${currentTargetSha}. Aborting transaction.`,
212
+ );
213
+ err.code = "CTX_GIT_STALE_BASE";
214
+ err.initialBase = baseSha;
215
+ err.currentBase = currentTargetSha;
216
+ throw err;
217
+ }
218
+
219
+ // 8. If finalize is not requested, return integration branch ready for combined verification
220
+ if (!finalize) {
221
+ // Remove worktree folder but keep integrationBranch in git
222
+ try {
223
+ await execGit(["worktree", "remove", "--force", worktreeDir], this.repoRoot);
224
+ worktreeDir = null;
225
+ } catch {}
226
+
227
+ return {
228
+ status: "READY_FOR_FINALIZATION",
229
+ targetRef,
230
+ baseSha,
231
+ integrationBranch,
232
+ integrationHeadSha,
233
+ appliedCommits: candidateCommits.length,
234
+ };
235
+ }
236
+
237
+ // 9. Explicit finalization: merge integration branch into targetRef in primary repo
238
+ // First remove the temporary worktree so git doesn't hold locks
239
+ await execGit(["worktree", "remove", "--force", worktreeDir], this.repoRoot);
240
+ worktreeDir = null;
241
+
242
+ if (mode === "squash") {
243
+ await execGit(["merge", "--squash", integrationBranch], this.repoRoot);
244
+ await execGit(["commit", "-m", squashCommitMessage], this.repoRoot);
245
+ } else {
246
+ await execGit(["merge", "--ff-only", integrationBranch], this.repoRoot);
247
+ }
248
+
249
+ const finalSha = await this.resolveRef(targetRef);
250
+
251
+ // Safe cleanup of temporary integration branch
252
+ await this.safeCleanup(integrationBranch);
253
+ integrationBranch = null;
254
+
255
+ return {
256
+ status: "MERGED",
257
+ targetRef,
258
+ baseSha,
259
+ finalSha,
260
+ mode,
261
+ appliedCommits: candidateCommits.length,
262
+ };
263
+ } finally {
264
+ // Guaranteed cleanup on exception or exit
265
+ if (worktreeDir) {
266
+ try {
267
+ await execGit(["worktree", "remove", "--force", worktreeDir], this.repoRoot);
268
+ } catch {}
269
+ }
270
+ if (integrationBranch && finalize) {
271
+ try {
272
+ await this.safeCleanup(integrationBranch);
273
+ } catch {}
274
+ }
275
+ this.lock.release();
276
+ }
277
+ }
278
+
279
+ async _removeWorktreeAndBranch(worktreeDir, branchName) {
280
+ if (worktreeDir) {
281
+ try {
282
+ await execGit(["worktree", "remove", "--force", worktreeDir], this.repoRoot);
283
+ } catch {}
284
+ }
285
+ if (branchName) {
286
+ try {
287
+ await this.safeCleanup(branchName);
288
+ } catch {}
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Safely deletes a temporary integration branch.
294
+ * Enforces strict safety: only branches with prefix "ctx-int-" can be deleted.
295
+ *
296
+ * @param {string} branchName
297
+ */
298
+ async safeCleanup(branchName) {
299
+ if (!branchName || !branchName.startsWith("ctx-int-")) {
300
+ throw new Error(`Unsafe branch cleanup blocked: branch "${branchName}" is not a temporary integration branch.`);
301
+ }
302
+
303
+ try {
304
+ await execGit(["branch", "-D", branchName], this.repoRoot);
305
+ return true;
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+ }
311
+
312
+ module.exports = {
313
+ TransactionalGitPipeline,
314
+ execGit,
315
+ };