thumbgate 1.34.3 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,220 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+
7
+ /**
8
+ * GitFastCache — High-performance shadow git metadata cache.
9
+ * Inspired by Cursor's Continuity/Spokes architecture (Aug 2026).
10
+ *
11
+ * Replaces recurring synchronous git subprocess calls (git rev-parse, git status, git diff)
12
+ * during PreToolUse evaluation with mtime-invalidated in-memory cache lookups (<0.2ms).
13
+ */
14
+
15
+ const FIXED_GIT_BIN_CANDIDATES = [
16
+ '/usr/bin/git',
17
+ '/opt/homebrew/bin/git',
18
+ '/usr/local/bin/git',
19
+ ];
20
+
21
+ function canExecuteAbsoluteBinary(candidate) {
22
+ const normalized = String(candidate || '').trim();
23
+ if (!normalized || !normalized.includes(path.sep)) return false;
24
+ try {
25
+ fs.accessSync(normalized, fs.constants.X_OK);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolve an absolute git binary path (no PATH-only spawn).
34
+ * Mirrors scripts/operational-integrity.js resolveGitBinary fixed-candidate pattern.
35
+ * @param {Object} [options]
36
+ * @returns {string|null}
37
+ */
38
+ function resolveGitBinary(options = {}) {
39
+ const env = options.env || process.env;
40
+ const candidates = Array.isArray(options.candidates) && options.candidates.length > 0
41
+ ? options.candidates
42
+ : [env.THUMBGATE_GIT_BIN, ...FIXED_GIT_BIN_CANDIDATES];
43
+
44
+ for (const candidate of candidates) {
45
+ if (!canExecuteAbsoluteBinary(candidate)) continue;
46
+ return String(candidate).trim();
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ const GIT_BIN = resolveGitBinary();
53
+
54
+ class GitFastCache {
55
+ constructor(options = {}) {
56
+ this.ttlMs = options.ttlMs || 3000;
57
+ this.cache = new Map(); // repoRoot -> CachedRepoState
58
+ this.gitBin = options.gitBin || GIT_BIN;
59
+ }
60
+
61
+ execGit(args, cwd) {
62
+ if (!this.gitBin) {
63
+ throw new Error('Git executable is unavailable in this runtime');
64
+ }
65
+ return execFileSync(this.gitBin, args, {
66
+ cwd,
67
+ encoding: 'utf8',
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ });
70
+ }
71
+
72
+ findRepoRoot(startDir = process.cwd()) {
73
+ let current = path.resolve(startDir);
74
+ try {
75
+ current = fs.realpathSync(current);
76
+ } catch {
77
+ // fallback
78
+ }
79
+ while (current !== path.dirname(current)) {
80
+ const gitDir = path.join(current, '.git');
81
+ if (fs.existsSync(gitDir)) {
82
+ return current;
83
+ }
84
+ current = path.dirname(current);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ getGitMetaDir(repoRoot) {
90
+ const gitPath = path.join(repoRoot, '.git');
91
+ if (!fs.existsSync(gitPath)) return null;
92
+
93
+ try {
94
+ const stat = fs.statSync(gitPath);
95
+ if (stat.isDirectory()) return fs.realpathSync(gitPath);
96
+ if (stat.isFile()) {
97
+ // Handle git worktree pointer file: "gitdir: /path/to/worktree"
98
+ const content = fs.readFileSync(gitPath, 'utf8').trim();
99
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
100
+ if (match?.[1]) {
101
+ return fs.realpathSync(path.resolve(repoRoot, match[1].trim()));
102
+ }
103
+ }
104
+ } catch {
105
+ return null;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ getCacheFingerprint(gitDir) {
111
+ if (!gitDir || !fs.existsSync(gitDir)) return 'empty';
112
+ try {
113
+ const headFile = path.join(gitDir, 'HEAD');
114
+ const indexFile = path.join(gitDir, 'index');
115
+ const headMtime = fs.existsSync(headFile) ? fs.statSync(headFile).mtimeMs : 0;
116
+ const indexMtime = fs.existsSync(indexFile) ? fs.statSync(indexFile).mtimeMs : 0;
117
+ return `${headMtime}:${indexMtime}`;
118
+ } catch {
119
+ return 'unknown';
120
+ }
121
+ }
122
+
123
+ getRepoState(targetDir = process.cwd()) {
124
+ const repoRoot = this.findRepoRoot(targetDir);
125
+ if (!repoRoot) {
126
+ return {
127
+ isGitRepo: false,
128
+ repoRoot: null,
129
+ branch: null,
130
+ headSha: null,
131
+ isDirty: false,
132
+ stagedFiles: [],
133
+ lookupTimeMs: 0,
134
+ };
135
+ }
136
+
137
+ const start = process.hrtime.bigint();
138
+ const gitDir = this.getGitMetaDir(repoRoot);
139
+ const fingerprint = this.getCacheFingerprint(gitDir);
140
+ const now = Date.now();
141
+
142
+ const cached = this.cache.get(repoRoot);
143
+ if (cached && cached.fingerprint === fingerprint && (now - cached.cachedAt) < this.ttlMs) {
144
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
145
+ return { ...cached.state, lookupTimeMs: durationMs, cached: true };
146
+ }
147
+
148
+ // Refresh state from git inspection
149
+ let branch = 'unknown';
150
+ let headSha = null;
151
+
152
+ if (gitDir) {
153
+ try {
154
+ const headContent = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
155
+ if (headContent.startsWith('ref: refs/heads/')) {
156
+ branch = headContent.replace('ref: refs/heads/', '');
157
+ const refFile = path.join(gitDir, 'refs', 'heads', branch);
158
+ if (fs.existsSync(refFile)) {
159
+ headSha = fs.readFileSync(refFile, 'utf8').trim();
160
+ }
161
+ } else {
162
+ headSha = headContent;
163
+ }
164
+ } catch {
165
+ // fallback
166
+ }
167
+ }
168
+
169
+ if (!headSha) {
170
+ try {
171
+ headSha = this.execGit(['rev-parse', 'HEAD'], repoRoot).trim();
172
+ } catch {
173
+ headSha = null;
174
+ }
175
+ }
176
+
177
+ let stagedFiles = [];
178
+ let isDirty = false;
179
+ try {
180
+ const stagedOut = this.execGit(['diff', '--cached', '--name-only'], repoRoot);
181
+ stagedFiles = stagedOut.split('\n').map((s) => s.trim()).filter(Boolean);
182
+ const statusOut = this.execGit(['status', '--porcelain'], repoRoot);
183
+ isDirty = statusOut.trim().length > 0;
184
+ } catch {
185
+ // fallback
186
+ }
187
+
188
+ const state = {
189
+ isGitRepo: true,
190
+ repoRoot,
191
+ branch,
192
+ headSha,
193
+ isDirty,
194
+ stagedFiles,
195
+ };
196
+
197
+ const finalFingerprint = this.getCacheFingerprint(gitDir);
198
+ this.cache.set(repoRoot, {
199
+ fingerprint: finalFingerprint,
200
+ cachedAt: now,
201
+ state,
202
+ });
203
+
204
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
205
+ return { ...state, lookupTimeMs: durationMs, cached: false };
206
+ }
207
+
208
+ clear() {
209
+ this.cache.clear();
210
+ }
211
+ }
212
+
213
+ const defaultCache = new GitFastCache();
214
+
215
+ module.exports = {
216
+ GitFastCache,
217
+ defaultCache,
218
+ resolveGitBinary,
219
+ FIXED_GIT_BIN_CANDIDATES,
220
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Write-Ahead Log (WAL) & Optimistic Concurrency Engine for Git Mutations
3
+ *
4
+ * Inspired by Cursor Continuity's WAL-first architecture (https://cursor.com/blog/git-at-any-scale).
5
+ * Provides an append-only, tamper-evident WAL and atomic Compare-And-Swap (CAS) index
6
+ * to prevent multi-agent race conditions during file mutations and tool calls.
7
+ *
8
+ * @module git-wal-sync
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const crypto = require('node:crypto');
14
+
15
+ /**
16
+ * Computes a SHA-256 digest of mutation payload.
17
+ *
18
+ * @param {object} payload - Mutation data
19
+ * @returns {string} Hex SHA-256
20
+ */
21
+ function computeChecksum(payload) {
22
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
23
+ }
24
+
25
+ /**
26
+ * Initializes a WAL session directory for a repository.
27
+ *
28
+ * @param {string} repoRoot - Path to repository root
29
+ * @returns {object} WAL session metadata
30
+ */
31
+ function initWalSession(repoRoot) {
32
+ const walDir = path.join(repoRoot, '.thumbgate', 'wal');
33
+ fs.mkdirSync(walDir, { recursive: true });
34
+
35
+ const indexPath = path.join(walDir, 'gitwal-index.json');
36
+ let index = { seq: 0, etag: 'e0', lastUpdated: Date.now() };
37
+
38
+ if (fs.existsSync(indexPath)) {
39
+ try {
40
+ index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
41
+ } catch {
42
+ index = { seq: 0, etag: 'e0', lastUpdated: Date.now() };
43
+ }
44
+ } else {
45
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
46
+ }
47
+
48
+ return {
49
+ repoRoot,
50
+ walDir,
51
+ indexPath,
52
+ index,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Appends an immutable WAL entry to the log.
58
+ *
59
+ * @param {object} walSession - Active WAL session
60
+ * @param {object} mutation - Proposed tool mutation (toolName, filePath, diff, etc.)
61
+ * @returns {object} Appended entry with assigned sequence number and checksum
62
+ */
63
+ function appendWalEntry(walSession, mutation) {
64
+ const seq = (walSession.index.seq || 0) + 1;
65
+ const entry = {
66
+ seq,
67
+ timestamp: Date.now(),
68
+ toolName: mutation.toolName || 'unknown',
69
+ filePath: mutation.filePath || null,
70
+ operation: mutation.operation || 'modify',
71
+ payloadChecksum: computeChecksum(mutation),
72
+ actor: mutation.actor || process.env.THUMBGATE_SESSION_AGENT || 'local-agent',
73
+ };
74
+
75
+ const entryFile = path.join(walSession.walDir, `entry-${String(seq).padStart(6, '0')}.wal.json`);
76
+ fs.writeFileSync(entryFile, JSON.stringify(entry, null, 2), 'utf8');
77
+
78
+ return entry;
79
+ }
80
+
81
+ /**
82
+ * Performs an atomic Compare-And-Swap (CAS) commit of the WAL index.
83
+ *
84
+ * @param {object} walSession - Active WAL session
85
+ * @param {number} expectedSeq - Expected prior sequence number
86
+ * @param {object} newEntry - The newly appended WAL entry
87
+ * @returns {{ ok: boolean, currentSeq: number, etag: string, error?: string }}
88
+ */
89
+ function casCommitIndex(walSession, expectedSeq, newEntry) {
90
+ let currentIndex = { seq: 0, etag: 'e0' };
91
+ if (fs.existsSync(walSession.indexPath)) {
92
+ try {
93
+ currentIndex = JSON.parse(fs.readFileSync(walSession.indexPath, 'utf8'));
94
+ } catch {
95
+ currentIndex = { seq: 0, etag: 'e0' };
96
+ }
97
+ }
98
+
99
+ if (currentIndex.seq !== expectedSeq) {
100
+ return {
101
+ ok: false,
102
+ currentSeq: currentIndex.seq,
103
+ etag: currentIndex.etag,
104
+ error: `CAS Precondition Failed: expected seq ${expectedSeq} but found ${currentIndex.seq}`,
105
+ };
106
+ }
107
+
108
+ const updatedIndex = {
109
+ seq: newEntry.seq,
110
+ etag: `e${newEntry.seq}`,
111
+ lastUpdated: Date.now(),
112
+ lastChecksum: newEntry.payloadChecksum,
113
+ };
114
+
115
+ fs.writeFileSync(walSession.indexPath, JSON.stringify(updatedIndex, null, 2), 'utf8');
116
+ walSession.index = updatedIndex;
117
+
118
+ return {
119
+ ok: true,
120
+ currentSeq: updatedIndex.seq,
121
+ etag: updatedIndex.etag,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Reads all WAL entries up to the current sequence number.
127
+ *
128
+ * @param {object} walSession - Active WAL session
129
+ * @returns {object[]} List of valid WAL entries
130
+ */
131
+ function readWalEntries(walSession) {
132
+ if (!fs.existsSync(walSession.walDir)) return [];
133
+
134
+ const files = fs.readdirSync(walSession.walDir)
135
+ .filter((f) => f.startsWith('entry-') && f.endsWith('.wal.json'))
136
+ .sort((a, b) => a.localeCompare(b));
137
+
138
+ const entries = [];
139
+ for (const f of files) {
140
+ try {
141
+ const content = fs.readFileSync(path.join(walSession.walDir, f), 'utf8');
142
+ entries.push(JSON.parse(content));
143
+ } catch {
144
+ // skip corrupted entries
145
+ }
146
+ }
147
+ return entries;
148
+ }
149
+
150
+ module.exports = {
151
+ computeChecksum,
152
+ initWalSession,
153
+ appendWalEntry,
154
+ casCommitIndex,
155
+ readWalEntries,
156
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+
6
+ /**
7
+ * HashAnchoredEditGuard — Pre-edit concurrency & content collision guard.
8
+ * Inspired by Dirac's Hash-Anchored Edit Engine (Aug 2026).
9
+ *
10
+ * Prevents race condition regressions where concurrent agent sessions or human operators
11
+ * modify code lines while an agent is generating an edit.
12
+ */
13
+
14
+ function computeChunkHash(content) {
15
+ return crypto.createHash('sha256').update(String(content || '')).digest('hex').slice(0, 16);
16
+ }
17
+
18
+ function evaluateHashAnchoredEdit(request = {}) {
19
+ const { filePath, targetContent, expectedHash, startLine, endLine } = request;
20
+
21
+ if (!filePath || !fs.existsSync(filePath)) {
22
+ return {
23
+ allowed: false,
24
+ reason: 'target_file_not_found',
25
+ message: `File does not exist: ${filePath}`,
26
+ };
27
+ }
28
+
29
+ let fileContent = '';
30
+ try {
31
+ fileContent = fs.readFileSync(filePath, 'utf8');
32
+ } catch (err) {
33
+ return {
34
+ allowed: false,
35
+ reason: 'file_read_error',
36
+ message: `Failed to read ${filePath}: ${err.message}`,
37
+ };
38
+ }
39
+
40
+ const lines = fileContent.split('\n');
41
+ let currentSlice = '';
42
+
43
+ if (typeof startLine === 'number' && typeof endLine === 'number' && startLine >= 1 && endLine <= lines.length) {
44
+ currentSlice = lines.slice(startLine - 1, endLine).join('\n');
45
+ } else if (targetContent) {
46
+ // Exact target content search
47
+ if (!fileContent.includes(targetContent)) {
48
+ return {
49
+ allowed: false,
50
+ reason: 'target_content_drift',
51
+ message: `Target code block was modified or does not match current file contents.`,
52
+ currentFileHash: computeChunkHash(fileContent),
53
+ };
54
+ }
55
+ currentSlice = targetContent;
56
+ }
57
+
58
+ const actualHash = computeChunkHash(currentSlice);
59
+
60
+ if (expectedHash && expectedHash !== actualHash) {
61
+ return {
62
+ allowed: false,
63
+ reason: 'hash_anchor_mismatch',
64
+ message: `Hash anchor drift detected: expected ${expectedHash}, found ${actualHash}. Another session or user modified this region.`,
65
+ expectedHash,
66
+ actualHash,
67
+ };
68
+ }
69
+
70
+ return {
71
+ allowed: true,
72
+ reason: 'anchor_verified',
73
+ chunkHash: actualHash,
74
+ lineCount: lines.length,
75
+ timestamp: new Date().toISOString(),
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ computeChunkHash,
81
+ evaluateHashAnchoredEdit,
82
+ };