pi-arc-lifecycle0.10.0
pi extension that runs workspace scripts and loads context files at Arc lifecycle stages
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-arc-lifecycle
A pi extension for Arc workspaces. On every agent launch it reads
/workspace/arc-lifecycle.yaml and:
- appends context files (each with an optional verbatim
prefix/suffix) to the system prompt - runs scripts at five stages:
start(first launch ever),resume(first launch after a reboot),before_agent_start,agent_before_settleandagent_settled(each user message)
Across a workspace's life
start runs once, on the very first message. Each later boot gets one resume, on its first
message. Nothing runs at boot itself; the stage fires when the next message launches pi.
What one pi launch does
The lock covers every script run, so two sessions sharing the workspace can't both run start.
Successes stay silent; other failures are reported once, on the next message.
Config
The yaml is validated once per edit and cached as /workspace/.arc-lifecycle/config.json; the
cache is rebuilt on the next message after the yaml changes. A start script added by an edit
runs on the next message, even in a workspace that has already started; a resume script
added by an edit also runs then if the workspace has restarted since start. The full format is the ArcLifecycleConfig type in src/config.ts; every context
entry and script uses exactly one of path/source/text (context) or run/source
(scripts).
version: 1
default_cwd: /workspace # optional; this is the default
thinking_level: high # optional; /effort <level> overrides per session
skill_mode: claudecode # optional; default pi
max_sleep_seconds: 5 # optional; refuse bash commands that start with a longer sleep
context: # appended to the system prompt, every message
- path: /workspace/Demeter/some/deeply/nested/dir/AGENTS.md
prefix: "## Project rules\nFollow these for all work in Demeter.\n\n"
suffix: "\n\n(End of project rules.)"
- source: # fetched once, during start
repo: GanazHQ/agent-config
ref: main
path: arc/AGENTS.md
enable_in_subagents: true # default false
- text: Use /workspace/scratchpad for temporary files. # inline text
- path: /workspace/Demeter/.claude/personal/kyle.md
users: [kyle@ganaz.com] # only in sessions kyle@ganaz.com started
skills: # skill directories (or directories of skills) loaded into pi
- path: /workspace/Demeter/.claude/personal/kyle/skills
users: [kyle@ganaz.com] # optional, as for context
scripts:
- name: uv-global
lifecycle: [start, resume]
run: mise use -g uv@0.11.3 # one-liner, run with bash -c
- name: dev-db
lifecycle: [start, resume]
source: # fetched once, during start
repo: GanazHQ/Demeter
ref: master # branch or commit SHA
path: devops/arc/db-up.sh
blocking: false # default true
timeout: 900 # seconds; default 300
- name: tool-path
lifecycle: [start, resume]
run: echo "PATH=/workspace/.tools/extra/bin:$PATH" >> "$ARC_LIFECYCLE_ENV"
- name: type-check
lifecycle: [agent_before_settle]
cwd: Demeter/node/apps/hr-ui # relative to default_cwd
run: pnpm -s type-check # silent on success
timeout: 120
max_continuations: 2 # default 1
- name: changelog-nudge
lifecycle: [agent_before_settle]
run: git -C /workspace/Demeter diff --quiet -- node/apps/backend/src/api || echo "You changed the API. Update CHANGELOG.md if you haven't."
- name: format-changed
lifecycle: [agent_settled]
run: cd /workspace/Demeter && git diff --name-only | xargs -r npx prettier --write
timeout: 60
Script graphs
Written as a map keyed by name instead of a list, scripts runs as a graph: every script
starts as soon as everything in its depends_on has passed, so independent scripts run at the
same time. A script without depends_on starts right away. The list form keeps running top to
bottom, each script waiting for the one before it (except blocking: false scripts, which
start in the background and don't hold up the next).
max_parallel: 4 # optional; blocking map-form scripts running at once, unlimited by default
scripts:
uv-global:
lifecycle: [start, resume]
run: mise use -g uv@0.11.3
mise-install: # waits for uv-global; gh-stack runs alongside both
lifecycle: [start, resume]
depends_on: [uv-global]
run: mise install
gh-stack:
lifecycle: [start, resume]
run: gh extension install github/gh-stack
type-check:
lifecycle: [agent_before_settle]
run: pnpm -s type-check
tests: # skipped, not run, while type-check fails
lifecycle: [agent_before_settle]
depends_on: [type-check]
run: pnpm -s test
- Each stage is its own graph. A dependency must list every stage its dependent lists, run in
subagents if its dependent does, and not use
blocking: false(nothing can wait for a background script). Unknown names and cycles are config errors. - A dependency passes when it exits 0 within its timeout. Otherwise everything that depends on it is skipped, and each skip is reported like a failure (start/resume skips are marked done).
- Until its stage finishes, a script's
$ARC_LIFECYCLE_ENVvalues reach only scripts that depend on it (directly or not), merged in yaml order. Once the stage finishes, every script's values apply to later stages and the agent, in yaml order, as with a list. agent_before_settlewaits for the whole graph, then sends output in yaml order. A skip is mentioned to the agent only alongside the output of the script that caused it.- If a launch dies partway through a
start/resumegraph, the next launch runs what's left; scripts already done count as passed or failed as recorded instate.json.
A longer design write-up is in docs/design.html (repo only, not published).
Behavior worth knowing
start/resumescripts are marked done whether they pass or fail; failures are reported to the agent once, on the next message.- Any output from an
agent_before_settlescript is sent to the agent, which gets another turn, up tomax_continuations(default 1) times per script per message. After that its output is only logged (a failing exit is still reported on the next message). Keep these scripts silent on success. - Lines of
KEY=VALUEwritten to$ARC_LIFECYCLE_ENVbecome environment variables for later scripts (in a graph, only its dependents until the stage finishes) and for the agent's bash tool on every later message. - Blocking scripts must not leave background processes holding their output; use
blocking: false(allowed only forstart/resumescripts) for long-running work. - Workflow agents (in-process sessions) only get items with
enable_in_subagents: true, and never runstart/resume. Arc'ssubagentchildren don't load extensions at all. thinking_level(optional, top level) sets pi's thinking level on every main-session message, overriding Arc's default. Only when it's set, a/effort <level>command is registered: it overrides the level for that session until/effortwith no argument resets it.users(on context entries and skills) limits an entry to sessions started by those emails; without it the entry applies to everyone. The starter isARC_SESSION_USER_EMAILas seen on the session's first launch, saved in.arc-lifecycle/session-users/<ARC_SESSION_ID>so it stays the same if the variable later changes. Sessions with no recorded starter only get entries withoutusers.resume_notice(optional, top level) is sent to a session, once, on its first message after the workspace restarted, e.g. to say that/tmp,~and Docker containers are gone. Each session is told separately; a session's first message never gets it.max_sleep_seconds(optional, top level) refuses abashcall whose command starts withsleep(after anycd … &&orcd …;) for longer than that many seconds. The model is told to end its turn to wait for a command it started (subagents and workflow agents, whose turn can't resume, aren't), or to run one bounded loop that exits on a condition, such astimeout 900 sh -c 'until <check>; do sleep 5; done'; a sleep inside a loop like that is allowed. (Claude Code's harness refuses these sleeps on its own; this brings the same guard to pi.) Without it, agents in Arc chainsleep 58andbackground_tasks outputcalls to wait for anything slower than the bash tool's 60-second blocking limit.- Arc copies
/workspace/.piinto a new.pi/resources/snapshot-<random>/for every pi launch, so skills loaded from there would get a new<location>in the system prompt on every message, and nothing after it could be read from the prompt cache. By default each such skill is listed at the same file under$ARC_LIFECYCLE_ROOT/.pi/skillsinstead (if it exists there), even without anarc-lifecycle.yaml. Setdisable_system_prompt_skill_stabilization: true(optional, top level) to list the snapshot paths as pi loaded them. skill_mode: claudecode(optional, top level; defaultpi) loads skills the way Claude Code does, since Claude models are trained on it. Its wording and layout are copied from what Claude Code 2.1.293 sends the Anthropic API:- The system prompt drops pi's skill list. After the user's message, a system message lists
skills not yet listed in the context by name and description (none marked
disable-model-invocation), plus a note for each skill the message names mid-sentence (/name). Models without mid-conversation system messages get it as<system-reminder>text instead. The yaml'scontextjoins the system prompt on each request, so that message isn't dropped with it. - The model loads a skill with a
Skilltool (skill,args). The tool result only saysLaunching skill: <name>; the skill's folder and body, with$ARGUMENTS,$0,${CLAUDE_SKILL_DIR}and the like filled in, follow as user text in the same turn. Calling it again with the same body says it's already loaded. - A message starting with
/name args(or pi's/skill:name args) arrives as Claude Code's<command-name>header and the skill body. - When a compaction summarizes listings away, the missing skills' listing is frozen into the
session at that moment and sent right after the summary on every request until the next
compaction. Skills loaded before the compaction aren't re-sent; the next
Skillcall loads the full body again. argsof"null"count as no arguments: Opus sometimes sends that string instead of omitting the optional field, which Claude Code would pass through asARGUMENTS: null.- A skill marked
disable-model-invocationruns from theSkilltool only when a person typed/namein the main session's current message, never from a subagent or workflow task. - Not supported:
context: fork,allowed-tools, per-skillmodel, inline!`command`and Claude Code's "invoked earlier" re-injection of loaded skills after compaction. If an earlier extension already replaced the system prompt, its copy of pi's skill list stays.
- The system prompt drops pi's skill list. After the user's message, a system message lists
skills not yet listed in the context by name and description (none marked
- Hook runs are serialized with the util-linux
flockbinary (Linux only). Scripts withblocking: falsekeep running after the lock is released.
Development
pnpm i
pnpm test # vitest; never calls a model
pnpm type-check
pnpm format
To release, bump version in package.json in your PR. When it merges to master,
.github/workflows/publish-arc-lifecycle.yaml stages it on npm with trusted publishing
(OIDC, GitHub environment npmjs-pi-arc-lifecycle); a maintainer then approves it on npmjs.com
(or npm stage approve <id>) with 2FA. ./publish.sh is the manual fallback.
ARC_LIFECYCLE_ROOT (default /workspace) and ARC_LIFECYCLE_BOOT_ID exist so tests can use a
scratch directory and fake a reboot.