brainclaw 0.19.2

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 (128) hide show
  1. package/LICENSE +74 -0
  2. package/README.md +226 -0
  3. package/dist/cli.js +1037 -0
  4. package/dist/commands/accept.js +149 -0
  5. package/dist/commands/adapter-openclaw-import.js +75 -0
  6. package/dist/commands/add-step.js +35 -0
  7. package/dist/commands/agent-board.js +106 -0
  8. package/dist/commands/audit.js +35 -0
  9. package/dist/commands/bootstrap.js +34 -0
  10. package/dist/commands/capability.js +104 -0
  11. package/dist/commands/changes.js +112 -0
  12. package/dist/commands/check-constraints.js +63 -0
  13. package/dist/commands/claim-resource.js +54 -0
  14. package/dist/commands/claim.js +92 -0
  15. package/dist/commands/complete-step.js +34 -0
  16. package/dist/commands/constraint.js +44 -0
  17. package/dist/commands/context-diff.js +32 -0
  18. package/dist/commands/context.js +63 -0
  19. package/dist/commands/decision.js +45 -0
  20. package/dist/commands/delete-plan.js +20 -0
  21. package/dist/commands/diff.js +99 -0
  22. package/dist/commands/doctor.js +1275 -0
  23. package/dist/commands/enable-agent.js +63 -0
  24. package/dist/commands/env.js +46 -0
  25. package/dist/commands/estimation-report.js +167 -0
  26. package/dist/commands/explore.js +47 -0
  27. package/dist/commands/export.js +381 -0
  28. package/dist/commands/handoff.js +63 -0
  29. package/dist/commands/history.js +22 -0
  30. package/dist/commands/hooks.js +123 -0
  31. package/dist/commands/init.js +356 -0
  32. package/dist/commands/install-hooks.js +115 -0
  33. package/dist/commands/instruction.js +56 -0
  34. package/dist/commands/list-agents.js +44 -0
  35. package/dist/commands/list-claims.js +45 -0
  36. package/dist/commands/list-instructions.js +50 -0
  37. package/dist/commands/list-plans.js +48 -0
  38. package/dist/commands/mcp-worker.js +12 -0
  39. package/dist/commands/mcp.js +2272 -0
  40. package/dist/commands/memory.js +283 -0
  41. package/dist/commands/metrics.js +175 -0
  42. package/dist/commands/plan-resource.js +62 -0
  43. package/dist/commands/plan.js +76 -0
  44. package/dist/commands/prune-candidates.js +36 -0
  45. package/dist/commands/prune.js +48 -0
  46. package/dist/commands/pull.js +25 -0
  47. package/dist/commands/push.js +28 -0
  48. package/dist/commands/rebuild.js +14 -0
  49. package/dist/commands/reflect-runtime-note.js +74 -0
  50. package/dist/commands/reflect.js +286 -0
  51. package/dist/commands/register-agent.js +29 -0
  52. package/dist/commands/reject.js +52 -0
  53. package/dist/commands/release-claim.js +41 -0
  54. package/dist/commands/release-claims.js +67 -0
  55. package/dist/commands/review.js +242 -0
  56. package/dist/commands/rollback.js +156 -0
  57. package/dist/commands/runtime-note.js +144 -0
  58. package/dist/commands/runtime-status.js +49 -0
  59. package/dist/commands/search.js +36 -0
  60. package/dist/commands/session-end.js +187 -0
  61. package/dist/commands/session-start.js +147 -0
  62. package/dist/commands/set-trust.js +92 -0
  63. package/dist/commands/setup.js +446 -0
  64. package/dist/commands/show-candidate.js +31 -0
  65. package/dist/commands/star-candidate.js +28 -0
  66. package/dist/commands/status.js +133 -0
  67. package/dist/commands/sync.js +159 -0
  68. package/dist/commands/tool.js +126 -0
  69. package/dist/commands/trap.js +74 -0
  70. package/dist/commands/update-handoff.js +23 -0
  71. package/dist/commands/update-plan.js +37 -0
  72. package/dist/commands/upgrade.js +382 -0
  73. package/dist/commands/use-candidate.js +35 -0
  74. package/dist/commands/version.js +96 -0
  75. package/dist/commands/watch.js +215 -0
  76. package/dist/commands/whoami.js +104 -0
  77. package/dist/core/agent-context.js +340 -0
  78. package/dist/core/agent-files.js +874 -0
  79. package/dist/core/agent-integrations.js +135 -0
  80. package/dist/core/agent-inventory.js +401 -0
  81. package/dist/core/agent-registry.js +420 -0
  82. package/dist/core/ai-agent-detection.js +140 -0
  83. package/dist/core/audit.js +85 -0
  84. package/dist/core/bootstrap.js +658 -0
  85. package/dist/core/brainclaw-version.js +433 -0
  86. package/dist/core/candidates.js +137 -0
  87. package/dist/core/circuit-breaker.js +118 -0
  88. package/dist/core/claims.js +72 -0
  89. package/dist/core/config.js +86 -0
  90. package/dist/core/context-diff.js +122 -0
  91. package/dist/core/context.js +1212 -0
  92. package/dist/core/contradictions.js +270 -0
  93. package/dist/core/coordination.js +86 -0
  94. package/dist/core/cross-project.js +99 -0
  95. package/dist/core/duplicates.js +72 -0
  96. package/dist/core/event-log.js +152 -0
  97. package/dist/core/events.js +56 -0
  98. package/dist/core/execution-context.js +204 -0
  99. package/dist/core/freshness.js +87 -0
  100. package/dist/core/global-registry.js +182 -0
  101. package/dist/core/host.js +10 -0
  102. package/dist/core/identity.js +151 -0
  103. package/dist/core/ids.js +56 -0
  104. package/dist/core/input-validation.js +81 -0
  105. package/dist/core/instructions.js +117 -0
  106. package/dist/core/io.js +191 -0
  107. package/dist/core/json-store.js +63 -0
  108. package/dist/core/lifecycle.js +45 -0
  109. package/dist/core/lock.js +129 -0
  110. package/dist/core/logger.js +49 -0
  111. package/dist/core/machine-profile.js +332 -0
  112. package/dist/core/markdown.js +120 -0
  113. package/dist/core/memory-git.js +133 -0
  114. package/dist/core/migration.js +247 -0
  115. package/dist/core/project-registry.js +64 -0
  116. package/dist/core/reflection-safety.js +21 -0
  117. package/dist/core/repo-analysis.js +133 -0
  118. package/dist/core/reputation.js +409 -0
  119. package/dist/core/runtime.js +134 -0
  120. package/dist/core/schema.js +580 -0
  121. package/dist/core/search.js +115 -0
  122. package/dist/core/security.js +66 -0
  123. package/dist/core/setup-state.js +50 -0
  124. package/dist/core/state.js +83 -0
  125. package/dist/core/store-resolution.js +119 -0
  126. package/dist/core/sync-remote.js +83 -0
  127. package/dist/core/traps.js +86 -0
  128. package/package.json +60 -0
@@ -0,0 +1,270 @@
1
+ const NEGATION_PAIRS = [
2
+ { positive: 'must', negative: 'must not', kind: 'negation_pair' },
3
+ { positive: 'must', negative: 'must never', kind: 'negation_pair' },
4
+ { positive: 'should', negative: 'should not', kind: 'negation_pair' },
5
+ { positive: 'should', negative: 'should never', kind: 'negation_pair' },
6
+ { positive: 'always', negative: 'never', kind: 'negation_pair' },
7
+ { positive: 'enable', negative: 'disable', kind: 'toggle_pair' },
8
+ { positive: 'enabled', negative: 'disabled', kind: 'toggle_pair' },
9
+ { positive: 'use', negative: 'do not use', kind: 'usage_pair' },
10
+ { positive: 'use', negative: 'avoid', kind: 'usage_pair' },
11
+ { positive: 'allow', negative: 'block', kind: 'policy_pair' },
12
+ { positive: 'allow', negative: 'deny', kind: 'policy_pair' },
13
+ { positive: 'required', negative: 'forbidden', kind: 'policy_pair' },
14
+ ];
15
+ const STOP_WORDS = new Set([
16
+ 'a',
17
+ 'an',
18
+ 'and',
19
+ 'are',
20
+ 'be',
21
+ 'by',
22
+ 'for',
23
+ 'from',
24
+ 'in',
25
+ 'is',
26
+ 'it',
27
+ 'of',
28
+ 'on',
29
+ 'or',
30
+ 'the',
31
+ 'to',
32
+ 'with',
33
+ 'must',
34
+ 'must not',
35
+ 'must never',
36
+ 'should',
37
+ 'should not',
38
+ 'should never',
39
+ 'always',
40
+ 'never',
41
+ 'enable',
42
+ 'disable',
43
+ 'enabled',
44
+ 'disabled',
45
+ 'use',
46
+ 'do not use',
47
+ 'avoid',
48
+ 'allow',
49
+ 'block',
50
+ 'deny',
51
+ 'required',
52
+ 'forbidden',
53
+ ]);
54
+ export function detectContradictions(state) {
55
+ const reports = [];
56
+ const constraints = state.active_constraints.filter((constraint) => constraint.status === 'active');
57
+ const decisions = state.recent_decisions;
58
+ comparePairs(constraints, 'constraints', reports);
59
+ comparePairs(decisions, 'decisions', reports);
60
+ compareCrossSection(constraints, decisions, reports);
61
+ return sortContradictions(reports);
62
+ }
63
+ export function detectNewItemContradictions(newText, newTags, newPaths, state, newProjectId) {
64
+ const reports = [];
65
+ const candidate = {
66
+ id: 'new_item',
67
+ text: newText,
68
+ tags: newTags,
69
+ related_paths: newPaths,
70
+ project_id: newProjectId,
71
+ };
72
+ const check = (existing, section) => {
73
+ const signal = detectComparableContradiction(candidate, existing);
74
+ if (!signal) {
75
+ return;
76
+ }
77
+ reports.push({
78
+ item_id: candidate.id,
79
+ conflicts_with: existing.id,
80
+ reason: signal.reason,
81
+ section,
82
+ score: signal.score,
83
+ severity: classifySeverity(signal.score),
84
+ kind: signal.kind,
85
+ });
86
+ };
87
+ for (const constraint of state.active_constraints.filter((item) => item.status === 'active')) {
88
+ check(constraint, 'constraints');
89
+ }
90
+ for (const decision of state.recent_decisions) {
91
+ check(decision, 'decisions');
92
+ }
93
+ return sortContradictions(reports);
94
+ }
95
+ export function summarizeContradictions(reports, maxItems = 2) {
96
+ if (reports.length === 0) {
97
+ return undefined;
98
+ }
99
+ const parts = reports.slice(0, maxItems).map((report) => `[${report.conflicts_with}] ${report.reason}`);
100
+ const suffix = reports.length > maxItems ? ` (+${reports.length - maxItems} more)` : '';
101
+ return `${reports.length} contradiction(s): ${parts.join('; ')}${suffix}`;
102
+ }
103
+ export function hasBlockingContradictions(reports) {
104
+ return reports.some((report) => report.severity === 'medium' || report.severity === 'high');
105
+ }
106
+ function comparePairs(items, section, reports) {
107
+ for (let index = 0; index < items.length; index++) {
108
+ for (let offset = index + 1; offset < items.length; offset++) {
109
+ const left = items[index];
110
+ const right = items[offset];
111
+ const signal = detectComparableContradiction(left, right);
112
+ if (!signal) {
113
+ continue;
114
+ }
115
+ reports.push({
116
+ item_id: left.id,
117
+ conflicts_with: right.id,
118
+ reason: signal.reason,
119
+ section,
120
+ score: signal.score,
121
+ severity: classifySeverity(signal.score),
122
+ kind: signal.kind,
123
+ });
124
+ }
125
+ }
126
+ }
127
+ function compareCrossSection(constraints, decisions, reports) {
128
+ for (const constraint of constraints) {
129
+ for (const decision of decisions) {
130
+ const signal = detectComparableContradiction(constraint, decision);
131
+ if (!signal) {
132
+ continue;
133
+ }
134
+ reports.push({
135
+ item_id: constraint.id,
136
+ conflicts_with: decision.id,
137
+ reason: signal.reason,
138
+ section: 'constraints_vs_decisions',
139
+ score: signal.score,
140
+ severity: classifySeverity(signal.score),
141
+ kind: signal.kind,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ function detectComparableContradiction(left, right) {
147
+ const domain = scoreDomainOverlap(left, right);
148
+ if (domain <= 0) {
149
+ return undefined;
150
+ }
151
+ const lexical = detectLexicalConflict(left.text, right.text);
152
+ if (!lexical) {
153
+ return undefined;
154
+ }
155
+ return {
156
+ kind: lexical.kind,
157
+ reason: lexical.reason,
158
+ score: lexical.score + domain,
159
+ };
160
+ }
161
+ function detectLexicalConflict(leftText, rightText) {
162
+ const normalizedLeft = normalize(leftText);
163
+ const normalizedRight = normalize(rightText);
164
+ const tokensLeft = tokenSet(normalizedLeft);
165
+ const tokensRight = tokenSet(normalizedRight);
166
+ const shared = sharedTokens(tokensLeft, tokensRight);
167
+ const jaccard = jaccardIndex(tokensLeft, tokensRight);
168
+ for (const pair of NEGATION_PAIRS) {
169
+ const leftHasPositive = normalizedLeft.includes(pair.positive);
170
+ const leftHasNegative = normalizedLeft.includes(pair.negative);
171
+ const rightHasPositive = normalizedRight.includes(pair.positive);
172
+ const rightHasNegative = normalizedRight.includes(pair.negative);
173
+ if (!((leftHasPositive && rightHasNegative) || (leftHasNegative && rightHasPositive))) {
174
+ continue;
175
+ }
176
+ if (shared.length === 0 && jaccard < 0.15) {
177
+ return undefined;
178
+ }
179
+ const overlapBoost = Math.min(shared.length, 3);
180
+ const score = 5 + overlapBoost + Math.round(jaccard * 4);
181
+ const overlapSummary = shared.slice(0, 3).join(', ');
182
+ const overlapText = overlapSummary ? ` around ${overlapSummary}` : '';
183
+ return {
184
+ kind: pair.kind,
185
+ reason: `"${pair.positive}" vs "${pair.negative}"${overlapText}`,
186
+ score,
187
+ };
188
+ }
189
+ return undefined;
190
+ }
191
+ function normalize(text) {
192
+ return text
193
+ .toLowerCase()
194
+ .replace(/[^a-z0-9\s/_-]/g, ' ')
195
+ .replace(/\s+/g, ' ')
196
+ .trim();
197
+ }
198
+ function tokenSet(normalized) {
199
+ const tokens = normalized
200
+ .split(/[^a-z0-9]+/)
201
+ .map((token) => token.trim())
202
+ .filter((token) => token.length > 2 && !STOP_WORDS.has(token));
203
+ return new Set(tokens);
204
+ }
205
+ function sharedTokens(left, right) {
206
+ const result = [];
207
+ for (const token of left) {
208
+ if (right.has(token)) {
209
+ result.push(token);
210
+ }
211
+ }
212
+ return result.sort();
213
+ }
214
+ function jaccardIndex(left, right) {
215
+ if (left.size === 0 || right.size === 0) {
216
+ return 0;
217
+ }
218
+ const intersection = sharedTokens(left, right).length;
219
+ const union = new Set([...left, ...right]).size;
220
+ return union === 0 ? 0 : intersection / union;
221
+ }
222
+ function scoreDomainOverlap(left, right) {
223
+ let score = 0;
224
+ if (pathsOverlap(left.related_paths, right.related_paths)) {
225
+ score += 3;
226
+ }
227
+ if (tagsOverlap(left.tags, right.tags)) {
228
+ score += 2;
229
+ }
230
+ if (left.project_id && right.project_id && left.project_id === right.project_id) {
231
+ score += 1;
232
+ }
233
+ return score;
234
+ }
235
+ function classifySeverity(score) {
236
+ if (score >= 10) {
237
+ return 'high';
238
+ }
239
+ if (score >= 7) {
240
+ return 'medium';
241
+ }
242
+ return 'low';
243
+ }
244
+ function pathsOverlap(left, right) {
245
+ if (!left || !right || left.length === 0 || right.length === 0) {
246
+ return false;
247
+ }
248
+ return left.some((leftPath) => right.some((rightPath) => leftPath.startsWith(rightPath) || rightPath.startsWith(leftPath) || leftPath === rightPath));
249
+ }
250
+ function tagsOverlap(left, right) {
251
+ return left.some((tag) => right.includes(tag));
252
+ }
253
+ function sortContradictions(reports) {
254
+ const severityRank = {
255
+ high: 3,
256
+ medium: 2,
257
+ low: 1,
258
+ };
259
+ return [...reports].sort((left, right) => {
260
+ const severityDelta = severityRank[right.severity] - severityRank[left.severity];
261
+ if (severityDelta !== 0) {
262
+ return severityDelta;
263
+ }
264
+ if (right.score !== left.score) {
265
+ return right.score - left.score;
266
+ }
267
+ return left.conflicts_with.localeCompare(right.conflicts_with);
268
+ });
269
+ }
270
+ //# sourceMappingURL=contradictions.js.map
@@ -0,0 +1,86 @@
1
+ import { findAgentIdentityByName, resolveAgentScope, resolveCurrentAgentIdentity } from './agent-registry.js';
2
+ import { loadConfig } from './config.js';
3
+ import { resolveCurrentHostId } from './host.js';
4
+ import { listClaims } from './claims.js';
5
+ import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
6
+ import { buildReputationSummary, findAgentReputationSummary } from './reputation.js';
7
+ import { listRuntimeNotes } from './runtime.js';
8
+ import { loadState, saveState } from './state.js';
9
+ export function buildCoordinationSnapshot(options = {}) {
10
+ const config = loadConfig(options.cwd);
11
+ const state = loadState(options.cwd);
12
+ const currentHost = resolveCurrentHostId();
13
+ const project = options.project ?? inferProjectFromTarget(options.target, config);
14
+ const agent = resolveAgentScope(options.agent, options.cwd);
15
+ const resolvedAgentIdentity = agent
16
+ ? (options.agent ? findAgentIdentityByName(agent, options.cwd) : resolveCurrentAgentIdentity(options.cwd))
17
+ : undefined;
18
+ const claims = listClaims(options.cwd).filter((claim) => claim.status === 'active');
19
+ const runtimeNotes = listRuntimeNotes({
20
+ agent,
21
+ hostId: options.host,
22
+ includeAllHosts: options.allHosts,
23
+ }, options.cwd);
24
+ const activePlans = state.plan_items.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
25
+ const openHandoffs = state.open_handoffs.filter((handoff) => handoff.status === 'open');
26
+ const instructions = resolveInstructions(loadInstructions(options.cwd), { project, agent });
27
+ const reputationSummary = options.includeReputation ? buildReputationSummary(options.cwd) : undefined;
28
+ const agentReputation = options.includeReputation && agent
29
+ ? findAgentReputationSummary(resolvedAgentIdentity?.agent_id ?? agent, options.cwd)
30
+ : undefined;
31
+ const filteredPlans = project
32
+ ? activePlans.filter((plan) => !plan.project || plan.project === project)
33
+ : activePlans;
34
+ const filteredClaims = project
35
+ ? claims.filter((claim) => !claim.project || claim.project === project)
36
+ : claims;
37
+ // perf.3: filter session lifecycle notes unless explicitly requested
38
+ const sessionMetaNoteTypes = new Set(['session_start', 'session_end']);
39
+ const visibleNotes = options.includeSessionMeta
40
+ ? runtimeNotes
41
+ : runtimeNotes.filter((note) => !sessionMetaNoteTypes.has(note.note_type ?? ''));
42
+ const sessionMetaHidden = runtimeNotes.length - visibleNotes.length;
43
+ const filteredNotes = project
44
+ ? visibleNotes.filter((note) => !note.project || note.project === project)
45
+ : visibleNotes;
46
+ // factor out handoff filter for reuse in auto-acknowledge
47
+ const filteredHandoffs = agent
48
+ ? openHandoffs.filter((h) => (!project || !h.project || h.project === project) && (h.to === agent || h.from === agent))
49
+ : (project ? openHandoffs.filter((h) => !h.project || h.project === project) : openHandoffs);
50
+ // perf.2: auto-acknowledge shown handoffs
51
+ if (options.autoAcknowledge && filteredHandoffs.length > 0) {
52
+ const toAckIds = new Set(filteredHandoffs.map((h) => h.id));
53
+ let changed = false;
54
+ for (const h of state.open_handoffs) {
55
+ if (toAckIds.has(h.id) && h.status === 'open') {
56
+ h.status = 'accepted';
57
+ changed = true;
58
+ }
59
+ }
60
+ if (changed)
61
+ saveState(state, options.cwd);
62
+ }
63
+ return {
64
+ project_id: config.project_id,
65
+ current_host: currentHost,
66
+ host_filter: options.host,
67
+ all_hosts: options.allHosts ?? false,
68
+ project,
69
+ agent,
70
+ agent_id: resolvedAgentIdentity?.agent_id,
71
+ active_plans: filteredPlans.map((plan) => ({
72
+ ...plan,
73
+ claims: filteredClaims.filter((claim) => claim.plan_id === plan.id),
74
+ })),
75
+ active_claims: agent
76
+ ? filteredClaims.filter((claim) => claim.agent === agent)
77
+ : filteredClaims,
78
+ runtime_notes: filteredNotes,
79
+ session_meta_hidden: sessionMetaHidden,
80
+ open_handoffs: filteredHandoffs,
81
+ resolved_instructions: instructions,
82
+ reputation_summary: reputationSummary,
83
+ agent_reputation: agentReputation,
84
+ };
85
+ }
86
+ //# sourceMappingURL=coordination.js.map
@@ -0,0 +1,99 @@
1
+ import path from 'node:path';
2
+ import { loadConfig } from './config.js';
3
+ import { loadState } from './state.js';
4
+ import { saveRuntimeNote } from './runtime.js';
5
+ import { memoryExists } from './io.js';
6
+ /**
7
+ * Resolves cross_project_links from config, converting relative paths to absolute.
8
+ */
9
+ export function resolveCrossProjectLinks(cwd) {
10
+ const baseCwd = cwd ?? process.cwd();
11
+ let config;
12
+ try {
13
+ config = loadConfig(cwd);
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ return (config.cross_project_links ?? []).map((link) => {
19
+ const absolutePath = path.isAbsolute(link.path)
20
+ ? link.path
21
+ : path.resolve(baseCwd, link.path);
22
+ const available = memoryExists(absolutePath);
23
+ let projectName = link.name ?? path.basename(absolutePath);
24
+ if (available) {
25
+ try {
26
+ const linkedConfig = loadConfig(absolutePath);
27
+ projectName = link.name ?? linkedConfig.project_name ?? projectName;
28
+ }
29
+ catch { /* use basename fallback */ }
30
+ }
31
+ return { ...link, absolutePath, projectName, available };
32
+ });
33
+ }
34
+ /**
35
+ * Detects cycles in cross_project_links (A → B → A).
36
+ * Returns the paths involved in any cycle found.
37
+ */
38
+ export function detectCrossProjectCycles(cwd) {
39
+ const baseCwd = path.resolve(cwd ?? process.cwd());
40
+ const cycles = [];
41
+ function walk(currentCwd, visited) {
42
+ let links = [];
43
+ try {
44
+ links = resolveCrossProjectLinks(currentCwd);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ for (const link of links) {
50
+ const normalized = path.resolve(link.absolutePath);
51
+ if (visited.includes(normalized)) {
52
+ cycles.push([...visited, normalized]);
53
+ return;
54
+ }
55
+ walk(normalized, [...visited, normalized]);
56
+ }
57
+ }
58
+ walk(baseCwd, [baseCwd]);
59
+ return cycles;
60
+ }
61
+ /**
62
+ * Loads state from a linked project (read-only).
63
+ */
64
+ export function loadCrossProjectState(absolutePath) {
65
+ return loadState(absolutePath);
66
+ }
67
+ /**
68
+ * Writes a runtime note into a target (publisher-linked) project's runtime dir.
69
+ * Used by bclaw_write_note --cross-project.
70
+ */
71
+ export function writeCrossProjectNote(targetAbsolutePath, note, sourceCwd) {
72
+ const links = resolveCrossProjectLinks(sourceCwd);
73
+ const link = links.find((l) => path.resolve(l.absolutePath) === path.resolve(targetAbsolutePath));
74
+ if (!link) {
75
+ throw new Error(`No cross_project_link configured for path: ${targetAbsolutePath}`);
76
+ }
77
+ if (link.role !== 'publisher') {
78
+ throw new Error(`Cross-project link to '${link.projectName}' is role=subscriber — cannot write notes. Set role: publisher to enable push.`);
79
+ }
80
+ if (!link.available) {
81
+ throw new Error(`Target project not found or not initialized: ${targetAbsolutePath}`);
82
+ }
83
+ saveRuntimeNote({ ...note, note_type: 'cross_project' }, targetAbsolutePath);
84
+ }
85
+ /**
86
+ * Returns the absolute path of a cross-project link by name or path fragment.
87
+ */
88
+ export function resolveCrossProjectTarget(nameOrPath, cwd) {
89
+ const links = resolveCrossProjectLinks(cwd);
90
+ const match = links.find((l) => l.projectName === nameOrPath ||
91
+ l.path === nameOrPath ||
92
+ l.absolutePath === nameOrPath ||
93
+ path.basename(l.absolutePath) === nameOrPath);
94
+ if (!match) {
95
+ throw new Error(`No cross_project_link found matching: '${nameOrPath}'. Check your config.yaml cross_project_links.`);
96
+ }
97
+ return match;
98
+ }
99
+ //# sourceMappingURL=cross-project.js.map
@@ -0,0 +1,72 @@
1
+ export function detectDuplicates(candidateText, candidateType, state, pendingCandidates) {
2
+ const matches = [];
3
+ const normalised = normalise(candidateText);
4
+ // Check state entries for the same type
5
+ const sectionMap = {
6
+ constraint: state.active_constraints,
7
+ decision: state.recent_decisions,
8
+ trap: state.known_traps,
9
+ handoff: state.open_handoffs.map(h => ({ ...h, text: h.text })),
10
+ };
11
+ const items = sectionMap[candidateType] ?? [];
12
+ for (const item of items) {
13
+ const sim = similarity(normalised, normalise(item.text));
14
+ if (sim >= 0.7) {
15
+ matches.push({
16
+ id: item.id,
17
+ source: 'state',
18
+ text: item.text,
19
+ reason: sim >= 0.95 ? 'exact match' : 'similar text',
20
+ });
21
+ }
22
+ }
23
+ // Check pending candidates of the same type
24
+ for (const c of pendingCandidates) {
25
+ if (c.type !== candidateType)
26
+ continue;
27
+ const sim = similarity(normalised, normalise(c.text));
28
+ if (sim >= 0.7) {
29
+ matches.push({
30
+ id: c.id,
31
+ source: 'candidate',
32
+ text: c.text,
33
+ reason: sim >= 0.95 ? 'exact match' : 'similar text',
34
+ });
35
+ }
36
+ }
37
+ return matches;
38
+ }
39
+ function normalise(text) {
40
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim();
41
+ }
42
+ /**
43
+ * Bigram-based similarity (Dice coefficient). Fast, no dependencies.
44
+ */
45
+ function similarity(a, b) {
46
+ if (a === b)
47
+ return 1;
48
+ if (a.length < 2 || b.length < 2)
49
+ return 0;
50
+ const bigramsA = bigrams(a);
51
+ const bigramsB = bigrams(b);
52
+ let intersection = 0;
53
+ const bCopy = new Map(bigramsB);
54
+ for (const [bg, count] of bigramsA) {
55
+ const bCount = bCopy.get(bg) ?? 0;
56
+ if (bCount > 0) {
57
+ intersection += Math.min(count, bCount);
58
+ }
59
+ }
60
+ const totalA = [...bigramsA.values()].reduce((s, v) => s + v, 0);
61
+ const totalB = [...bigramsB.values()].reduce((s, v) => s + v, 0);
62
+ return (2 * intersection) / (totalA + totalB);
63
+ }
64
+ function bigrams(text) {
65
+ const result = new Map();
66
+ for (let i = 0; i < text.length - 1; i++) {
67
+ const bg = text.slice(i, i + 2);
68
+ result.set(bg, (result.get(bg) ?? 0) + 1);
69
+ }
70
+ return result;
71
+ }
72
+ //# sourceMappingURL=duplicates.js.map
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { memoryDir } from './io.js';
4
+ import { nowISO } from './ids.js';
5
+ import { logger } from './logger.js';
6
+ const EVENT_LOG_FILE = 'events.jsonl';
7
+ const CURSORS_DIR = '.cursors';
8
+ // --- Writer ---
9
+ export function appendEvent(event, cwd) {
10
+ try {
11
+ const full = {
12
+ ts: event.ts ?? nowISO(),
13
+ agent: event.agent ?? 'unknown',
14
+ agent_id: event.agent_id,
15
+ action: event.action,
16
+ item_type: event.item_type,
17
+ item_id: event.item_id,
18
+ summary: event.summary,
19
+ };
20
+ const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
21
+ const logPath = eventLogPath(cwd);
22
+ fs.appendFileSync(logPath, line + '\n', 'utf-8');
23
+ }
24
+ catch (err) {
25
+ logger.debug('Failed to write event log entry:', err);
26
+ }
27
+ }
28
+ // --- Reader ---
29
+ export function readAllEvents(cwd) {
30
+ const logPath = eventLogPath(cwd);
31
+ if (!fs.existsSync(logPath))
32
+ return [];
33
+ const lines = fs.readFileSync(logPath, 'utf-8').split('\n').filter(Boolean);
34
+ const events = [];
35
+ for (const line of lines) {
36
+ try {
37
+ events.push(JSON.parse(line));
38
+ }
39
+ catch {
40
+ // skip malformed
41
+ }
42
+ }
43
+ return events;
44
+ }
45
+ function cursorsDir(cwd) {
46
+ return path.join(memoryDir(cwd), CURSORS_DIR);
47
+ }
48
+ function cursorPath(agent, cwd) {
49
+ return path.join(cursorsDir(cwd), `${agent}.json`);
50
+ }
51
+ function loadCursor(agent, cwd) {
52
+ const fp = cursorPath(agent, cwd);
53
+ if (!fs.existsSync(fp))
54
+ return { offset: 0, last_read: '' };
55
+ try {
56
+ return JSON.parse(fs.readFileSync(fp, 'utf-8'));
57
+ }
58
+ catch {
59
+ return { offset: 0, last_read: '' };
60
+ }
61
+ }
62
+ function saveCursor(agent, cursor, cwd) {
63
+ const dir = cursorsDir(cwd);
64
+ if (!fs.existsSync(dir)) {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ }
67
+ fs.writeFileSync(cursorPath(agent, cwd), JSON.stringify(cursor), 'utf-8');
68
+ }
69
+ /**
70
+ * Read events unseen by this agent since their last read.
71
+ * Updates the cursor after reading.
72
+ */
73
+ export function readUnseenEvents(agent, cwd) {
74
+ const logPath = eventLogPath(cwd);
75
+ if (!fs.existsSync(logPath))
76
+ return [];
77
+ const cursor = loadCursor(agent, cwd);
78
+ const stat = fs.statSync(logPath);
79
+ if (stat.size <= cursor.offset)
80
+ return [];
81
+ // Read from offset
82
+ const fd = fs.openSync(logPath, 'r');
83
+ const buffer = Buffer.alloc(stat.size - cursor.offset);
84
+ fs.readSync(fd, buffer, 0, buffer.length, cursor.offset);
85
+ fs.closeSync(fd);
86
+ const newContent = buffer.toString('utf-8');
87
+ const lines = newContent.split('\n').filter(Boolean);
88
+ const events = [];
89
+ for (const line of lines) {
90
+ try {
91
+ const evt = JSON.parse(line);
92
+ // Exclude events from self
93
+ if (evt.agent !== agent) {
94
+ events.push(evt);
95
+ }
96
+ }
97
+ catch {
98
+ // skip
99
+ }
100
+ }
101
+ // Update cursor
102
+ saveCursor(agent, { offset: stat.size, last_read: nowISO() }, cwd);
103
+ return events;
104
+ }
105
+ /**
106
+ * Build a compact notification summary from unseen events.
107
+ */
108
+ export function buildNotificationSummary(events) {
109
+ if (events.length === 0)
110
+ return undefined;
111
+ const summary = {};
112
+ for (const evt of events) {
113
+ const key = `${evt.action}:${evt.item_type}`;
114
+ summary[key] = (summary[key] ?? 0) + 1;
115
+ }
116
+ return summary;
117
+ }
118
+ // --- Rotation ---
119
+ const MAX_EVENT_LOG_BYTES = 10 * 1024 * 1024; // 10MB
120
+ /**
121
+ * Check if the event log needs rotation. Returns true if rotated.
122
+ */
123
+ export function rotateEventLogIfNeeded(cwd) {
124
+ const logPath = eventLogPath(cwd);
125
+ if (!fs.existsSync(logPath))
126
+ return false;
127
+ const stat = fs.statSync(logPath);
128
+ if (stat.size < MAX_EVENT_LOG_BYTES)
129
+ return false;
130
+ try {
131
+ const archiveName = `events.${Date.now()}.jsonl`;
132
+ const archivePath = path.join(memoryDir(cwd), archiveName);
133
+ fs.renameSync(logPath, archivePath);
134
+ // Reset all cursors
135
+ const dir = cursorsDir(cwd);
136
+ if (fs.existsSync(dir)) {
137
+ for (const file of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) {
138
+ fs.unlinkSync(path.join(dir, file));
139
+ }
140
+ }
141
+ return true;
142
+ }
143
+ catch (err) {
144
+ logger.debug('Failed to rotate event log:', err);
145
+ return false;
146
+ }
147
+ }
148
+ // --- Helpers ---
149
+ function eventLogPath(cwd) {
150
+ return path.join(memoryDir(cwd), EVENT_LOG_FILE);
151
+ }
152
+ //# sourceMappingURL=event-log.js.map