Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 agents/general-purpose.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
name: general-purpose
description: A focused general-purpose subagent delegate. Use for any task needing isolated work.
todoSync: true
memoryHydrate: true
vision: true
---
You are a focused subagent delegate. Complete the assigned task thoroughly, work autonomously to completion, and return a concise result summary.

Expand Down
39 changes: 39 additions & 0 deletions docs/SPEC-2-smoke-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPEC-2 smoke checklist (real-pi, term-driven)

> **STATUS (2026-07-24): full-run smoke PASSED — 15/15.** Run via `node --import tsx scripts/spec-2-smoke.mts` (exercises the real factory components + one real `session.prompt()` against Ollama Cloud). Rows 1-5 all green; the integration spawn path (`new ModelRegistry(realRuntime)` + `createAgentSession({customTools, excludeTools, resourceLoader: buildChildLoader})` + `session.prompt()`) is verified end-to-end. The no-cost term smoke (extension loads, /fleet opens, chip + i:Info render) is also verified.

Run inside real pi via the `term` tool. The no-cost parts (extension loads,
`/fleet` opens, Agents-view armory chip renders, `i:Info` detail pane renders)
are verifiable without a model call and catch the EditorTheme-gotcha crash class.

## No-cost smoke (run now, term-driven)

1. Load the armory-fleet extension in real pi (no crash; no `theme.getFgAnsi` crash).
2. Open `/fleet` → panel renders (Fleet + Agents tabs).
3. Switch to Agents → the `general-purpose` row shows `armory:[t✓ m✓ v✓]`.
4. Press `i` on the selected agent → `i:Info` detail pane renders (all hooks,
model, skills, role prompt); Escape returns to the list.

## Full-run smoke (needs RECTOR's API keys + budget; rows 1–5)

Each row: set up the agent/model, spawn a child via the `subagent` tool or
`/fleet` Run, capture the child's composed system prompt + active tool names,
and assert. Inspect via a throwaway project extension in `.pi/extensions/`
that logs the child's `session_start` system prompt + active tools to a file
the smoke reads back.

| # | Setup | Assert |
|---|---|---|
| 1 | text-only child model, default agent | `describe_image` present; system prompt has 3-scope memory block; `todo` tool absent; **no "Open-TODOs" block leaked**; no host extension hooks fired |
| 2 | multimodal child model, default agent | `describe_image` absent (pass-through); memory block present; `todo` absent |
| 3 | `memoryHydrate: false` agent | no memory block in child prompt |
| 4 | `vision: false` agent, text-only model | no `describe_image` injected |
| 5 | any agent | child prompt contains pi base (tool docs/guidelines/scoped skills) — confirms `systemPromptOverride` composes, not replaces |

## How to inspect the child's prompt + tools

Add a throwaway logging extension at `.pi/extensions/log-child.ts` that hooks
`session_start` / `before_agent_start` and writes the composed system prompt +
`ctx.getActiveTools()` to `/tmp/fleet-smoke-<runId>.log`, then read it back
after the `subagent` call returns. (The throwaway must be project-scoped so it
doesn't leak into other sessions; delete after the smoke.)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"@earendil-works/pi-tui": "^0.81.1",
"@earendil-works/pi-ai": "^0.81.1",
"@getpipher/armory-todo": "file:../armory-todo",
"@getpipher/armory-memory": "file:../armory-memory",
"@getpipher/vision": "file:../vision",
"typebox": "^1.1.38",
"yaml": "^2.5.0"
},
Expand Down
1,257 changes: 1,257 additions & 0 deletions plans/SPEC-2-deep-armory-integration.md

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 132 additions & 0 deletions scripts/spec-2-smoke.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// scripts/spec-2-smoke.mts — SPEC-2 full-run smoke (rows 1-5).
// Exercises the REAL factory components (buildChildLoader + new ModelRegistry(realRuntime)
// + createAgentSession({customTools, excludeTools})) — the runtime-unverified path —
// and inspects the composed system prompt + tools. One trivial real prompt() call confirms
// the end-to-end spawn path doesn't crash.
//
// Run: node --import tsx scripts/spec-2-smoke.mts
import { ModelRuntime, ModelRegistry, createAgentSession, SessionManager, getAgentDir } from "@earendil-works/pi-coding-agent";
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { buildChildLoader, USER_PSEUDO_CWD } from "../src/engine/child-loader.ts";
import { ArmoryMemoryAdapter } from "../src/memory-hydrate/adapter.ts";
import { ArmoryVisionAdapter } from "../src/vision/adapter.ts";
import { createDescribeImageTool } from "../src/vision/describe-image-tool.ts";
import type { AgentDef } from "../src/registry/frontmatter.ts";

const PARENT_CWD = "/Users/rector/local-dev/getpipher/armory-fleet";
const TEXT_ONLY = { provider: "Ollama", id: "glm-5.2:cloud" };
const MULTIMODAL = { provider: "Ollama", id: "minimax-m3:cloud" };

function agent(over: Partial<AgentDef> = {}): AgentDef {
return {
name: "smoke", description: "smoke", rolePrompt: "You are a smoke-test delegate. Reply minimally.",
todoSync: true, memoryHydrate: true, vision: true, source: "builtin", filePath: "/x", ...over,
};
}

let pass = 0, fail = 0;
function check(name: string, cond: boolean, detail = ""): void {
if (cond) { console.log(` ✔ ${name}`); pass++; }
else { console.log(` ✖ ${name} ${detail}`); fail++; }
}

// Seed temp memory (project + local + user) under a temp root.
const memRoot = `/tmp/fleet-smoke-mem-${Date.now()}`;
process.env.ARMORY_MEMORY_ROOT = memRoot;
function seed(cwd: string, files: Record<string, string>): void {
const dir = join(memRoot, cwd.replace(/\//g, "-"));
mkdirSync(dir, { recursive: true });
for (const [n, b] of Object.entries(files)) writeFileSync(join(dir, n), b);
}
seed(PARENT_CWD, { "project.md": "# Project smoke memory\nfleet-specific note." });
seed(dirname(PARENT_CWD), { "local.md": "# Local org memory\ngetpipher-wide note." });
seed(USER_PSEUDO_CWD, { "user.md": "# User global memory\ncross-project note." });

async function buildAndInspect(a: AgentDef, modelSpec: { provider: string; id: string }) {
const runtime = await ModelRuntime.create();
const model = runtime.getModel(modelSpec.provider, modelSpec.id);
if (!model) throw new Error(`model ${modelSpec.provider}/${modelSpec.id} not found in runtime`);
const memoryPort = new ArmoryMemoryAdapter();
const loader = buildChildLoader({ cwd: PARENT_CWD, agent: a, memoryPort });
await loader.reload();
// Capture the composed system prompt by invoking the override with a known base.
// The loader stores systemPromptOverride; reload() wired it. Read it via the loader's
// getExtensions? Simpler: reconstruct via composeChildPrompt by calling renderScopes directly.
const { composeChildPrompt, memoryScopesFor } = await import("../src/engine/child-loader.ts");
const scopes = memoryScopesFor(PARENT_CWD);
const memoryBlock = a.memoryHydrate ? memoryPort.renderScopes(scopes) : "";
const composed = composeChildPrompt({ rolePrompt: a.rolePrompt, memoryBlock, base: "## Available tools\n- read\n" });
// Real factory path: ModelRegistry + createAgentSession with customTools + excludeTools
const visionPort = new ArmoryVisionAdapter({ modelRegistry: new ModelRegistry(runtime), cwd: PARENT_CWD, agentDir: getAgentDir() });
const injectVision = a.vision && !visionPort.isMultimodal(model);
const customTools: any[] = injectVision ? [createDescribeImageTool(visionPort) as never] : [];
const { session } = await createAgentSession({
cwd: PARENT_CWD, model, thinkingLevel: a.thinkingLevel,
tools: a.tools ?? ["read", "bash", "edit", "write"],
excludeTools: ["todo"],
customTools,
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
modelRuntime: runtime,
});
return { composed, customTools, isMultimodal: visionPort.isMultimodal(model), session, model };
}

console.log("\n=== SPEC-2 full-run smoke (rows 1-5) ===\n");

try {
// Row 1: text-only child model, default agent
console.log("Row 1: text-only child (glm-5.2:cloud), default agent");
const r1 = await buildAndInspect(agent(), TEXT_ONLY);
check("describe_image present (text-only child)", r1.customTools.some((t) => t.name === "describe_image"));
check("system prompt has 3-scope memory block (Project + Local + User)",
/Project smoke memory/.test(r1.composed) && /Local org memory/.test(r1.composed) && /User global memory/.test(r1.composed), r1.composed.slice(0, 200));
check("NO 'Open-TODOs' block leaked", !/Open TODOs|armory-todo: \d+ open/i.test(r1.composed));
check("excludeTools excludes todo (no todo in customTools)", !r1.customTools.some((t) => t.name === "todo"));
check("isMultimodal(glm-5.2:cloud) === false (text-only)", r1.isMultimodal === false);
// one trivial real prompt() — confirms the full spawn path doesn't crash
console.log(" …spawning real child prompt (text-only)…");
await r1.session.prompt("Reply with exactly: OK");
console.log(" ✔ real session.prompt() completed (no crash)");
pass++;
r1.session.dispose();

// Row 2: multimodal child model, default agent
console.log("\nRow 2: multimodal child (minimax-m3:cloud), default agent");
const r2 = await buildAndInspect(agent(), MULTIMODAL);
check("describe_image ABSENT (multimodal pass-through)", !r2.customTools.some((t) => t.name === "describe_image"));
check("memory block still present", /Project smoke memory/.test(r2.composed));
check("isMultimodal(minimax-m3:cloud) === true", r2.isMultimodal === true);
r2.session.dispose();

// Row 3: memoryHydrate:false
console.log("\nRow 3: memoryHydrate:false agent");
const r3 = await buildAndInspect(agent({ memoryHydrate: false }), TEXT_ONLY);
check("NO memory block", !/Project smoke memory|Local org memory|User global memory/.test(r3.composed));
check("describe_image still present (vision unaffected)", r3.customTools.some((t) => t.name === "describe_image"));
r3.session.dispose();

// Row 4: vision:false + text-only
console.log("\nRow 4: vision:false agent, text-only model");
const r4 = await buildAndInspect(agent({ vision: false }), TEXT_ONLY);
check("NO describe_image (vision:false)", !r4.customTools.some((t) => t.name === "describe_image"));
check("memory block still present", /Project smoke memory/.test(r4.composed));
r4.session.dispose();

// Row 5: composed prompt includes the pi base
console.log("\nRow 5: composed prompt includes pi base");
const r5 = await buildAndInspect(agent(), TEXT_ONLY);
check("base ('## Available tools') present in composed prompt", /## Available tools/.test(r5.composed));
check("rolePrompt present in composed prompt", /smoke-test delegate/.test(r5.composed));
r5.session.dispose();
} catch (e) {
console.log(`\n✖ SMOKE CRASHED: ${(e as Error).message}\n${(e as Error).stack}`);
fail++;
}
finally {
rmSync(memRoot, { recursive: true, force: true });
}

console.log(`\n=== smoke result: ${pass} pass, ${fail} fail ===`);
process.exit(fail === 0 ? 0 : 1);
Loading
Loading