pi-crew 0.9.62 → 0.9.65

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 (60) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +47 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/explorer.md +1 -1
  5. package/agents/planner.md +1 -1
  6. package/agents/reviewer.md +1 -1
  7. package/agents/security-reviewer.md +1 -1
  8. package/agents/test-engineer.md +1 -1
  9. package/agents/writer.md +1 -1
  10. package/dist/index.mjs +478 -366
  11. package/package.json +7 -3
  12. package/scripts/analyze-run.mjs +1333 -0
  13. package/scripts/pty_probe.py +10 -8
  14. package/scripts/resource-sampler.mjs +482 -0
  15. package/skills/real-test-pi-crew/SKILL.md +6 -6
  16. package/src/agents/agent-config.ts +4 -0
  17. package/src/agents/agent-serializer.ts +1 -0
  18. package/src/agents/discover-agents.ts +8 -0
  19. package/src/config/role-tools.ts +49 -1
  20. package/src/observability/event-to-metric.ts +29 -0
  21. package/src/observability/metrics-primitives.ts +41 -3
  22. package/src/prompt/prompt-runtime.ts +6 -0
  23. package/src/prompt/scratchpad-lifecycle.ts +605 -0
  24. package/src/runtime/README.md +1 -1
  25. package/src/runtime/broker/crew-broker.ts +0 -16
  26. package/src/runtime/child-pi/child-pi-spawn.ts +42 -1
  27. package/src/runtime/child-pi/child-pi.ts +5 -0
  28. package/src/runtime/effectiveness.ts +23 -1
  29. package/src/runtime/merge-gate.ts +202 -0
  30. package/src/runtime/model/model-fallback.ts +11 -0
  31. package/src/runtime/model/pi-args.ts +1 -1
  32. package/src/runtime/model/provider-extensions.ts +31 -12
  33. package/src/runtime/output/progress-tracker.ts +3 -33
  34. package/src/runtime/recovery/crash-recovery.ts +1 -1
  35. package/src/runtime/scratchpad/README.md +184 -0
  36. package/src/runtime/scratchpad/engine.ts +648 -0
  37. package/src/runtime/scratchpad/guest.ts +360 -0
  38. package/src/runtime/scratchpad/index.ts +22 -0
  39. package/src/runtime/scratchpad/protocol.ts +88 -0
  40. package/src/runtime/scratchpad/snapshot-hmac.ts +161 -0
  41. package/src/runtime/scratchpad/snapshot-lookup.ts +74 -0
  42. package/src/runtime/scratchpad/transform.ts +363 -0
  43. package/src/runtime/task-runner/child-executor.ts +48 -31
  44. package/src/runtime/team-runner.ts +128 -203
  45. package/src/schema/team-tool-schema.ts +2 -0
  46. package/src/teams/discover-teams.ts +2 -0
  47. package/src/teams/team-config.ts +7 -0
  48. package/src/teams/team-serializer.ts +1 -0
  49. package/src/ui/mascot.ts +1 -14
  50. package/teams/default.team.md +1 -0
  51. package/teams/fast-fix.team.md +1 -0
  52. package/src/observability/event-bus.ts +0 -86
  53. package/src/plugins/plugin-define.ts +0 -6
  54. package/src/plugins/plugin-registry.ts +0 -32
  55. package/src/plugins/plugins/index.ts +0 -3
  56. package/src/plugins/plugins/nextjs.ts +0 -19
  57. package/src/plugins/plugins/vite.ts +0 -10
  58. package/src/plugins/plugins/vitest.ts +0 -9
  59. package/src/runtime/child-pi/child-pi-pool.ts +0 -68
  60. package/src/runtime/iteration-hooks.ts +0 -305
@@ -3,11 +3,17 @@
3
3
  * Uses the excludeTools option from pi v0.77.0.
4
4
  */
5
5
 
6
+ import { permissionForRole } from "../runtime/role-permission.ts";
7
+
6
8
  export interface RoleToolConfig {
7
9
  /** Explicit list of tools to use (if undefined, use all default tools) */
8
10
  tools?: string[];
9
11
  /** Tools to exclude from the default set */
10
12
  excludeTools?: string[];
13
+ /** Phase 1 scratchpad: opt-in the persistent Bun-free JS evaluator (execute tool)
14
+ * for this role. Only effective on WRITE roles — read-only roles are gated
15
+ * out by `isScratchpadEnabledForRole` (S-6, privilege-elevation guard). */
16
+ scratchpad?: boolean;
11
17
  }
12
18
 
13
19
  export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
@@ -44,9 +50,11 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
44
50
  excludeTools: ["edit", "write", "bash", "web"],
45
51
  },
46
52
 
47
- // Executor - Full access (default)
53
+ // Executor - Full access (default). Phase 1 scratchpad-enabled (stateful
54
+ // evaluator compounds intermediate results across execute calls).
48
55
  executor: {
49
56
  // No restrictions - full tool access
57
+ scratchpad: true,
50
58
  },
51
59
 
52
60
  // Reviewer - Read and review, no write
@@ -78,12 +86,16 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
78
86
  verifier: {
79
87
  tools: ["read", "grep", "find", "ls", "bash"],
80
88
  excludeTools: ["edit", "write", "web"],
89
+ // Phase 1 scratchpad: multi-cell test/verify flows reuse parsed state.
90
+ scratchpad: true,
81
91
  },
82
92
 
83
93
  // Test Engineer - Can write tests (F1: hyphenated key)
84
94
  "test-engineer": {
85
95
  tools: ["read", "edit", "write", "bash", "ls"],
86
96
  excludeTools: ["web"],
97
+ // Phase 1 scratchpad: build/run test suites with state across cells.
98
+ scratchpad: true,
87
99
  },
88
100
  };
89
101
 
@@ -116,3 +128,39 @@ export function getRestrictedRoles(): string[] {
116
128
  .filter(([, config]) => config.tools !== undefined || config.excludeTools !== undefined)
117
129
  .map(([role]) => role);
118
130
  }
131
+
132
+ /** Agent frontmatter shape consumed by the scratchpad opt-in check. */
133
+ export interface ScratchpadAgentOption {
134
+ scratchpad?: boolean;
135
+ }
136
+
137
+ /**
138
+ * Phase 1: is the scratchpad (execute tool) enabled for this role/agent?
139
+ *
140
+ * Decision order (load-bearing — do not reorder):
141
+ * 1. S-6 (SECURITY, privilege-elevation guard): read-only roles NEVER enable,
142
+ * regardless of any agent frontmatter opt-in. A `scratchpad: true` on a
143
+ * read-only role (e.g. security-reviewer, whose tool-set is read/grep/find
144
+ * with NO bash) would grant full-trust JS execution (fs/network/env/
145
+ * child_process) inside a role designed to have no execution — a real
146
+ * elevation. `permissionForRole` is default-deny (unknown → read_only), so
147
+ * typos/underscore/case-drift cannot bypass this.
148
+ * 2. F6 (kill-switch): `agent.scratchpad === false` WINS over the role default,
149
+ * giving an emergency off-switch for an experimental full-trust feature.
150
+ * 3. agent explicit opt-in (`scratchpad: true`) OR role default (`scratchpad:
151
+ * true` in ROLE_TOOL_CONFIGS).
152
+ *
153
+ * F10: `permissionForRole` and `getToolConfig` both expect HYPHENATED role
154
+ * strings (runtime convention); a raw underscore role would default-deny
155
+ * wrongly, so normalize FIRST.
156
+ */
157
+ export function isScratchpadEnabledForRole(role: string, agent?: ScratchpadAgentOption): boolean {
158
+ // F10: normalize underscore→hyphen before every downstream lookup.
159
+ const normalized = role.includes("_") ? role.replaceAll("_", "-") : role;
160
+ // S-6 (điều kiện tiên quyết): read-only gate trước mọi agent flag.
161
+ if (permissionForRole(normalized) === "read_only") return false;
162
+ // F6: explicit-false agent override (kill-switch) wins over role default.
163
+ if (agent?.scratchpad === false) return false;
164
+ // agent explicit opt-in OR role default.
165
+ return agent?.scratchpad === true || getToolConfig(normalized).scratchpad === true;
166
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { MetricRegistry } from "./metric-registry.ts";
3
+ import { getCardinalityEvictions } from "./metrics-primitives.ts";
3
4
 
4
5
  function recordValue(value: unknown): Record<string, unknown> {
5
6
  return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
@@ -41,6 +42,19 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
41
42
  const deadletterCount = registry.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
42
43
  const overflowCount = registry.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
43
44
  const supervisorContactCount = registry.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
45
+ const unboundedConcurrencyCount = registry.counter(
46
+ "crew.limits.unbounded_total",
47
+ "Runs that enabled allowUnboundedConcurrency (advisory; bypasses hard cap)",
48
+ );
49
+ // Gauge reflecting cumulative label-combination evictions across all
50
+ // metrics in this process. Updated lazily on every metric event so the
51
+ // value stays fresh without a separate timer. Non-zero = aggregation
52
+ // for high-cardinality labels is unreliable. See metrics-primitives.ts
53
+ // `enforceLabelCap`.
54
+ const cardinalityEvictedGauge = registry.gauge(
55
+ "crew.metrics.cardinality_evicted",
56
+ "Cumulative label-combination evictions (non-zero = unreliable aggregation)",
57
+ );
44
58
  registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
45
59
  const runDuration = registry.histogram(
46
60
  "crew.run.duration_ms",
@@ -146,11 +160,26 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
146
160
  });
147
161
  },
148
162
  ],
163
+ [
164
+ "crew.limits.unbounded",
165
+ () => {
166
+ // Fires when a run enables allowUnboundedConcurrency (bypasses the
167
+ // hard cap of 8 and the worker cap). One emit per affected run.
168
+ unboundedConcurrencyCount.inc({});
169
+ },
170
+ ],
149
171
  ];
150
172
 
151
173
  const unsubscribers: Array<() => void> = [];
152
174
  for (const [event, handler] of handlers) {
153
175
  const unsubscribe = events?.on?.(event, (data: unknown) => {
176
+ // Refresh the cardinality-eviction gauge on every event so the
177
+ // value reflects the latest eviction state without a timer.
178
+ try {
179
+ cardinalityEvictedGauge.set({}, getCardinalityEvictions());
180
+ } catch {
181
+ /* gauge refresh must never break event delivery */
182
+ }
154
183
  try {
155
184
  handler(data);
156
185
  } catch {
@@ -36,13 +36,51 @@ 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. */
39
+ /**
40
+ * Maximum number of unique label combinations per metric.
41
+ *
42
+ * When this cap is reached, the oldest label combination is silently
43
+ * evicted (FIFO by insertion order, with MRU promotion on use). Read
44
+ * {@link getCardinalityEvictions} to detect when aggregation has become
45
+ * unreliable for high-cardinality labels.
46
+ */
40
47
  const MAX_LABEL_COMBINATIONS = 10_000;
41
48
 
42
- function enforceLabelCap(map: Map<string, unknown>, metricName: string): void {
49
+ /**
50
+ * Cumulative count of label-combination evictions across all metrics in
51
+ * this process. Incremented every time {@link enforceLabelCap} drops an
52
+ * entry. Exported so observability wiring and OTLP/Prometheus exporters
53
+ * can surface `crew.metrics.cardinality_evicted` without risking the
54
+ * recursion of routing this signal through a `Counter` (a Counter itself
55
+ * consumes a label slot and could self-evict).
56
+ */
57
+ let cardinalityEvictions = 0;
58
+
59
+ /**
60
+ * Returns the cumulative number of metric label-combination evictions
61
+ * that have occurred in this process. A non-zero value means at least
62
+ * one metric exceeded {@link MAX_LABEL_COMBINATIONS} and silently dropped
63
+ * the oldest label combination — aggregation for high-cardinality labels
64
+ * is unreliable.
65
+ */
66
+ export function getCardinalityEvictions(): number {
67
+ return cardinalityEvictions;
68
+ }
69
+
70
+ /** Reset the eviction counter. Intended for tests only. */
71
+ export function _resetCardinalityEvictionsForTests(): void {
72
+ cardinalityEvictions = 0;
73
+ }
74
+
75
+ function enforceLabelCap(map: Map<string, unknown>, _metricName: string): void {
43
76
  while (map.size > MAX_LABEL_COMBINATIONS) {
44
77
  const firstKey = map.keys().next().value;
45
- if (firstKey !== undefined) map.delete(firstKey);
78
+ if (firstKey !== undefined) {
79
+ map.delete(firstKey);
80
+ cardinalityEvictions++;
81
+ } else {
82
+ break;
83
+ }
46
84
  }
47
85
  }
48
86
 
@@ -4,6 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { startChildBrokerClient } from "../runtime/broker/crew-broker-child.ts";
5
5
  import { logInternalError } from "../utils/internal-error.ts";
6
6
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
7
+ import { registerScratchpadLifecycle } from "./scratchpad-lifecycle.ts";
7
8
 
8
9
  export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
9
10
  export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
@@ -377,4 +378,9 @@ export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
377
378
  if (rewritten === event.systemPrompt) return;
378
379
  return { systemPrompt: rewritten };
379
380
  });
381
+
382
+ // ── Phase 1 scratchpad (T6): conditional `execute` tool + EngineManager
383
+ // lifecycle. Registers nothing when PI_CREW_SCRATCHPAD !== "1" (D3) and
384
+ // only flushes+kills the engine on session_shutdown reason "quit" (F3).
385
+ registerScratchpadLifecycle(pi);
380
386
  }