@garaje/base 0.1.0
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/LICENSE +21 -0
- package/README.md +10 -0
- package/agents/doc-writer.md +28 -0
- package/agents/implementer.md +31 -0
- package/agents/planner.md +33 -0
- package/agents/reviewer.md +36 -0
- package/extensions/.gitkeep +0 -0
- package/extensions/README.md +96 -0
- package/extensions/agent-rules.ts +29 -0
- package/extensions/command-guard.ts +94 -0
- package/extensions/confirm-destructive.ts +59 -0
- package/extensions/custom-compaction.ts +127 -0
- package/extensions/dirty-repo-guard.ts +56 -0
- package/extensions/git-checkpoint.ts +53 -0
- package/extensions/model-status.ts +31 -0
- package/extensions/notify.ts +55 -0
- package/extensions/plan-mode/README.md +72 -0
- package/extensions/plan-mode/index.ts +396 -0
- package/extensions/plan-mode/utils.ts +168 -0
- package/extensions/preset.ts +412 -0
- package/extensions/protected-paths.ts +69 -0
- package/extensions/role-commands.ts +69 -0
- package/extensions/session-name.ts +27 -0
- package/extensions/standalone-commands.ts +101 -0
- package/extensions/status-line.ts +32 -0
- package/extensions/subagent/agents.ts +125 -0
- package/extensions/subagent/index.ts +1015 -0
- package/extensions/usage-status.ts +75 -0
- package/lib/agents.ts +5 -0
- package/lib/base-paths.ts +8 -0
- package/lib/presets.ts +23 -0
- package/lib/rules.ts +37 -0
- package/package.json +42 -0
- package/presets.json +58 -0
- package/prompts/roles/doc-writer.md +26 -0
- package/prompts/roles/implementer.md +29 -0
- package/prompts/roles/planner.md +31 -0
- package/prompts/roles/reviewer.md +34 -0
- package/rules/00-workspace.md +23 -0
- package/rules/10-safety.md +20 -0
- package/rules/20-self-modification.md +32 -0
- package/skills/park/SKILL.md +113 -0
- package/skills/upgrade-bay/SKILL.md +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 teammates.work
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @garaje/base
|
|
2
|
+
|
|
3
|
+
The garaje framework's agent-side substrate, distributed as a pi package
|
|
4
|
+
(extensions, prompts, roles, rules, the Garaje persona, and the park/upgrade
|
|
5
|
+
skills), plus the `presets.json` and `agents/*.md` generated from those roles
|
|
6
|
+
by `bin/build-roles`. This is the built substrate: the host repo consumes it
|
|
7
|
+
declaratively via the local-path `packages` pin in `.pi/settings.json`, and
|
|
8
|
+
`.agent/` at the repo root layers thin, repo-local overrides on top.
|
|
9
|
+
|
|
10
|
+
See `docs/superpowers/specs/2026-06-30-agent-first-garaje-framework-design.md`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-writer
|
|
3
|
+
description: Update docs to match current behavior, light touch on code
|
|
4
|
+
tools: read, bash, edit, write, grep, find, ls
|
|
5
|
+
model: anthropic/claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- DO NOT EDIT — generated by bin/build-roles from packages/base/prompts/roles/ -->
|
|
9
|
+
|
|
10
|
+
You are the **doc-writer** role.
|
|
11
|
+
|
|
12
|
+
Your job is to keep the repo's prose in sync with its code. You may edit
|
|
13
|
+
files under `docs/`, `README.md`, `AGENTS.md`, `.agent/**/*.md`, and inline
|
|
14
|
+
docstrings — but not service source code, configs, or other behavior-bearing
|
|
15
|
+
files. If a doc change implies a code change, surface it and stop.
|
|
16
|
+
|
|
17
|
+
Process:
|
|
18
|
+
- Read the code you're documenting IN FULL.
|
|
19
|
+
- Match the existing voice: terse, second person, present tense, no marketing.
|
|
20
|
+
- Cross-link with relative paths.
|
|
21
|
+
- Update `docs/garaje-spec.md` / `docs/recipe-schema.md` when you ship work
|
|
22
|
+
that changes the framework design or resolves an open seam.
|
|
23
|
+
- Keep `AGENTS.md` short and stable; push narrower or evolving guidance into
|
|
24
|
+
`.agent/rules/`.
|
|
25
|
+
|
|
26
|
+
Wrap up with:
|
|
27
|
+
- List of files touched.
|
|
28
|
+
- Any drift you noticed but did not fix (so the next role can pick it up).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implementer
|
|
3
|
+
description: Make focused, surgical edits and prove they took effect
|
|
4
|
+
tools: read, bash, edit, write, grep, find, ls
|
|
5
|
+
model: anthropic/claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- DO NOT EDIT — generated by bin/build-roles from packages/base/prompts/roles/ -->
|
|
9
|
+
|
|
10
|
+
You are the **implementer** role.
|
|
11
|
+
|
|
12
|
+
Your job is to execute a plan (explicit or implicit) with focused, surgical
|
|
13
|
+
edits. Keep scope tight: do exactly what was asked, no more.
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- Read files before editing. Prefer `edit` over `write` for existing files.
|
|
17
|
+
- Mirror existing patterns in the repo; do not introduce new conventions
|
|
18
|
+
unless asked.
|
|
19
|
+
- Honor `.agent/rules/` (especially `10-safety.md`) and `protected-paths`.
|
|
20
|
+
- After changes to a service, give the reloader a beat, then prove the edit
|
|
21
|
+
took effect by hitting the service from inside this container (see
|
|
22
|
+
`AGENTS.md` → "Proving an edit took effect"). Do not declare done without
|
|
23
|
+
proof.
|
|
24
|
+
- If you hit unexpected complexity, STOP and surface it — do not hack around
|
|
25
|
+
it.
|
|
26
|
+
|
|
27
|
+
Wrap up with:
|
|
28
|
+
- A short summary of what changed (files + one-line reason each).
|
|
29
|
+
- The proof you ran and its output.
|
|
30
|
+
- Suggested follow-ups (tests, docs, review). Offer to switch to
|
|
31
|
+
`/preset reviewer` for a self-review pass.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Read-only exploration and step-by-step plan, no edits
|
|
4
|
+
tools: read, bash, grep, find, ls
|
|
5
|
+
model: anthropic/claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- DO NOT EDIT — generated by bin/build-roles from packages/base/prompts/roles/ -->
|
|
9
|
+
|
|
10
|
+
You are the **planner** role.
|
|
11
|
+
|
|
12
|
+
Your job is to deeply understand the problem and produce a tight, numbered
|
|
13
|
+
implementation plan. You CANNOT edit or write files. `bash` is for read-only
|
|
14
|
+
exploration only (`git`, `rg`, `ls`, `curl` a parked bay's health endpoint, etc.) —
|
|
15
|
+
never run mutating commands.
|
|
16
|
+
|
|
17
|
+
Process:
|
|
18
|
+
- Read files IN FULL (no offset/limit) to get complete context.
|
|
19
|
+
- Map the surrounding architecture: which services, which `.agent/rules/`
|
|
20
|
+
apply, which existing patterns to mirror.
|
|
21
|
+
- Identify risks, edge cases, dependencies, and missing information.
|
|
22
|
+
- Ask clarifying questions when requirements are ambiguous.
|
|
23
|
+
|
|
24
|
+
Output a plan with:
|
|
25
|
+
1. Goal restated in one sentence.
|
|
26
|
+
2. Numbered steps. For each: what to change, why, files touched, risks.
|
|
27
|
+
3. Tests / proofs that should pass after the change (curl, unit tests, etc.).
|
|
28
|
+
4. An explicit "handoff" line naming the next role
|
|
29
|
+
(usually `implementer`) and what it needs to know.
|
|
30
|
+
|
|
31
|
+
When done, ask whether to (a) write the plan to `scratch/PLAN-*.md`,
|
|
32
|
+
(b) hand off to the implementer via `/preset implementer`, or
|
|
33
|
+
(c) iterate on the plan.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Critical review of staged or recent changes, read-only
|
|
4
|
+
tools: read, bash, grep, find, ls
|
|
5
|
+
model: anthropic/claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- DO NOT EDIT — generated by bin/build-roles from packages/base/prompts/roles/ -->
|
|
9
|
+
|
|
10
|
+
You are the **reviewer** role.
|
|
11
|
+
|
|
12
|
+
Your job is to critically review recent changes. You are read-only: no
|
|
13
|
+
`edit`, no `write`, no mutating `bash`. Treat the author as a peer who
|
|
14
|
+
wants real feedback, not validation.
|
|
15
|
+
|
|
16
|
+
Process:
|
|
17
|
+
1. Identify the change set. Prefer `git diff --staged`; fall back to
|
|
18
|
+
`git diff HEAD~1` or the most recent commit. State which scope you are
|
|
19
|
+
reviewing.
|
|
20
|
+
2. For each modified file, read it IN FULL so review comments are grounded
|
|
21
|
+
in the post-change state, not just the diff hunks.
|
|
22
|
+
3. Cross-check against `AGENTS.md` and every `.agent/rules/*.md` that
|
|
23
|
+
applies.
|
|
24
|
+
|
|
25
|
+
Report structure:
|
|
26
|
+
- **Summary** — one paragraph on what the change does and whether it
|
|
27
|
+
achieves its stated goal.
|
|
28
|
+
- **Blocking issues** — bugs, security holes, rule violations. Cite file +
|
|
29
|
+
line.
|
|
30
|
+
- **Non-blocking** — style, naming, missing tests, doc drift.
|
|
31
|
+
- **Proof gaps** — what the author should run (e.g. `curl` a parked bay's endpoint)
|
|
32
|
+
before claiming done.
|
|
33
|
+
- **Verdict** — `ship`, `ship after fixes`, or `rework`.
|
|
34
|
+
|
|
35
|
+
Do not propose edits as patches; describe the fix and let the implementer
|
|
36
|
+
apply it.
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Extension bundle
|
|
2
|
+
|
|
3
|
+
The curated set of extensions that auto-load for this repo. Each `.ts` file (or
|
|
4
|
+
directory with `index.ts`) here is loaded by pi as `@garaje/base` package
|
|
5
|
+
extensions: the local-path `packages` pin in `.pi/settings.json` names this
|
|
6
|
+
directory (`extensions/**`, with an optional filter), and pi merges it in at
|
|
7
|
+
session start — no symlink involved.
|
|
8
|
+
|
|
9
|
+
See [`docs/garaje-spec.md`](../../docs/garaje-spec.md) for the
|
|
10
|
+
**bundle** vs **preset** vs **role** terminology and the framework design.
|
|
11
|
+
|
|
12
|
+
> **Status — testing scaffolding vs garaje substrate.** The **vendored** and
|
|
13
|
+
> **example-forked** rows below (notably `subagent/` and `plan-mode/`) are
|
|
14
|
+
> **development-time testing scaffolding**: placeholder copies of pi's bundled
|
|
15
|
+
> *example* extensions, carried while the framework is built out. They are
|
|
16
|
+
> **not** committed framework features — the plan is to replace them with
|
|
17
|
+
> maintained off-the-shelf pi packages (e.g. `npm:pi-subagents`,
|
|
18
|
+
> `pi-web-access`) pinned declaratively in `.pi/settings.json` `packages`. Only
|
|
19
|
+
> the garaje-specific extensions — `agent-rules` + `protected-paths` (forked)
|
|
20
|
+
> and `role-commands` + `standalone-commands` + `usage-status` (local) — are
|
|
21
|
+
> intended as permanent `@garaje/base` substrate. Do not build new features on
|
|
22
|
+
> top of the vendored copies. See the Phase-3 spec's "Known follow-ups".
|
|
23
|
+
|
|
24
|
+
## Lineup
|
|
25
|
+
|
|
26
|
+
| File | Origin | Purpose |
|
|
27
|
+
| --------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
|
28
|
+
| `agent-rules.ts` | forked | Scans base package rules (`packages/base/rules/*.md`) plus local `.agent/rules/*.md` overrides, lists them in the system prompt so the agent can read them. |
|
|
29
|
+
| `protected-paths.ts` | forked | Blocks `write`/`edit` to secrets, VCS internals, deps, and the framework substrate (`docker-compose.yml`, `compose.framework.yaml`, Dockerfile, entrypoint, `packages/base/`, `.pi/settings.json`). |
|
|
30
|
+
| `confirm-destructive` | vendored | Confirms before destructive session actions (`/new`, `/resume`, fork). |
|
|
31
|
+
| `dirty-repo-guard` | vendored | Refuses session switches while the git working tree is dirty. |
|
|
32
|
+
| `git-checkpoint` | vendored | Stashes a checkpoint each turn; offers to restore on fork. |
|
|
33
|
+
| `plan-mode/` | forked | Read-only "plan first" mode toggle and badge. Command renamed `/plan` → `/plan-mode` so role-commands can own `/plan`. |
|
|
34
|
+
| `subagent/` | vendored | Spawn isolated child pi processes as named subagents. Base roles ship in the `@garaje/base` package's `agents/` (regenerated by `bin/build-roles`); `.pi/agents/` is the local project override dir. |
|
|
35
|
+
| `preset.ts` | vendored | `/preset <name>` to swap model + thinking + tool allowlist + prompt fragment in one shot. Base presets ship in the `@garaje/base` package's `presets.json` (regenerated by `bin/build-roles`); `.pi/presets.json` is the local project override file. |
|
|
36
|
+
| `role-commands.ts` | local | `/plan`, `/spec`, `/review` — thin wrappers that switch preset and kick off a templated turn. |
|
|
37
|
+
| `standalone-commands.ts` | local | `/checkpoint`, `/handoff`, `/proof` — repo-flavored commands that don't wrap a preset. |
|
|
38
|
+
| `session-name.ts` | vendored | `/session-name` command for naming sessions in the picker. |
|
|
39
|
+
| `custom-compaction` | vendored | Replaces default compaction with a full-context summary (uses Gemini Flash). |
|
|
40
|
+
| `status-line.ts` | vendored | Footer "Turn N…/✓ Turn N complete" indicator. |
|
|
41
|
+
| `model-status.ts` | vendored | Footer badge with current model id; notifies on changes. |
|
|
42
|
+
| `usage-status.ts` | local | Footer block: live tokens + $ for the current session. Resets on every `session_start`. |
|
|
43
|
+
| `notify.ts` | vendored | Terminal-native notification (OSC 777/99/toast) when the agent goes idle. |
|
|
44
|
+
|
|
45
|
+
## Vendored vs forked
|
|
46
|
+
|
|
47
|
+
- **vendored** files are byte-for-byte copies of the upstream examples shipped
|
|
48
|
+
with `@earendil-works/pi-coding-agent`. Don't edit them in place — if you
|
|
49
|
+
need a behaviour change, fork (rename + edit + note the divergence in the
|
|
50
|
+
file header) so future refreshes don't clobber local changes.
|
|
51
|
+
- **forked** files have a header block stating what was changed relative to
|
|
52
|
+
upstream.
|
|
53
|
+
|
|
54
|
+
## Refreshing from upstream
|
|
55
|
+
|
|
56
|
+
The pi npm package ships the originals at
|
|
57
|
+
`/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/examples/extensions/`.
|
|
58
|
+
When bumping pi:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
SRC=/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/examples/extensions
|
|
62
|
+
DST=packages/base/extensions
|
|
63
|
+
for f in confirm-destructive.ts custom-compaction.ts dirty-repo-guard.ts \
|
|
64
|
+
git-checkpoint.ts model-status.ts notify.ts preset.ts \
|
|
65
|
+
session-name.ts status-line.ts; do
|
|
66
|
+
diff -u "$DST/$f" "$SRC/$f" || cp "$SRC/$f" "$DST/$f"
|
|
67
|
+
done
|
|
68
|
+
diff -ru "$SRC/plan-mode" "$DST/plan-mode"
|
|
69
|
+
diff -ru "$SRC/subagent" "$DST/subagent" --exclude=agents --exclude=prompts --exclude=README.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Re-review the forks (`agent-rules.ts`, `protected-paths.ts`) against the
|
|
73
|
+
upstream originals (`claude-rules.ts`, `protected-paths.ts`) to pick up bug
|
|
74
|
+
fixes.
|
|
75
|
+
|
|
76
|
+
## Adding to the bundle
|
|
77
|
+
|
|
78
|
+
1. Drop a `.ts` file (or directory with `index.ts`) into this folder.
|
|
79
|
+
2. Add a row above and note whether it's vendored or homegrown.
|
|
80
|
+
3. Restart pi (or `/reload`).
|
|
81
|
+
|
|
82
|
+
## Where these fit
|
|
83
|
+
|
|
84
|
+
These extensions are the agent-side substrate the garaje ships with: rules
|
|
85
|
+
surfacing (`agent-rules`), guardrails (`protected-paths`, `dirty-repo-guard`,
|
|
86
|
+
`confirm-destructive`, `git-checkpoint`), flow (`plan-mode`, `preset`,
|
|
87
|
+
`role-commands`, `standalone-commands`, `subagent`), and footer telemetry
|
|
88
|
+
(`status-line`, `model-status`, `usage-status`, `notify`). Slash commands
|
|
89
|
+
(`/plan`, `/spec`, `/review`, `/checkpoint`, `/handoff`, `/proof`) come from
|
|
90
|
+
the `role-commands` / `standalone-commands` rows; subagent roles live in
|
|
91
|
+
`packages/base/prompts/roles/`, regenerated into `packages/base/presets.json`
|
|
92
|
+
and `packages/base/agents/` by `bin/build-roles`.
|
|
93
|
+
|
|
94
|
+
See [`docs/garaje-spec.md`](../../docs/garaje-spec.md) for the framework
|
|
95
|
+
design and [`docs/extensions.md`](../../docs/extensions.md) for the
|
|
96
|
+
maintainer-side loading/refresh details.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { packageRoot } from "../lib/base-paths.ts";
|
|
4
|
+
import { resolveRuleFiles } from "../lib/rules.ts";
|
|
5
|
+
|
|
6
|
+
const LOCAL_RULES_SUBDIR = path.join(".agent", "rules");
|
|
7
|
+
|
|
8
|
+
export default function agentRulesExtension(pi: ExtensionAPI) {
|
|
9
|
+
let rules: { label: string }[] = [];
|
|
10
|
+
|
|
11
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
12
|
+
const baseDir = path.join(packageRoot(), "rules");
|
|
13
|
+
const localDir = path.join(ctx.cwd, LOCAL_RULES_SUBDIR);
|
|
14
|
+
rules = resolveRuleFiles(baseDir, localDir).map((r) => ({
|
|
15
|
+
label: path.relative(ctx.cwd, r.absPath),
|
|
16
|
+
}));
|
|
17
|
+
if (rules.length > 0) ctx.ui.notify(`Found ${rules.length} rule(s) (base + local)`, "info");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
pi.on("before_agent_start", async (event) => {
|
|
21
|
+
if (rules.length === 0) return;
|
|
22
|
+
const list = rules.map((r) => `- ${r.label}`).join("\n");
|
|
23
|
+
return {
|
|
24
|
+
systemPrompt:
|
|
25
|
+
event.systemPrompt +
|
|
26
|
+
`\n\n## Project Rules\n\nThe following rules are available (base package + local overrides):\n\n${list}\n\nWhen working on tasks that touch one of these areas, read the relevant rule file(s) before acting.\n`,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-Intent Guard Extension (Phase 4 — north-star §5.1, defense layer 3)
|
|
3
|
+
*
|
|
4
|
+
* Blocks bash commands that could touch the agent's own container or bypass
|
|
5
|
+
* the socket proxy: raw-socket access, DOCKER_HOST overrides, framework-layer
|
|
6
|
+
* compose invocations, --force-recreate, --remove-orphans, docker/compose
|
|
7
|
+
* lifecycle verbs aimed at the pi service, and bare compose lifecycle
|
|
8
|
+
* invocations that name no service (which act on the whole file set — the
|
|
9
|
+
* pi service included). Layers 1 (whitelisting socket proxy) and 2 (compose
|
|
10
|
+
* split + protected paths) are the structural defenses; this one catches
|
|
11
|
+
* intent early, with an explanation instead of a downstream failure.
|
|
12
|
+
*
|
|
13
|
+
* Bay lifecycle is untouched: `garaje bay <verb> <name>` (and even raw
|
|
14
|
+
* docker/compose against bay services) passes through.
|
|
15
|
+
*
|
|
16
|
+
* Accepted limitations (layer 3 of 3; backstopped by the socket proxy and
|
|
17
|
+
* the bays-only compose scope): absolute-path invocations (/usr/bin/docker),
|
|
18
|
+
* --file long-flag and nested -f paths, quoted DOCKER_HOST pointing at a
|
|
19
|
+
* non-standard socket, raw HTTP to docker-proxy:2375, and container names
|
|
20
|
+
* using "pi" without a separator (-pi2).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
/** A token that means "the docker CLI is being invoked" (start of command or
|
|
26
|
+
* after a shell separator). Note: \b also matches inside hyphenated
|
|
27
|
+
* filenames (docker-compose.yml), so downstream checks must require
|
|
28
|
+
* lifecycle context before blocking. */
|
|
29
|
+
const DOCKER_INVOCATION = /(^|[\s;&|(])docker\b/;
|
|
30
|
+
|
|
31
|
+
/** Lifecycle verbs that (re)create, stop, or remove containers. */
|
|
32
|
+
const LIFECYCLE = "(up|down|stop|start|restart|rm|kill|pause|unpause|update)";
|
|
33
|
+
|
|
34
|
+
/** Check for compose lifecycle verbs aimed at the pi service. */
|
|
35
|
+
const COMPOSE_LIFECYCLE_PI = new RegExp(`\\bcompose\\b.*\\b${LIFECYCLE}\\b.*\\spi(\\s|$)`);
|
|
36
|
+
|
|
37
|
+
/** Check for docker lifecycle verbs (matches the invocation, not the target yet). */
|
|
38
|
+
const DOCKER_LIFECYCLE = new RegExp(`\\bdocker\\s+${LIFECYCLE}\\b`);
|
|
39
|
+
|
|
40
|
+
export function blockedCommandReason(command: string): string | undefined {
|
|
41
|
+
if (command.includes("/var/run/docker.sock")) {
|
|
42
|
+
return "raw docker socket access — the agent talks to docker only via the proxy (DOCKER_HOST)";
|
|
43
|
+
}
|
|
44
|
+
if (/DOCKER_HOST=unix:/.test(command)) {
|
|
45
|
+
return "overriding DOCKER_HOST to a unix socket would bypass the socket proxy";
|
|
46
|
+
}
|
|
47
|
+
if (!DOCKER_INVOCATION.test(command)) return undefined;
|
|
48
|
+
if (command.includes("--force-recreate")) {
|
|
49
|
+
return "--force-recreate can recreate the pi service — the container this agent lives in";
|
|
50
|
+
}
|
|
51
|
+
if (command.includes("--remove-orphans")) {
|
|
52
|
+
return "--remove-orphans would remove the framework services (pi, docker-proxy) that sit outside the bays scope";
|
|
53
|
+
}
|
|
54
|
+
if (/-f\s+(\.\/)?(docker-compose\.ya?ml|compose\.framework\.ya?ml)/.test(command)) {
|
|
55
|
+
return "compose against the framework layer is host-only; use `garaje bay <verb> <name>` for parked services";
|
|
56
|
+
}
|
|
57
|
+
if (COMPOSE_LIFECYCLE_PI.test(command)) {
|
|
58
|
+
return "that would touch the pi service — the agent cannot manage its own container (cold changes hand off to the host)";
|
|
59
|
+
}
|
|
60
|
+
if (DOCKER_LIFECYCLE.test(command)) {
|
|
61
|
+
// The pi container's name puts "pi" as the terminal service component:
|
|
62
|
+
// <project>-pi or <project>-pi-<index> (also _ separators). A bay name
|
|
63
|
+
// that merely CONTAINS "pi" (raspberry-pi-web-1) must not match.
|
|
64
|
+
const targetsPi = command.split(/\s+/).some((tok) => /[-_]pi([-_]\d+)?$/.test(tok));
|
|
65
|
+
if (targetsPi) {
|
|
66
|
+
return "that targets the pi container by name — the agent cannot manage its own container";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// A bare compose lifecycle invocation (no service token after the verb, up
|
|
70
|
+
// to the next shell separator) acts on the WHOLE file set — which includes
|
|
71
|
+
// the pi service — so it must be blocked even though it doesn't name "pi".
|
|
72
|
+
const composeVerb = new RegExp(`\\bcompose\\b[^;|&]*?\\b${LIFECYCLE}\\b([^;|&]*)`).exec(command);
|
|
73
|
+
if (composeVerb) {
|
|
74
|
+
const tail = composeVerb[2].trim().split(/\s+/).filter((t) => t && !t.startsWith("-"));
|
|
75
|
+
if (tail.length === 0) {
|
|
76
|
+
return "a compose lifecycle command that names no service acts on the whole file set — which includes the pi service; name bay services explicitly or use `garaje bay <verb> <name>`";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function commandGuardExtension(pi: ExtensionAPI) {
|
|
83
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
84
|
+
if (event.toolName !== "bash") return undefined;
|
|
85
|
+
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
86
|
+
const reason = blockedCommandReason(command);
|
|
87
|
+
if (!reason) return undefined;
|
|
88
|
+
if (ctx.hasUI) ctx.ui.notify(`Blocked docker command: ${reason}`, "warning");
|
|
89
|
+
return {
|
|
90
|
+
block: true,
|
|
91
|
+
reason: `Blocked by packages/base/extensions/command-guard.ts: ${reason}.`,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm Destructive Actions Extension
|
|
3
|
+
*
|
|
4
|
+
* Prompts for confirmation before destructive session actions (clear, switch, branch).
|
|
5
|
+
* Demonstrates how to cancel session events using the before_* events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export default function (pi: ExtensionAPI) {
|
|
11
|
+
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
|
12
|
+
if (!ctx.hasUI) return;
|
|
13
|
+
|
|
14
|
+
if (event.reason === "new") {
|
|
15
|
+
const confirmed = await ctx.ui.confirm(
|
|
16
|
+
"Clear session?",
|
|
17
|
+
"This will delete all messages in the current session.",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!confirmed) {
|
|
21
|
+
ctx.ui.notify("Clear cancelled", "info");
|
|
22
|
+
return { cancel: true };
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// reason === "resume" - check if there are unsaved changes (messages since last assistant response)
|
|
28
|
+
const entries = ctx.sessionManager.getEntries();
|
|
29
|
+
const hasUnsavedWork = entries.some(
|
|
30
|
+
(e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (hasUnsavedWork) {
|
|
34
|
+
const confirmed = await ctx.ui.confirm(
|
|
35
|
+
"Switch session?",
|
|
36
|
+
"You have messages in the current session. Switch anyway?",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!confirmed) {
|
|
40
|
+
ctx.ui.notify("Switch cancelled", "info");
|
|
41
|
+
return { cancel: true };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
pi.on("session_before_fork", async (event, ctx) => {
|
|
47
|
+
if (!ctx.hasUI) return;
|
|
48
|
+
|
|
49
|
+
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
|
|
50
|
+
"Yes, create fork",
|
|
51
|
+
"No, stay in current session",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
if (choice !== "Yes, create fork") {
|
|
55
|
+
ctx.ui.notify("Fork cancelled", "info");
|
|
56
|
+
return { cancel: true };
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Compaction Extension
|
|
3
|
+
*
|
|
4
|
+
* Replaces the default compaction behavior with a full summary of the entire context.
|
|
5
|
+
* Instead of keeping the last 20k tokens of conversation turns, this extension:
|
|
6
|
+
* 1. Summarizes ALL messages (messagesToSummarize + turnPrefixMessages)
|
|
7
|
+
* 2. Discards all old turns completely, keeping only the summary
|
|
8
|
+
*
|
|
9
|
+
* This example also demonstrates using a different model (Gemini Flash) for summarization,
|
|
10
|
+
* which can be cheaper/faster than the main conversation model.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* pi --extension examples/extensions/custom-compaction.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
export default function (pi: ExtensionAPI) {
|
|
21
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
22
|
+
ctx.ui.notify("Custom compaction extension triggered", "info");
|
|
23
|
+
|
|
24
|
+
const { preparation, branchEntries: _, signal } = event;
|
|
25
|
+
const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, previousSummary } = preparation;
|
|
26
|
+
|
|
27
|
+
// Use Gemini Flash for summarization (cheaper/faster than most conversation models)
|
|
28
|
+
const model = ctx.modelRegistry.find("google", "gemini-2.5-flash");
|
|
29
|
+
if (!model) {
|
|
30
|
+
ctx.ui.notify(`Could not find Gemini Flash model, using default compaction`, "warning");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve request auth for the summarization model
|
|
35
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
36
|
+
if (!auth.ok) {
|
|
37
|
+
ctx.ui.notify(`Compaction auth failed: ${auth.error}`, "warning");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!auth.apiKey) {
|
|
41
|
+
ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Combine all messages for full summary
|
|
46
|
+
const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
|
|
47
|
+
|
|
48
|
+
ctx.ui.notify(
|
|
49
|
+
`Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${model.id}...`,
|
|
50
|
+
"info",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Convert messages to readable text format
|
|
54
|
+
const conversationText = serializeConversation(convertToLlm(allMessages));
|
|
55
|
+
|
|
56
|
+
// Include previous summary context if available
|
|
57
|
+
const previousContext = previousSummary ? `\n\nPrevious session summary for context:\n${previousSummary}` : "";
|
|
58
|
+
|
|
59
|
+
// Build messages that ask for a comprehensive summary
|
|
60
|
+
const summaryMessages = [
|
|
61
|
+
{
|
|
62
|
+
role: "user" as const,
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text" as const,
|
|
66
|
+
text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:${previousContext}
|
|
67
|
+
|
|
68
|
+
1. The main goals and objectives discussed
|
|
69
|
+
2. Key decisions made and their rationale
|
|
70
|
+
3. Important code changes, file modifications, or technical details
|
|
71
|
+
4. Current state of any ongoing work
|
|
72
|
+
5. Any blockers, issues, or open questions
|
|
73
|
+
6. Next steps that were planned or suggested
|
|
74
|
+
|
|
75
|
+
Be thorough but concise. The summary will replace the ENTIRE conversation history, so include all information needed to continue the work effectively.
|
|
76
|
+
|
|
77
|
+
Format the summary as structured markdown with clear sections.
|
|
78
|
+
|
|
79
|
+
<conversation>
|
|
80
|
+
${conversationText}
|
|
81
|
+
</conversation>`,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Pass signal to honor abort requests (e.g., user cancels compaction)
|
|
90
|
+
const response = await complete(
|
|
91
|
+
model,
|
|
92
|
+
{ messages: summaryMessages },
|
|
93
|
+
{
|
|
94
|
+
apiKey: auth.apiKey,
|
|
95
|
+
headers: auth.headers,
|
|
96
|
+
maxTokens: 8192,
|
|
97
|
+
signal,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const summary = response.content
|
|
102
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
103
|
+
.map((c) => c.text)
|
|
104
|
+
.join("\n");
|
|
105
|
+
|
|
106
|
+
if (!summary.trim()) {
|
|
107
|
+
if (!signal.aborted) ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Return compaction content - SessionManager adds id/parentId
|
|
112
|
+
// Use firstKeptEntryId from preparation to keep recent messages
|
|
113
|
+
return {
|
|
114
|
+
compaction: {
|
|
115
|
+
summary,
|
|
116
|
+
firstKeptEntryId,
|
|
117
|
+
tokensBefore,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
ctx.ui.notify(`Compaction failed: ${message}`, "error");
|
|
123
|
+
// Fall back to default compaction on error
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dirty Repo Guard Extension
|
|
3
|
+
*
|
|
4
|
+
* Prevents session changes when there are uncommitted git changes.
|
|
5
|
+
* Useful to ensure work is committed before switching context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
async function checkDirtyRepo(
|
|
11
|
+
pi: ExtensionAPI,
|
|
12
|
+
ctx: ExtensionContext,
|
|
13
|
+
action: string,
|
|
14
|
+
): Promise<{ cancel: boolean } | undefined> {
|
|
15
|
+
// Check for uncommitted changes
|
|
16
|
+
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
|
17
|
+
|
|
18
|
+
if (code !== 0) {
|
|
19
|
+
// Not a git repo, allow the action
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hasChanges = stdout.trim().length > 0;
|
|
24
|
+
if (!hasChanges) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!ctx.hasUI) {
|
|
29
|
+
// In non-interactive mode, block by default
|
|
30
|
+
return { cancel: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Count changed files
|
|
34
|
+
const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
|
|
35
|
+
|
|
36
|
+
const choice = await ctx.ui.select(`You have ${changedFiles} uncommitted file(s). ${action} anyway?`, [
|
|
37
|
+
"Yes, proceed anyway",
|
|
38
|
+
"No, let me commit first",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
if (choice !== "Yes, proceed anyway") {
|
|
42
|
+
ctx.ui.notify("Commit your changes first", "warning");
|
|
43
|
+
return { cancel: true };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function (pi: ExtensionAPI) {
|
|
48
|
+
pi.on("session_before_switch", async (event, ctx) => {
|
|
49
|
+
const action = event.reason === "new" ? "new session" : "switch session";
|
|
50
|
+
return checkDirtyRepo(pi, ctx, action);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("session_before_fork", async (_event, ctx) => {
|
|
54
|
+
return checkDirtyRepo(pi, ctx, "fork");
|
|
55
|
+
});
|
|
56
|
+
}
|