@yaag/extension 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.
@@ -0,0 +1,45 @@
1
+ import { SetupWorkspaceError, type SetupWorkspaceExecutor } from "./setup-workspace.ts";
2
+
3
+ const DESCRIPTION = "Install or refresh editor types for programs under .yaag/.";
4
+
5
+ /** The only pi UI capability workspace setup needs. */
6
+ export interface SetupWorkspaceCommandContext {
7
+ readonly cwd: string;
8
+ readonly ui: {
9
+ notify(message: string, type?: "info" | "warning" | "error"): void;
10
+ };
11
+ }
12
+
13
+ /** Definition suitable for `ExtensionAPI.registerCommand`. */
14
+ export interface SetupWorkspaceCommand {
15
+ readonly description: string;
16
+ handler(args: string, ctx: SetupWorkspaceCommandContext): Promise<void>;
17
+ }
18
+
19
+ /**
20
+ * Creates `/yaag-setup-workspace`, the user-facing workspace setup command.
21
+ *
22
+ * It notifies the CLI report on success and expected setup failures on error;
23
+ * it has no Run, progress, summary, usage, or follow-up behavior.
24
+ */
25
+ export function createSetupWorkspaceCommand(
26
+ executor: SetupWorkspaceExecutor,
27
+ ): SetupWorkspaceCommand {
28
+ return {
29
+ description: DESCRIPTION,
30
+ async handler(args, ctx): Promise<void> {
31
+ try {
32
+ const report = await executor.execute({ dir: args.trim() || ctx.cwd });
33
+ ctx.ui.notify(report, "info");
34
+ } catch (error: unknown) {
35
+ ctx.ui.notify(message(error), "error");
36
+ }
37
+ },
38
+ };
39
+ }
40
+
41
+ function message(error: unknown): string {
42
+ if (error instanceof SetupWorkspaceError) return error.message;
43
+ if (error instanceof Error) return error.message;
44
+ return String(error);
45
+ }
@@ -0,0 +1,45 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { SetupWorkspaceExecutor } from "./setup-workspace.ts";
4
+
5
+ const parameters = Type.Object({
6
+ dir: Type.Optional(
7
+ Type.String({
8
+ description: "Workspace root. Defaults to pi's current working directory.",
9
+ }),
10
+ ),
11
+ });
12
+
13
+ /** Dependencies for the direct `yaag_setup_workspace` CLI wrapper. */
14
+ export interface SetupWorkspaceToolOptions {
15
+ readonly executor: SetupWorkspaceExecutor;
16
+ }
17
+
18
+ const DESCRIPTION = [
19
+ "Set up editor types for an Orchestration Program workspace.",
20
+ "Use this when a program author sees missing @yaag/runtime editor types.",
21
+ "Normally run it only once per workspace, then rerun after an upgrade to refresh generated declarations.",
22
+ ].join("\n");
23
+
24
+ /**
25
+ * Creates `yaag_setup_workspace`, a direct wrapper around the Bun CLI setup command.
26
+ *
27
+ * It has no Run registry, Run id, Lifecycle Events, summaries, cassette behavior,
28
+ * usage, or follow-up channel. Throws actionable missing-Bun, setup-failure, or
29
+ * cancellation errors.
30
+ */
31
+ export function createSetupWorkspaceTool(
32
+ options: SetupWorkspaceToolOptions,
33
+ ): ToolDefinition<typeof parameters, undefined> {
34
+ const { executor } = options;
35
+ return {
36
+ name: "yaag_setup_workspace",
37
+ label: "Set Up Workspace",
38
+ description: DESCRIPTION,
39
+ parameters,
40
+ async execute(_id, params, signal, _onUpdate, ctx) {
41
+ const report = await executor.execute({ dir: params.dir ?? ctx.cwd, signal });
42
+ return { content: [{ type: "text", text: report }], details: undefined };
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,69 @@
1
+ import { type CliChild, type CliChildOptions, startCliChild } from "./cli-child.ts";
2
+ import { statusReport } from "./status.ts";
3
+
4
+ /** Injectable boundary for starting the Bun CLI child. */
5
+ export type StartCliChild = (options: CliChildOptions) => CliChild;
6
+
7
+ /** Dependencies resolved once when the extension loads. */
8
+ export interface SetupWorkspaceExecutorOptions {
9
+ readonly bun: string | null;
10
+ readonly cli: string;
11
+ readonly startChild?: StartCliChild;
12
+ }
13
+
14
+ /** One requested workspace-setup execution. */
15
+ export interface SetupWorkspaceRequest {
16
+ readonly dir: string;
17
+ readonly signal?: AbortSignal;
18
+ }
19
+
20
+ /** Shared setup boundary used by the model tool and slash command. */
21
+ export interface SetupWorkspaceExecutor {
22
+ execute(request: SetupWorkspaceRequest): Promise<string>;
23
+ }
24
+
25
+ /** An expected Bun bridge, CLI, or cancellation failure during workspace setup. */
26
+ export class SetupWorkspaceError extends Error {
27
+ constructor(message: string) {
28
+ super(message);
29
+ this.name = "SetupWorkspaceError";
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Creates the sole extension-side executor for `bun cli.ts setup-workspace`.
35
+ *
36
+ * Resolves with the CLI report verbatim. Throws `SetupWorkspaceError` for a
37
+ * missing Bun bridge, failed CLI invocation, or cancellation; it never owns
38
+ * workspace artifact policy.
39
+ */
40
+ export function createSetupWorkspaceExecutor(
41
+ options: SetupWorkspaceExecutorOptions,
42
+ ): SetupWorkspaceExecutor {
43
+ const { bun, cli, startChild = startCliChild } = options;
44
+ return {
45
+ async execute({ dir, signal }: SetupWorkspaceRequest): Promise<string> {
46
+ if (bun === null) throw new SetupWorkspaceError(statusReport(null, cli));
47
+ const child = startChild({ bun, cli, argv: ["setup-workspace", dir] });
48
+ signal?.addEventListener("abort", child.stop, { once: true });
49
+ if (signal?.aborted === true) child.stop();
50
+ try {
51
+ const outcome = await child.outcome;
52
+ if (signal?.aborted === true)
53
+ throw new SetupWorkspaceError("yaag_setup_workspace: cancelled");
54
+ if (outcome.code !== 0)
55
+ throw new SetupWorkspaceError(failure(outcome.code, outcome.stderr));
56
+ return outcome.stdout;
57
+ } finally {
58
+ signal?.removeEventListener("abort", child.stop);
59
+ }
60
+ },
61
+ };
62
+ }
63
+
64
+ function failure(code: number | null, stderr: string): string {
65
+ const tail = stderr.trimEnd();
66
+ const what =
67
+ code === null ? "workspace setup was killed" : `workspace setup failed (exit ${code})`;
68
+ return tail === "" ? `yaag_setup_workspace: ${what}` : `yaag_setup_workspace: ${what}\n${tail}`;
69
+ }
@@ -0,0 +1,84 @@
1
+ import { applyEvent, initialSummary, type LifecycleEvent, type RunSummary } from "@yaag/runtime";
2
+ import { startCliChild } from "./cli-child.ts";
3
+ import { readEvents } from "./event-reader.ts";
4
+
5
+ /** Everything one Run of the CLI produced, once the child has exited. */
6
+ export interface RunOutcome {
7
+ readonly code: number | null;
8
+ /** The Run's return value, verbatim from stdout. */
9
+ readonly stdout: string;
10
+ /** The bounded tail of the child's error output. */
11
+ readonly stderr: string;
12
+ readonly summary: RunSummary;
13
+ }
14
+
15
+ export interface StartRunOptions {
16
+ readonly bun: string;
17
+ readonly cli: string;
18
+ readonly file: string;
19
+ /** Opaque JSON, forwarded untouched: the program validates it (ADR-0010). */
20
+ readonly args?: string;
21
+ /** Cassette path to write, forwarded as `--record` (ADR-0013). */
22
+ readonly record?: string;
23
+ /** Cassette path to resume from, forwarded as `--resume` (ADR-0014). */
24
+ readonly resume?: string;
25
+ /**
26
+ * Called after each event is folded. `sequence` identifies this fd 3 stream
27
+ * occurrence, not an event value or timestamp; it makes renderer observation
28
+ * idempotent while preserving equal-valued consecutive events.
29
+ */
30
+ readonly onProgress?: (summary: RunSummary, event: LifecycleEvent, sequence: number) => void;
31
+ }
32
+
33
+ export interface SpawnRunOptions extends StartRunOptions {
34
+ readonly signal?: AbortSignal;
35
+ }
36
+
37
+ /** A Run in flight: its eventual outcome, and the reap ladder that ends it. */
38
+ export interface RunHandle {
39
+ readonly outcome: Promise<RunOutcome>;
40
+ readonly stop: () => void;
41
+ }
42
+
43
+ /**
44
+ * Starts one Orchestration Program as a child `bun` process without waiting for
45
+ * it. Descriptor 3 remains exclusive to Run Lifecycle Events (ADR-0016).
46
+ *
47
+ * Never throws for a failed Run — the exit code and error tail are part of its
48
+ * outcome; callers choose how that failure reaches the Host Session.
49
+ */
50
+ export function startRun(options: StartRunOptions): RunHandle {
51
+ const argv = ["run", options.file, "--events-fd", "3"];
52
+ if (options.args !== undefined) argv.push("--args", options.args);
53
+ if (options.record !== undefined) argv.push("--record", options.record);
54
+ if (options.resume !== undefined) argv.push("--resume", options.resume);
55
+ const child = startCliChild({ bun: options.bun, cli: options.cli, argv, events: true });
56
+ let summary: RunSummary = initialSummary();
57
+ let sequence = 0;
58
+ const outcome = Promise.all([
59
+ child.outcome,
60
+ child.events === undefined
61
+ ? Promise.resolve()
62
+ : readEvents(child.events, (event) => {
63
+ summary = applyEvent(summary, event);
64
+ sequence += 1;
65
+ options.onProgress?.(summary, event, sequence);
66
+ }),
67
+ ]).then(([ended]) => ({ ...ended, summary }));
68
+ return { outcome, stop: child.stop };
69
+ }
70
+
71
+ /**
72
+ * Runs one Orchestration Program to completion, stopping it if `signal` aborts.
73
+ * The blocking convenience wrapper over {@link startRun}; same failure contract.
74
+ */
75
+ export async function spawnRun(options: SpawnRunOptions): Promise<RunOutcome> {
76
+ const handle = startRun(options);
77
+ options.signal?.addEventListener("abort", handle.stop, { once: true });
78
+ if (options.signal?.aborted === true) handle.stop();
79
+ try {
80
+ return await handle.outcome;
81
+ } finally {
82
+ options.signal?.removeEventListener("abort", handle.stop);
83
+ }
84
+ }
@@ -0,0 +1,118 @@
1
+ import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type { RunSummary } from "@yaag/runtime";
3
+ import { renderSnapshot, TreeState } from "@yaag/tui";
4
+ import { Type } from "typebox";
5
+ import type { RegisteredRun, RunRegistry } from "./run-registry.ts";
6
+ import { toUsage } from "./usage.ts";
7
+
8
+ const parameters = Type.Object({
9
+ id: Type.Optional(Type.String({ description: "The Run id returned by yaag_run (e.g. r1)" })),
10
+ });
11
+
12
+ /** Canonical facts returned by a requested status snapshot. */
13
+ export interface SnapshotDetails {
14
+ readonly id: string;
15
+ readonly summary: RunSummary;
16
+ readonly result?: string;
17
+ }
18
+
19
+ /** Details are populated only when a specific Run was requested. */
20
+ export type StatusDetails = SnapshotDetails | Record<never, never>;
21
+
22
+ export interface StatusToolOptions {
23
+ readonly now?: () => number;
24
+ readonly width?: number;
25
+ }
26
+
27
+ const DESCRIPTION = [
28
+ "Show a snapshot of a Run started in this Host Session.",
29
+ "",
30
+ "With an id, returns that Run's live snapshot or its retained final snapshot.",
31
+ "Without an id, lists every Run started this session in registration order.",
32
+ ].join("\n");
33
+
34
+ /**
35
+ * The `yaag_status` tool renders session-local Run snapshots for the model.
36
+ *
37
+ * Throws if an id was not started in this session; finished Runs remain
38
+ * queryable and are never restarted or controlled by this tool.
39
+ */
40
+ export function createStatusTool(
41
+ registry: RunRegistry,
42
+ options: StatusToolOptions = {},
43
+ ): ToolDefinition<typeof parameters, StatusDetails> {
44
+ const now = options.now ?? Date.now;
45
+ const width = options.width ?? 80;
46
+ return {
47
+ name: "yaag_status",
48
+ label: "Run Status",
49
+ description: DESCRIPTION,
50
+ parameters,
51
+ async execute(_id, params) {
52
+ if (params.id === undefined) return overview(registry.runs, now, width);
53
+ const found = registry.lookup(params.id);
54
+ if (found.state === "unknown") throw unknownId(params.id, registry.knownIds);
55
+ const details = detailsFor(found);
56
+ return {
57
+ content: [{ type: "text", text: snapshot(details, now, width) }],
58
+ details,
59
+ usage: toUsage(details.summary),
60
+ };
61
+ },
62
+ };
63
+ }
64
+
65
+ function overview(
66
+ runs: readonly RegisteredRun[],
67
+ now: () => number,
68
+ width: number,
69
+ ): AgentToolResult<StatusDetails> {
70
+ if (runs.length === 0)
71
+ return {
72
+ content: [{ type: "text", text: "No Runs started this session." }],
73
+ details: { summary: undefined },
74
+ };
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: runs.map((run) => snapshot(detailsFor(run), now, width, true)).join("\n"),
80
+ },
81
+ ],
82
+ details: { summary: undefined },
83
+ };
84
+ }
85
+
86
+ function detailsFor(run: RegisteredRun): SnapshotDetails {
87
+ if (run.state === "live") return { id: run.run.id, summary: run.run.summary };
88
+ const { outcome } = run.run;
89
+ return {
90
+ id: run.run.id,
91
+ summary: run.run.summary,
92
+ ...(outcome.kind === "fulfilled" && outcome.outcome.code === 0
93
+ ? { result: outcome.outcome.stdout.trimEnd() }
94
+ : {}),
95
+ };
96
+ }
97
+
98
+ function snapshot(
99
+ details: SnapshotDetails,
100
+ now: () => number,
101
+ width: number,
102
+ headerOnly = false,
103
+ ): string {
104
+ const state = TreeState.fromSummary(details.summary);
105
+ const lines = renderSnapshot(state, {
106
+ now: now(),
107
+ width,
108
+ label: details.id,
109
+ ...(details.result === undefined ? {} : { result: details.result }),
110
+ });
111
+ return (headerOnly ? lines.slice(0, 1) : lines).join("\n");
112
+ }
113
+
114
+ function unknownId(id: string, knownIds: readonly string[]): Error {
115
+ const known =
116
+ knownIds.length === 0 ? "there are no known ids" : `known ids: ${knownIds.join(", ")}`;
117
+ return new Error(`yaag_status: no Run with id ${id}; ${known}`);
118
+ }
package/src/status.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The one message the extension has to say about itself: either the bridge to
3
+ * the Bun CLI is in place, or exactly what is missing and how to install it.
4
+ */
5
+ export function statusReport(bun: string | null, cli: string): string {
6
+ if (bun === null) {
7
+ return (
8
+ "yaag needs Bun and could not find it on PATH or in ~/.bun/bin. " +
9
+ "Install it with: curl -fsSL https://bun.sh/install | bash"
10
+ );
11
+ }
12
+ return `yaag ready — bun: ${bun}, cli: ${cli}`;
13
+ }
@@ -0,0 +1,34 @@
1
+ /** Default tail size: enough for a stack trace, small enough to hand a model. */
2
+ const DEFAULT_LIMIT = 8192;
3
+
4
+ /**
5
+ * The last `limit` bytes of a child's error output.
6
+ *
7
+ * Bounded on purpose: a Run that floods stderr must not grow the host session's
8
+ * memory, and only the end of the output explains why it failed.
9
+ */
10
+ export class StderrTail {
11
+ readonly #limit: number;
12
+ #chunks: Buffer[] = [];
13
+ #size = 0;
14
+
15
+ constructor(limit: number = DEFAULT_LIMIT) {
16
+ this.#limit = limit;
17
+ }
18
+
19
+ /** Appends output, discarding whatever no longer fits in the tail. */
20
+ write(chunk: string | Buffer): void {
21
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
22
+ this.#chunks.push(buffer);
23
+ this.#size += buffer.length;
24
+ if (this.#size <= this.#limit) return;
25
+ const joined = Buffer.concat(this.#chunks).subarray(this.#size - this.#limit);
26
+ this.#chunks = [joined];
27
+ this.#size = joined.length;
28
+ }
29
+
30
+ /** The retained tail, decoded as UTF-8. */
31
+ text(): string {
32
+ return Buffer.concat(this.#chunks).toString("utf8");
33
+ }
34
+ }
@@ -0,0 +1,115 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type { RunSummary } from "@yaag/runtime";
3
+ import { Type } from "typebox";
4
+ import type { RunRegistry } from "./run-registry.ts";
5
+ import { toUsage } from "./usage.ts";
6
+
7
+ /** What the stop result carries for rendering — never shown to the model. */
8
+ export interface StopDetails {
9
+ readonly summary: RunSummary;
10
+ }
11
+
12
+ const parameters = Type.Object({
13
+ id: Type.String({ description: "The Run id to stop, as returned by yaag_run (e.g. r1)" }),
14
+ });
15
+
16
+ const DESCRIPTION = [
17
+ "Stop a background Run started by yaag_run, by its Run id.",
18
+ "",
19
+ "The Run's Agents are reaped, and the report says what it got through and what",
20
+ "it spent before it stopped.",
21
+ ].join("\n");
22
+
23
+ /** What a stopped Run got through and what it cost, from the fold (ADR-0006). */
24
+ export function stopReport(id: string, summary: RunSummary): string {
25
+ const agents = Object.keys(summary.agents).length;
26
+ const cost = summary.incomplete
27
+ ? `at least $${summary.cost.toFixed(4)}`
28
+ : `$${summary.cost.toFixed(4)}`;
29
+ const tokens = summary.tokens === null ? "unknown tokens" : `${summary.tokens.total} tokens`;
30
+ return [
31
+ `Run ${id} stopped.`,
32
+ `Program: ${summary.program === "" ? "unknown" : summary.program}`,
33
+ `Agents seen: ${agents}`,
34
+ `Asks settled: ${summary.asksSettled}/${summary.asksStarted}`,
35
+ `Spent: ${cost}, ${tokens}`,
36
+ ...Object.entries(summary.agents)
37
+ .sort(([left], [right]) => left.localeCompare(right))
38
+ .map(([name, agent]) => agentReport(name, agent)),
39
+ ].join("\n");
40
+ }
41
+
42
+ function agentReport(name: string, agent: RunSummary["agents"][string]): string {
43
+ const state = agentStateReport(agent);
44
+ const cost = agent.cost === null ? "unknown cost" : `$${agent.cost.toFixed(4)}`;
45
+ const tokens = agent.tokens === null ? "unknown tokens" : `${agent.tokens.total} tokens`;
46
+ const branch = agent.branch === null ? "" : `, branch ${agent.branch}`;
47
+ return `Agent ${name}: ${state}, ${cost}, ${tokens}${branch}`;
48
+ }
49
+
50
+ function agentStateReport(agent: RunSummary["agents"][string]): string {
51
+ switch (agent.state) {
52
+ case "idle":
53
+ return "idle";
54
+ case "asking":
55
+ return `ask #${agent.askIndex}${agent.promptGist === "" ? "" : `, ${agent.promptGist}`}`;
56
+ case "exited":
57
+ return "exited";
58
+ default: {
59
+ const never: never = agent;
60
+ throw new Error(`yaag_stop: unhandled Agent state: ${JSON.stringify(never)}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ function toError(reason: unknown): Error {
66
+ return reason instanceof Error ? reason : new Error(String(reason));
67
+ }
68
+
69
+ /**
70
+ * The `yaag_stop` tool: ends one background Run by name, through the same reap
71
+ * ladder an abort uses (ADR-0008).
72
+ *
73
+ * `execute` throws when the id is unknown or its Run has already finished —
74
+ * both are worth saying out loud rather than reporting a stop that did nothing.
75
+ */
76
+ export function createStopTool(
77
+ registry: RunRegistry,
78
+ ): ToolDefinition<typeof parameters, StopDetails> {
79
+ return {
80
+ name: "yaag_stop",
81
+ label: "Stop Run",
82
+ description: DESCRIPTION,
83
+ parameters,
84
+ async execute(_id, params) {
85
+ const status = registry.lookup(params.id);
86
+ switch (status.state) {
87
+ case "unknown":
88
+ throw new Error(`yaag_stop: no Run with id ${params.id}`);
89
+ case "finished":
90
+ throw new Error(`yaag_stop: Run ${params.id} has already finished`);
91
+ case "live": {
92
+ const { run } = status;
93
+ run.stop();
94
+ try {
95
+ await run.outcome;
96
+ } catch {}
97
+ const settled = registry.lookup(run.id);
98
+ if (settled.state !== "finished")
99
+ throw new Error(`yaag_stop: Run ${run.id} settled without registry retention`);
100
+ if (settled.run.outcome.kind === "rejected") throw toError(settled.run.outcome.reason);
101
+ const { outcome } = settled.run.outcome;
102
+ return {
103
+ content: [{ type: "text", text: stopReport(run.id, outcome.summary) }],
104
+ details: { summary: outcome.summary },
105
+ usage: toUsage(outcome.summary),
106
+ };
107
+ }
108
+ default: {
109
+ const never: never = status;
110
+ throw new Error(`yaag_stop: unhandled Run status: ${JSON.stringify(never)}`);
111
+ }
112
+ }
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,53 @@
1
+ /** Builds the host-visible yaag block appended to pi's system prompt. */
2
+ export function yaagPromptBlock(directories: readonly string[]): string {
3
+ const list = directories.join(", ");
4
+ return [
5
+ "## yaag Orchestration Programs",
6
+ "",
7
+ `Program Directories (each contains \`.yaag/\`): ${list}.`,
8
+ "Orchestration Programs are the `.ts` files there: TypeScript modules that",
9
+ "default-export `defineRun(...)`. Run one with the `yaag_run` tool; inspect",
10
+ "its declared args with `yaag_describe`.",
11
+ "",
12
+ "Minimal program:",
13
+ "",
14
+ "```ts",
15
+ 'import { defineRun, prompt } from "@yaag/runtime";',
16
+ "",
17
+ "export default defineRun({",
18
+ ' name: "example",',
19
+ " async run(ctx) {",
20
+ ' const agent = await ctx.spawn({ name: "worker" });',
21
+ " const text = await agent.ask(prompt`Do one focused thing and report.`);",
22
+ " return text; // the Run's result",
23
+ " },",
24
+ "});",
25
+ "```",
26
+ "",
27
+ "Full authoring surface (defineAgent, args schemas, ask limits, worktrees):",
28
+ "read `<program dir>/.yaag/types/runtime/index.d.ts`.",
29
+ ].join("\n");
30
+ }
31
+
32
+ interface BeforePrompt {
33
+ readonly systemPrompt: string;
34
+ }
35
+
36
+ /**
37
+ * Creates the `before_agent_start` handler. Caches the first non-empty scan
38
+ * for the session; while empty it re-scans each turn so a mid-session
39
+ * `yaag_setup_workspace` surfaces on the next turn.
40
+ */
41
+ export function createSystemPromptAppender(
42
+ scan: () => Promise<readonly string[]>,
43
+ ): (event: BeforePrompt) => Promise<{ systemPrompt: string } | undefined> {
44
+ let cached: readonly string[] | null = null;
45
+ return async (event) => {
46
+ if (cached === null) {
47
+ const dirs = await scan();
48
+ if (dirs.length === 0) return undefined;
49
+ cached = dirs;
50
+ }
51
+ return { systemPrompt: `${event.systemPrompt}\n\n${yaagPromptBlock(cached)}` };
52
+ };
53
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ /**
4
+ * A fully typed ExtensionContext stub for tool tests.
5
+ *
6
+ * Every capability a Run tool never touches throws on access, so a test that
7
+ * accidentally depends on session state fails loudly instead of silently.
8
+ */
9
+ export class TestExtensionContext implements ExtensionContext {
10
+ readonly mode: ExtensionContext["mode"] = "json";
11
+ readonly hasUI = false;
12
+ readonly model: ExtensionContext["model"] = undefined;
13
+ readonly scopedModels: ExtensionContext["scopedModels"] = [];
14
+ readonly signal: ExtensionContext["signal"] = undefined;
15
+
16
+ constructor(readonly cwd: string) {}
17
+
18
+ get ui(): ExtensionContext["ui"] {
19
+ return unavailable("ui");
20
+ }
21
+
22
+ get sessionManager(): ExtensionContext["sessionManager"] {
23
+ return unavailable("sessionManager");
24
+ }
25
+
26
+ get modelRegistry(): ExtensionContext["modelRegistry"] {
27
+ return unavailable("modelRegistry");
28
+ }
29
+
30
+ isIdle(): boolean {
31
+ return true;
32
+ }
33
+
34
+ isProjectTrusted(): boolean {
35
+ return true;
36
+ }
37
+
38
+ abort(): void {}
39
+
40
+ hasPendingMessages(): boolean {
41
+ return false;
42
+ }
43
+
44
+ shutdown(): void {}
45
+
46
+ getContextUsage(): ReturnType<ExtensionContext["getContextUsage"]> {
47
+ return undefined;
48
+ }
49
+
50
+ compact(): void {}
51
+
52
+ getSystemPrompt(): string {
53
+ return "";
54
+ }
55
+ }
56
+
57
+ function unavailable<T>(dependency: string): T {
58
+ throw new Error(`unexpected test context dependency: ${dependency}`);
59
+ }