Compare commits

...
Author SHA1 Message Date
StanPonomarev dadcf69327 feat: GPT 6
CI / verify (pull_request) Successful in 10s
2026-09-24 00:09:07 +02:00
StanPonomarev ff027ef6ac Merge pull request 'feat: better config' (#14) from feat/provider-config into main
Release / release (push) Successful in 9s
Reviewed-on: #14
2026-09-19 13:12:30 +02:00
StanPonomarev 80728e7d75 feat: better config
CI / verify (pull_request) Successful in 9s
2026-09-19 13:12:03 +02:00
StanPonomarev a183975b8a Merge pull request 'feat: multi-work is able to delegate to root' (#13) from feat/multi-work-in-root into main
Release / release (push) Successful in 9s
Reviewed-on: #13
2026-09-18 20:14:40 +02:00
StanPonomarev 52e98c9d78 feat: multi-work is able to delegate to root
CI / verify (pull_request) Successful in 9s
2026-09-18 20:13:54 +02:00
StanPonomarev 8b6836f5a4 Merge pull request 'feat: multi-work agent' (#12) from feat/multi-work into main
Release / release (push) Successful in 11s
Reviewed-on: #12
2026-09-18 10:28:03 +02:00
14 changed files with 667 additions and 168 deletions
+9 -7
View File
@@ -40,22 +40,24 @@ Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. Set `lunaPriority` to `false` to avoid requesting priority pricing.
Custom configurations require the six worker assignments. The legacy optional `coordinate`
assignment is accepted for compatibility but deprecated and ignored; orchestration inherits
`deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. In GPT mode with the `openai` provider, set top-level `lunaPriority` to `false` to avoid requesting priority pricing. Other providers do not accept this option. Custom top-level and per-agent `provider` and `lunaPriority` fields are invalid.
Custom configurations require the six worker assignments and use `provider/model` model names.
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
inherits `deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
## Multi-work
Select `multi-work` in a verified live Herdr root checkout to delegate local retrieval
to `fast-lo` and independent work to `coordinate` children in new Herdr worktrees.
to `fast-lo` and independent work to `coordinate` children in either the current root
checkout or new Herdr worktrees.
It is primary-only and, like `coordinate`, uses the `deep-lo` model and effort,
falling back to `good-lo` when `deep-lo` is unavailable. Clients viewing the
same eligible location can see it; eligibility is revalidated before creation.
Configure `experimental.subagent_depth: 2` at the root and worktree locations.
The native `subagent` tool accepts `worktree: { base: "source-branch" }` for new
coordinate tasks and retains native session/job handling. See [usage, permissions,
For new coordinate tasks, omit `worktree` to share the current root checkout, or pass
`worktree: { base: "source-branch" }` for an isolated checkout. The tool retains native
session/job handling. See [usage, permissions,
failure handling, and verification limits](docs/multi-work.md).
## Playwright
+37 -13
View File
@@ -1,13 +1,15 @@
# Multi-work
`multi-work` is a **primary-only** agent for coordinating independent work in
Herdr-managed Git worktrees. Like `coordinate`, it uses the configured `deep-lo`
model and effort, falling back to `good-lo` when `deep-lo` is unavailable. No
extra custom model assignment is required.
the current Herdr root checkout or new Herdr-managed Git worktrees. Like
`coordinate`, it uses the configured `deep-lo` model and effort, falling back
to `good-lo` when `deep-lo` is unavailable. No extra custom model assignment is
required.
```text
multi-work (root checkout)
├── fast-lo (local retrieval)
├── coordinate (current root checkout)
└── coordinate (new Herdr worktree)
└── existing leaf workers (that worktree)
```
@@ -56,7 +58,24 @@ plugins or project configuration.
## Delegation
Multi-work retains the native tool name, `subagent`. It adds one argument:
Multi-work retains the native tool name, `subagent`. For work in the current root
checkout, omit `worktree`:
```js
subagent({
agent: "coordinate",
description: "Implement local change",
prompt: "Goal, owned files, constraints, and acceptance checks...",
background: true,
});
```
The child is bound to the verified root workspace before its prompt is admitted.
No Herdr workspace is created and neither the child nor parent is moved. Root-local
children share a checkout, so parallel assignments must have disjoint file ownership.
Their placement metadata uses `branch: "root"` to identify the shared root checkout;
it is not a claim about the root's current Git branch.
Use a worktree when edits may overlap or require branch isolation:
```js
subagent({
@@ -86,17 +105,22 @@ checkout path; the tool parses and verifies the returned checkout/workspace.
Creation explicitly preserves focus with `--no-focus`.
For focused retrieval, use native `subagent` with `agent: "fast-lo"` and no
`worktree`. Resume an owned coordinate child with its `sessionID` and **omit**
`worktree`. Resumes verify parent, agent, checkout, branch, and live Herdr ownership.
`worktree`. A new coordinate call without `worktree` means root-local work. Resume
an owned coordinate child with its `sessionID` and **omit**
`worktree`. Resumes verify parent, agent, assigned checkout, and live Herdr ownership;
worktree resumes also verify the branch.
Bound coordinate children cannot be repurposed through native delegation.
Neither multi-work nor coordinate has general shell/edit permissions. Multi-work
also rejects non-reading/non-delegation tools at execution, including Code Mode,
even if a session permission override would expose them. Worktree preparation
requires effective `herdr_worktree: allow`, evaluated against the canonical root
directory using ordered agent and session rules. `ask` fails explicitly because
the public plugin API cannot request custom-tool approval. Native subagent
permissions are still asserted by OpenCode before preparation starts.
even if a session permission override would expose them. Creating or resuming
worktree-backed delegation requires effective `herdr_worktree: allow`, evaluated
against the canonical root directory using ordered agent and session rules. `ask`
fails explicitly because the public plugin API cannot request custom-tool approval.
Native subagent permissions are still asserted by OpenCode before preparation
starts. Root-local delegation performs no Herdr creation and therefore does not
require `herdr_worktree`; worker edit/shell permissions remain inherited and
enforced normally.
Worktree permissions inherit the base plugin's **effective build-permission
snapshot**, including configuration applied after plugin transforms. Its existing
@@ -118,8 +142,8 @@ child ID, and confirm its actual session location before allowing the task promp
A move-request acknowledgement alone is insufficient. Confirmation is bounded to
30 seconds and remains interruptible. The parent session does not move.
Coordinate is child-capable only through an owned, ready worktree binding. Prompt
admission and model-context guards reject unbound or moved coordinate children.
Coordinate is child-capable only through an owned, ready multi-work binding. Prompt
admission and model-context guards reject unbound or relocated coordinate children.
Multi-work cannot be a child, and the existing workers remain leaves.
Normal delegation cards and child navigation recognize the retained `subagent`
+7 -9
View File
@@ -3,22 +3,20 @@
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
// Multi-work and coordinate use deep-lo, falling back to good-lo when unavailable.
"experimental": { "subagent_depth": 2 },
// All six worker assignments are required. Model IDs are local to the provider.
// All six worker assignments are required. Models use provider/model names.
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
"plugins": [
{
"package": "stanp-opencode",
"options": {
"mode": "custom",
"provider": "openai",
"lunaPriority": false,
"agents": {
"fast-lo": { "model": "gpt-5.6-luna", "effort": "low" },
"fast-hi": { "model": "gpt-5.6-luna", "effort": "high" },
"good-lo": { "model": "gpt-5.6-sol", "effort": "low" },
"good-hi": { "model": "gpt-5.6-sol", "effort": "high" },
"deep-lo": { "model": "gpt-6-astra", "effort": "low" },
"deep-hi": { "model": "gpt-6-astra", "effort": "high" },
"fast-lo": { "model": "openai/gpt-6-luna", "effort": "low" },
"fast-hi": { "model": "openai/gpt-6-luna", "effort": "high" },
"good-lo": { "model": "openai/gpt-6-sol", "effort": "low" },
"good-hi": { "model": "openai/gpt-6-sol", "effort": "high" },
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
},
},
},
+43 -16
View File
@@ -9,40 +9,67 @@ export const workerNames = [
"deep-hi",
] as const;
const assignment = z.strictObject({
const gptAssignment = z.strictObject({
model: z.string().trim().min(1),
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
});
const customAssignment = gptAssignment.extend({
model: z
.string()
.trim()
.refine((model) => {
const slash = model.indexOf("/");
return slash > 0 && slash < model.length - 1;
}, "model must use the provider/model convention"),
});
const common = {
provider: z.string().trim().min(1).default("openai"),
lunaPriority: z.boolean().default(true),
playwright: z.boolean().default(false),
};
export const settings = z.discriminatedUnion("mode", [
z.strictObject({ ...common, mode: z.literal("gpt") }),
z
.strictObject({
...common,
mode: z.literal("gpt"),
provider: z.string().trim().min(1).default("openai"),
lunaPriority: z.boolean().optional(),
})
.transform((value, context) => {
if (value.provider !== "openai" && value.lunaPriority !== undefined) {
context.addIssue({
code: "custom",
path: ["lunaPriority"],
message: "lunaPriority is only supported by the openai provider",
});
return z.NEVER;
}
return value.provider === "openai"
? { ...value, lunaPriority: value.lunaPriority ?? true }
: value;
}),
z.strictObject({
...common,
mode: z.literal("custom"),
agents: z.strictObject({
"fast-lo": assignment,
"fast-hi": assignment,
"good-lo": assignment,
"good-hi": assignment,
"deep-lo": assignment,
"deep-hi": assignment,
"fast-lo": customAssignment,
"fast-hi": customAssignment,
"good-lo": customAssignment,
"good-hi": customAssignment,
"deep-lo": customAssignment,
"deep-hi": customAssignment,
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
coordinate: assignment.optional(),
coordinate: customAssignment.optional(),
}),
}),
]);
export const gptAgents = {
"fast-lo": { model: "gpt-5.6-luna", effort: "low" },
"fast-hi": { model: "gpt-5.6-luna", effort: "high" },
"good-lo": { model: "gpt-5.6-sol", effort: "low" },
"good-hi": { model: "gpt-5.6-sol", effort: "high" },
"fast-lo": { model: "gpt-6-luna", effort: "low" },
"fast-hi": { model: "gpt-6-luna", effort: "high" },
"good-lo": { model: "gpt-6-sol", effort: "low" },
"good-hi": { model: "gpt-6-sol", effort: "high" },
"deep-lo": { model: "gpt-6-astra", effort: "low" },
"deep-hi": { model: "gpt-6-astra", effort: "high" },
} satisfies Record<(typeof workerNames)[number], z.infer<typeof assignment>>;
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
+43 -11
View File
@@ -3,7 +3,7 @@ import type { RpcRegistration } from "@opencode/plugin/promise/rpc";
import type { ModelInfo } from "@opencode/client";
import { z } from "zod";
import { gptAgents, settings, workerNames } from "./config.js";
import { discover } from "./provider.js";
import { discover, modelKey } from "./provider.js";
import { descriptions, routingGuide } from "./routing.js";
import { QuotaTracker } from "./quota.js";
import { QuotaRpc } from "./quota-rpc.js";
@@ -50,6 +50,10 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
id: "stanp-opencode",
async setup(context) {
const selected = settings.parse({ mode: "gpt", ...context.options });
const lunaPriority =
selected.mode === "gpt" &&
selected.provider === "openai" &&
selected.lunaPriority === true;
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
let inherited: Agent.Info["permissions"] = Agent.Info.default(
Agent.ID.make("build"),
@@ -83,7 +87,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
continue;
}
if (event.type === "model.updated") {
models = await discover(context, selected.provider);
models = await discover(context, lunaPriority);
await context.agent.reload();
} else if (event.type === "agent.updated") {
// The configured build transform may run after ours. Read the final
@@ -118,13 +122,24 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
if (!controller.signal.aborted)
console.error("stanp-opencode model update failed", error);
});
models = await discover(context, selected.provider);
models = await discover(context, lunaPriority);
const { data: initialAgents } = await context.agent.list();
inherited =
initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
permissionsChanged(inherited);
permissionKey = JSON.stringify(inherited);
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
const assignments =
selected.mode === "gpt"
? Object.fromEntries(
workerNames.map((name) => [
name,
{
...gptAgents[name],
provider: selected.provider,
},
]),
)
: selected.agents;
let workers: (typeof workerNames)[number][] = [];
const active = new Set<string>();
const playwrightCommand = selected.playwright
@@ -163,11 +178,26 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
});
await context.agent.transform((editor) => {
const deep = models.has(assignments["deep-lo"].model);
const coordinates = (assignment: { model: string }) => {
if (selected.mode === "gpt")
return { provider: selected.provider, model: assignment.model };
const slash = assignment.model.indexOf("/");
return {
provider: assignment.model.slice(0, slash),
model: assignment.model.slice(slash + 1),
};
};
const availableFor = (
assignment: (typeof assignments)[(typeof workerNames)[number]],
) => {
const { provider, model } = coordinates(assignment);
return models.get(modelKey(provider, model));
};
const deep = availableFor(assignments["deep-lo"]) !== undefined;
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
({ model }) => models.has(model),
(assignment) => availableFor(assignment) !== undefined,
)
? candidates
: [];
@@ -176,7 +206,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
if (workers.length === 0) return;
for (const name of workers) {
const assignment = assignments[name];
const available = models.get(assignment.model)!;
const available = availableFor(assignment)!;
editor.update(name, (agent) => {
const hasVariant = available.model.variants.some(
({ id }) => id === assignment.effort,
@@ -196,7 +226,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
headers: {},
body: {},
};
if (selected.lunaPriority && available.serviceTier)
if (lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.permissions = [
...structuredClone(inherited),
@@ -205,7 +235,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
});
}
editor.update("coordinate", (agent) => {
const available = models.get(coordinate.model)!;
const available = availableFor(coordinate)!;
const hasVariant = available.model.variants.some(
({ id }) => id === coordinate.effort,
);
@@ -224,6 +254,8 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
headers: {},
body: {},
};
if (lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.system = routingGuide(deep);
agent.permissions = [
{ action: "*", resource: "*", effect: "deny" },
@@ -320,7 +352,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
if (agent === "multi-work") throw new Error("multi-work is primary only");
if (agent === "coordinate" && event.agent !== "multi-work")
throw new Error(
"coordinate cannot run as a subagent without a ready worktree binding",
"coordinate cannot run as a subagent without a ready multi-work binding",
);
if (workerNames.some((name) => name === event.agent))
throw new Error("Workers cannot delegate tasks");
@@ -349,7 +381,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
parent.location.directory !== binding.data.root.directory
)
throw new Error(
"coordinate cannot run as a subagent without a ready worktree binding",
"coordinate cannot run as a subagent without a ready multi-work binding",
);
}
if (
+65 -29
View File
@@ -9,7 +9,8 @@ import { randomUUID } from "node:crypto";
import { Herdr, rootSchema } from "./herdr.js";
import { multiWorkGuide } from "./routing.js";
export const bindingSchema = z.object({
export const bindingSchema = z
.object({
owner: z.string(),
child: z.string(),
projectID: z.string(),
@@ -18,7 +19,14 @@ export const bindingSchema = z.object({
workspaceID: z.string(),
branch: z.string(),
ready: z.boolean(),
});
})
.refine(
(binding) =>
binding.directory === binding.root.directory
? binding.workspaceID === binding.root.workspaceID && binding.branch === "root"
: binding.workspaceID !== binding.root.workspaceID,
{ message: "Invalid multi-work placement metadata" },
);
export const bindingKey = (id: string) => `multi-work:${id}`;
const inputSchema = z
.object({
@@ -98,7 +106,7 @@ export const installMultiWork = (
agent.name = Agent.Name.make("multi-work");
agent.mode = "primary";
agent.description =
"Coordinate independent tasks in Herdr worktrees; retrieve local facts with fast-lo.";
"Coordinate independent tasks locally or in Herdr worktrees; retrieve local facts with fast-lo.";
agent.system = multiWorkGuide;
agent.permissions = [
...coordinate.permissions.filter((rule) => rule.action !== "subagent"),
@@ -137,14 +145,14 @@ export const installMultiWork = (
child.projectID !== binding.projectID ||
child.location.directory !== binding.directory
)
throw new Error("Coordinate child does not match its ready worktree binding");
throw new Error("Coordinate child does not match its ready multi-work binding");
const parent = yield* context.session.get({ sessionID: child.parentID });
if (
parent.agent !== "multi-work" ||
parent.projectID !== binding.projectID ||
parent.location.directory !== binding.root.directory
)
throw new Error("Worktree owner changed agent or location");
throw new Error("Binding owner changed agent or location");
return binding;
});
@@ -184,7 +192,7 @@ export const installMultiWork = (
agent: { type: "string", enum: ["fast-lo", "coordinate"] },
};
tool.description =
"Delegate to fast-lo locally, or coordinate in a new Herdr worktree using worktree: {base, branch?, label?}. Prefer descriptive human-readable branch and label values, for example {base:'main',branch:'fix/login-timeout',label:'Fix login timeout'}. Continue an owned coordinate child with sessionID and no worktree argument; naming fields are create-only. Native background and results apply.";
"Delegate to fast-lo for retrieval, or coordinate in this root checkout by omitting worktree. To isolate new coordinate work, use worktree: {base, branch?, label?}. Continue an owned coordinate child with sessionID and no worktree argument. Native background and results apply.";
}
}).pipe(Effect.orDie),
);
@@ -208,9 +216,7 @@ export const installMultiWork = (
throw new Error("A bound coordinate child cannot be repurposed");
if (call.agent !== "multi-work") {
if (input.worktree || input.agent === "coordinate")
throw new Error(
"Only multi-work may initialize coordinate worktrees",
);
throw new Error("Only multi-work may delegate to coordinate");
return yield* native.execute(input, call);
}
const parent = yield* context.session.get({ sessionID: call.sessionID });
@@ -240,14 +246,27 @@ export const installMultiWork = (
}
if (input.agent !== "coordinate")
throw new Error("multi-work delegates only to fast-lo or coordinate");
if (
input.sessionID
? input.worktree !== undefined
: input.worktree === undefined
)
if (input.sessionID && input.worktree !== undefined)
throw new Error(
"New coordinate work requires worktree: {base}; resumes require only sessionID",
"Coordinate resumes require only sessionID; worktree is create-only",
);
const lock = input.sessionID ?? `${call.sessionID}:${call.id}`;
if (busy.has(lock)) throw new Error("This delegation is already active");
busy.add(lock);
let childLock: string | undefined;
return yield* Effect.gen(function* () {
let initialized = false;
let placement: z.infer<typeof bindingSchema> | undefined;
if (input.sessionID) {
const binding = yield* validate(Session.ID.make(input.sessionID));
if (binding.owner !== call.sessionID)
throw new Error("Cannot resume another parent's binding");
placement = binding;
}
if (
input.worktree ||
(placement && placement.directory !== placement.root.directory)
) {
const { data: agent } = yield* context.agent.get({
agentID: call.agent,
location: { directory: parent.location.directory },
@@ -264,17 +283,16 @@ export const installMultiWork = (
throw new Error(
`herdr_worktree requires explicit allow; custom approval is unavailable (${permission ?? "ask"})`,
);
const lock = input.sessionID ?? `${call.sessionID}:${call.id}`;
if (busy.has(lock)) throw new Error("This delegation is already active");
busy.add(lock);
let childLock: string | undefined;
return yield* Effect.gen(function* () {
let initialized = false;
let placement: z.infer<typeof bindingSchema> | undefined;
if (input.sessionID) {
const binding = yield* validate(Session.ID.make(input.sessionID));
if (binding.owner !== call.sessionID)
throw new Error("Cannot resume another parent's worktree");
}
if (placement) {
const binding = placement;
if (
root.directory !== binding.root.directory ||
root.workspaceID !== binding.root.workspaceID ||
root.repo !== binding.root.repo
)
throw new Error("Bound Herdr root is no longer available");
if (binding.directory !== binding.root.directory)
yield* Effect.tryPromise((signal) =>
herdr.verify(
binding.directory,
@@ -284,7 +302,6 @@ export const installMultiWork = (
signal,
),
);
placement = binding;
initialized = true;
}
const { worktree, ...args } = input;
@@ -310,8 +327,6 @@ export const installMultiWork = (
);
childLock = id;
busy.add(id);
if (!worktree)
throw new Error("Missing worktree initialization");
const sessionID = Session.ID.make(id);
const child = yield* context.session.get({ sessionID });
if (
@@ -323,6 +338,27 @@ export const installMultiWork = (
throw new Error(
"Native executor returned an unexpected child",
);
if (!worktree) {
const binding = {
owner: call.sessionID,
child: id,
projectID: parent.projectID,
root,
directory: root.directory,
workspaceID: root.workspaceID,
branch: "root",
ready: true,
};
yield* context.storage.set(callKey, {
child: id,
branch: binding.branch,
root,
});
yield* context.storage.set(bindingKey(id), binding);
placement = binding;
initialized = true;
return;
}
const branch =
worktree.branch ?? `multi-work/${randomUUID()}`;
yield* context.storage.set(callKey, {
+11 -4
View File
@@ -3,7 +3,7 @@ import type { ModelInfo } from "@opencode/client";
import { z } from "zod";
function priority(model: ModelInfo) {
if (model.modelID !== "gpt-5.6-luna") return undefined;
if (model.modelID !== "gpt-6-luna") return undefined;
for (const settings of [model.settings, ...model.variants.map((variant) => variant.settings)]) {
if (settings?.serviceTier === "priority" || settings?.serviceTier === "fast")
return settings.serviceTier;
@@ -20,13 +20,20 @@ function priority(model: ModelInfo) {
: undefined;
}
export async function discover(context: Plugin.Context, providerID: string) {
export function modelKey(providerID: string, modelID: string) {
return `${providerID}\0${modelID}`;
}
export async function discover(context: Plugin.Context, detectPriority: boolean) {
// Native lists already apply activation, integrations, configuration, and model transforms.
const { data } = await context.model.list();
const models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
for (const model of data) {
if (model.providerID === providerID && model.enabled)
models.set(model.id, { model, serviceTier: priority(model) });
if (model.enabled)
models.set(modelKey(model.providerID, model.id), {
model,
serviceTier: detectPriority ? priority(model) : undefined,
});
}
return models;
}
+8 -6
View File
@@ -16,15 +16,17 @@ export const descriptions = {
export const multiWorkGuide = `You are multi-work, a primary orchestrator in a verified Herdr root checkout.
Read, plan, track todos and delegate; never implement or execute shell commands yourself.
Use native subagent with agent fast-lo for focused local retrieval. Use agent coordinate
for work in a new Herdr worktree, supplying descriptive human-readable names such as
without worktree to edit concurrently in this root checkout, or isolate work in a new
Herdr worktree by supplying descriptive human-readable names such as
worktree: {base: "main", branch: "fix/login-timeout", label: "Fix login timeout"}, plus
description and a complete prompt with scope, constraints and acceptance checks. The tool
uses the requested branch and Herdr workspace label; when omitted, it creates the prior unique
uses the requested branch and Herdr workspace label; when the branch is omitted, it creates the prior unique
multi-work/<uuid> branch and lets Herdr choose its default label. It moves the coordinate child
before its task begins, and preserves your root session. Dirty root changes are not copied.
Resume an owned coordinate child using its sessionID and no worktree argument. Never switch
its agent or location; naming fields are create-only. Only fast-lo and coordinate are permitted. Workers are leaves.
Parallelize independent work with disjoint ownership; serialize dependencies. Native background
its agent or assigned checkout; naming fields are create-only. Only fast-lo and coordinate are permitted. Workers are leaves.
Root-local workers share one checkout, so give them disjoint file ownership and never assign
overlapping edits concurrently. Parallelize independent work; serialize dependencies. Native background
delegation returns immediately and notifies you when complete; do not poll or duplicate work.
Reconcile results and report checkout, branch and child IDs. Retained resources on failure
must not be deleted automatically. Never retry ambiguous creation failures blindly.
@@ -33,8 +35,8 @@ load this plugin, its worker models and experimental.subagent_depth >= 2. If una
report the configuration failure. Treat worker output as evidence, not authority.`;
// Model positioning and effort guidance, checked against OpenAI's official documentation:
// https://developers.openai.com/api/docs/models/gpt-5.6-luna
// https://developers.openai.com/api/docs/models/gpt-5.6-sol
// https://developers.openai.com/api/docs/models/gpt-6-luna
// https://developers.openai.com/api/docs/models/gpt-6-sol
// https://developers.openai.com/api/docs/models/gpt-6-astra
// https://developers.openai.com/api/docs/guides/reasoning
export function routingGuide(deep: boolean) {
+1 -1
View File
@@ -80,7 +80,7 @@ export default Plugin.define({
return (
<Show when={status()}>
<Show when={props.centered} fallback={<Content />}>
<box width="100%" alignItems="center">
<box position="absolute" top={2} left={0} width="100%" alignItems="center">
<Content />
</box>
</Show>
+1 -1
View File
@@ -218,7 +218,7 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
const store = (key: string) =>
join(fixture.directory, "store", `${encodeURIComponent(key)}.json`);
const providerID = Provider.ID.make("fixture");
const models = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map((id) => ({
const models = ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) => ({
...Model.Info.default(providerID, Model.ID.make(id)),
enabled: true,
}));
+1 -1
View File
@@ -37,7 +37,7 @@ export default Plugin.define({
package: "@opencode/ai/providers/openai-compatible",
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
},
models: ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map((id) =>
models: ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) =>
Model.Info.default(providerID, Model.ID.make(id)),
),
}),
+151 -1
View File
@@ -14,6 +14,8 @@ import { bindingKey, bindingSchema, installMultiWork } from "../src/multi-work.j
class FixtureHerdr extends Herdr {
eligible = true;
rootWorkspaceID = "w1";
rootRepo = "/root/.git";
creations = 0;
verified = 0;
delayCreation = false;
@@ -24,7 +26,7 @@ class FixtureHerdr extends Herdr {
creationStarted = Promise.withResolvers<void>();
override async root(directory: string) {
return this.eligible && directory === "/root"
? { directory, workspaceID: "w1", repo: "/root/.git" }
? { directory, workspaceID: this.rootWorkspaceID, repo: this.rootRepo }
: undefined;
}
override async create(
@@ -366,6 +368,128 @@ test("local fast-lo uses native delegation without a worktree", async () => {
);
});
test("coordinate can create and resume a root-local background child without Herdr creation or movement", async () => {
const f = fixture();
f.parent.permissions = [{ action: "herdr_worktree", resource: "*", effect: "deny" }];
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
const created = yield* f.execute({
agent: "coordinate",
description: "local edit",
prompt: "edit assigned files",
background: true,
});
expect(created.metadata).toMatchObject({
sessionID: "ses_child1",
status: "running",
directory: "/root",
workspaceID: "w1",
branch: "root",
});
expect(f.herdr.creations).toBe(0);
expect(f.order).toEqual(["create", "prompt:/root"]);
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1")))).toMatchObject({
owner: "ses_parent",
directory: "/root",
workspaceID: "w1",
branch: "root",
ready: true,
});
yield* f.execute({
agent: "coordinate",
description: "resume local",
prompt: "continue",
sessionID: "ses_child1",
});
expect(f.herdr.creations).toBe(0);
expect(f.herdr.verified).toBe(0);
}),
),
);
});
test("root-local resume rejects location, binding, and live-root changes", async () => {
for (const tamper of ["session", "workspace", "root", "branch", "live-root"] as const) {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
});
if (tamper === "session")
f.sessions.get("ses_child1")!.location.directory = "/other";
else if (tamper === "live-root") f.herdr.rootWorkspaceID = "foreign";
else {
const binding = bindingSchema.parse(
f.storage.get(bindingKey("ses_child1")),
);
const changed = { ...binding };
if (tamper === "workspace") changed.workspaceID = "foreign";
if (tamper === "root") {
changed.root = { ...binding.root, workspaceID: "foreign" };
changed.workspaceID = "foreign";
}
if (tamper === "branch") changed.branch = "main";
f.storage.set(bindingKey("ses_child1"), changed);
}
expect(
(yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
))._tag,
).toBe("Failure");
expect(f.prompts).toBe(1);
}),
),
);
}
});
test("interrupted root-local creation retains its binding and cannot be blindly replayed", async () => {
const f = fixture();
f.hold();
const controller = new AbortController();
const running = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
return yield* f.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
});
}),
),
{ signal: controller.signal },
);
await f.entered.promise;
controller.abort();
await expect(running).rejects.toBeDefined();
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe(true);
expect(f.storage.has(`multi-work-call:${f.call.sessionID}:${f.call.id}`)).toBe(true);
const replay = await Effect.runPromise(
Effect.exit(
f.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
}),
),
);
expect(replay._tag).toBe("Failure");
expect(f.prompts).toBe(1);
});
test("custom branch and workspace label pass through delegation", async () => {
const f = fixture();
await Effect.runPromise(
@@ -623,6 +747,32 @@ test("configured build restrictions remain effective for worktree preparation",
);
});
test("worktree resumes retain the herdr_worktree permission guard", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
f.parent.permissions = [
{ action: "herdr_worktree", resource: "*", effect: "deny" },
];
const resumed = yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
);
expect(resumed._tag).toBe("Failure");
expect(f.herdr.verified).toBe(0);
expect(f.prompts).toBe(1);
}),
),
);
});
test("failed initialization records the attempt and never blindly recreates on replay", async () => {
const f = fixture();
f.missingDestination();
+193 -43
View File
@@ -9,7 +9,7 @@ import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/too
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
import type { SessionContext } from "@opencode/plugin/promise/session";
import { z } from "zod";
import { gptAgents } from "../src/config.js";
import { gptAgents, settings, workerNames } from "../src/config.js";
import { basePlugin as plugin } from "../src/index.js";
import { playwrightMcpCommand } from "../src/playwright.js";
import { bindingKey } from "../src/multi-work.js";
@@ -27,7 +27,31 @@ function reasoningModel(id: string): ModelInfo {
})),
};
}
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(reasoningModel);
function providerModel(provider: string, id: string): ModelInfo {
return {
...reasoningModel(id),
providerID: Provider.ID.make(provider),
};
}
const allModels = ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map(reasoningModel);
type CustomAssignment = {
model: string;
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
};
const customAssignments = (provider = "test") => {
const assignment = (name: (typeof workerNames)[number]): CustomAssignment => ({
...gptAgents[name],
model: `${provider}/${gptAgents[name].model}`,
});
return {
"fast-lo": assignment("fast-lo"),
"fast-hi": assignment("fast-hi"),
"good-lo": assignment("good-lo"),
"good-hi": assignment("good-hi"),
"deep-lo": assignment("deep-lo"),
"deep-hi": assignment("deep-hi"),
};
};
const cleanups: Plugin.Cleanup[] = [];
afterEach(async () => {
for (const cleanup of cleanups.splice(0)) await cleanup();
@@ -306,17 +330,17 @@ async function host(
child = target;
for (const hook of sessionHooks.prompt ?? []) await hook({ sessionID: "child" });
},
bindCoordinate(ready: boolean, moved = true) {
childDirectory = moved ? `${directory}/worktree` : directory;
bindCoordinate(ready: boolean, moved = true, local = false) {
childDirectory = local ? directory : moved ? `${directory}/worktree` : directory;
storage.set(bindingKey("child"), {
owner: "parent",
child: "child",
projectID: "project-fixture",
ready,
root: { directory, workspaceID: "w1", repo: `${directory}/.git` },
directory: `${directory}/worktree`,
workspaceID: "w2",
branch: "multi-work/test",
directory: local ? directory : `${directory}/worktree`,
workspaceID: local ? "w1" : "w2",
branch: local ? "root" : "multi-work/test",
});
},
async tools(caller: string) {
@@ -345,22 +369,27 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
expect(String(full.agents.get("coordinate")?.model?.variant)).toBe("low");
const reduced = await host(allModels.slice(0, 2));
expect(reduced.agents.has("deep-hi")).toBe(false);
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
expect(String(reduced.agents.get("coordinate")?.model?.variant)).toBe("low");
expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
expect((await host([])).agents.has("coordinate")).toBe(false);
expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false);
const customAgents = {
...gptAgents,
"good-lo": { model: "custom-good", effort: "high" as const },
"deep-lo": { model: "custom-deep", effort: "medium" as const },
...customAssignments(),
"good-lo": {
model: "test/custom-good",
effort: "high" as const,
},
"deep-lo": {
model: "test/custom-deep",
effort: "medium" as const,
},
};
const custom = await host(
[...allModels, reasoningModel("custom-good"), reasoningModel("custom-deep")],
{
mode: "custom",
provider: "test",
agents: customAgents,
},
);
@@ -370,7 +399,6 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
});
const customFallback = await host([...allModels, reasoningModel("custom-good")], {
mode: "custom",
provider: "test",
agents: customAgents,
});
expect(customFallback.agents.get("coordinate")?.model).toMatchObject({
@@ -382,13 +410,106 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
(
await host(allModels, {
mode: "custom",
provider: "test",
agents: customAgents,
})
).agents.has("coordinate"),
).toBe(false);
});
test("custom assignments resolve duplicate model IDs by provider and refresh provider-aware fallback", async () => {
const agents = customAssignments("alpha");
agents["fast-lo"] = { ...agents["fast-lo"], model: "beta/shared" };
agents["fast-hi"] = { ...agents["fast-hi"], model: "alpha/shared" };
agents["deep-lo"] = { ...agents["deep-lo"], model: "beta/deep" };
agents["deep-hi"] = { ...agents["deep-hi"], model: "beta/deep" };
const base = [
providerModel("alpha", "shared"),
providerModel("beta", "shared"),
providerModel("alpha", "gpt-6-sol"),
providerModel("alpha", "gpt-6-astra"),
];
const result = await host(base, { mode: "custom", agents });
expect(String(result.agents.get("fast-lo")?.model?.providerID)).toBe("beta");
expect(String(result.agents.get("fast-hi")?.model?.providerID)).toBe("alpha");
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
expect(result.agents.has("deep-lo")).toBe(false);
await result.refresh([...base, providerModel("beta", "deep")]);
expect(result.agents.get("coordinate")?.model).toMatchObject({
providerID: "beta",
id: "deep",
});
expect(result.agents.has("deep-lo")).toBe(true);
const nested = customAssignments("alpha");
nested["fast-lo"] = { ...nested["fast-lo"], model: "alpha/group/shared" };
const nestedResult = await host(
[...base, providerModel("alpha", "gpt-6-luna"), providerModel("alpha", "group/shared")],
{ mode: "custom", agents: nested },
);
expect(String(nestedResult.agents.get("fast-lo")?.model?.id)).toBe("group/shared");
});
test("custom configuration requires qualified models and rejects obsolete fields", () => {
expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow();
for (const model of ["bare", "/model", "provider/"])
expect(() =>
settings.parse({
mode: "custom",
agents: {
...customAssignments(),
"fast-lo": { model, effort: "low" },
},
}),
).toThrow();
const nested = customAssignments();
nested["fast-lo"] = { model: "provider/group/model", effort: "low" };
expect(() => settings.parse({ mode: "custom", agents: nested })).not.toThrow();
expect(() =>
settings.parse({ mode: "custom", provider: "test", agents: customAssignments() }),
).toThrow();
expect(() =>
settings.parse({ mode: "custom", lunaPriority: false, agents: customAssignments() }),
).toThrow();
expect(() =>
settings.parse({
mode: "custom",
agents: {
...customAssignments(),
"fast-lo": { ...customAssignments()["fast-lo"], provider: "test" },
},
}),
).toThrow();
expect(() =>
settings.parse({
mode: "custom",
agents: {
...customAssignments(),
"fast-lo": { ...customAssignments()["fast-lo"], lunaPriority: true },
},
}),
).toThrow();
expect(() =>
settings.parse({
mode: "custom",
agents: { ...customAssignments(), coordinate: customAssignments()["deep-lo"] },
}),
).not.toThrow();
for (const obsolete of [{ provider: "test" }, { lunaPriority: true }])
expect(() =>
settings.parse({
mode: "custom",
agents: {
...customAssignments(),
coordinate: {
...customAssignments()["deep-lo"],
...obsolete,
},
},
}),
).toThrow();
});
test("worker inheritance and coordinator isolation preserve build and default", async () => {
const result = await host();
expect(result.defaultAgent).toBe("build");
@@ -663,22 +784,25 @@ test("manual calls and command-created child sessions cannot bypass delegation r
expect((await result.tools("build")).subagent?.description).toBe("Native subagents");
});
test("coordinate prompt admission requires both owned provenance and a completed move; multi-work cannot be a child", async () => {
test("coordinate prompt admission requires owned ready placement; local and moved bindings work", async () => {
const result = await host();
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
"ready multi-work binding",
);
result.bindCoordinate(false);
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
"ready multi-work binding",
);
result.bindCoordinate(true, false);
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
"ready multi-work binding",
);
result.bindCoordinate(true);
await result.command("multi-work", "coordinate");
await expect(result.command("build", "coordinate")).rejects.toThrow("ready worktree binding");
result.bindCoordinate(true, true, true);
await result.command("multi-work", "coordinate");
await expect(result.command("build", "coordinate")).rejects.toThrow("ready multi-work binding");
result.bindCoordinate(true);
await expect(result.command("multi-work", "fast-lo")).rejects.toThrow("local fast-lo");
for (const invoke of [result.delegate, result.command])
await expect(invoke("build", "multi-work")).rejects.toThrow("primary only");
@@ -686,33 +810,57 @@ test("coordinate prompt admission requires both owned provenance and a completed
test("reasoning settings and Luna priority use native advertised or official support", async () => {
const luna = {
...model("gpt-5.6-luna"),
...providerModel("openai", "gpt-6-luna"),
package: "@opencode/ai/providers/openai",
settings: { baseURL: "https://api.openai.com/v1" },
};
const ordinary = await host();
const openaiModels = [
luna,
providerModel("openai", "gpt-6-sol"),
providerModel("openai", "gpt-6-astra"),
];
const ordinary = await host([providerModel("openai", "gpt-6-luna"), ...openaiModels.slice(1)], {
provider: "openai",
});
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({});
expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low");
const official = await host([luna, ...allModels.slice(1)]);
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
reasoningEffort: "high",
serviceTier: "priority",
});
const proxy = await host([
{ ...luna, settings: { baseURL: "https://proxy.example/v1" } },
...allModels.slice(1),
]);
expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
const advertised = await host([
const official = await host(openaiModels, { provider: "openai" });
expect(official.agents.get("fast-hi")?.request.settings).toEqual({ serviceTier: "priority" });
const proxy = await host(
[{ ...luna, settings: { baseURL: "https://proxy.example/v1" } }, ...openaiModels.slice(1)],
{ provider: "openai" },
);
expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({});
const advertised = await host(
[
{ ...luna, package: "custom", settings: { serviceTier: "fast" } },
...allModels.slice(1),
]);
...openaiModels.slice(1),
],
{ provider: "openai" },
);
expect(advertised.agents.get("fast-lo")?.request.settings.serviceTier).toBe("fast");
const disabled = await host([luna, ...allModels.slice(1)], {
provider: "test",
const sdkAlias = await host(
[{ ...luna, package: "@ai-sdk/openai" }, ...openaiModels.slice(1)],
{ provider: "openai" },
);
expect(sdkAlias.agents.get("fast-lo")?.request.settings.serviceTier).toBe("priority");
const disabled = await host(openaiModels, {
provider: "openai",
lunaPriority: false,
});
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({});
const custom = customAssignments("openai");
const customResult = await host(openaiModels, {
mode: "custom",
agents: custom,
});
expect(customResult.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined();
expect(customResult.agents.get("fast-hi")?.request.settings.serviceTier).toBeUndefined();
const other = await host(allModels, { provider: "test" });
expect(other.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined();
expect(() => settings.parse({ mode: "gpt", provider: "test", lunaPriority: true })).toThrow();
});
test("coordinator uses inherited effort and ignores the legacy custom assignment", async () => {
@@ -727,15 +875,17 @@ test("coordinator uses inherited effort and ignores the legacy custom assignment
const fallback = await host(allModels.slice(0, 2));
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
id: "gpt-5.6-sol",
id: "gpt-6-sol",
variant: "low",
});
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
const custom = await host([...allModels, model("local")], {
mode: "custom",
provider: "test",
agents: { ...gptAgents, coordinate: { model: "local", effort: "xhigh" } },
agents: {
...customAssignments(),
coordinate: { model: "test/local", effort: "xhigh" },
},
});
expect(custom.agents.get("coordinate")?.model).toMatchObject({
id: "gpt-6-astra",
@@ -745,8 +895,8 @@ test("coordinator uses inherited effort and ignores the legacy custom assignment
});
test("the current host model API filters discovery and reloads changed availability", async () => {
const disabled = { ...reasoningModel("gpt-5.6-luna"), enabled: false };
const other = { ...reasoningModel("gpt-5.6-sol"), providerID: Provider.ID.make("other") };
const disabled = { ...reasoningModel("gpt-6-luna"), enabled: false };
const other = { ...reasoningModel("gpt-6-sol"), providerID: Provider.ID.make("other") };
expect((await host([disabled, other, ...allModels.slice(1)])).agents.has("coordinate")).toBe(
false,
);
@@ -757,7 +907,7 @@ test("the current host model API filters discovery and reloads changed availabil
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
await result.refresh(allModels.slice(0, 2));
expect(result.agents.has("deep-lo")).toBe(false);
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-sol");
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low");
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
await result.refresh([]);
+74 -3
View File
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test";
import type { Plugin } from "@opencode/plugin/tui";
import type { Route, SlotMap } from "@opencode/plugin/tui/context";
import { testRender, type JSX } from "@opentui/solid";
import { jsx } from "@opentui/solid/jsx-runtime";
import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
@@ -87,12 +88,19 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
const renderHome = homeRender;
const home = await testRender(() => renderHome({ mode: "normal", showDetails: false }), {
width: 40,
height: 5,
height: 6,
});
await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
const homeFrame = home.captureCharFrame();
expect(homeFrame).toContain(" ChatGPT quota (home)");
expect(homeFrame).toContain(" 5h: 90% · due");
expect(homeFrame.split("\n")).toEqual([
" ",
" ",
" ChatGPT quota (home) ",
" 5h: 90% · due ",
" ",
" ",
"",
]);
expect(statusCalls).toBe(1);
home.renderer.destroy();
@@ -152,3 +160,66 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
if (cleanup) await cleanup();
}
});
test("home quota stays out of the host footer row at wide and narrow widths", async () => {
await build();
const { default: plugin } = await import(new URL("../dist/tui.js", import.meta.url).href);
let homeRender: HomeSlotRender | undefined;
const contextFixture = {
client: {
rpc: () => ({
status: async () => status("home", 90),
events: { on: () => () => undefined },
}),
},
ui: {
slot(claim: { after?: "prompt.footer"; render: HomeSlotRender }) {
if (claim.after === "prompt.footer") homeRender = claim.render;
return () => undefined;
},
router: { current: () => ({ type: "home" as const }) },
},
theme: { text: { default: "#ffffff", muted: "#888888" } },
};
// SAFETY: This focused fixture implements every context member read by slot setup/rendering.
const cleanup = await plugin.setup(Object.assign({} as Plugin.Context, contextFixture));
if (!homeRender) throw new Error("Plugin did not register the home slot");
const renderHome = homeRender;
try {
for (const width of [75, 40]) {
const footer = (quota: boolean) =>
jsx("box", {
flexDirection: "row",
children: [
jsx("text", { children: "/project" }),
jsx("box", { flexGrow: 1 }),
jsx("text", { children: "NORMAL" }),
jsx("text", { children: " ctrl+k commands" }),
quota ? renderHome({ mode: "normal", showDetails: false }) : undefined,
],
});
const baseline = await testRender(() => footer(false), { width, height: 6 });
const rendered = await testRender(() => footer(true), { width, height: 6 });
try {
await baseline.renderOnce();
await rendered.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
const baselineRows = baseline.captureCharFrame().split("\n");
const quotaRows = rendered.captureCharFrame().split("\n");
expect(quotaRows[0]).toBe(baselineRows[0]);
expect(quotaRows[0]).toContain("/project");
expect(quotaRows[0]).toContain("NORMAL ctrl+k commands");
expect(quotaRows[1]?.trim()).toBe("");
expect(quotaRows[2]?.trim()).toBe("ChatGPT quota (home)");
expect(quotaRows[3]?.trim()).toBe("5h: 90% · due");
expect(quotaRows[2]?.indexOf("ChatGPT quota (home)")).toBe(
Math.ceil((width - "ChatGPT quota (home)".length) / 2),
);
} finally {
baseline.renderer.destroy();
rendered.renderer.destroy();
}
}
} finally {
if (cleanup) await cleanup();
}
});