pi-crew 0.9.46 → 0.9.48

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 (46) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +16 -2
  3. package/dist/build-meta.json +289 -164
  4. package/dist/index.mjs +1744 -2732
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +77 -0
  7. package/docs/decisions/2026-07-21-broker-windows-perms.md +91 -0
  8. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +99 -0
  9. package/docs/decisions/README.md +3 -0
  10. package/docs/publishing.md +29 -0
  11. package/package.json +3 -1
  12. package/scripts/build-bundle.mjs +7 -0
  13. package/scripts/postinstall.mjs +60 -1
  14. package/scripts/pty_probe.py +174 -0
  15. package/skills/real-test-pi-crew/SKILL.md +659 -0
  16. package/src/config/config.ts +42 -1
  17. package/src/config/defaults.ts +45 -1
  18. package/src/config/types.ts +19 -0
  19. package/src/extension/register.ts +6 -1
  20. package/src/extension/registration/context-builder.ts +4 -0
  21. package/src/extension/registration/lifecycle-handlers.ts +166 -3
  22. package/src/extension/registration/registration-types.ts +9 -0
  23. package/src/prompt/prompt-runtime.ts +108 -0
  24. package/src/runtime/broker-issuer.ts +37 -0
  25. package/src/runtime/child-pi-spawn.ts +53 -0
  26. package/src/runtime/child-pi.ts +42 -11
  27. package/src/runtime/crew-broker-child.ts +88 -0
  28. package/src/runtime/crew-broker-client.ts +673 -0
  29. package/src/runtime/crew-broker-tokens.ts +84 -0
  30. package/src/runtime/crew-broker.ts +1276 -0
  31. package/src/runtime/dynamic-workflow-context.ts +7 -3
  32. package/src/runtime/dynamic-workflow-runner.ts +1 -1
  33. package/src/runtime/plan-templates.ts +8 -6
  34. package/src/schema/config-schema.ts +14 -0
  35. package/src/state/mailbox.ts +43 -0
  36. package/src/ui/key-utils.ts +42 -0
  37. package/src/ui/keybinding-map.ts +29 -3
  38. package/src/ui/run-dashboard.ts +28 -0
  39. package/src/ui/settings-overlay.ts +42 -22
  40. package/src/utils/ndjson.ts +115 -0
  41. package/src/utils/session-utils.ts +30 -0
  42. package/src/utils/socket-path.ts +127 -0
  43. package/workflows/default.workflow.md +1 -1
  44. package/workflows/fast-fix.workflow.md +1 -1
  45. package/workflows/plan-execute.workflow.md +1 -1
  46. package/workflows/review.workflow.md +1 -1
@@ -9,9 +9,10 @@ import { atomicWriteFile } from "../state/atomic-write.ts";
9
9
  import type { WorkerExitStatus } from "../state/types.ts";
10
10
  import { logInternalError } from "../utils/internal-error.ts";
11
11
  import { redactSecretString } from "../utils/redaction.ts";
12
+ import { getActiveBrokerIssuer } from "./broker-issuer.ts";
12
13
  import { FINAL_DRAIN_MS, HARD_KILL_MS, POST_EXIT_STDIO_GUARD_MS, RESPONSE_TIMEOUT_MS } from "./child-pi-constants.ts";
13
14
  import { appendBoundedTail, clearHardKillTimer, killProcessTree, registerActiveChild, unregisterActiveChild } from "./child-pi-kill.ts";
14
- import { assertOnlyControlEnvKeys, buildChildPiSpawnOptions, prepareSpawnContext } from "./child-pi-spawn.ts";
15
+ import { buildFinalChildPiSpawnOptions, prepareSpawnContext } from "./child-pi-spawn.ts";
15
16
  import { ChildPiSteeringController } from "./child-pi-steering.ts";
16
17
  // Internal helpers for active-child bookkeeping (extracted to child-pi-kill.ts).
17
18
  import { ChildPiLineObserver } from "./child-pi-streams.ts";
@@ -25,7 +26,9 @@ export {
25
26
  // ── Re-export from child-pi-spawn.ts (H-7 decomposition step 6) ──
26
27
  // buildChildPiSpawnOptions was previously exported from child-pi.ts. Keep the
27
28
  // public API surface stable by re-exporting from the new module.
28
- export { buildChildPiSpawnOptions } from "./child-pi-spawn.ts";
29
+ // buildFinalChildPiSpawnOptions (BLOCKER 2 / S5) — composed spawn helper that
30
+ // owns the canary + filter + spread sequence; lives in child-pi-spawn.ts.
31
+ export { buildChildPiSpawnOptions, buildFinalChildPiSpawnOptions } from "./child-pi-spawn.ts";
29
32
  // ── Re-export from child-pi-streams.ts (H-7 decomposition step 4) ──
30
33
  export { ChildPiLineObserver } from "./child-pi-streams.ts";
31
34
 
@@ -142,6 +145,25 @@ export interface ChildPiRunInput {
142
145
  role?: string;
143
146
  /** Root directory for artifacts (used to validate transcriptPath). */
144
147
  artifactsRoot?: string;
148
+ /**
149
+ * Optional broker spawn context (Phase 0 inter-pi broker). When present,
150
+ * `prepareSpawnContext` injects `PI_CREW_BROKER_SOCKET` and
151
+ * `PI_CREW_BROKER_TOKEN` into the child env (control-namespace keys,
152
+ * accepted by `assertOnlyControlEnvKeys`). The token is a 128-bit random
153
+ * value issued by the parent's CrewBroker at spawn time; it lives only
154
+ * in the parent's in-memory `Map<runId, token>` and in the child's env.
155
+ * NEVER persisted to manifest, events, mailbox, or any run-dir file.
156
+ */
157
+ brokerSpawn?: { socketPath: string; token: string };
158
+ /**
159
+ * Optional Phase 0 broker credentials issuer. Called only when
160
+ * `brokerSpawn` is not already set on the input. The default no-op
161
+ * issuer returns undefined, leaving the child to run without broker
162
+ * credentials (today's behavior — file paths remain authoritative).
163
+ * The production wiring passes a closure that delegates to the
164
+ * session's `CrewBrokerLifecycleController.issueForChild`.
165
+ */
166
+ brokerIssuer?: (runId: string) => Promise<{ socketPath: string; token: string } | undefined>;
145
167
  }
146
168
 
147
169
  export interface ChildPiRunResult {
@@ -368,19 +390,28 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
368
390
  // prepareSpawnContext builds the worker args, attaches the steering file env,
369
391
  // and handles the pre-spawn abort check (returns an immediate-abort result
370
392
  // if the parent signal has already fired).
371
- const spawnPrep = prepareSpawnContext(input, effectiveTask);
393
+ //
394
+ // Phase 0 broker: if the caller did not pre-fill `brokerSpawn`, ask the
395
+ // optional issuer for one. The issuer is gated by the lifecycle controller
396
+ // (root-session + flag); a no-op issuer yields undefined (no credentials).
397
+ let brokerSpawn = input.brokerSpawn;
398
+ const brokerIssuer = input.brokerIssuer ?? getActiveBrokerIssuer();
399
+ if (!brokerSpawn && brokerIssuer && input.runId) {
400
+ try {
401
+ brokerSpawn = await brokerIssuer(input.runId);
402
+ } catch {
403
+ brokerSpawn = undefined;
404
+ }
405
+ }
406
+ const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask);
372
407
  if (spawnPrep.kind === "aborted") return spawnPrep.result;
373
408
  const { spawnSpec, mergedEnv, tempDir, builtEnv } = spawnPrep.ctx;
374
409
  try {
375
410
  return await new Promise<ChildPiRunResult>((resolve) => {
376
- // Runtime canary: verify built.env doesn't accidentally contain
377
- // secret keys. We assert on builtEnv (not mergedEnv) because mergedEnv
378
- // contains ALL process.env keys (PATH, HOME, SHELL, etc.) which is
379
- // expected; those are filtered by the allowlist in buildChildPiSpawnOptions
380
- // before reaching the child. The canary guards against accidental
381
- // additions to built.env leaking secrets to children.
382
- assertOnlyControlEnvKeys(builtEnv);
383
- const child = spawn(spawnSpec.command, spawnSpec.args, buildChildPiSpawnOptions(input.cwd, mergedEnv, input.model));
411
+ // Compose the final SpawnOptions: canary + filter + spread are now
412
+ // owned by buildFinalChildPiSpawnOptions (see child-pi-spawn.ts, BLOCKER 2 / S5).
413
+ const spawnOptions = buildFinalChildPiSpawnOptions(input.cwd, mergedEnv, builtEnv, input.model);
414
+ const child = spawn(spawnSpec.command, spawnSpec.args, spawnOptions);
384
415
  if (child.pid) {
385
416
  registerActiveChild(child.pid, child);
386
417
  input.onSpawn?.(child.pid);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * crew-broker-child.ts — Child-side broker bootstrap.
3
+ *
4
+ * Runs INSIDE a child pi worker (via prompt-runtime). When the parent injected
5
+ * broker credentials (`PI_CREW_BROKER_SOCKET` + `PI_CREW_BROKER_TOKEN` +
6
+ * `PI_CREW_BROKER_RUN_ID` + `PI_CREW_BROKER_TASK_ID`), it constructs a
7
+ * `CrewBrokerClient`, establishes the persistent connection, and routes
8
+ * pushed `mailbox.message` steer frames to the caller's `onSteer` handler
9
+ * (which sanitizes + delivers via `pi.sendMessage`).
10
+ *
11
+ * If any credential is absent (the default — flag off / not wired), this is a
12
+ * no-op: the child keeps using the file-poll steering path. The broker socket
13
+ * is a latency accelerator layered ON TOP of the durable file path, never a
14
+ * replacement — so a connect failure is invisible to the worker.
15
+ */
16
+
17
+ import type * as net from "node:net";
18
+ import { type BrokerEventFrame, CrewBrokerClient, type CrewBrokerClientOptions } from "./crew-broker-client.ts";
19
+
20
+ export interface ChildBrokerClientHandle {
21
+ /** True once the client has completed the handshake. */
22
+ readonly active: boolean;
23
+ /** Tear down the connection. Idempotent. */
24
+ close(): Promise<void>;
25
+ }
26
+
27
+ export interface StartChildBrokerClientOptions {
28
+ /** Env source (defaults to process.env). */
29
+ env?: NodeJS.ProcessEnv;
30
+ /** Invoked with the raw (untrusted) steer body and optional message id for
31
+ * every pushed `mailbox.message` whose kind === "steer". The caller MUST
32
+ * sanitize before delivering to the agent and may use `id` for dedup. */
33
+ onSteer?: (message: string, id?: string) => void;
34
+ /** Optional observer for every received event frame (diagnostics/tests). */
35
+ onEvent?: (event: BrokerEventFrame) => void;
36
+ /** Test seam: override the net module. */
37
+ netModule?: typeof net;
38
+ /** Test seam: override client construction. */
39
+ clientFactory?: (opts: CrewBrokerClientOptions) => CrewBrokerClient;
40
+ }
41
+
42
+ const NOOP_HANDLE: ChildBrokerClientHandle = {
43
+ active: false,
44
+ close: async () => {},
45
+ };
46
+
47
+ export function startChildBrokerClient(options: StartChildBrokerClientOptions = {}): ChildBrokerClientHandle {
48
+ const env = options.env ?? process.env;
49
+ const socketPath = env.PI_CREW_BROKER_SOCKET;
50
+ const token = env.PI_CREW_BROKER_TOKEN;
51
+ const runId = env.PI_CREW_BROKER_RUN_ID;
52
+ const taskId = env.PI_CREW_BROKER_TASK_ID;
53
+ if (!socketPath || !token || !runId || !taskId) {
54
+ return NOOP_HANDLE;
55
+ }
56
+
57
+ const factory = options.clientFactory ?? ((o: CrewBrokerClientOptions) => new CrewBrokerClient(o));
58
+ const client = factory({
59
+ runId,
60
+ taskId,
61
+ socketPath,
62
+ token,
63
+ netModule: options.netModule,
64
+ onEvent: (ev) => {
65
+ options.onEvent?.(ev);
66
+ if (ev.event === "mailbox.message" && options.onSteer) {
67
+ const data = ev.data as { id?: unknown; kind?: unknown; body?: unknown } | undefined;
68
+ if (data && data.kind === "steer" && typeof data.body === "string") {
69
+ options.onSteer(data.body, typeof data.id === "string" ? data.id : undefined);
70
+ }
71
+ }
72
+ },
73
+ });
74
+
75
+ // Establish the persistent connection so the broker can push events. This
76
+ // is fire-and-forget: on any failure the client becomes sticky-fallback and
77
+ // the file-poll steering path remains fully functional.
78
+ void client.reconnect().catch(() => {});
79
+
80
+ return {
81
+ get active() {
82
+ return client.mode === "connected";
83
+ },
84
+ close: async () => {
85
+ await client.close();
86
+ },
87
+ };
88
+ }