Loading the notebook…
Loading the notebook…
Notebooks · Workspace, live UI and observability · 67
Point an agent at a repository and it just works. The SHIPIT Workspace (shipit_agent.workspace) is a set of zero-code, file-based conventions you check into a project:
Its imports and arguments are checked against shipit-agent 3.0. The outputs were saved when it was written, for an earlier release.
from pathlib import Path
import sys
ROOT = (
Path.cwd().resolve().parent
if Path.cwd().name == "notebooks"
else Path.cwd().resolve()
)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))import json
import tempfile
from pathlib import Path
from shipit_agent import Agent
from shipit_agent.llms import ShipitLLM
def fresh_project() -> Path:
"""A brand-new temp dir to act as a project root (self-contained per cell)."""
root = Path(tempfile.mkdtemp(prefix="shipit_ws_"))
return root
def show(label, text, limit=900):
print(f"=== {label} ===")
print(text[:limit] + ("…" if len(text) > limit else ""))SHIPIT.md / AGENTS.mdload_project_memory(project_root) searches, in order, for:
SHIPIT.md AGENTS.md .shipit/SHIPIT.md .shipit/AGENTS.md
Every file that exists is included (project context first), then a
user-global ~/.shipit/SHIPIT.md (and ~/.shipit/AGENTS.md) is appended so
personal rules apply everywhere. Each file becomes its own labelled block.
When you build an Agent(..., project_root=...), this block is auto-loaded
into the system prompt — no wiring required.
root = fresh_project()
# A repo-root SHIPIT.md: the primary project instructions file.
(root / "SHIPIT.md").write_text(
"# Project rules\n"
"- Always write tests for new behavior.\n"
"- Prefer small, focused functions.\n",
encoding="utf-8",
)
# Build an agent rooted at the repo — instructions load automatically.
agent = Agent(llm=ShipitLLM(), project_root=root)
print("SHIPIT.md content is in the system prompt:",
"Always write tests" in agent.prompt)
print()
show("agent.prompt (tail)", agent.prompt[-500:])SHIPIT.md content is in the system prompt: True === agent.prompt (tail) === instead of simulating output. Quality bar: - Keep outputs clear and complete. - Verify important results before returning them. - Surface residual uncertainty instead of hiding it. - Avoid repeated failed actions; adjust strategy after an error. # Project instructions The following instructions come from this project's SHIPIT.md / AGENTS.md. Follow them unless the user says otherwise. <!-- SHIPIT.md --> # Project rules - Always write tests for new behavior. - Prefer small, focused functions.
@importAll discovered files are concatenated as separate blocks. A file can also
pull in another with a line that is exactly @relative/path — the
imported file is resolved relative to the importing file and appended as its
own labelled block (depth-limited and cycle-safe).
Here we add an AGENTS.md, a .shipit/SHIPIT.md, and an @import of a
shared style guide.
root = fresh_project()
(root / ".shipit").mkdir(parents=True, exist_ok=True)
# Imported file (pulled in via @import below).
(root / "STYLE.md").write_text(
"# Style guide\n- Line length <= 88.\n", encoding="utf-8"
)
# Primary file imports STYLE.md via a line that is exactly `@STYLE.md`.
(root / "SHIPIT.md").write_text(
"# Project rules\n- Always write tests.\n\[email]\n",
encoding="utf-8",
)
# The conventional AGENTS.md is ALSO picked up (separate block).
(root / "AGENTS.md").write_text(
"# Agent notes\n- This repo deploys on merge to main.\n",
encoding="utf-8",
)
# And a nested .shipit/SHIPIT.md (separate block again).
(root / ".shipit" / "SHIPIT.md").write_text(
"# Workspace rules\n- Use the staging DB locally.\n",
encoding="utf-8",
)
from shipit_agent.workspace import load_project_memory
# include_user=False keeps this demo deterministic (ignores ~/.shipit).
memory = load_project_memory(root, include_user=False)
print(memory)# Project instructions The following instructions come from this project's SHIPIT.md / AGENTS.md. Follow them unless the user says otherwise. <!-- STYLE.md --> # Style guide - Line length <= 88. <!-- SHIPIT.md --> # Project rules - Always write tests. <!-- AGENTS.md --> # Agent notes - This repo deploys on merge to main. <!-- .shipit/SHIPIT.md --> # Workspace rules - Use the staging DB locally.
Notice the four <!-- label --> blocks: the imported STYLE.md,
SHIPIT.md, AGENTS.md, and .shipit/SHIPIT.md. The import is appended as
its own block (not inlined where the @ line sat) — and because the
importer resolves its imports as it is read, STYLE.md lands just ahead of
the SHIPIT.md body that pulled it in. The @STYLE.md line itself is
stripped from the importing file.
The
Agentpath usesinclude_user=Trueby default, so a personal~/.shipit/SHIPIT.mdwould also be appended. We passinclude_user=Falsein these direct calls purely so the printed output is reproducible.
auto_project_memory=FalseSometimes you want an agent that ignores project files (e.g. a sandboxed
sub-agent). Set auto_project_memory=False and the system prompt is left
untouched.
root = fresh_project()
(root / "SHIPIT.md").write_text("# Secret rules\n- Do X.\n", encoding="utf-8")
on = Agent(llm=ShipitLLM(), project_root=root)
off = Agent(llm=ShipitLLM(), project_root=root, auto_project_memory=False)
print("default (auto_project_memory=True): rules loaded? ",
"Secret rules" in on.prompt)
print("opted-out (auto_project_memory=False): rules loaded?",
"Secret rules" in off.prompt)default (auto_project_memory=True): rules loaded? True opted-out (auto_project_memory=False): rules loaded? False
.shipit/commands/*.mdDrop a markdown file at .shipit/commands/<name>.md; its body becomes a
reusable prompt invoked with agent.run("/<name> args"). Placeholders are
substituted before the model sees anything:
$ARGUMENTS → everything after the command name$1, $2, … → individual whitespace-separated argsA leading YAML frontmatter block (---…---) is stripped. Because expansion
is a pure prompt transform, it works with any provider.
root = fresh_project()
commands = root / ".shipit" / "commands"
commands.mkdir(parents=True, exist_ok=True)
# A command using $1 (first arg). No f-string: $1/$ARGUMENTS are literal.
(commands / "greet.md").write_text(
"Greet $1 warmly and ask what they are working on today.",
encoding="utf-8",
)
# A command with frontmatter + $ARGUMENTS (the whole arg string).
(commands / "review.md").write_text(
"---\n"
"description: Review a file for bugs\n"
"---\n"
"Carefully review the file $ARGUMENTS for bugs and style issues. "
"List concrete fixes.",
encoding="utf-8",
)
agent = Agent(llm=ShipitLLM(), project_root=root)
# /greet Ada -> the expanded prompt reaches the (echo) model.
result = agent.run("/greet Ada")
# The echo output is `system prompt + expanded user turn`; print the tail so the
# substituted command body ($1 -> Ada) is visible (the head is the system prompt).
print("expanded user turn (last line the model saw):")
print(" …", result.output.splitlines()[-1])
print()
show("full echo (system + expanded /greet)", result.output)expanded user turn (last line the model saw): … Greet Ada warmly and ask what they are working on today. === full echo (system + expanded /greet) === You are Shipit, a capable general-purpose agent runtime. Core behavior: - Be accurate, direct, and execution-oriented. - Solve the user's task end-to-end when possible instead of stopping at analysis. - Use tools when they materially improve correctness, freshness, or efficiency. - Prefer structured evidence over guesses. Tool behavior: - Read tool descriptions and tool prompts carefully before calling them. - Use the smallest correct tool for the job. - When a task is complex, plan before acting. - When information may be outdated, prefer web and external tools over stale assumptions. - When a task needs files, artifacts, or code execution, use the relevant tools instead of simulating output. Quality bar: - Keep outputs clear and complete. - Verify important results before returning them. - Surface residual uncertainty instead of hiding it. - Avoid repeated failed actions; adjust str…
The printed tail is "Greet Ada warmly and ask what they are working on
today." — the literal $1 in greet.md was replaced with Ada and the
command body was sent as the user turn. The full echo shows the system-prompt
preamble first because ShipitLLM echoes system prompt + last user message.
# Inspect the command machinery directly.
from shipit_agent.workspace import discover_commands, expand_command
print("discovered commands:", list(discover_commands(root)))
print()
print("expand /review:")
print(expand_command(root, "/review src/app.py"))
print()
# An unknown command returns None, so a normal prompt passes through unchanged.
print("expand /unknown ->", expand_command(root, "/unknown blah"))discovered commands: ['greet', 'review'] expand /review: Carefully review the file src/app.py for bugs and style issues. List concrete fixes. expand /unknown -> None
expand_command returns None for anything that isn't a known command, so
ordinary prompts (and unrecognised /slashes) flow straight through to the
model untouched. Note the review.md frontmatter was stripped from the
expansion.
.shipit/settings.jsonCheck a policy into the repo: permissions (mode + allow/ask/deny globs),
env vars, and a default model. A user-global ~/.shipit/settings.json is
merged underneath the project file.
load_settings(root) → a WorkspaceSettings dataclass..to_permission_engine() → a ready PermissionEngine (or None when
nothing constrains tools).Agent.for_project(llm=..., project_root=root) wires it all up: builtin
tools, project memory, slash commands, and the permission engine.root = fresh_project()
(root / ".shipit").mkdir(parents=True, exist_ok=True)
(root / ".shipit" / "settings.json").write_text(
json.dumps(
{
"model": "anthropic/claude-3-5-sonnet",
"permissions": {
"mode": "default",
"deny": ["bash", "*_delete"],
"ask": ["sql"],
"allow": ["read*"],
},
"env": {"SHIPIT_LLM_PROVIDER": "anthropic"},
},
indent=2,
),
encoding="utf-8",
)
from shipit_agent.workspace import load_settings
settings = load_settings(root, include_user=False)
print("model:", settings.model)
print("deny :", settings.deny)
print("ask :", settings.ask)
print("allow:", settings.allow)
print("env :", settings.env)
engine = settings.to_permission_engine()
print("permission engine:", type(engine).__name__)model: anthropic/claude-3-5-sonnet
deny : ['bash', '*_delete']
ask : ['sql']
allow: ['read*']
env : {'SHIPIT_LLM_PROVIDER': 'anthropic'}
permission engine: PermissionEngineAgent.for_project enforces the policyWe demonstrate enforcement fully offline with a tiny scripted LLM that tries
to call the denied bash tool. The runtime blocks it before it runs and emits
a tool_denied event on result.events.
from shipit_agent.llms.base import LLMResponse
from shipit_agent.models import ToolCall
class ScriptedLLM:
"""Deterministic offline LLM: replays a fixed list of responses."""
def __init__(self, responses):
self._responses = list(responses)
self._i = 0
def complete(self, *, messages, tools=None, system_prompt=None,
metadata=None, **kwargs):
if self._i < len(self._responses):
resp = self._responses[self._i]
self._i += 1
return resp
return LLMResponse(content="done")from shipit_agent import Agent, FunctionTool
def bash(command: str = "") -> str:
return f"(pretend) ran: {command}"
# The model's first move is to call the DENIED `bash` tool.
scripted = ScriptedLLM([
LLMResponse(tool_calls=[ToolCall(name="bash",
arguments={"command": "rm -rf /"})]),
LLMResponse(content="Understood — I won't run that."),
])
agent = Agent.for_project(
llm=scripted,
project_root=str(root), # same root: settings.json deny=['bash', ...]
tools=[FunctionTool.from_callable(bash, name="bash",
description="Run a shell command.")],
)
result = agent.run("Please clean up the workspace.")
denied = [e for e in result.events if e.type == "tool_denied"]
print("final output:", result.output)
print("tool_denied events:",
[(e.message, e.payload.get("reason")) for e in denied])final output: Understood — I won't run that.
tool_denied events: [('Tool blocked: bash', "'bash' is on the deny list.")]The bash call never executed — the deny rule from .shipit/settings.json
(loaded automatically by Agent.for_project) blocked it, the model received a
"… was NOT run …" tool message, and we can audit the decision via the
tool_denied event.
| Convention | File | Loaded by |
|---|---|---|
| Project memory | SHIPIT.md / AGENTS.md / .shipit/SHIPIT.md | Agent(project_root=…) (auto) |
| User-global memory | ~/.shipit/SHIPIT.md | always appended |
| Slash commands | .shipit/commands/*.md | agent.run("/name …") |
| Settings / policy | .shipit/settings.json | Agent.for_project(…) |
All of it is plain files + prompt/config transforms — provider-agnostic, and shown here end-to-end with zero network calls.