pi-crew 0.6.1 → 0.6.3
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 +160 -0
- package/README.md +70 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +194 -34
- package/src/extension/registration/commands.ts +2 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
package/src/hooks/registry.ts
CHANGED
|
@@ -5,19 +5,42 @@ import { runEventBus } from "../ui/run-event-bus.ts";
|
|
|
5
5
|
|
|
6
6
|
const registry = new Map<HookName, HookDefinition[]>();
|
|
7
7
|
|
|
8
|
+
// Track hook IDs registered by pi-crew for scope-aware cleanup
|
|
9
|
+
const _piCrewHookIds = new Set<number>();
|
|
10
|
+
let _nextHookId = 1;
|
|
11
|
+
|
|
8
12
|
// SECURITY: Hooks are currently global (registered once, applied to all workspaces).
|
|
9
13
|
// For multi-workspace environments, consider filtering hooks by workspace scope:
|
|
10
14
|
// const workspaceHooks = getHooks(name).filter(h => !h.workspaceId || h.workspaceId === ctx.workspaceId);
|
|
11
15
|
// This prevents globally-registered hooks from operating on runs they weren't designed for.
|
|
12
16
|
|
|
13
|
-
export function registerHook(definition: HookDefinition):
|
|
17
|
+
export function registerHook(definition: HookDefinition): number {
|
|
18
|
+
const hookId = _nextHookId++;
|
|
19
|
+
_piCrewHookIds.add(hookId);
|
|
14
20
|
const hooks = registry.get(definition.name) ?? [];
|
|
15
|
-
hooks.push(definition);
|
|
21
|
+
hooks.push({ ...definition, _hookId: hookId });
|
|
16
22
|
registry.set(definition.name, hooks);
|
|
23
|
+
return hookId;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export function clearHooks(): void {
|
|
20
27
|
registry.clear();
|
|
28
|
+
_piCrewHookIds.clear();
|
|
29
|
+
_nextHookId = 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Scope-aware hook clearing: only removes hooks registered by pi-crew
|
|
33
|
+
export function clearHooksScoped(): void {
|
|
34
|
+
for (const [name, hooks] of registry) {
|
|
35
|
+
const remaining = hooks.filter((h) => !("_hookId" in h && _piCrewHookIds.has((h as { _hookId?: number })._hookId ?? -1)));
|
|
36
|
+
if (remaining.length === 0) {
|
|
37
|
+
registry.delete(name);
|
|
38
|
+
} else {
|
|
39
|
+
registry.set(name, remaining);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
_piCrewHookIds.clear();
|
|
43
|
+
_nextHookId = 1;
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
export function getHooks(name: HookName): HookDefinition[] {
|
|
@@ -27,28 +50,69 @@ export function getHooks(name: HookName): HookDefinition[] {
|
|
|
27
50
|
export async function executeHook(name: HookName, ctx: HookContext): Promise<HookExecutionReport> {
|
|
28
51
|
const hooks = getHooks(name);
|
|
29
52
|
if (hooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
|
|
30
|
-
// SECURITY:
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
53
|
+
// SECURITY: Filter hooks by workspace scope.
|
|
54
|
+
// - Global hooks (no workspaceId) match all contexts UNLESS ctx explicitly
|
|
55
|
+
// opts out via `includeGlobalHooks: false`. This is the documented
|
|
56
|
+
// behavior: "Hooks without workspaceId match ALL workspaces".
|
|
57
|
+
// - Scoped hooks (workspaceId set) require an exact match with ctx.workspaceId.
|
|
58
|
+
// - If ctx has no workspaceId, scoped hooks are excluded (they can't match).
|
|
59
|
+
// - `includeGlobalHooks: true` on ctx forces inclusion of global hooks even
|
|
60
|
+
// when ctx has a workspaceId.
|
|
61
|
+
const scopedHooks = hooks.filter((h) => {
|
|
62
|
+
// Scoped hook: must match ctx.workspaceId exactly
|
|
63
|
+
if (h.workspaceId !== undefined) {
|
|
64
|
+
return h.workspaceId === ctx.workspaceId;
|
|
65
|
+
}
|
|
66
|
+
// Global hook (no workspaceId): include unless ctx explicitly excludes
|
|
67
|
+
return ctx.includeGlobalHooks !== false;
|
|
68
|
+
});
|
|
37
69
|
if (scopedHooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
|
|
38
|
-
const POLLUTED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
70
|
+
const POLLUTED_KEYS = new Set(["__proto__", "constructor", "prototype", "hasOwnProperty", "toString", "valueOf", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__"].map((k) => k.toLowerCase().normalize("NFKC")));
|
|
39
71
|
function sanitizeMergeData(data: Record<string, unknown>): Record<string, unknown> {
|
|
40
72
|
const clean: Record<string, unknown> = {};
|
|
41
73
|
for (const [k, v] of Object.entries(data)) {
|
|
42
|
-
if (!POLLUTED_KEYS.has(k))
|
|
74
|
+
if (!POLLUTED_KEYS.has(k.toLowerCase())) {
|
|
75
|
+
if (v !== null && typeof v === "object") {
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
// Sanitize array elements that are objects
|
|
78
|
+
clean[k] = v.map((item) => (item !== null && typeof item === "object" && !Array.isArray(item) ? sanitizeMergeData(item as Record<string, unknown>) : item));
|
|
79
|
+
} else {
|
|
80
|
+
clean[k] = sanitizeMergeData(v as Record<string, unknown>);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
clean[k] = v;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
43
86
|
}
|
|
44
87
|
return clean;
|
|
45
88
|
}
|
|
89
|
+
// Sanitize ctx by stripping dangerous property names before passing to handlers.
|
|
90
|
+
// Hook authors must NOT set these keys directly on ctx: [...POLLUTED_KEYS]
|
|
91
|
+
// This sanitization runs at the start of executeHook to prevent prototype pollution attacks.
|
|
92
|
+
function sanitizeContext(ctx: HookContext): HookContext {
|
|
93
|
+
for (const key of Object.keys(ctx)) {
|
|
94
|
+
if (POLLUTED_KEYS.has(key.toLowerCase())) {
|
|
95
|
+
delete ctx[key];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ctx;
|
|
99
|
+
}
|
|
100
|
+
function sanitizeErrorMessage(message: string): string {
|
|
101
|
+
// Remove file paths, environment variable references, and other potentially sensitive data
|
|
102
|
+
return message
|
|
103
|
+
.replace(/\/[^:\s]+/g, "[path]")
|
|
104
|
+
.replace(/\b[A-Z_0-9]+\s*=/g, "[env]")
|
|
105
|
+
.replace(/\b\d+\.\d+\.\d+\.\d+\b/g, "[ip]");
|
|
106
|
+
}
|
|
46
107
|
const start = Date.now();
|
|
47
108
|
const diagnostics: string[] = [];
|
|
48
109
|
let capturedModifications: Record<string, unknown> | undefined;
|
|
49
110
|
for (const hook of scopedHooks) {
|
|
50
111
|
try {
|
|
51
|
-
const result: HookResult = await hook.handler(ctx);
|
|
112
|
+
const result: HookResult = await hook.handler(sanitizeContext(ctx));
|
|
113
|
+
// SECURITY: Sanitize any direct mutations the handler may have made to ctx.
|
|
114
|
+
// This prevents hooks from injecting dangerous properties via direct ctx assignment.
|
|
115
|
+
sanitizeContext(ctx);
|
|
52
116
|
if (hook.mode === "blocking" && result.outcome === "block") {
|
|
53
117
|
return { hookName: name, outcome: "block", durationMs: Date.now() - start, reason: result.reason };
|
|
54
118
|
}
|
|
@@ -57,7 +121,7 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
|
|
|
57
121
|
capturedModifications = { ...result.data };
|
|
58
122
|
}
|
|
59
123
|
} catch (error) {
|
|
60
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
const message = sanitizeErrorMessage(error instanceof Error ? error.message : String(error));
|
|
61
125
|
if (hook.mode === "blocking") {
|
|
62
126
|
return { hookName: name, outcome: "block", durationMs: Date.now() - start, reason: `Hook error: ${message}` };
|
|
63
127
|
}
|
package/src/hooks/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface HookContext {
|
|
|
31
31
|
runId: string;
|
|
32
32
|
taskId?: string;
|
|
33
33
|
cwd: string;
|
|
34
|
+
// NOTE: Hooks receive a shared mutable context object. A hook may directly set
|
|
35
|
+
// properties on ctx, but MUST NOT set dangerous names like "__proto__",
|
|
36
|
+
// "constructor", "prototype", etc. (see registry.ts POLLUTED_KEYS).
|
|
37
|
+
// Hook authors are trusted but must avoid prototype pollution attacks.
|
|
38
|
+
// The sanitizeMergeData function prevents dangerous properties from propagating
|
|
39
|
+
// via result.data merge, and a runtime guard sanitizes ctx before passing to
|
|
40
|
+
// handlers AND after handler execution to catch direct mutations.
|
|
34
41
|
[key: string]: unknown;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -47,6 +54,8 @@ export interface HookDefinition {
|
|
|
47
54
|
// SECURITY: Optional workspace scoping. When set, the hook only executes for
|
|
48
55
|
// runs in the specified workspace. When absent, the hook applies to all runs.
|
|
49
56
|
workspaceId?: string;
|
|
57
|
+
/** @internal Internal hook ID for scope-aware cleanup. */
|
|
58
|
+
_hookId?: number;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export interface HookExecutionReport {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface Plugin {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly enablers: readonly string[];
|
|
4
|
+
readonly entryPatterns?: readonly string[];
|
|
5
|
+
readonly configPatterns?: readonly string[];
|
|
6
|
+
readonly toolingDependencies?: readonly string[];
|
|
7
|
+
readonly pathAliases?: readonly string[][];
|
|
8
|
+
readonly virtualModulePrefixes?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PluginRegistry {
|
|
12
|
+
private plugins: Plugin[] = [];
|
|
13
|
+
|
|
14
|
+
register(plugin: Plugin): void {
|
|
15
|
+
this.plugins.push(plugin);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
activePlugins(allDeps: string[]): Plugin[] {
|
|
19
|
+
return this.plugins.filter((p) =>
|
|
20
|
+
p.enablers.some((enabler) => {
|
|
21
|
+
if (enabler.endsWith("/")) {
|
|
22
|
+
return allDeps.some((d) => d.startsWith(enabler));
|
|
23
|
+
}
|
|
24
|
+
return allDeps.includes(enabler);
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
allPlugins(): Plugin[] {
|
|
30
|
+
return [...this.plugins];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { definePlugin } from "../plugin-define.ts";
|
|
2
|
+
|
|
3
|
+
export const NextJsPlugin = definePlugin({
|
|
4
|
+
name: "nextjs",
|
|
5
|
+
enablers: ["next"],
|
|
6
|
+
entryPatterns: [
|
|
7
|
+
"src/app/**/*.{ts,tsx}",
|
|
8
|
+
"src/pages/**/*.{ts,tsx}",
|
|
9
|
+
"src/app/**/page.{ts,tsx}",
|
|
10
|
+
"src/app/**/layout.{ts,tsx}",
|
|
11
|
+
"src/app/**/route.{ts,tsx}",
|
|
12
|
+
"middleware.{ts,js}",
|
|
13
|
+
"next.config.{ts,js,mjs}",
|
|
14
|
+
],
|
|
15
|
+
configPatterns: ["next.config.{ts,js,mjs}"],
|
|
16
|
+
toolingDependencies: ["next", "@next/font", "@next/mdx"],
|
|
17
|
+
pathAliases: [["~", "src"]],
|
|
18
|
+
virtualModulePrefixes: ["next:"],
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { definePlugin } from "../plugin-define.ts";
|
|
2
|
+
|
|
3
|
+
export const VitePlugin = definePlugin({
|
|
4
|
+
name: "vite",
|
|
5
|
+
enablers: ["vite", "rolldown-vite"],
|
|
6
|
+
entryPatterns: [
|
|
7
|
+
"src/main.{ts,tsx,js,jsx}",
|
|
8
|
+
"src/index.{ts,tsx,js,jsx}",
|
|
9
|
+
"index.html",
|
|
10
|
+
],
|
|
11
|
+
configPatterns: ["vite.config.{ts,js,mts,mjs}"],
|
|
12
|
+
toolingDependencies: ["vite"],
|
|
13
|
+
virtualModulePrefixes: ["virtual:"],
|
|
14
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { definePlugin } from "../plugin-define.ts";
|
|
2
|
+
|
|
3
|
+
export const VitestPlugin = definePlugin({
|
|
4
|
+
name: "vitest",
|
|
5
|
+
enablers: ["vitest"],
|
|
6
|
+
entryPatterns: [
|
|
7
|
+
"**/*.test.{ts,tsx}",
|
|
8
|
+
"**/*.spec.{ts,tsx}",
|
|
9
|
+
"src/**/*.test.{ts,tsx}",
|
|
10
|
+
"src/**/*.spec.{ts,tsx}",
|
|
11
|
+
],
|
|
12
|
+
configPatterns: ["vitest.config.{ts,js,mjs}", "vite.config.ts"],
|
|
13
|
+
toolingDependencies: ["vitest"],
|
|
14
|
+
});
|
|
@@ -6,6 +6,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
7
|
import { appendEvent } from "../state/event-log.ts";
|
|
8
8
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
9
|
+
import {
|
|
10
|
+
registerWorker,
|
|
11
|
+
unregisterWorker,
|
|
12
|
+
} from "./orphan-worker-registry.ts";
|
|
9
13
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
10
14
|
|
|
11
15
|
|
|
@@ -179,15 +183,24 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
179
183
|
"NPM_CONFIG_REGISTRY",
|
|
180
184
|
"NPM_CONFIG_USERCONFIG",
|
|
181
185
|
"NPM_CONFIG_GLOBALCONFIG",
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"
|
|
186
|
+
// FIX: explicit list matches child-pi.ts to prevent regression.
|
|
187
|
+
// PI_CREW_PARENT_PID is needed for parent-guard (liveness check).
|
|
188
|
+
"PI_CREW_DEPTH",
|
|
189
|
+
"PI_CREW_MAX_DEPTH",
|
|
190
|
+
"PI_CREW_INHERIT_PROJECT_CONTEXT",
|
|
191
|
+
"PI_CREW_INHERIT_SKILLS",
|
|
192
|
+
"PI_CREW_PARENT_PID",
|
|
193
|
+
"PI_TEAMS_DEPTH",
|
|
194
|
+
"PI_TEAMS_MAX_DEPTH",
|
|
195
|
+
"PI_TEAMS_INHERIT_PROJECT_CONTEXT",
|
|
196
|
+
"PI_TEAMS_INHERIT_SKILLS",
|
|
197
|
+
"PI_TEAMS_PI_BIN",
|
|
198
|
+
"PI_TEAMS_MOCK_CHILD_PI",
|
|
199
|
+
"PI_CREW_ALLOW_MOCK",
|
|
185
200
|
],
|
|
186
201
|
});
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
delete filteredEnv.PI_CREW_EXECUTE_WORKERS;
|
|
190
|
-
delete filteredEnv.PI_TEAMS_EXECUTE_WORKERS;
|
|
202
|
+
// FIX: removed delete workarounds — with explicit allowlist, these vars
|
|
203
|
+
// are no longer auto-leaked. Matches child-pi.ts.
|
|
191
204
|
|
|
192
205
|
const loader = resolveTypeScriptLoader();
|
|
193
206
|
if (!loader) {
|
|
@@ -223,6 +236,21 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
223
236
|
});
|
|
224
237
|
child.unref();
|
|
225
238
|
|
|
239
|
+
// Track this worker in the orphan registry so it can be killed on
|
|
240
|
+
// session_start of a future session if the parent pi process is killed.
|
|
241
|
+
if (child.pid) {
|
|
242
|
+
registerWorker(
|
|
243
|
+
child.pid,
|
|
244
|
+
manifest.ownerSessionId ?? "unknown",
|
|
245
|
+
manifest.runId,
|
|
246
|
+
process.pid, // parentPid — used by cleanup to verify session is dead
|
|
247
|
+
);
|
|
248
|
+
// Best-effort: unregister when child exits. Background-runner writes
|
|
249
|
+
// the marker file before it dies, so we unregister on the next
|
|
250
|
+
// cleanup tick. But the child "exit" event won't fire because we
|
|
251
|
+
// unref'd and the stdio is piped + ignored.
|
|
252
|
+
}
|
|
253
|
+
|
|
226
254
|
return { pid: child.pid, logPath };
|
|
227
255
|
}
|
|
228
256
|
|