crbro-memory 2.10.0 → 2.11.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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -465
  3. package/SECURITY.md +114 -12
  4. package/bin/crbro.mjs +539 -300
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +139 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +667 -232
  25. package/dist/engine/doctor.js +312 -14
  26. package/dist/engine/heat.js +81 -37
  27. package/dist/engine/hippocampus.js +18 -7
  28. package/dist/engine/hookrefresh.js +296 -0
  29. package/dist/engine/hookverify.js +15 -3
  30. package/dist/engine/installer.js +1022 -0
  31. package/dist/engine/installio.js +279 -0
  32. package/dist/engine/keychain.js +721 -162
  33. package/dist/engine/maintenance.js +355 -87
  34. package/dist/engine/modinstall.js +98 -45
  35. package/dist/engine/prefrontal.js +29 -6
  36. package/dist/engine/quarantine.js +204 -0
  37. package/dist/engine/secrets.js +41 -5
  38. package/dist/engine/shelf.js +25 -3
  39. package/dist/engine/synapses.js +183 -80
  40. package/dist/engine/triggers.js +141 -11
  41. package/dist/index.js +37 -3
  42. package/dist/mcp-surface.json +1 -0
  43. package/dist/miner/extractor.js +64 -17
  44. package/dist/miner/scheduler.js +144 -68
  45. package/dist/search/index.js +797 -176
  46. package/dist/search/semantic.js +208 -18
  47. package/dist/search/tokenize.js +44 -0
  48. package/dist/server.js +583 -373
  49. package/dist/sync/git.js +203 -72
  50. package/dist/sync/space.js +100 -25
  51. package/dist/sync/validate.js +204 -0
  52. package/dist/utils/budget.js +15 -2
  53. package/dist/utils/fs.js +40 -6
  54. package/dist/utils/ids.js +128 -14
  55. package/dist/utils/lock.js +94 -7
  56. package/hooks/crbro-codex.mjs +284 -0
  57. package/hooks/crbro-guard.mjs +281 -173
  58. package/hooks/crbro-hooks.shipped.json +20 -0
  59. package/hooks/crbro-lifecycle.mjs +16 -4
  60. package/hooks/crbro-subagent.mjs +180 -180
  61. package/package.json +9 -5
  62. package/dist/daemon/daemon.d.ts +0 -25
  63. package/dist/daemon/daemon.d.ts.map +0 -1
  64. package/dist/daemon/daemon.js.map +0 -1
  65. package/dist/daemon/endpoint.d.ts +0 -66
  66. package/dist/daemon/endpoint.d.ts.map +0 -1
  67. package/dist/daemon/endpoint.js.map +0 -1
  68. package/dist/daemon/lines.d.ts +0 -55
  69. package/dist/daemon/lines.d.ts.map +0 -1
  70. package/dist/daemon/lines.js.map +0 -1
  71. package/dist/daemon/proxy.d.ts +0 -55
  72. package/dist/daemon/proxy.d.ts.map +0 -1
  73. package/dist/daemon/proxy.js.map +0 -1
  74. package/dist/engine/backup.d.ts +0 -68
  75. package/dist/engine/backup.d.ts.map +0 -1
  76. package/dist/engine/backup.js.map +0 -1
  77. package/dist/engine/brain.d.ts +0 -79
  78. package/dist/engine/brain.d.ts.map +0 -1
  79. package/dist/engine/brain.js.map +0 -1
  80. package/dist/engine/cortex.d.ts +0 -682
  81. package/dist/engine/cortex.d.ts.map +0 -1
  82. package/dist/engine/cortex.js.map +0 -1
  83. package/dist/engine/dates.d.ts +0 -28
  84. package/dist/engine/dates.d.ts.map +0 -1
  85. package/dist/engine/dates.js.map +0 -1
  86. package/dist/engine/doctor.d.ts +0 -93
  87. package/dist/engine/doctor.d.ts.map +0 -1
  88. package/dist/engine/doctor.js.map +0 -1
  89. package/dist/engine/evidence.d.ts +0 -26
  90. package/dist/engine/evidence.d.ts.map +0 -1
  91. package/dist/engine/evidence.js.map +0 -1
  92. package/dist/engine/heat.d.ts +0 -28
  93. package/dist/engine/heat.d.ts.map +0 -1
  94. package/dist/engine/heat.js.map +0 -1
  95. package/dist/engine/hippocampus.d.ts +0 -60
  96. package/dist/engine/hippocampus.d.ts.map +0 -1
  97. package/dist/engine/hippocampus.js.map +0 -1
  98. package/dist/engine/hookverify.d.ts +0 -42
  99. package/dist/engine/hookverify.d.ts.map +0 -1
  100. package/dist/engine/hookverify.js.map +0 -1
  101. package/dist/engine/keychain.d.ts +0 -32
  102. package/dist/engine/keychain.d.ts.map +0 -1
  103. package/dist/engine/keychain.js.map +0 -1
  104. package/dist/engine/maintenance.d.ts +0 -218
  105. package/dist/engine/maintenance.d.ts.map +0 -1
  106. package/dist/engine/maintenance.js.map +0 -1
  107. package/dist/engine/modinstall.d.ts +0 -215
  108. package/dist/engine/modinstall.d.ts.map +0 -1
  109. package/dist/engine/modinstall.js.map +0 -1
  110. package/dist/engine/postmortem.d.ts +0 -81
  111. package/dist/engine/postmortem.d.ts.map +0 -1
  112. package/dist/engine/postmortem.js.map +0 -1
  113. package/dist/engine/prefrontal.d.ts +0 -64
  114. package/dist/engine/prefrontal.d.ts.map +0 -1
  115. package/dist/engine/prefrontal.js.map +0 -1
  116. package/dist/engine/projects.d.ts +0 -20
  117. package/dist/engine/projects.d.ts.map +0 -1
  118. package/dist/engine/projects.js.map +0 -1
  119. package/dist/engine/secrets.d.ts +0 -30
  120. package/dist/engine/secrets.d.ts.map +0 -1
  121. package/dist/engine/secrets.js.map +0 -1
  122. package/dist/engine/shelf.d.ts +0 -155
  123. package/dist/engine/shelf.d.ts.map +0 -1
  124. package/dist/engine/shelf.js.map +0 -1
  125. package/dist/engine/source.d.ts +0 -6
  126. package/dist/engine/source.d.ts.map +0 -1
  127. package/dist/engine/source.js.map +0 -1
  128. package/dist/engine/synapses.d.ts +0 -82
  129. package/dist/engine/synapses.d.ts.map +0 -1
  130. package/dist/engine/synapses.js.map +0 -1
  131. package/dist/engine/triggers.d.ts +0 -53
  132. package/dist/engine/triggers.d.ts.map +0 -1
  133. package/dist/engine/triggers.js.map +0 -1
  134. package/dist/engine/usage.d.ts +0 -61
  135. package/dist/engine/usage.d.ts.map +0 -1
  136. package/dist/engine/usage.js.map +0 -1
  137. package/dist/index.d.ts +0 -2
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js.map +0 -1
  140. package/dist/miner/extractor.d.ts +0 -12
  141. package/dist/miner/extractor.d.ts.map +0 -1
  142. package/dist/miner/extractor.js.map +0 -1
  143. package/dist/miner/index.d.ts +0 -61
  144. package/dist/miner/index.d.ts.map +0 -1
  145. package/dist/miner/index.js.map +0 -1
  146. package/dist/miner/scheduler.d.ts +0 -30
  147. package/dist/miner/scheduler.d.ts.map +0 -1
  148. package/dist/miner/scheduler.js.map +0 -1
  149. package/dist/miner/types.d.ts +0 -11
  150. package/dist/miner/types.d.ts.map +0 -1
  151. package/dist/miner/types.js.map +0 -1
  152. package/dist/search/index.d.ts +0 -328
  153. package/dist/search/index.d.ts.map +0 -1
  154. package/dist/search/index.js.map +0 -1
  155. package/dist/search/quality.d.ts +0 -18
  156. package/dist/search/quality.d.ts.map +0 -1
  157. package/dist/search/quality.js.map +0 -1
  158. package/dist/search/semantic.d.ts +0 -80
  159. package/dist/search/semantic.d.ts.map +0 -1
  160. package/dist/search/semantic.js.map +0 -1
  161. package/dist/search/synonyms.d.ts +0 -2
  162. package/dist/search/synonyms.d.ts.map +0 -1
  163. package/dist/search/synonyms.js.map +0 -1
  164. package/dist/search/tokenize.d.ts +0 -39
  165. package/dist/search/tokenize.d.ts.map +0 -1
  166. package/dist/search/tokenize.js.map +0 -1
  167. package/dist/server.d.ts +0 -41
  168. package/dist/server.d.ts.map +0 -1
  169. package/dist/server.js.map +0 -1
  170. package/dist/sync/git.d.ts +0 -36
  171. package/dist/sync/git.d.ts.map +0 -1
  172. package/dist/sync/git.js.map +0 -1
  173. package/dist/sync/materialize.d.ts +0 -36
  174. package/dist/sync/materialize.d.ts.map +0 -1
  175. package/dist/sync/materialize.js.map +0 -1
  176. package/dist/sync/ops.d.ts +0 -147
  177. package/dist/sync/ops.d.ts.map +0 -1
  178. package/dist/sync/ops.js.map +0 -1
  179. package/dist/sync/space.d.ts +0 -118
  180. package/dist/sync/space.d.ts.map +0 -1
  181. package/dist/sync/space.js.map +0 -1
  182. package/dist/types/index.d.ts +0 -369
  183. package/dist/types/index.d.ts.map +0 -1
  184. package/dist/types/index.js.map +0 -1
  185. package/dist/utils/budget.d.ts +0 -44
  186. package/dist/utils/budget.d.ts.map +0 -1
  187. package/dist/utils/budget.js.map +0 -1
  188. package/dist/utils/fs.d.ts +0 -59
  189. package/dist/utils/fs.d.ts.map +0 -1
  190. package/dist/utils/fs.js.map +0 -1
  191. package/dist/utils/hash.d.ts +0 -20
  192. package/dist/utils/hash.d.ts.map +0 -1
  193. package/dist/utils/hash.js.map +0 -1
  194. package/dist/utils/ids.d.ts +0 -47
  195. package/dist/utils/ids.d.ts.map +0 -1
  196. package/dist/utils/ids.js.map +0 -1
  197. package/dist/utils/lock.d.ts +0 -13
  198. package/dist/utils/lock.d.ts.map +0 -1
  199. package/dist/utils/lock.js.map +0 -1
  200. package/dist/utils/transcripts.d.ts +0 -63
  201. package/dist/utils/transcripts.d.ts.map +0 -1
  202. package/dist/utils/transcripts.js.map +0 -1
  203. package/dist/version.d.ts +0 -26
  204. package/dist/version.d.ts.map +0 -1
  205. package/dist/version.js.map +0 -1
package/dist/sync/git.js CHANGED
@@ -11,31 +11,48 @@
11
11
  // forever. And it must never rewrite what it stores: git on Windows converts
12
12
  // line endings by default, which turns one appended line into two different
13
13
  // lines on two machines, and the logs here are append-only.
14
+ //
15
+ // 2.11: a third. Inside the shared daemon a synchronous git call blocks every
16
+ // client for as long as a push takes (up to 30 s). Each multi-step operation is
17
+ // written once as a generator of git calls and driven either synchronously or
18
+ // asynchronously; the server path uses the async drivers.
14
19
  var __importDefault = (this && this.__importDefault) || function (mod) {
15
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
21
  };
17
22
  Object.defineProperty(exports, "__esModule", { value: true });
18
23
  exports.GITIGNORE = exports.GITATTRIBUTES = void 0;
24
+ exports.resolveGit = resolveGit;
19
25
  exports.git = git;
26
+ exports.gitAsync = gitAsync;
20
27
  exports.gitAvailable = gitAvailable;
28
+ exports.gitAvailableAsync = gitAvailableAsync;
21
29
  exports.hardenRepo = hardenRepo;
22
30
  exports.initSpace = initSpace;
31
+ exports.initSpaceAsync = initSpaceAsync;
23
32
  exports.cloneSpace = cloneSpace;
33
+ exports.cloneSpaceAsync = cloneSpaceAsync;
24
34
  exports.syncSpace = syncSpace;
35
+ exports.syncSpaceAsync = syncSpaceAsync;
36
+ exports._resetGitMemoForTests = _resetGitMemoForTests;
25
37
  const node_child_process_1 = require("node:child_process");
26
38
  const node_fs_1 = require("node:fs");
27
39
  const node_path_1 = __importDefault(require("node:path"));
28
40
  /**
29
41
  * Everything that could make git stop and ask a person something, turned off.
30
42
  * Without this the server hangs on the first private repository it meets.
43
+ * A GIT_SSH_COMMAND the user set is kept: replacing it silently overrode their
44
+ * own ssh setup (G16).
31
45
  */
32
- const SILENT_ENV = {
33
- GIT_TERMINAL_PROMPT: '0',
34
- GIT_ASKPASS: '',
35
- SSH_ASKPASS: '',
36
- GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new',
37
- GCM_INTERACTIVE: 'never',
38
- };
46
+ function silentEnv() {
47
+ return {
48
+ ...process.env,
49
+ GIT_TERMINAL_PROMPT: '0',
50
+ GIT_ASKPASS: '',
51
+ SSH_ASKPASS: '',
52
+ GIT_SSH_COMMAND: process.env['GIT_SSH_COMMAND'] || 'ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new',
53
+ GCM_INTERACTIVE: 'never',
54
+ };
55
+ }
39
56
  function classify(stderr) {
40
57
  const s = stderr.toLowerCase();
41
58
  if (s.includes('could not resolve host') || s.includes('unable to access') ||
@@ -48,38 +65,125 @@ function classify(stderr) {
48
65
  return 'conflict';
49
66
  return undefined;
50
67
  }
68
+ // ─── Which git ───────────────────────────────────────────────────
69
+ //
70
+ // On Windows a bare "git" is looked up in the child's working directory before
71
+ // PATH. Every call here runs with cwd = a space directory, whose contents come
72
+ // from teammates: a git.exe committed at its root would run as the user (G10).
73
+ // So the executable is resolved once, from absolute PATH entries only.
74
+ let resolved = null;
75
+ function resolveGit() {
76
+ if (process.platform !== 'win32')
77
+ return 'git';
78
+ if (resolved && (0, node_fs_1.existsSync)(resolved))
79
+ return resolved;
80
+ resolved = null;
81
+ for (const entry of (process.env['PATH'] || process.env['Path'] || '').split(node_path_1.default.delimiter)) {
82
+ const d = entry.trim().replace(/^"|"$/g, '');
83
+ if (!d || !node_path_1.default.isAbsolute(d))
84
+ continue;
85
+ const exe = node_path_1.default.join(d, 'git.exe');
86
+ try {
87
+ if ((0, node_fs_1.statSync)(exe).isFile()) {
88
+ resolved = exe;
89
+ return exe;
90
+ }
91
+ }
92
+ catch { /* not here */ }
93
+ }
94
+ // Not on PATH: the bare name fails with ENOENT from a neutral directory
95
+ // (see git()), which reports no_git rather than running a planted binary.
96
+ return 'git';
97
+ }
98
+ function neutralCwd(cwd) {
99
+ // With an absolute executable the space directory is a safe cwd. Without one
100
+ // (git not found), run from the system directory and pass -C instead.
101
+ if (process.platform === 'win32' && resolveGit() === 'git') {
102
+ const root = process.env['SystemRoot'] || 'C:\\Windows';
103
+ return { cwd: root, args: ['-C', cwd] };
104
+ }
105
+ return { cwd, args: [] };
106
+ }
107
+ function toResult(status, signal, stdout, stderr, error) {
108
+ if (error) {
109
+ const code = error.code;
110
+ return { ok: false, stdout: '', stderr: String(error.message || ''), reason: code === 'ENOENT' ? 'no_git' : 'timeout' };
111
+ }
112
+ if (signal)
113
+ return { ok: false, stdout, stderr, reason: 'timeout' };
114
+ return { ok: status === 0, stdout, stderr, reason: status === 0 ? undefined : classify(stderr) };
115
+ }
51
116
  function git(args, cwd, timeoutMs = 20_000) {
52
- const r = (0, node_child_process_1.spawnSync)('git', args, {
53
- cwd,
117
+ const where = neutralCwd(cwd);
118
+ const r = (0, node_child_process_1.spawnSync)(resolveGit(), [...where.args, ...args], {
119
+ cwd: where.cwd,
54
120
  encoding: 'utf-8',
55
121
  timeout: timeoutMs,
56
122
  windowsHide: true,
57
- env: { ...process.env, ...SILENT_ENV },
123
+ env: silentEnv(),
124
+ });
125
+ return toResult(r.status, r.signal, r.stdout || '', r.stderr || '', r.error);
126
+ }
127
+ /** The same call without blocking the event loop. */
128
+ function gitAsync(args, cwd, timeoutMs = 20_000) {
129
+ return new Promise(resolve => {
130
+ const where = neutralCwd(cwd);
131
+ let stdout = '';
132
+ let stderr = '';
133
+ let settled = false;
134
+ let timedOut = false;
135
+ const done = (r) => { if (!settled) {
136
+ settled = true;
137
+ clearTimeout(timer);
138
+ resolve(r);
139
+ } };
140
+ let child;
141
+ try {
142
+ child = (0, node_child_process_1.spawn)(resolveGit(), [...where.args, ...args], {
143
+ cwd: where.cwd, windowsHide: true, env: silentEnv(), stdio: ['ignore', 'pipe', 'pipe'],
144
+ });
145
+ }
146
+ catch (err) {
147
+ done(toResult(null, null, '', '', err));
148
+ return;
149
+ }
150
+ const timer = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs);
151
+ child.stdout.setEncoding('utf8');
152
+ child.stderr.setEncoding('utf8');
153
+ child.stdout.on('data', d => { stdout += d; });
154
+ child.stderr.on('data', d => { stderr += d; });
155
+ child.on('error', err => done(toResult(null, null, stdout, stderr, err)));
156
+ child.on('close', (code, signal) => done(timedOut
157
+ ? { ok: false, stdout, stderr, reason: 'timeout' }
158
+ : toResult(code, signal, stdout, stderr)));
58
159
  });
59
- if (r.error) {
60
- const code = r.error.code;
61
- return {
62
- ok: false,
63
- stdout: '',
64
- stderr: String(r.error.message || ''),
65
- reason: code === 'ENOENT' ? 'no_git' : 'timeout',
66
- };
67
- }
68
- if (r.signal) {
69
- return { ok: false, stdout: r.stdout || '', stderr: r.stderr || '', reason: 'timeout' };
70
- }
71
- const stderr = r.stderr || '';
72
- return {
73
- ok: r.status === 0,
74
- stdout: r.stdout || '',
75
- stderr,
76
- reason: r.status === 0 ? undefined : classify(stderr),
77
- };
160
+ }
161
+ function driveSync(g) {
162
+ let step = g.next();
163
+ while (!step.done)
164
+ step = g.next(git(step.value.args, step.value.cwd, step.value.timeoutMs));
165
+ return step.value;
166
+ }
167
+ async function driveAsync(g) {
168
+ let step = g.next();
169
+ while (!step.done)
170
+ step = g.next(await gitAsync(step.value.args, step.value.cwd, step.value.timeoutMs));
171
+ return step.value;
78
172
  }
79
173
  /** Is git installed at all? */
80
174
  function gitAvailable() {
81
175
  return git(['--version'], process.cwd(), 5_000).ok;
82
176
  }
177
+ // Whether git is installed does not change between two syncs; asking costs a
178
+ // process start. Kept for a minute, failures included.
179
+ let availability = null;
180
+ async function gitAvailableAsync() {
181
+ if (availability && Date.now() - availability.at < 60_000)
182
+ return availability.ok;
183
+ const ok = (await gitAsync(['--version'], process.cwd(), 5_000)).ok;
184
+ availability = { ok, at: Date.now() };
185
+ return ok;
186
+ }
83
187
  /**
84
188
  * Settings that have to be on the repository itself, not on the machine.
85
189
  *
@@ -89,18 +193,21 @@ function gitAvailable() {
89
193
  * becomes two lines, and the memory quietly duplicates itself. `* -text` in
90
194
  * .gitattributes plus this setting stops it at both ends.
91
195
  */
92
- function hardenRepo(dir) {
93
- git(['config', 'core.autocrlf', 'false'], dir);
94
- git(['config', 'core.safecrlf', 'false'], dir);
95
- git(['config', 'merge.ours.driver', 'true'], dir);
196
+ function* hardenGen(dir) {
197
+ yield { args: ['config', 'core.autocrlf', 'false'], cwd: dir };
198
+ yield { args: ['config', 'core.safecrlf', 'false'], cwd: dir };
199
+ yield { args: ['config', 'merge.ours.driver', 'true'], cwd: dir };
96
200
  // Identity, so committing works on a machine with no global git config.
97
- if (!git(['config', 'user.email'], dir).stdout.trim()) {
98
- git(['config', 'user.email', 'crbro@localhost'], dir);
201
+ if (!(yield { args: ['config', 'user.email'], cwd: dir }).stdout.trim()) {
202
+ yield { args: ['config', 'user.email', 'crbro@localhost'], cwd: dir };
99
203
  }
100
- if (!git(['config', 'user.name'], dir).stdout.trim()) {
101
- git(['config', 'user.name', 'CRBRO'], dir);
204
+ if (!(yield { args: ['config', 'user.name'], cwd: dir }).stdout.trim()) {
205
+ yield { args: ['config', 'user.name', 'CRBRO'], cwd: dir };
102
206
  }
103
207
  }
208
+ function hardenRepo(dir) {
209
+ driveSync(hardenGen(dir));
210
+ }
104
211
  exports.GITATTRIBUTES = '# CRBRO shared memory. Do not edit.\n' +
105
212
  '# -text keeps git from rewriting line endings: these logs are append-only\n' +
106
213
  '# and a rewritten ending turns one line into two on the next merge.\n' +
@@ -108,71 +215,95 @@ exports.GITATTRIBUTES = '# CRBRO shared memory. Do not edit.\n' +
108
215
  '*.jsonl merge=union\n';
109
216
  exports.GITIGNORE = '# Local bookkeeping. Never shared.\n' +
110
217
  '.local/\n';
111
- /** Prepare a brand-new space so the first person to clone finds real history. */
112
- async function initSpace(dir, remote, branch = 'main') {
113
- await node_fs_1.promises.mkdir(dir, { recursive: true });
114
- const init = git(['init', '-b', branch], dir);
218
+ function* initGen(dir, remote, branch) {
219
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
220
+ const init = yield { args: ['init', '-b', branch], cwd: dir };
115
221
  if (!init.ok && !init.stdout.includes('Reinitialized')) {
116
- const legacy = git(['init'], dir);
222
+ const legacy = yield { args: ['init'], cwd: dir };
117
223
  if (!legacy.ok)
118
224
  return legacy;
119
- git(['checkout', '-b', branch], dir);
225
+ yield { args: ['checkout', '-b', branch], cwd: dir };
120
226
  }
121
- hardenRepo(dir);
122
- await node_fs_1.promises.writeFile(node_path_1.default.join(dir, '.gitattributes'), exports.GITATTRIBUTES, 'utf-8');
123
- await node_fs_1.promises.writeFile(node_path_1.default.join(dir, '.gitignore'), exports.GITIGNORE, 'utf-8');
124
- git(['remote', 'remove', 'origin'], dir);
125
- const add = git(['remote', 'add', 'origin', remote], dir);
227
+ yield* hardenGen(dir);
228
+ (0, node_fs_1.writeFileSync)(node_path_1.default.join(dir, '.gitattributes'), exports.GITATTRIBUTES, 'utf-8');
229
+ (0, node_fs_1.writeFileSync)(node_path_1.default.join(dir, '.gitignore'), exports.GITIGNORE, 'utf-8');
230
+ yield { args: ['remote', 'remove', 'origin'], cwd: dir };
231
+ // "--": a remote that starts with "-" is a URL, never an option.
232
+ const add = yield { args: ['remote', 'add', '--', 'origin', remote], cwd: dir };
126
233
  if (!add.ok)
127
234
  return add;
128
- git(['add', '-A', '.'], dir);
129
- git(['commit', '-q', '-m', 'CRBRO shared space'], dir);
235
+ yield { args: ['add', '-A', '.'], cwd: dir };
236
+ yield { args: ['commit', '-q', '-m', 'CRBRO shared space'], cwd: dir };
130
237
  // Push the first commit before anyone clones. Skipping this is how two
131
238
  // people end up with unrelated histories and a merge that refuses to run —
132
239
  // each of them keeping their own half of the memory without noticing.
133
- return git(['push', '-u', 'origin', `HEAD:${branch}`], dir, 30_000);
240
+ return yield { args: ['push', '-u', 'origin', `HEAD:${branch}`], cwd: dir, timeoutMs: 30_000 };
134
241
  }
135
- /** Clone an existing space. */
136
- function cloneSpace(dir, remote, branch = 'main') {
242
+ /** Prepare a brand-new space so the first person to clone finds real history. */
243
+ async function initSpace(dir, remote, branch = 'main') {
244
+ return driveSync(initGen(dir, remote, branch));
245
+ }
246
+ function initSpaceAsync(dir, remote, branch = 'main') {
247
+ return driveAsync(initGen(dir, remote, branch));
248
+ }
249
+ function* cloneGen(dir, remote, branch) {
137
250
  const parent = node_path_1.default.dirname(dir);
138
251
  const name = node_path_1.default.basename(dir);
139
- const r = git(['clone', '--branch', branch, remote, name], parent, 60_000);
252
+ const r = yield { args: ['clone', '--branch', branch, '--', remote, name], cwd: parent, timeoutMs: 60_000 };
140
253
  if (r.ok)
141
- hardenRepo(dir);
254
+ yield* hardenGen(dir);
142
255
  return r;
143
256
  }
144
- /**
145
- * Bring in everyone else's notes and send ours.
146
- * Offline is a normal outcome, not an error: the work stays local and goes out
147
- * next time. Nothing here can lose a note, because nobody edits anyone's file.
148
- */
149
- function syncSpace(dir, author, branch = 'main', timeoutMs = 30_000) {
150
- git(['add', '-A', '.'], dir);
151
- const hayCambios = !git(['diff', '--cached', '--quiet'], dir).ok;
257
+ /** Clone an existing space. */
258
+ function cloneSpace(dir, remote, branch = 'main') {
259
+ return driveSync(cloneGen(dir, remote, branch));
260
+ }
261
+ function cloneSpaceAsync(dir, remote, branch = 'main') {
262
+ return driveAsync(cloneGen(dir, remote, branch));
263
+ }
264
+ function* syncGen(dir, author, branch, timeoutMs) {
265
+ yield { args: ['add', '-A', '.'], cwd: dir };
266
+ const hayCambios = !(yield { args: ['diff', '--cached', '--quiet'], cwd: dir }).ok;
152
267
  if (hayCambios) {
153
- git(['commit', '-q', '-m', `crbro ${author} ${new Date().toISOString()}`], dir);
268
+ yield { args: ['commit', '-q', '-m', `crbro ${author} ${new Date().toISOString()}`], cwd: dir };
154
269
  }
155
- const fetched = git(['fetch', 'origin', branch], dir, timeoutMs);
270
+ const fetched = yield { args: ['fetch', 'origin', '--', branch], cwd: dir, timeoutMs };
156
271
  if (!fetched.ok)
157
272
  return { pulled: fetched, pushed: null };
158
- const merged = git(['merge', '--no-edit', '--allow-unrelated-histories', `origin/${branch}`], dir, timeoutMs);
273
+ const merged = yield { args: ['merge', '--no-edit', '--allow-unrelated-histories', `origin/${branch}`], cwd: dir, timeoutMs };
159
274
  if (!merged.ok) {
160
275
  // Union merging makes this rare, but a hand-edited file could still clash.
161
276
  // Keeping both sides and stopping beats guessing.
162
- git(['merge', '--abort'], dir);
277
+ yield { args: ['merge', '--abort'], cwd: dir };
163
278
  return { pulled: { ...merged, reason: 'conflict' }, pushed: null };
164
279
  }
165
280
  // Push, and if someone landed first, take their work and try once more.
166
- let pushed = git(['push', 'origin', `HEAD:${branch}`], dir, timeoutMs);
281
+ let pushed = yield { args: ['push', 'origin', `HEAD:${branch}`], cwd: dir, timeoutMs };
167
282
  for (let intento = 0; intento < 2 && !pushed.ok && pushed.reason !== 'auth'; intento++) {
168
- if (!git(['fetch', 'origin', branch], dir, timeoutMs).ok)
283
+ if (!(yield { args: ['fetch', 'origin', '--', branch], cwd: dir, timeoutMs }).ok)
169
284
  break;
170
- if (!git(['merge', '--no-edit', `origin/${branch}`], dir, timeoutMs).ok) {
171
- git(['merge', '--abort'], dir);
285
+ if (!(yield { args: ['merge', '--no-edit', `origin/${branch}`], cwd: dir, timeoutMs }).ok) {
286
+ yield { args: ['merge', '--abort'], cwd: dir };
172
287
  break;
173
288
  }
174
- pushed = git(['push', 'origin', `HEAD:${branch}`], dir, timeoutMs);
289
+ pushed = yield { args: ['push', 'origin', `HEAD:${branch}`], cwd: dir, timeoutMs };
175
290
  }
176
291
  return { pulled: merged, pushed };
177
292
  }
293
+ /**
294
+ * Bring in everyone else's notes and send ours.
295
+ * Offline is a normal outcome, not an error: the work stays local and goes out
296
+ * next time. Nothing here can lose a note, because nobody edits anyone's file.
297
+ */
298
+ function syncSpace(dir, author, branch = 'main', timeoutMs = 30_000) {
299
+ return driveSync(syncGen(dir, author, branch, timeoutMs));
300
+ }
301
+ function syncSpaceAsync(dir, author, branch = 'main', timeoutMs = 30_000) {
302
+ return driveAsync(syncGen(dir, author, branch, timeoutMs));
303
+ }
304
+ /** Tests: forget the resolved executable and the availability memo. */
305
+ function _resetGitMemoForTests() {
306
+ resolved = null;
307
+ availability = null;
308
+ }
178
309
  //# sourceMappingURL=git.js.map
@@ -37,6 +37,7 @@ const secrets_js_1 = require("../engine/secrets.js");
37
37
  const evidence_js_1 = require("../engine/evidence.js");
38
38
  const ops_js_1 = require("./ops.js");
39
39
  const git_js_1 = require("./git.js");
40
+ const validate_js_1 = require("./validate.js");
40
41
  // Fuera de shared/: el bloqueo debe sobrevivir al traslado de un espacio.
41
42
  // Orden: shared-state -> shared-outbox-state -> JSON. Nunca a la inversa.
42
43
  // Git retiene shared-state, pero NUNCA shared-outbox-state. Los emisores de
@@ -97,16 +98,24 @@ async function getIdentity(brain, author) {
97
98
  await (0, fs_js_1.writeJSON)(p, id);
98
99
  return id;
99
100
  }
101
+ /**
102
+ * The folder of a space. Throws for a name that is not a plain slug or that
103
+ * would resolve outside shared/: with "..", create used to run `git add -A`
104
+ * and push the whole brain to whatever remote it was given (G1).
105
+ */
100
106
  function spaceDir(brain, name) {
101
- return node_path_1.default.join(brain.paths.shared, name);
107
+ return (0, validate_js_1.safeSpaceDir)(brain.paths.shared, name);
102
108
  }
109
+ /** The space's config, or null — also for a name that cannot be a space. */
103
110
  async function readSpace(brain, name) {
111
+ if ((0, validate_js_1.spaceNameProblem)(name))
112
+ return null;
104
113
  return (0, fs_js_1.readJSON)(node_path_1.default.join(spaceDir(brain, name), 'space.json'));
105
114
  }
106
115
  async function listSpaces(brain) {
107
116
  try {
108
117
  const entries = await node_fs_1.promises.readdir(brain.paths.shared, { withFileTypes: true });
109
- return entries.filter(e => e.isDirectory()).map(e => e.name);
118
+ return entries.filter(e => e.isDirectory() && !(0, validate_js_1.spaceNameProblem)(e.name)).map(e => e.name);
110
119
  }
111
120
  catch {
112
121
  return [];
@@ -116,8 +125,16 @@ async function listSpaces(brain) {
116
125
  async function createSpace(brain, name, remote, author, branch = 'main') {
117
126
  return withSharedState(brain, () => createSpaceUnlocked(brain, name, remote, author, branch));
118
127
  }
128
+ /** One message for a name, remote or branch that cannot be used. */
129
+ function invalidInput(name, remote, branch) {
130
+ const problem = (0, validate_js_1.spaceNameProblem)(name) || (0, validate_js_1.remoteProblem)(remote) || (0, validate_js_1.branchProblem)(branch);
131
+ return problem ? { ok: false, message: `Nothing was created or cloned. ${problem}` } : null;
132
+ }
119
133
  async function createSpaceUnlocked(brain, name, remote, author, branch) {
120
- if (!(0, git_js_1.gitAvailable)()) {
134
+ const invalido = invalidInput(name, remote, branch);
135
+ if (invalido)
136
+ return invalido;
137
+ if (!(await (0, git_js_1.gitAvailableAsync)())) {
121
138
  return { ok: false, message: 'git is not installed, and it is what carries the shared memory. Install it and try again.' };
122
139
  }
123
140
  const dir = spaceDir(brain, name);
@@ -125,30 +142,33 @@ async function createSpaceUnlocked(brain, name, remote, author, branch) {
125
142
  return { ok: false, message: `The space "${name}" already exists on this machine.` };
126
143
  }
127
144
  const id = await getIdentity(brain, author);
128
- const pushed = await (0, git_js_1.initSpace)(dir, remote, branch);
145
+ const pushed = await (0, git_js_1.initSpaceAsync)(dir, remote, branch);
129
146
  const cfg = { v: 1, name, created_by: id.author, created: (0, fs_js_1.now)(), branch };
130
147
  await (0, fs_js_1.writeJSON)(node_path_1.default.join(dir, 'space.json'), cfg);
131
148
  await node_fs_1.promises.mkdir(node_path_1.default.join(dir, '.local'), { recursive: true });
132
- (0, git_js_1.git)(['add', '-A', '.'], dir);
133
- (0, git_js_1.git)(['commit', '-q', '-m', `CRBRO space "${name}"`], dir);
134
- const again = (0, git_js_1.git)(['push', 'origin', `HEAD:${branch}`], dir, 30_000);
149
+ await (0, git_js_1.gitAsync)(['add', '-A', '.'], dir);
150
+ await (0, git_js_1.gitAsync)(['commit', '-q', '-m', `CRBRO space "${name}"`], dir);
151
+ const again = await (0, git_js_1.gitAsync)(['push', 'origin', `HEAD:${branch}`], dir, 30_000);
135
152
  if (!pushed.ok && !again.ok) {
136
153
  return {
137
154
  ok: false,
138
155
  message: 'The space was created locally but could not reach the remote. Check the URL and that you have push access.',
139
- detail: (again.stderr || pushed.stderr).slice(0, 300),
156
+ detail: (0, secrets_js_1.redact)((again.stderr || pushed.stderr).slice(0, 300)).text,
140
157
  };
141
158
  }
142
159
  return {
143
160
  ok: true,
144
- message: `Space "${name}" created and pushed. Anyone you invite to ${remote} can now join it.`,
161
+ message: (0, secrets_js_1.redact)(`Space "${name}" created and pushed. Anyone you invite to ${remote} can now join it.`).text,
145
162
  };
146
163
  }
147
164
  async function joinSpace(brain, name, remote, author, branch = 'main') {
148
165
  return withSharedState(brain, () => joinSpaceUnlocked(brain, name, remote, author, branch));
149
166
  }
150
167
  async function joinSpaceUnlocked(brain, name, remote, author, branch) {
151
- if (!(0, git_js_1.gitAvailable)()) {
168
+ const invalido = invalidInput(name, remote, branch);
169
+ if (invalido)
170
+ return invalido;
171
+ if (!(await (0, git_js_1.gitAvailableAsync)())) {
152
172
  return { ok: false, message: 'git is not installed, and it is what carries the shared memory. Install it and try again.' };
153
173
  }
154
174
  const dir = spaceDir(brain, name);
@@ -157,14 +177,14 @@ async function joinSpaceUnlocked(brain, name, remote, author, branch) {
157
177
  }
158
178
  await getIdentity(brain, author);
159
179
  await node_fs_1.promises.mkdir(brain.paths.shared, { recursive: true });
160
- const r = (0, git_js_1.cloneSpace)(dir, remote, branch);
180
+ const r = await (0, git_js_1.cloneSpaceAsync)(dir, remote, branch);
161
181
  if (!r.ok) {
162
182
  return {
163
183
  ok: false,
164
184
  message: r.reason === 'auth'
165
185
  ? 'Cloned nothing: no access to that repository. Ask to be added as a collaborator.'
166
186
  : 'Could not clone the space. Check the URL and your connection.',
167
- detail: r.stderr.slice(0, 300),
187
+ detail: (0, secrets_js_1.redact)(r.stderr.slice(0, 300)).text,
168
188
  };
169
189
  }
170
190
  await node_fs_1.promises.mkdir(node_path_1.default.join(dir, '.local'), { recursive: true });
@@ -175,7 +195,7 @@ function neuronOps(neuron, id) {
175
195
  const at = (0, fs_js_1.now)();
176
196
  const ops = [
177
197
  { v: ops_js_1.OPS_VERSION, op: 'neuron', nid: neuron.id, by: id.author, at,
178
- name: neuron.name, ntype: neuron.type, domain: neuron.domain },
198
+ name: (0, secrets_js_1.redact)(neuron.name || '').text, ntype: neuron.type, domain: neuron.domain },
179
199
  ];
180
200
  for (const f of neuron.facts || []) {
181
201
  if (!f.text)
@@ -186,7 +206,7 @@ function neuronOps(neuron, id) {
186
206
  ...(f.reference ? { reference: f.reference } : {}),
187
207
  ...(f.temporal ? { temporal: f.temporal } : {}),
188
208
  ...(f.verification_invalidated_at ? { verification_invalidated_at: f.verification_invalidated_at } : {}),
189
- ...(f.keys?.length ? { keys: f.keys } : {}),
209
+ ...(f.keys?.length ? { keys: f.keys.map(k => (0, secrets_js_1.redact)(k).text) } : {}),
190
210
  ...(f.shelf_life ? { shelf: f.shelf_life } : {}) });
191
211
  if (f.verified) {
192
212
  ops.push({ v: ops_js_1.OPS_VERSION, op: 'verify', nid: neuron.id, by: id.author, at: f.verified, eid: fid, ekind: 'fact',
@@ -194,14 +214,14 @@ function neuronOps(neuron, id) {
194
214
  }
195
215
  if (f.status === 'superseded' || f.status === 'retracted') {
196
216
  ops.push({ v: ops_js_1.OPS_VERSION, op: 'status', nid: neuron.id, by: id.author,
197
- at: f.revised || at, fid, to: f.status, why: f.revision_note });
217
+ at: f.revised || at, fid, to: f.status, why: f.revision_note ? (0, secrets_js_1.redact)(f.revision_note).text : undefined });
198
218
  }
199
219
  }
200
220
  for (const d of neuron.decisions || []) {
201
221
  if (!d.text)
202
222
  continue;
203
223
  ops.push({ v: ops_js_1.OPS_VERSION, op: 'decision', nid: neuron.id, by: id.author,
204
- at: d.date || at, did: (0, ops_js_1.entryId)(d.text), text: d.text, why: d.rationale });
224
+ at: d.date || at, did: (0, ops_js_1.entryId)(d.text), text: d.text, why: d.rationale ? (0, secrets_js_1.redact)(d.rationale).text : d.rationale });
205
225
  }
206
226
  // Dated entries carry their own date in `at`, so the team sees when a
207
227
  // pattern, error or debt was really recorded, not when it was shared.
@@ -273,7 +293,21 @@ async function prepareShare(brain, cortex, neuronRef, spaceName) {
273
293
  for (const [k, v] of Object.entries(neuron.entry_verification || {})) {
274
294
  revisarReferencia(`entry_verification[${k}].reference`, v.reference);
275
295
  }
276
- (neuron.decisions || []).forEach((d, i) => revisar(`decisions[${i}]`, d.text));
296
+ (neuron.decisions || []).forEach((d, i) => {
297
+ revisar(`decisions[${i}]`, d.text);
298
+ revisar(`decisions[${i}].rationale`, d.rationale);
299
+ });
300
+ // Every other field that travels in an op, not only the entry texts (G8):
301
+ // a credential in a rationale, an alias or a revision note went out while
302
+ // the dry run reported nothing blocked.
303
+ (neuron.facts || []).forEach((f, i) => {
304
+ (f.keys || []).forEach((k, j) => revisar(`facts[${i}].keys[${j}]`, k));
305
+ if (f.revision_note)
306
+ revisar(`facts[${i}].revision_note`, f.revision_note);
307
+ });
308
+ revisar('name', neuron.name);
309
+ revisar('summary', neuron.summary);
310
+ (neuron.tags || []).forEach((t, i) => revisar(`tags[${i}]`, t));
277
311
  (neuron.patterns || []).forEach((p, i) => revisar(`patterns[${i}]`, p));
278
312
  (neuron.errors || []).forEach((e, i) => revisar(`errors[${i}]`, e));
279
313
  (neuron.debts || []).forEach((d, i) => revisar(`debts[${i}]`, d));
@@ -471,6 +505,7 @@ async function emitOps(brain, neuronId, make) {
471
505
  });
472
506
  }
473
507
  // ─── Sync ────────────────────────────────────────────────────────
508
+ const MAX_LOG_BYTES = 32 * 1024 * 1024;
474
509
  async function readAllOps(dir, neuronId) {
475
510
  const opsDir = node_path_1.default.join(dir, 'neurons', neuronId, 'ops');
476
511
  let files;
@@ -485,7 +520,11 @@ async function readAllOps(dir, neuronId) {
485
520
  if (!f.endsWith('.jsonl'))
486
521
  continue;
487
522
  try {
488
- const raw = await node_fs_1.promises.readFile(node_path_1.default.join(opsDir, f), 'utf-8');
523
+ const p = node_path_1.default.join(opsDir, f);
524
+ // A log that size is not notes; reading it whole would only cost memory.
525
+ if ((await node_fs_1.promises.stat(p)).size > MAX_LOG_BYTES)
526
+ continue;
527
+ const raw = await node_fs_1.promises.readFile(p, 'utf-8');
489
528
  all.push(...(0, ops_js_1.decodeOps)(raw).ops);
490
529
  }
491
530
  catch { /* unreadable log: skip it, keep the rest */ }
@@ -503,11 +542,16 @@ async function syncSpaceUnlocked(brain, cortex, spaceName, timeoutMs) {
503
542
  if (!cfg)
504
543
  return { ...base, state: 'not_joined', message: `You are not in a space called "${spaceName}".` };
505
544
  await withOutboxState(brain, () => drainOutboxUnlocked(brain, spaceName));
506
- if (!(0, git_js_1.gitAvailable)())
545
+ if (!(await (0, git_js_1.gitAvailableAsync)()))
507
546
  return { ...base, state: 'no_git', message: 'git is not installed on this machine.' };
547
+ // space.json arrives from the remote: its branch is checked like any input.
548
+ const branch = cfg.branch || 'main';
549
+ if ((0, validate_js_1.branchProblem)(branch)) {
550
+ return { ...base, state: 'conflict', message: 'The space.json of this space names an invalid branch. Nothing was synced.' };
551
+ }
508
552
  const dir = spaceDir(brain, spaceName);
509
553
  const id = await getIdentity(brain);
510
- const { pulled, pushed } = (0, git_js_1.syncSpace)(dir, id.author, cfg.branch || 'main', timeoutMs);
554
+ const { pulled, pushed } = await (0, git_js_1.syncSpaceAsync)(dir, id.author, branch, timeoutMs);
511
555
  if (!pulled.ok) {
512
556
  const state = (pulled.reason === 'offline' || pulled.reason === 'timeout') ? 'offline'
513
557
  : pulled.reason === 'auth' ? 'auth'
@@ -531,10 +575,30 @@ async function syncSpaceUnlocked(brain, cortex, spaceName, timeoutMs) {
531
575
  catch { /* nothing shared yet */ }
532
576
  // Neurons this machine stopped following: no merge, no markShared.
533
577
  const fuera = new Set(await unsharedList(brain));
578
+ const mapa = await sharedMap(brain);
579
+ const intactas = [];
580
+ let descartadas = 0;
534
581
  for (const nid of neuronIds) {
535
582
  if (fuera.has(nid))
536
583
  continue;
537
- const ops = await readAllOps(dir, nid);
584
+ // A folder name is what becomes a file name in the cortex: only what
585
+ // CRBRO itself would have written.
586
+ if (!validate_js_1.NEURON_DIR_RE.test(nid))
587
+ continue;
588
+ const owner = mapa[nid];
589
+ // Followed in another space: that space's sync owns it.
590
+ if (owner && owner !== spaceName)
591
+ continue;
592
+ // A local neuron you never shared here stays yours. Until 2.10 a
593
+ // teammate could create neurons/<your id>/ and the next sync merged into
594
+ // it and marked it shared, so everything you learned there went out (G2).
595
+ if (!owner && await (0, fs_js_1.fileExists)(brain.paths.neuron(nid))) {
596
+ intactas.push(nid);
597
+ continue;
598
+ }
599
+ const leidas = await readAllOps(dir, nid);
600
+ const { ops, dropped } = (0, validate_js_1.sanitizeIncomingOps)(nid, leidas);
601
+ descartadas += dropped;
538
602
  if (ops.length === 0)
539
603
  continue;
540
604
  const { report, created } = await cortex.mergeFromSync(nid, ops);
@@ -547,8 +611,13 @@ async function syncSpaceUnlocked(brain, cortex, spaceName, timeoutMs) {
547
611
  base.neurons_touched.push(nid);
548
612
  base.merged.push(report);
549
613
  }
550
- await markSharedUnlocked(brain, nid, spaceName);
614
+ if (!owner)
615
+ await markSharedUnlocked(brain, nid, spaceName);
551
616
  }
617
+ if (intactas.length)
618
+ base.left_alone = intactas;
619
+ if (descartadas)
620
+ base.ops_dropped = descartadas;
552
621
  const pending = await withOutboxState(brain, () => drainOutboxUnlocked(brain, spaceName));
553
622
  // Git solo envió lo que había antes: lo añadido después espera el próximo sync.
554
623
  base.pushed = !!pushed?.ok && pending === 0;
@@ -559,6 +628,12 @@ async function syncSpaceUnlocked(brain, cortex, spaceName, timeoutMs) {
559
628
  if (!base.pushed && pushed) {
560
629
  base.message += ' Your own notes could not be pushed this time; they will go with the next sync.';
561
630
  }
631
+ if (intactas.length) {
632
+ base.message += ` ${intactas.length} neuron(s) in the space have the id of a private neuron of yours and were left ` +
633
+ `alone (${intactas.slice(0, 5).join(', ')}${intactas.length > 5 ? ', …' : ''}). To merge one, crbro_share it into "${spaceName}".`;
634
+ }
635
+ if (descartadas)
636
+ base.message += ` ${descartadas} malformed incoming note(s) were ignored.`;
562
637
  return base;
563
638
  }
564
639
  /** Sync every space. Used at boot and at consolidation, with a short budget. */
@@ -593,7 +668,7 @@ function attachSync(brain, cortex) {
593
668
  ...(change.reference ? { reference: change.reference } : {}),
594
669
  ...(change.temporal ? { temporal: change.temporal } : {}),
595
670
  ...(change.verification_invalidated_at ? { verification_invalidated_at: change.verification_invalidated_at } : {}),
596
- ...(change.keys?.length ? { keys: change.keys } : {}),
671
+ ...(change.keys?.length ? { keys: change.keys.map(k => (0, secrets_js_1.redact)(k).text) } : {}),
597
672
  ...(change.shelf ? { shelf: change.shelf } : {}) }];
598
673
  }
599
674
  if (change.kind === 'verify') {
@@ -601,11 +676,11 @@ function attachSync(brain, cortex) {
601
676
  ...(change.verification ? { verification: change.verification } : {}) }];
602
677
  }
603
678
  if (change.kind === 'status') {
604
- return [{ ...comun, op: 'status', fid: change.fid, to: change.to, why: change.why }];
679
+ return [{ ...comun, op: 'status', fid: change.fid, to: change.to, why: change.why ? (0, secrets_js_1.redact)(change.why).text : change.why }];
605
680
  }
606
681
  if (change.kind === 'decision') {
607
682
  return [{ ...comun, op: 'decision', did: (0, ops_js_1.entryId)(change.text),
608
- text: change.text, why: change.why }];
683
+ text: change.text, why: change.why ? (0, secrets_js_1.redact)(change.why).text : change.why }];
609
684
  }
610
685
  if (change.kind === 'error') {
611
686
  return [{ ...comun, op: 'error', text: change.text }];