bare-agent 0.26.0 → 0.26.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.
@@ -1,7 +1,7 @@
1
1
  # bareagent — Integration Guide
2
2
 
3
3
  > For AI assistants and developers wiring bareagent into a project.
4
- > v0.26.0 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | Apache 2.0
4
+ > v0.26.2 | Node.js >= 18 | zero required deps (`bareguard >=0.9.0 <0.13.0` optional peer for governance) | Apache 2.0
5
5
  >
6
6
  > Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
@@ -80,7 +80,7 @@
80
80
  "cron-parser": "^4.9.0"
81
81
  },
82
82
  "peerDependencies": {
83
- "bareguard": "^0.9.0",
83
+ "bareguard": ">=0.9.0 <0.13.0",
84
84
  "better-sqlite3": ">=9.0.0"
85
85
  },
86
86
  "peerDependenciesMeta": {
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "devDependencies": {
103
103
  "@types/node": "^22.19.19",
104
- "bareguard": "^0.9.0",
104
+ "bareguard": ">=0.9.0 <0.13.0",
105
105
  "litectx": "^0.26.0",
106
106
  "typescript": "^5.7.0"
107
107
  }
@@ -27,9 +27,9 @@ export type GateDecision = {
27
27
  */
28
28
  rule?: string | undefined;
29
29
  /**
30
- * - Human-readable reason.
30
+ * - Human-readable reason (bareguard's Decision emits null when absent).
31
31
  */
32
- reason?: string | undefined;
32
+ reason?: string | null | undefined;
33
33
  /**
34
34
  * - Arbitrary structured context.
35
35
  */
@@ -21,7 +21,7 @@ const { HaltError } = require('./errors');
21
21
  * @property {string} [outcome] - 'allow' when permitted.
22
22
  * @property {string} [severity] - 'halt' for halt-severity denials.
23
23
  * @property {string} [rule] - The matched rule name.
24
- * @property {string} [reason] - Human-readable reason.
24
+ * @property {string | null} [reason] - Human-readable reason (bareguard's Decision emits null when absent).
25
25
  * @property {Record<string, any>} [context] - Arbitrary structured context.
26
26
  */
27
27
 
@@ -165,39 +165,65 @@ class CLIPipeProvider {
165
165
 
166
166
  let stdout = '';
167
167
  let stderr = '';
168
- let killed = false;
169
168
 
170
- child.stdout.on('data', d => { stdout += d; this.onChunk?.(d.toString()); });
171
- child.stderr.on('data', d => { stderr += d; });
172
-
173
- child.on('error', err => {
174
- reject(new ProviderError(`[CLIPipeProvider] failed to spawn "${this.command}": ${err.message}`, /** @type {any} */ ({ status: 0 })));
175
- });
169
+ // Settle exactly once, no matter which combination of events fires. 'close' can be
170
+ // withheld indefinitely when the CLI spawns a grandchild that inherits its stdio pipes
171
+ // (the child exits, but the pipes stay open) — observed live as a generate() promise
172
+ // that never settled. Every path below funnels through settle().
173
+ let settled = false;
174
+ /** @type {NodeJS.Timeout[]} */
175
+ const timers = [];
176
+ const later = (fn, ms) => { timers.push(setTimeout(fn, ms)); };
177
+ const settle = (/** @type {Error|null} */ err, text = '') => {
178
+ if (settled) return;
179
+ settled = true;
180
+ for (const t of timers) clearTimeout(t);
181
+ if (err) reject(err); else resolve(text);
182
+ };
176
183
 
177
- child.on('close', code => {
178
- if (killed) return; // timeout already rejected
184
+ const finish = (/** @type {number|null} */ code) => {
179
185
  if (code !== 0) {
180
- return reject(new ProviderError(`[CLIPipeProvider] process exited with code ${code}: ${stderr.trim()}`, /** @type {any} */ ({ status: code })));
186
+ // The claude CLI reports errors on STDOUT (a JSON envelope) with stderr often
187
+ // empty — fall back to a stdout tail so the operator never sees a blank reason.
188
+ const detail = stderr.trim() || (stdout.trim() ? `(stderr empty) stdout: ${stdout.trim().slice(-400)}` : '');
189
+ return settle(new ProviderError(`[CLIPipeProvider] process exited with code ${code}: ${detail}`, /** @type {any} */ ({ status: code })));
181
190
  }
182
191
  const text = stdout.trim();
183
192
  if (!text) {
184
- return reject(new ProviderError('[CLIPipeProvider] process produced no output', /** @type {any} */ ({ status: 0 })));
193
+ return settle(new ProviderError('[CLIPipeProvider] process produced no output', /** @type {any} */ ({ status: 0 })));
194
+ }
195
+ settle(null, text);
196
+ };
197
+
198
+ child.stdout.on('data', d => {
199
+ stdout += d;
200
+ try {
201
+ this.onChunk?.(d.toString());
202
+ } catch (err) {
203
+ // an observer callback must fail the call loudly, never crash the host process
204
+ settle(new ProviderError(`[CLIPipeProvider] onChunk callback threw: ${/** @type {Error} */ (err).message}`, /** @type {any} */ ({ status: 0 })));
185
205
  }
186
- resolve(text);
206
+ });
207
+ child.stderr.on('data', d => { stderr += d; });
208
+
209
+ child.on('error', err => {
210
+ settle(new ProviderError(`[CLIPipeProvider] failed to spawn "${this.command}": ${err.message}`, /** @type {any} */ ({ status: 0 })));
187
211
  });
188
212
 
189
- // Timeout handling
190
- const timer = setTimeout(() => {
191
- killed = true;
213
+ // Primary completion path: all stdio drained.
214
+ child.on('close', code => finish(code));
215
+
216
+ // Fallback: the process exited but 'close' is being held open by inherited pipes.
217
+ // Give real drainage a short grace, then finish with what has arrived — a bounded
218
+ // wait, never a hang.
219
+ child.on('exit', code => later(() => finish(code), 2000));
220
+
221
+ later(() => {
192
222
  child.kill('SIGTERM');
193
- setTimeout(() => {
194
- try { child.kill('SIGKILL'); } catch (_) {}
195
- }, 1000);
196
- reject(new ProviderError(`[CLIPipeProvider] timed out after ${this.timeout}ms`, /** @type {any} */ ({ status: 0 })));
223
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch (_) {} }, 1000).unref?.();
224
+ settle(new ProviderError(`[CLIPipeProvider] timed out after ${this.timeout}ms`, /** @type {any} */ ({ status: 0 })));
197
225
  }, this.timeout);
198
226
 
199
- child.on('close', () => clearTimeout(timer));
200
-
201
227
  // Write prompt to stdin — catch errors silently (process may exit early)
202
228
  child.stdin.on('error', () => {});
203
229
  child.stdin.end(prompt);