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.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/bin/mcp.mjs +61 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +192 -0
- package/dist/agents/aider.js.map +1 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +236 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +203 -0
- package/dist/agents/codex.js.map +1 -0
- package/dist/agents/direct-llm.d.ts +22 -0
- package/dist/agents/direct-llm.js +254 -0
- package/dist/agents/direct-llm.js.map +1 -0
- package/dist/agents/mock.d.ts +10 -0
- package/dist/agents/mock.js +170 -0
- package/dist/agents/mock.js.map +1 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +631 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/agents/provider.js.map +1 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +268 -0
- package/dist/compression/compressor.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/contextos/loader.d.ts +46 -0
- package/dist/contextos/loader.js +120 -0
- package/dist/contextos/loader.js.map +1 -0
- package/dist/contextos/selector.d.ts +46 -0
- package/dist/contextos/selector.js +80 -0
- package/dist/contextos/selector.js.map +1 -0
- package/dist/core/action-dispatcher.d.ts +15 -0
- package/dist/core/action-dispatcher.js +28 -0
- package/dist/core/action-dispatcher.js.map +1 -0
- package/dist/core/action-schema.d.ts +72 -0
- package/dist/core/action-schema.js +86 -0
- package/dist/core/action-schema.js.map +1 -0
- package/dist/core/repl-interface.d.ts +48 -0
- package/dist/core/repl-interface.js +5 -0
- package/dist/core/repl-interface.js.map +1 -0
- package/dist/core/repl.d.ts +59 -0
- package/dist/core/repl.js +353 -0
- package/dist/core/repl.js.map +1 -0
- package/dist/core/rlm.d.ts +65 -0
- package/dist/core/rlm.js +564 -0
- package/dist/core/rlm.js.map +1 -0
- package/dist/core/runtime.py +470 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.js +19 -0
- package/dist/core/types.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/env.js.map +1 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.js +173 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/labs/cli.d.ts +15 -0
- package/dist/labs/cli.js +295 -0
- package/dist/labs/cli.js.map +1 -0
- package/dist/labs/episodic-memory.d.ts +132 -0
- package/dist/labs/episodic-memory.js +391 -0
- package/dist/labs/episodic-memory.js.map +1 -0
- package/dist/labs/interactive-swarm.d.ts +20 -0
- package/dist/labs/interactive-swarm.js +1173 -0
- package/dist/labs/interactive-swarm.js.map +1 -0
- package/dist/labs/interactive.d.ts +10 -0
- package/dist/labs/interactive.js +2099 -0
- package/dist/labs/interactive.js.map +1 -0
- package/dist/labs/ts-analyzer.d.ts +74 -0
- package/dist/labs/ts-analyzer.js +379 -0
- package/dist/labs/ts-analyzer.js.map +1 -0
- package/dist/labs/viewer.d.ts +12 -0
- package/dist/labs/viewer.js +1284 -0
- package/dist/labs/viewer.js.map +1 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +247 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.js +192775 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +117 -0
- package/dist/mcp/session.js +450 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/state.d.ts +42 -0
- package/dist/mcp/state.js +537 -0
- package/dist/mcp/state.js.map +1 -0
- package/dist/mcp/tools/contextos.d.ts +17 -0
- package/dist/mcp/tools/contextos.js +583 -0
- package/dist/mcp/tools/contextos.js.map +1 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +393 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/orchestration/reviewer-gate.d.ts +43 -0
- package/dist/orchestration/reviewer-gate.js +280 -0
- package/dist/orchestration/reviewer-gate.js.map +1 -0
- package/dist/orchestration/reviewer-prompt.d.ts +18 -0
- package/dist/orchestration/reviewer-prompt.js +67 -0
- package/dist/orchestration/reviewer-prompt.js.map +1 -0
- package/dist/orchestration/verification-runner.d.ts +53 -0
- package/dist/orchestration/verification-runner.js +352 -0
- package/dist/orchestration/verification-runner.js.map +1 -0
- package/dist/plugins/adapter.d.ts +11 -0
- package/dist/plugins/adapter.js +11 -0
- package/dist/plugins/adapter.js.map +1 -0
- package/dist/plugins/core/archive.d.ts +9 -0
- package/dist/plugins/core/archive.js +128 -0
- package/dist/plugins/core/archive.js.map +1 -0
- package/dist/plugins/core/digest.d.ts +11 -0
- package/dist/plugins/core/digest.js +70 -0
- package/dist/plugins/core/digest.js.map +1 -0
- package/dist/plugins/core/grants.d.ts +22 -0
- package/dist/plugins/core/grants.js +90 -0
- package/dist/plugins/core/grants.js.map +1 -0
- package/dist/plugins/core/index.d.ts +10 -0
- package/dist/plugins/core/index.js +11 -0
- package/dist/plugins/core/index.js.map +1 -0
- package/dist/plugins/core/pinning.d.ts +15 -0
- package/dist/plugins/core/pinning.js +86 -0
- package/dist/plugins/core/pinning.js.map +1 -0
- package/dist/plugins/core/types.d.ts +105 -0
- package/dist/plugins/core/types.js +22 -0
- package/dist/plugins/core/types.js.map +1 -0
- package/dist/plugins/core/updater.d.ts +16 -0
- package/dist/plugins/core/updater.js +64 -0
- package/dist/plugins/core/updater.js.map +1 -0
- package/dist/plugins/loader.d.ts +65 -0
- package/dist/plugins/loader.js +110 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/prompts/orchestrator.d.ts +6 -0
- package/dist/prompts/orchestrator.js +114 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/routing/model-resolver.d.ts +27 -0
- package/dist/routing/model-resolver.js +265 -0
- package/dist/routing/model-resolver.js.map +1 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/routing/model-router.js.map +1 -0
- package/dist/runtime/attestations.cjs +260 -0
- package/dist/runtime/reviewer-pipeline.cjs +319 -0
- package/dist/runtime/sandbox.cjs +468 -0
- package/dist/runtime/state-machine.cjs +885 -0
- package/dist/runtime/thread-store.cjs +175 -0
- package/dist/runtime/transactional-git.cjs +315 -0
- package/dist/runtime/verification-pipeline.cjs +435 -0
- package/dist/sandbox/execution.d.ts +165 -0
- package/dist/sandbox/execution.js +497 -0
- package/dist/sandbox/execution.js.map +1 -0
- package/dist/security/file-policy.d.ts +22 -0
- package/dist/security/file-policy.js +53 -0
- package/dist/security/file-policy.js.map +1 -0
- package/dist/security/repository-boundary.d.ts +21 -0
- package/dist/security/repository-boundary.js +119 -0
- package/dist/security/repository-boundary.js.map +1 -0
- package/dist/security/secret-filter.d.ts +36 -0
- package/dist/security/secret-filter.js +149 -0
- package/dist/security/secret-filter.js.map +1 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +624 -0
- package/dist/swarm.js.map +1 -0
- package/dist/threads/cache.d.ts +64 -0
- package/dist/threads/cache.js +241 -0
- package/dist/threads/cache.js.map +1 -0
- package/dist/threads/manager.d.ts +94 -0
- package/dist/threads/manager.js +914 -0
- package/dist/threads/manager.js.map +1 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +135 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/log.js.map +1 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +755 -0
- package/dist/ui/onboarding.js.map +1 -0
- package/dist/ui/run-log.d.ts +50 -0
- package/dist/ui/run-log.js +60 -0
- package/dist/ui/run-log.js.map +1 -0
- package/dist/ui/spinner.d.ts +41 -0
- package/dist/ui/spinner.js +172 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/streaming-feed.d.ts +72 -0
- package/dist/ui/streaming-feed.js +386 -0
- package/dist/ui/streaming-feed.js.map +1 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/summary.js.map +1 -0
- package/dist/ui/text-input.d.ts +19 -0
- package/dist/ui/text-input.js +266 -0
- package/dist/ui/text-input.js.map +1 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/command-exists.d.ts +11 -0
- package/dist/utils/command-exists.js +41 -0
- package/dist/utils/command-exists.js.map +1 -0
- package/dist/utils/process-security.d.ts +19 -0
- package/dist/utils/process-security.js +90 -0
- package/dist/utils/process-security.js.map +1 -0
- package/dist/utils/rolling-buffer.d.ts +16 -0
- package/dist/utils/rolling-buffer.js +30 -0
- package/dist/utils/rolling-buffer.js.map +1 -0
- package/dist/utils/safe-spawn.d.ts +13 -0
- package/dist/utils/safe-spawn.js +49 -0
- package/dist/utils/safe-spawn.js.map +1 -0
- package/dist/worktree/manager.d.ts +69 -0
- package/dist/worktree/manager.js +593 -0
- package/dist/worktree/manager.js.map +1 -0
- package/dist/worktree/merge.d.ts +58 -0
- package/dist/worktree/merge.js +389 -0
- package/dist/worktree/merge.js.map +1 -0
- 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
|
+
};
|