Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1414142
feat(coding-agent): add async-by-default bash() to the kernel runtime
snimu Aug 21, 2026
d4fc6ab
chore(coding-agent): tighten bash() comments and changelog fragment
snimu Aug 21, 2026
8a04223
Merge remote-tracking branch 'origin/main' into repl/01-bash-tool
snimu Aug 23, 2026
b00a176
fix(coding-agent): address bash() review findings — exact tail retent…
snimu Aug 23, 2026
dcc7328
fix(coding-agent): tree-kill bash() process trees on Windows and make…
snimu Aug 23, 2026
07ace49
fix(coding-agent): carry bash() status pipe as stdin so strict-POSIX …
snimu Aug 24, 2026
2f9b740
fix(coding-agent): close status fds when wake-pipe creation fails and…
snimu Aug 24, 2026
938e68c
Merge remote-tracking branch 'origin/main' into repl/01-bash-tool
snimu Aug 24, 2026
f4b4196
fix(coding-agent): kill full process trees in the Windows orphan reap…
snimu Aug 24, 2026
be2f706
fix(coding-agent): kill one-shot await bash() on cancel, resolve Wind…
snimu Aug 24, 2026
4d57859
fix(coding-agent): confirm group exit before a cancelled one-shot bas…
snimu Aug 24, 2026
161474b
fix(coding-agent): shield the cancelled-await bash() confirm-exit fro…
snimu Aug 24, 2026
b83e09b
fix(coding-agent): flag the pump's read-to-commit window so the bash(…
snimu Aug 24, 2026
b8c9368
Merge remote-tracking branch 'origin/main' into repl/01-bash-tool
snimu Aug 25, 2026
69c8b4a
fix(coding-agent): resolve taskkill via absolute System32 path in the…
snimu Aug 25, 2026
b244459
Merge remote-tracking branch 'origin/main' into repl/01-bash-tool
snimu Aug 25, 2026
1dda76b
fix(coding-agent): write the inactive orphan-journal record only afte…
snimu Aug 25, 2026
60a0ca3
fix(coding-agent): count an already-exited leader as delivered in the…
snimu Aug 25, 2026
e2ffa3a
fix(coding-agent): resolve trusted absolute shell host-side and close…
snimu Aug 25, 2026
fb61ffc
fix(coding-agent): best-effort-kill pid-only orphan records in daemon…
snimu Aug 25, 2026
486d2e4
fix(coding-agent): resolve the kernel bash shell only from trusted ab…
snimu Aug 25, 2026
2c6f451
fix(coding-agent): contain Windows bash children with kill-on-close j…
snimu Aug 25, 2026
2ca2e03
fix(coding-agent): fall back to taskkill when job termination fails
snimu Aug 25, 2026
a0f185e
fix(coding-agent): start Windows bash children suspended and fail clo…
snimu Aug 26, 2026
933f12b
Merge remote-tracking branch 'origin/main' into repl/01-bash-tool
snimu Aug 26, 2026
8d9e4b7
fix(coding-agent): kill the whole tree for enriched orphan records on…
snimu Aug 26, 2026
22f878d
fix(coding-agent): skip the liveness check when the orphan identity i…
snimu Aug 26, 2026
01cd753
fix(coding-agent): journal Windows bash() children before job assignm…
snimu Aug 26, 2026
ec78cd7
fix(coding-agent): pre-create the Windows bash() job before spawn and…
snimu Aug 26, 2026
7d0fe21
fix(coding-agent): create Windows bash() children atomically inside t…
snimu Aug 26, 2026
4e68837
chore(coding-agent): compress the atomic-job-association comments to …
snimu Aug 26, 2026
e31c652
fix(coding-agent): retain the Windows bash() process handle through j…
snimu Aug 26, 2026
242b34c
fix(coding-agent): serialize Windows bash() abort cleanup under the k…
snimu Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/coding-agent/.changes/repl-01-bash-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Added an async-by-default `bash()` callable to the kernel runtime: it returns a live handle immediately (pid/tail/poll/kill/await), bounds in-memory output, and enrolls children in the orphan-process journal so kernel teardown reaps them.
- Fixed bash() orphan-journal writes marking a child inactive even when the kill signal was not delivered; the record now stays active on delivery failure so the host reaper still owns the process (on Windows a shell that already exited counts as delivered, so clean exits still retire their record).
10 changes: 10 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [Unreleased]

- Fixed bash() cells failing under strict-POSIX shells (dash) when the status pipe landed on a multi-digit fd.
- Fixed two bash() spawn races: status-channel fds no longer leak when pipe creation fails mid-setup, and a status-socket gate keeps the command from starting until its pid is journaled (a kernel kill in that window now stops the child instead of orphaning it past the reaper).
- Fixed the Windows orphan reaper killing only the journaled bash() shell pid; it now uses taskkill /T so descendants die with the tree, matching the in-kernel bash() kill paths, and resolves taskkill via an absolute System32 path (with NoDefaultCurrentDirectoryInExePath) so a planted CWD taskkill.exe cannot hijack cleanup.
- Hardened bash(): cancelling `await bash(cmd)` now kills the command's process group (background handles are unaffected), Windows helper binaries resolve via absolute System32 paths, kill() retries taskkill for already-reaped Windows trees, and orphan-journal enrollment fails closed when configured.
- Hardened bash() further: the host now injects an absolute default shell into the kernel (no PATH lookup; /bin/bash else /bin/sh on POSIX), macOS start-id lookup uses /bin/ps, and Windows worker-teardown orphan kills go through hardened taskkill /T.
- Hardened Windows bash execution: the kernel shell is resolved only from trusted absolute paths (never PATH), and bash children are contained by kill-on-close job objects so a crashed kernel cannot leak process trees (taskkill remains only as a fallback when job creation fails).
- Hardened Windows bash() containment: children start suspended and enter their kill-on-close job before running (no escape window); bash() now raises instead of falling back to jobless taskkill when containment fails.

## [0.8.1] - 2026-08-26

- Fixed syntax highlighting in the expanded python tool-call view: triple-quoted strings spanning multiple lines now keep their string color instead of only the first line.
Expand Down
26 changes: 25 additions & 1 deletion packages/coding-agent/src/cli/daemon-ps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync }
import { basename, dirname, join, resolve } from "node:path";
import chalk from "chalk";
import { APP_NAME, getAgentDir, VERSION } from "../config.js";
import { isOrphanProcessIdentityCurrent, readActiveOrphanProcesses } from "../core/orphan-process-journal.js";
import {
isOrphanProcessIdentityCurrent,
killOrphanProcess,
readActiveOrphanProcesses,
shouldReapOrphanProcess,
} from "../core/orphan-process-journal.js";
import { getProcessStartId } from "../core/session-lease.js";
import { DaemonClient } from "../modes/daemon/daemon-client.js";
import {
Expand Down Expand Up @@ -938,9 +943,28 @@ async function forceStopTrackedWorkers(
failures.push(`could not read child process records for worker ${descriptor.workerId}: ${String(error)}`);
}
for (const orphan of orphans) {
// Pid-only records go through the platform predicate (stopTrackedProcess needs a startId).
if (orphan.processStartId === undefined) {
if (shouldReapOrphanProcess(orphan)) {
killOrphanProcess(orphan.pid);
}
continue;
}
Comment thread
snimu marked this conversation as resolved.
if (!isOrphanProcessIdentityCurrent(orphan)) {
continue;
}
if (process.platform === "win32") {
// taskkill /T, like the sibling reapers: signalling only the shell pid leaves its descendants alive.
await assertAdmission();
if (isOrphanProcessIdentityCurrent(orphan)) {
killOrphanProcess(orphan.pid);
}
if (isProcessAlive(orphan.pid)) {
cleanupWorkerRecords = false;
failures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);
}
continue;
Comment thread
cursor[bot] marked this conversation as resolved.
}
if (!(await stopTrackedProcess(orphan.pid, orphan.processStartId, assertAdmission))) {
cleanupWorkerRecords = false;
failures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);
Expand Down
16 changes: 4 additions & 12 deletions packages/coding-agent/src/cli/owned-session-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import type { AgentSession } from "../core/agent-session.js";
import type { AgentSessionRuntime } from "../core/agent-session-runtime.js";
import {
clearOrphanProcessJournal,
isOrphanProcessIdentityCurrent,
killOrphanProcess,
ORPHAN_PROCESS_JOURNAL_ENV,
readActiveOrphanProcesses,
shouldReapOrphanProcess,
} from "../core/orphan-process-journal.js";
import { SESSION_LEASE_OWNER_ID_ENV, SESSION_LEASES_ENABLED_ENV } from "../core/session-lease.js";
import { attachJsonlLineReader, serializeJsonLine } from "../modes/rpc/jsonl.js";
Expand Down Expand Up @@ -297,19 +298,10 @@ export async function runOwnedSessionWorkerFrontend(
}
}
for (const orphan of readActiveOrphanProcesses(orphanProcessJournalPath, workerPid)) {
if (!isOrphanProcessIdentityCurrent(orphan)) {
if (!shouldReapOrphanProcess(orphan)) {
continue;
}
const { pid } = orphan;
try {
process.kill(process.platform === "win32" ? pid : -pid, "SIGKILL");
} catch {
try {
process.kill(pid, "SIGKILL");
} catch {
// The detached resource may already have exited.
}
}
killOrphanProcess(orphan.pid);
}
clearOrphanProcessJournal(orphanProcessJournalPath);
};
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8956,6 +8956,8 @@ export class AgentSession {
const notifyRestore = !this._ipythonRuntimeBuilt;
this._ipythonKernelProvisioner = new IpythonKernelProvisioner(this._cwd, {
env: this._rlmKernelEnv(),
commandPrefix: this.settingsManager.getShellCommandPrefix(),
shellPath: this.settingsManager.getShellPath(),
sessionId: this.sessionId,
hostHandlers: this._createKernelHostHandlers(),
pythonSkills,
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/src/core/kernel/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const REQUIRED_HARNESS_METHODS = [
"delete_prompt_note",
"record_refinement",
];
const RUNTIME_READY_CHECK = `import inspect; import rlm; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert hasattr(rlm, 'run'); assert callable(rlm); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm); assert callable(rlm.host_request); assert callable(rlm.find_models); assert callable(rlm.rlm.find_models); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background')`;
const RUNTIME_READY_CHECK = `import inspect; import rlm; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert hasattr(rlm, 'run'); assert callable(rlm); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm); assert callable(rlm.host_request); assert callable(rlm.find_models); assert callable(rlm.rlm.find_models); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background'); from rlm.bash import BashHandle, BashResult; assert callable(rlm.bash); assert all(callable(getattr(BashHandle, _m, None)) for _m in ('tail', 'output', 'poll', 'kill')); assert {'exit_code', 'output', 'duration'} <= set(BashResult.__dataclass_fields__)`;
const BOOTSTRAP_VERSION_FILE = ".bootstrap-version";
const BOOTSTRAP_LOCK_NAME = ".bootstrap.lock";
const BOOTSTRAP_LOCK_RETRY_MS = 100;
Expand Down
18 changes: 15 additions & 3 deletions packages/coding-agent/src/core/kernel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { setTimeout as sleep } from "node:timers/promises";
import { registerSessionResourceCleanup } from "@earendil-works/pi-ai";
import { v4 as uuid } from "uuid";
import { Dealer, Subscriber } from "zeromq";
import { recordOrphanProcessState } from "../orphan-process-journal.js";
import { reapKernelOrphanProcesses, recordOrphanProcessState } from "../orphan-process-journal.js";
import { ensureKernelPython, type KernelBootstrapProgressHandler, type KernelPythonSkill } from "./bootstrap.js";
import { type ForkedKernelHandle, ForkServerUnavailable, forkKernel, isForkServerEnabled } from "./fork-server.js";
import {
Expand Down Expand Up @@ -708,7 +708,7 @@ export class KernelManager {
cwd: this.options.cwd,
// Applied fresh in the child (the template's env snapshot may be stale).
// No JPY_PARENT_PID: forked children watch the forkserver by getppid().
env: { ...process.env, ...this.options.env },
env: { ...process.env, ...this.options.env, PRIME_AGENT_KERNEL_OWNER_PID: String(process.pid) },
});
if (this.startStale(generation)) {
// Nobody owns this kernel; the protocol kill is id-keyed and safe.
Expand Down Expand Up @@ -742,7 +742,12 @@ export class KernelManager {
const kernel = spawn(python, ["-m", "ipykernel_launcher", "-f", connection.path], {
cwd: this.options.cwd,
// ipykernel's parent poller exits the kernel if this pid dies (covers SIGKILL of the owner).
env: { ...process.env, ...this.options.env, JPY_PARENT_PID: String(process.pid) },
env: {
...process.env,
...this.options.env,
JPY_PARENT_PID: String(process.pid),
PRIME_AGENT_KERNEL_OWNER_PID: String(process.pid),
},
stdio: ["ignore", "pipe", "pipe"],
});
this.kernel = kernel;
Expand Down Expand Up @@ -1542,6 +1547,9 @@ export class KernelManager {
}
// Same rule as the forked branch below: inactive only when the signal proved the pid still ours.
if (directPid !== undefined && signaled) recordOrphanProcessState(directPid, false);
// A killed/crashed kernel cannot run its own shutdown hook, so the host
// reaps the bash() process groups it journaled under this kernel pid.
if (directPid !== undefined) reapKernelOrphanProcesses(directPid);
} else if (this.forkedKernel) {
const forked = this.forkedKernel;
// The journal is raw-pid keyed, so inactive is written only on "signaled"
Expand All @@ -1553,8 +1561,12 @@ export class KernelManager {
.kill(killSignal === "SIGKILL" ? "KILL" : "TERM")
.then((outcome) => {
if (outcome === "signaled") recordOrphanProcessState(forked.pid, false);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
// Reap again: bash children journaled while the async kill was in flight.
reapKernelOrphanProcesses(forked.pid);
})
.catch(() => this.appendKernelDiagnostic("forkserver kill unconfirmed; leaving orphan record active"));
// Same reaping as the direct branch: bash() children journaled under this kernel pid.
reapKernelOrphanProcesses(forked.pid);
}
this.kernel = undefined;
this.forkedKernel = undefined;
Expand Down
8 changes: 4 additions & 4 deletions packages/coding-agent/src/core/kernel/state-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ def _prime_agent_snapshot_state():
ip = None
ns = ip.user_ns if ip is not None else _b.globals()
hidden = _b.set(_b.getattr(ip, "user_ns_hidden", {}) or {}) if ip is not None else _b.set()
# rlm, mcp, and asyncio are re-created by the kernel bootstrap on every
# start; never snapshot them.
always_skip = {"rlm", "mcp", "asyncio", "In", "Out", "get_ipython", "exit", "quit", "open"}
# rlm, mcp, bash, and asyncio are re-created by the kernel bootstrap on
# every start; never snapshot them.
always_skip = {"rlm", "mcp", "bash", "asyncio", "In", "Out", "get_ipython", "exit", "quit", "open"}

class SnapshotSizeLimitExceeded(_b.Exception):
pass
Expand Down Expand Up @@ -262,7 +262,7 @@ def _prime_agent_list_state_names():
ip = None
ns = ip.user_ns if ip is not None else _b.globals()
hidden = _b.set(_b.getattr(ip, "user_ns_hidden", {}) or {}) if ip is not None else _b.set()
always_skip = {"rlm", "mcp", "asyncio", "In", "Out", "get_ipython", "exit", "quit", "open"}
always_skip = {"rlm", "mcp", "bash", "asyncio", "In", "Out", "get_ipython", "exit", "quit", "open"}
names = []
for name in _b.list(ns.keys()):
if name.startswith("_") or name in hidden or name in always_skip:
Expand Down
94 changes: 89 additions & 5 deletions packages/coding-agent/src/core/orphan-process-journal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { spawnSync } from "node:child_process";
import { closeSync, fsyncSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
import { win32 } from "node:path";
import { getProcessStartId } from "./session-lease.js";

export const ORPHAN_PROCESS_JOURNAL_ENV = "PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL";
Expand All @@ -7,14 +9,18 @@ interface OrphanProcessRecord {
version: 1;
pid: number;
ownerPid: number;
/** Set on records written by a kernel (e.g. bash() children) so the host can reap per kernel. */
kernelPid?: number;
processStartId?: string;
active: boolean;
recordedAt: string;
}

export interface ActiveOrphanProcess {
pid: number;
processStartId: string;
kernelPid?: number;
/** Missing on identity-free records: old journals or host writes whose start-id query failed (kernels no longer write pid-only records). */
processStartId?: string;
}

export function recordOrphanProcessState(pid: number, active: boolean): void {
Expand Down Expand Up @@ -75,18 +81,96 @@ export function readActiveOrphanProcesses(path: string, ownerPid: number): Activ
// A crash can truncate only the final append.
}
}
// Pid-only actives (no processStartId) still surface from old journals or
// host writes whose start-id query failed; reapers decide per-platform.
return [...latest.values()]
.filter(
(record): record is OrphanProcessRecord & { processStartId: string } =>
record.active && typeof record.processStartId === "string",
(record) =>
record.active && (record.processStartId === undefined || typeof record.processStartId === "string"),
)
.map((record) => ({ pid: record.pid, processStartId: record.processStartId }));
.map((record) => ({
pid: record.pid,
...(Number.isInteger(record.kernelPid) ? { kernelPid: record.kernelPid } : {}),
...(typeof record.processStartId === "string" ? { processStartId: record.processStartId } : {}),
}));
}

export function isOrphanProcessIdentityCurrent(orphan: ActiveOrphanProcess): boolean {
return getProcessStartId(orphan.pid) === orphan.processStartId;
// Pid-only records can never claim identity (undefined === undefined must not match).
return orphan.processStartId !== undefined && getProcessStartId(orphan.pid) === orphan.processStartId;
Comment thread
snimu marked this conversation as resolved.
}

/**
* Identity-free records cannot prove the pid still names the journaled process.
* On win32 the kernel's kill-on-close job already reaped its tree when it died,
* so a bare-pid taskkill only risks killing a reused pid. POSIX keeps the
* best-effort kill (group-scoped, and the spawn gate makes pid-only actives
* host-written rarities there).
*/
export function shouldReapOrphanProcess(orphan: ActiveOrphanProcess): boolean {
if (orphan.processStartId === undefined) {
return process.platform !== "win32";
}
return isOrphanProcessIdentityCurrent(orphan);
}

export function clearOrphanProcessJournal(path: string): void {
rmSync(path, { force: true });
}

// Kills still-active bash() children journaled by the given kernel pid; sibling kernels' records are untouched.
export function reapKernelOrphanProcesses(kernelPid: number): void {
const path = process.env[ORPHAN_PROCESS_JOURNAL_ENV];
if (!path || !Number.isInteger(kernelPid) || kernelPid <= 0) {
return;
}
let orphans: ActiveOrphanProcess[];
try {
orphans = readActiveOrphanProcesses(path, process.pid);
} catch {
return;
}
for (const orphan of orphans) {
if (orphan.kernelPid !== kernelPid || orphan.pid === kernelPid) {
continue;
}
if (!shouldReapOrphanProcess(orphan)) {
continue;
}
// Inactive only after a delivered signal; a stale record is neutralized by the startId check.
if (killOrphanProcess(orphan.pid)) {
recordOrphanProcessState(orphan.pid, false);
}
}
}

// Hardened cross-platform tree kill for journaled orphans: absolute System32
// taskkill /T on win32 (a bare name could resolve a planted CWD taskkill.exe),
// process-group then pid SIGKILL elsewhere.
export function killOrphanProcess(pid: number): boolean {
if (process.platform === "win32") {
// In-kernel bash() kill paths use taskkill /T; the reaper must kill the same tree, not just the shell pid.
const result = spawnSync(
win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe"),
["/F", "/T", "/PID", String(pid)],
{
stdio: "ignore",
timeout: 10_000,
env: { ...process.env, NoDefaultCurrentDirectoryInExePath: "1" },
},
);
return result.status === 0;
}
try {
process.kill(-pid, "SIGKILL");
return true;
} catch {
try {
process.kill(pid, "SIGKILL");
return true;
} catch {
// The orphan may already have exited.
}
}
return false;
}
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/prompts/rlm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const IPYTHON_CONTROL_PROMPT = [
"",
"When running shell commands from IPython, use `%%bash` cells. If you use `%%bash`, it must be the first line of the code cell: no comments, spaces, blank lines, imports, or Python statements before it. Avoid `!cmd` shell escapes for project commands so shell behavior is explicit and multi-line commands share one shell context.",
"",
"`bash(command)` starts a shell command in the background and returns a handle immediately: `h = bash('npm test')`. Use `h.pid` / `h.running` for liveness, `h.tail(n)` / `h.output()` for combined stdout+stderr so far, `h.poll()` for a non-blocking result, `h.kill()` to terminate (SIGTERM, escalating to SIGKILL; on Windows kill() uses taskkill /T and detached or reparented descendants may survive), and `await h` (or `await bash('cmd')`) for the completed result with exit_code, output, and duration. Prefer bash() for long-running commands so the turn keeps working; `%%bash` remains the way to run quick inline shell with cell-visible output.",
"",
"Important: do not install dependencies into the IPython kernel just to make an external project import or run there. If a project import, test, script, CLI, or dependency check is needed, run it through that project's own environment and normal command interface. For example, in a Python repo use its documented commands, `uv run ...`, `.venv/bin/python ...`, or the active project interpreter from the repo root. Treat failures from that native environment as the relevant result.",
"",
"Use Python for reading, searching, and editing files — it gives you reusable variables you can slice, filter, and act on without re-reading. Always assign read/search results to named variables so you can revisit them later.",
Expand Down
Loading
Loading