pi-crew 0.5.18 → 0.5.19

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.19] — Final Sweep: 8 MEDIUM/LOW Fixes + 2 Test Fixes (2026-06-03)
4
+
5
+ ### Highlights
6
+ - **All remaining issues fixed** — 4-agent review sweep found 0 CRITICAL/HIGH
7
+ - 2 pre-existing test failures fixed (env isolation)
8
+ - Memory bounds added to security log and metrics primitives
9
+ - Defensive path validation in streaming/sidechain output
10
+ - Production cleanup now clears hooks
11
+
12
+ ### Fixes
13
+
14
+ #### MEDIUM: Memory bounds
15
+ - `securityEventLog` in `discover-agents.ts` capped at 1,000 entries (was unbounded)
16
+ - `Counter`/`Gauge`/`Histogram` Maps in `metrics-primitives.ts` capped at 10,000 label combinations
17
+
18
+ #### LOW: Code quality
19
+ - `console.warn` → `logInternalError` in `settings-store.ts` and `discover-agents.ts`
20
+ - `crewEventBus` dead code documented (retained for future use)
21
+ - `clearHooks()` called in production cleanup path (`register.ts`)
22
+ - `assertSafePathId` added to `streaming-output.ts` and `sidechain-output.ts`
23
+
24
+ #### Test fixes
25
+ - `adaptive-implementation.test.ts`: replaced `restoreEnv` with `delete` to prevent leaked `PI_CREW_ROLE`
26
+ - `subagent-tools-integration.test.ts`: added env isolation to first test case
27
+
28
+ ### Stats
29
+ - Test suite: 2688 pass + 1 skip, 0 fail
30
+ - TypeScript: 0 errors
31
+ - Files changed: 9
32
+
3
33
  ## [0.5.18] — Final Review Fixes (2026-06-03)
4
34
 
5
35
  ### Highlights
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.5.18",
3
+ "version": "0.5.19",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -103,7 +103,9 @@ interface SecurityEvent {
103
103
 
104
104
  /**
105
105
  * Security event log. In production, this should be sent to a security SIEM.
106
+ * Bounded at MAX_SECURITY_LOG_ENTRIES to prevent unbounded memory growth.
106
107
  */
108
+ const MAX_SECURITY_LOG_ENTRIES = 1000;
107
109
  const securityEventLog: SecurityEvent[] = [];
108
110
 
109
111
  /**
@@ -113,11 +115,16 @@ const securityEventLog: SecurityEvent[] = [];
113
115
  */
114
116
  function logSecurityEvent(event: SecurityEvent): void {
115
117
  securityEventLog.push(event);
118
+ // Evict oldest entries when cap exceeded
119
+ while (securityEventLog.length > MAX_SECURITY_LOG_ENTRIES) {
120
+ securityEventLog.shift();
121
+ }
116
122
 
117
- // Console output for development/debugging (redacted in production)
118
- const prefix = "\x1b[33m[SECURITY]\x1b[0m"; // Yellow warning
119
- console.warn(
120
- `${prefix} ${event.type}: agent="${event.name}" reason="${event.reason}" time=${new Date(event.timestamp).toISOString()}`
123
+ // Log security events via structured logger
124
+ logInternalError(
125
+ `security.${event.type}`,
126
+ undefined,
127
+ `agent="${event.name}" reason="${event.reason}"`,
121
128
  );
122
129
  }
123
130
 
@@ -195,10 +202,10 @@ function checkProjectAgentShadowsBuiltin(name: string): void {
195
202
  reason: "project_shadows_protected_builtin",
196
203
  timestamp: Date.now(),
197
204
  });
198
- console.warn(
199
- `\x1b[33m[SECURITY WARNING]\x1b[0m Project agent "${name}" shadows a protected builtin. ` +
200
- `This agent will be loaded but builtin agents take priority. ` +
201
- `If this is intentional, consider using a different name.`
205
+ logInternalError(
206
+ `security.agent_shadow_warning`,
207
+ undefined,
208
+ `Project agent "${name}" shadows a protected builtin. Builtin agents take priority.`,
202
209
  );
203
210
  return;
204
211
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from "./async-notifier.ts";
19
19
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
20
20
  import { registerCleanupHandler } from "./crew-cleanup.ts";
21
+ import { clearHooks } from "../hooks/registry.ts";
21
22
  import { notifyActiveRuns } from "./session-summary.ts";
22
23
 
23
24
  let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
@@ -1109,6 +1110,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1109
1110
  otlpExporter = undefined;
1110
1111
  metricRegistry = undefined;
1111
1112
  deliveryCoordinator?.dispose();
1113
+ clearHooks();
1112
1114
  overflowTracker?.dispose();
1113
1115
  deliveryCoordinator = undefined;
1114
1116
  overflowTracker = undefined;
@@ -71,4 +71,12 @@ class EventBus {
71
71
  }
72
72
  }
73
73
 
74
+ /**
75
+ * Global event bus for crew lifecycle events.
76
+ *
77
+ * NOTE: Currently only emits — no production subscribers yet.
78
+ * The `runEventBus` (from `ui/run-event-bus.ts`) is the active event system.
79
+ * This bus is retained for future observability/SIEM integration.
80
+ * See also: progress-tracker.ts which emits agent:progress events.
81
+ */
74
82
  export const crewEventBus = EventBus.getInstance();
@@ -36,6 +36,16 @@ interface StoredHistogram {
36
36
 
37
37
  export const DEFAULT_HISTOGRAM_BUCKETS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] as const;
38
38
 
39
+ /** Maximum number of unique label combinations per metric. */
40
+ const MAX_LABEL_COMBINATIONS = 10_000;
41
+
42
+ function enforceLabelCap(map: Map<string, unknown>, metricName: string): void {
43
+ while (map.size > MAX_LABEL_COMBINATIONS) {
44
+ const firstKey = map.keys().next().value;
45
+ if (firstKey !== undefined) map.delete(firstKey);
46
+ }
47
+ }
48
+
39
49
  function normalizeLabels(labels: MetricLabels = {}): MetricLabels {
40
50
  const normalized: MetricLabels = {};
41
51
  for (const [key, value] of Object.entries(labels).sort(([left], [right]) => left.localeCompare(right))) normalized[key] = value;
@@ -70,6 +80,7 @@ export class Counter extends Metric {
70
80
  const key = labelKey(labels);
71
81
  const current = this.values.get(key) ?? { labels: normalizeLabels(labels), value: 0 };
72
82
  this.values.set(key, { labels: current.labels, value: current.value + delta });
83
+ enforceLabelCap(this.values, this.name);
73
84
  }
74
85
 
75
86
  value(labels: MetricLabels = {}): number {
@@ -87,6 +98,7 @@ export class Gauge extends Metric {
87
98
  set(labels: MetricLabels = {}, value: number): void {
88
99
  if (!Number.isFinite(value)) return;
89
100
  this.values.set(labelKey(labels), { labels: normalizeLabels(labels), value });
101
+ enforceLabelCap(this.values, this.name);
90
102
  }
91
103
 
92
104
  add(labels: MetricLabels = {}, delta: number): void {
@@ -123,6 +135,7 @@ export class Histogram extends Metric {
123
135
  current.sum += value;
124
136
  current.count += 1;
125
137
  if (!existing) this.observations.set(key, current);
138
+ enforceLabelCap(this.observations, this.name);
126
139
  }
127
140
 
128
141
  quantile(labels: MetricLabels = {}, q: number): number {
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { homedir } from "node:os";
4
+ import { logInternalError } from "../utils/internal-error.ts";
4
5
  import type { JoinMode } from "./group-join.ts";
5
6
 
6
7
  export interface CrewSettings {
@@ -71,7 +72,7 @@ function readSettingsFile(filePath: string): CrewSettings {
71
72
  return sanitizeSettings(JSON.parse(fs.readFileSync(filePath, "utf-8")));
72
73
  } catch (err) {
73
74
  const reason = err instanceof Error ? err.message : String(err);
74
- console.warn(`[pi-crew] Ignoring malformed settings at ${filePath}: ${reason}`);
75
+ logInternalError("settings-store.read", err, `Ignoring malformed settings at ${filePath}`);
75
76
  return {};
76
77
  }
77
78
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { isSafePathId } from "../utils/safe-paths.ts";
3
4
  import { redactSecrets } from "../utils/redaction.ts";
4
5
 
5
6
  export interface SidechainEntry {
@@ -17,6 +18,7 @@ export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry
17
18
  }
18
19
 
19
20
  export function sidechainOutputPath(stateRoot: string, taskId: string): string {
21
+ if (!isSafePathId(taskId)) throw new Error(`Invalid taskId: ${taskId}`);
20
22
  return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
21
23
  }
22
24
 
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { isSafePathId } from "../utils/safe-paths.ts";
3
4
  import type { TeamRunManifest } from "../state/types.ts";
4
5
 
5
6
  export interface StreamingOutputHandle {
@@ -9,6 +10,7 @@ export interface StreamingOutputHandle {
9
10
  }
10
11
 
11
12
  export function createStreamingOutput(manifest: TeamRunManifest, taskId: string): StreamingOutputHandle {
13
+ if (!isSafePathId(taskId)) throw new Error(`Invalid taskId: ${taskId}`);
12
14
  const outputDir = path.join(manifest.artifactsRoot, "streaming");
13
15
  fs.mkdirSync(outputDir, { recursive: true });
14
16
  const outputPath = path.join(outputDir, `${taskId}.md`);
@@ -37,6 +39,7 @@ export function createStreamingOutput(manifest: TeamRunManifest, taskId: string)
37
39
  }
38
40
 
39
41
  export function readStreamingOutput(manifest: TeamRunManifest, taskId: string): string {
42
+ if (!isSafePathId(taskId)) return "";
40
43
  const outputPath = path.join(manifest.artifactsRoot, "streaming", `${taskId}.md`);
41
44
  if (!fs.existsSync(outputPath)) return "";
42
45
  try {