brainclaw 1.16.0 → 1.18.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 (85) hide show
  1. package/README.md +22 -8
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-review.js +2 -2
  4. package/dist/commands/code-map.js +4 -1
  5. package/dist/commands/codev.js +61 -30
  6. package/dist/commands/doctor.js +14 -1
  7. package/dist/commands/harvest.js +241 -25
  8. package/dist/commands/inbox.js +10 -4
  9. package/dist/commands/loop.js +2 -2
  10. package/dist/commands/loops-handlers.js +82 -1
  11. package/dist/commands/mcp-catalog.js +12 -4
  12. package/dist/commands/mcp-read-handlers.js +90 -7
  13. package/dist/commands/mcp-schemas.generated.js +3 -0
  14. package/dist/commands/mcp-write-coordination.js +159 -40
  15. package/dist/commands/mcp.js +11 -2
  16. package/dist/core/agent-capability.js +7 -2
  17. package/dist/core/agent-files.js +53 -2
  18. package/dist/core/agent-integrations.js +1 -0
  19. package/dist/core/agentrun-reconciler.js +171 -7
  20. package/dist/core/agentruns.js +6 -1
  21. package/dist/core/code-map/aggregate.js +473 -0
  22. package/dist/core/code-map/backend.js +36 -10
  23. package/dist/core/code-map/freshness.js +36 -1
  24. package/dist/core/code-map/lang/c/imports.scm +12 -0
  25. package/dist/core/code-map/lang/c/index.js +150 -0
  26. package/dist/core/code-map/lang/c/tags.scm +68 -0
  27. package/dist/core/code-map/lang/cpp/imports.scm +14 -0
  28. package/dist/core/code-map/lang/cpp/index.js +149 -0
  29. package/dist/core/code-map/lang/cpp/tags.scm +87 -0
  30. package/dist/core/code-map/lang/csharp/imports.scm +20 -0
  31. package/dist/core/code-map/lang/csharp/index.js +224 -0
  32. package/dist/core/code-map/lang/csharp/tags.scm +63 -0
  33. package/dist/core/code-map/lang/go/imports.scm +13 -0
  34. package/dist/core/code-map/lang/go/index.js +139 -0
  35. package/dist/core/code-map/lang/go/tags.scm +36 -0
  36. package/dist/core/code-map/lang/providers.js +12 -1
  37. package/dist/core/code-map/lang/ruby/imports.scm +24 -0
  38. package/dist/core/code-map/lang/ruby/index.js +198 -0
  39. package/dist/core/code-map/lang/ruby/tags.scm +49 -0
  40. package/dist/core/code-map/lang/rust/imports.scm +44 -0
  41. package/dist/core/code-map/lang/rust/index.js +136 -0
  42. package/dist/core/code-map/lang/rust/tags.scm +47 -0
  43. package/dist/core/code-map/query.js +229 -80
  44. package/dist/core/code-map/types.js +18 -0
  45. package/dist/core/code-map/work-section.js +8 -7
  46. package/dist/core/codev-responses.js +16 -0
  47. package/dist/core/dispatcher.js +209 -29
  48. package/dist/core/execution-adapters.js +29 -3
  49. package/dist/core/ideation-loop-close.js +124 -0
  50. package/dist/core/loops/artifact-resolver.js +197 -0
  51. package/dist/core/loops/attempt-reservation.js +576 -0
  52. package/dist/core/loops/commit-intent.js +494 -0
  53. package/dist/core/loops/facade-schema.js +48 -0
  54. package/dist/core/loops/impl-bind.js +144 -0
  55. package/dist/core/loops/index.js +1 -1
  56. package/dist/core/loops/iteration-engine.js +29 -0
  57. package/dist/core/loops/lock.js +14 -0
  58. package/dist/core/loops/project-resolution.js +157 -0
  59. package/dist/core/loops/reconcile-turn.js +369 -0
  60. package/dist/core/loops/result-reducers.js +88 -0
  61. package/dist/core/loops/store.js +46 -7
  62. package/dist/core/loops/types.js +139 -11
  63. package/dist/core/loops/verbs.js +9 -3
  64. package/dist/core/loops/verify-command.js +209 -0
  65. package/dist/core/messaging.js +58 -5
  66. package/dist/core/review-loop-close.js +106 -34
  67. package/dist/core/review-loop-turn-dispatch.js +445 -0
  68. package/dist/core/runtime-signals.js +68 -0
  69. package/dist/core/schema.js +34 -0
  70. package/dist/core/worktree.js +240 -22
  71. package/dist/facts.js +10 -10
  72. package/dist/facts.json +9 -9
  73. package/dist/wasm/tree-sitter-c.wasm +0 -0
  74. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  75. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  76. package/dist/wasm/tree-sitter-go.wasm +0 -0
  77. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  78. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  79. package/docs/cli.md +1 -1
  80. package/docs/code-map.md +22 -6
  81. package/docs/concepts/loop-engine.md +28 -2
  82. package/docs/concepts/observer-protocol.md +22 -0
  83. package/docs/integrations/codex.md +19 -3
  84. package/docs/mcp-schema-changelog.md +43 -1
  85. package/package.json +1 -1
@@ -0,0 +1,197 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { memoryDir } from '../io.js';
5
+ /**
6
+ * Safe canonical artifact resolver (pln#630 §7).
7
+ *
8
+ * The single central resolver every loop-artifact reader/writer must use. It
9
+ * replaces the ad-hoc `path.join(dir, body.ref)` (hooks/bootstrap-write.ts) that
10
+ * joined a WORKER-CONTROLLED `ref` straight onto a store dir — a path-traversal
11
+ * hole (`ref: "../../../etc/passwd"` escaped the artifacts dir).
12
+ *
13
+ * The safety protocol, mandatory before any state mutation (§7):
14
+ * 1. Brainclaw-generated target basenames — `<artifact_id>.<ext>`, never a
15
+ * worker-supplied name.
16
+ * 2. Worker source paths validated by `realpath` CONTAINMENT (reject `../`
17
+ * escapes and symlink-out) before any read.
18
+ * 3. Atomic temp-copy + fsync + rename into the canonical store.
19
+ * 4. size + sha256 validation against the attempt's expected_artifacts.
20
+ * 5. Deterministic (artifact_id-keyed) target + hash check = per-turn
21
+ * idempotency: a crash between copy and the artifact/event write retries
22
+ * without duplicating (re-copy of an identical payload is a no-op).
23
+ *
24
+ * Canonical home (unifies the two conflicting doc paths §7):
25
+ * .brainclaw/loops/artifacts/<lop_id>/<artifact_id>.<ext>
26
+ * Migration is new-then-legacy on READ, reject-on-hash-mismatch; writes go to the
27
+ * new path only.
28
+ */
29
+ export class ArtifactResolverError extends Error {
30
+ code;
31
+ constructor(code, message) {
32
+ super(message);
33
+ this.code = code;
34
+ this.name = 'ArtifactResolverError';
35
+ }
36
+ }
37
+ /** Legacy on-disk home for ref-based payloads (pre-§7). Read fallback only. */
38
+ function legacyArtifactsDir(loopId, cwd) {
39
+ return path.join(memoryDir(cwd ?? process.cwd()), 'loops', 'threads', loopId, 'artifacts');
40
+ }
41
+ /** Canonical home for a loop's artifact payloads (§7). */
42
+ export function canonicalArtifactsDir(loopId, cwd) {
43
+ return path.join(memoryDir(cwd ?? process.cwd()), 'loops', 'artifacts', loopId);
44
+ }
45
+ /**
46
+ * The canonical absolute path for a brainclaw-owned artifact payload. The
47
+ * basename is derived ENTIRELY from brainclaw-generated ids (never a worker
48
+ * string), so it cannot traverse. `ext` is sanitized to a bare alnum extension.
49
+ */
50
+ export function canonicalArtifactPath(loopId, artifactId, ext, cwd) {
51
+ const safeExt = ext.replace(/^\.+/, '').replace(/[^A-Za-z0-9]/g, '') || 'txt';
52
+ return path.join(canonicalArtifactsDir(loopId, cwd), `${artifactId}.${safeExt}`);
53
+ }
54
+ /**
55
+ * Validate that a worker-relative path resolves to a real file CONTAINED within
56
+ * `workerRoot` (no `../` escape, no symlink pointing outside). Returns the
57
+ * validated absolute path; throws `containment_violation` / `source_missing`
58
+ * otherwise. This is the mandatory gate before ANY read of a worker-produced
59
+ * artifact (§7 / invariant #7).
60
+ */
61
+ export function resolveContainedWorkerPath(workerRoot, workerRelPath, _cwd) {
62
+ // realpath the containment ROOT first (it must exist and be a directory).
63
+ let rootReal;
64
+ try {
65
+ rootReal = fs.realpathSync(workerRoot);
66
+ }
67
+ catch {
68
+ throw new ArtifactResolverError('source_missing', `resolveContainedWorkerPath: worker root ${workerRoot} does not resolve`);
69
+ }
70
+ // Reject an absolute worker path outright — an expected artifact is always
71
+ // worker-RELATIVE; an absolute path is a red flag we never join.
72
+ if (path.isAbsolute(workerRelPath)) {
73
+ throw new ArtifactResolverError('containment_violation', `resolveContainedWorkerPath: absolute worker path "${workerRelPath}" rejected`);
74
+ }
75
+ const joined = path.resolve(rootReal, workerRelPath);
76
+ // Lexical containment check on the joined path BEFORE touching the FS (guards
77
+ // the case where the target itself does not exist yet).
78
+ const rootWithSep = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
79
+ if (joined !== rootReal && !joined.startsWith(rootWithSep)) {
80
+ throw new ArtifactResolverError('containment_violation', `resolveContainedWorkerPath: "${workerRelPath}" escapes worker root`);
81
+ }
82
+ // realpath the target and re-check containment — defeats a symlink inside the
83
+ // root that points outside it (lexical check alone would pass).
84
+ let targetReal;
85
+ try {
86
+ targetReal = fs.realpathSync(joined);
87
+ }
88
+ catch {
89
+ throw new ArtifactResolverError('source_missing', `resolveContainedWorkerPath: "${workerRelPath}" does not resolve to a file under the worker root`);
90
+ }
91
+ if (targetReal !== rootReal && !targetReal.startsWith(rootWithSep)) {
92
+ throw new ArtifactResolverError('containment_violation', `resolveContainedWorkerPath: "${workerRelPath}" resolves (via symlink) outside the worker root`);
93
+ }
94
+ return targetReal;
95
+ }
96
+ function sha256OfFile(absPath) {
97
+ const buf = fs.readFileSync(absPath);
98
+ return { sha256: crypto.createHash('sha256').update(buf).digest('hex'), byte_count: buf.length };
99
+ }
100
+ /**
101
+ * Copy a containment-validated worker source into the canonical store — atomically
102
+ * (temp + fsync + rename), with size/sha256 validation and per-turn idempotency
103
+ * (§7). Idempotent: if the deterministic target already holds the same bytes, this
104
+ * is a no-op; if it holds DIFFERENT bytes, that is a hard `canonical_hash_conflict`
105
+ * (a deterministic-id collision or corruption — never silently overwrite).
106
+ */
107
+ export function copyArtifactToCanonicalStore(input) {
108
+ const { loopId, artifactId, ext, sourceAbsPath, expectedSha256, expectedByteCount, cwd } = input;
109
+ if (!fs.existsSync(sourceAbsPath)) {
110
+ throw new ArtifactResolverError('source_missing', `copyArtifactToCanonicalStore: source ${sourceAbsPath} missing`);
111
+ }
112
+ // Read the source EXACTLY ONCE (review Finding 4): hash + validate + write the
113
+ // SAME buffer, so a source mutation between a validate-read and a copy-read can
114
+ // never let bytes whose hash differs from the reported/validated sha256 become
115
+ // canonical state.
116
+ const buf = fs.readFileSync(sourceAbsPath);
117
+ const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
118
+ const byte_count = buf.length;
119
+ // Validate against the attempt's declared expectations BEFORE any write.
120
+ if (expectedSha256 !== undefined && expectedSha256 !== sha256) {
121
+ throw new ArtifactResolverError('sha256_mismatch', `copyArtifactToCanonicalStore: sha256 ${sha256} != expected ${expectedSha256}`);
122
+ }
123
+ if (expectedByteCount !== undefined && expectedByteCount !== byte_count) {
124
+ throw new ArtifactResolverError('byte_count_mismatch', `copyArtifactToCanonicalStore: byte_count ${byte_count} != expected ${expectedByteCount}`);
125
+ }
126
+ const canonicalPath = canonicalArtifactPath(loopId, artifactId, ext, cwd);
127
+ // Idempotency: a matching target is a no-op; a mismatching target is a conflict.
128
+ if (fs.existsSync(canonicalPath)) {
129
+ const existing = sha256OfFile(canonicalPath);
130
+ if (existing.sha256 === sha256) {
131
+ return { canonicalPath, sha256, byte_count, idempotent: true };
132
+ }
133
+ throw new ArtifactResolverError('canonical_hash_conflict', `copyArtifactToCanonicalStore: ${canonicalPath} already exists with a DIFFERENT hash (${existing.sha256} vs ${sha256}) — refusing to overwrite`);
134
+ }
135
+ const dir = path.dirname(canonicalPath);
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ // Atomic temp-copy + fsync + rename of the ALREADY-HASHED buffer. The temp name
138
+ // is process/id-scoped so concurrent copies of distinct artifacts never collide.
139
+ const tmpPath = path.join(dir, `.${artifactId}.${process.pid}.tmp`);
140
+ const fd = fs.openSync(tmpPath, 'w');
141
+ try {
142
+ let off = 0;
143
+ while (off < buf.length)
144
+ off += fs.writeSync(fd, buf, off, buf.length - off);
145
+ fs.fsyncSync(fd);
146
+ }
147
+ finally {
148
+ fs.closeSync(fd);
149
+ }
150
+ try {
151
+ fs.renameSync(tmpPath, canonicalPath);
152
+ }
153
+ catch (err) {
154
+ // A racing writer may have created the target between our existence check and
155
+ // the rename. Re-check idempotency rather than clobbering.
156
+ fs.rmSync(tmpPath, { force: true });
157
+ if (fs.existsSync(canonicalPath) && sha256OfFile(canonicalPath).sha256 === sha256) {
158
+ return { canonicalPath, sha256, byte_count, idempotent: true };
159
+ }
160
+ throw err;
161
+ }
162
+ return { canonicalPath, sha256, byte_count, idempotent: false };
163
+ }
164
+ /**
165
+ * Read an artifact payload from the canonical store, falling back to the legacy
166
+ * `loops/threads/<loop_id>/artifacts/<ref>` path for pre-§7 artifacts. When BOTH
167
+ * exist, their hashes MUST match (reject-on-mismatch migration safety §7). An
168
+ * `expectedSha256` is validated against whichever copy is returned.
169
+ */
170
+ export function readCanonicalArtifact(loopId, artifactId, ext, opts = {}) {
171
+ const { legacyRef, expectedSha256, cwd } = opts;
172
+ const canonicalPath = canonicalArtifactPath(loopId, artifactId, ext, cwd);
173
+ const legacyPath = legacyRef ? path.join(legacyArtifactsDir(loopId, cwd), legacyRef) : undefined;
174
+ const canonicalExists = fs.existsSync(canonicalPath);
175
+ const legacyExists = legacyPath !== undefined && fs.existsSync(legacyPath);
176
+ if (!canonicalExists && !legacyExists) {
177
+ throw new ArtifactResolverError('artifact_missing', `readCanonicalArtifact: ${artifactId} not found (canonical nor legacy)`);
178
+ }
179
+ if (canonicalExists && legacyExists) {
180
+ // Migration overlap — both must agree, else refuse (never trust a divergent legacy copy).
181
+ const c = sha256OfFile(canonicalPath);
182
+ const l = sha256OfFile(legacyPath);
183
+ if (c.sha256 !== l.sha256) {
184
+ throw new ArtifactResolverError('canonical_hash_conflict', `readCanonicalArtifact: ${artifactId} canonical/legacy hash mismatch (${c.sha256} vs ${l.sha256})`);
185
+ }
186
+ }
187
+ const readPath = canonicalExists ? canonicalPath : legacyPath;
188
+ const buf = fs.readFileSync(readPath);
189
+ if (expectedSha256 !== undefined) {
190
+ const actual = crypto.createHash('sha256').update(buf).digest('hex');
191
+ if (actual !== expectedSha256) {
192
+ throw new ArtifactResolverError('sha256_mismatch', `readCanonicalArtifact: ${artifactId} sha256 ${actual} != expected ${expectedSha256}`);
193
+ }
194
+ }
195
+ return buf;
196
+ }
197
+ //# sourceMappingURL=artifact-resolver.js.map