Skip to content
Closed
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: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack",
"version": "0.15.25-shared.9",
"version": "0.15.25-shared.10",
"description": "Shared pstack engineering workflows for Claude Code and Codex, with one configuration and host-specific runtime adapters.",
"author": {
"name": "Local adaptation; upstream Lauren Tan"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack-codex",
"version": "0.15.25-shared.9",
"version": "0.15.25-shared.10",
"description": "Shared pstack engineering workflows for Codex and Claude Code, with one configuration and native host adapters.",
"author": {
"name": "Lauren Tan; local Codex adaptation"
Expand Down
2 changes: 1 addition & 1 deletion ADAPTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Model tiering by role follows upstream again. Poteto-mode's Subagents section re

`verification workers` is a local role with no upstream line. Upstream runs orchestrate's verifier and show-me-your-work's handback review on a different model family than the work. On Codex or Claude Code another family needs an `xagent:` entry, so verification takes its own role instead of sharing `swarm workers` with fan-out workers, and each host can point it at the other vendor while swarms keep their own worker. A swarm's verify lanes, as in autopilot's swarm-verify, and the handback review read it too. On T3 the helper requires `--checked` for this role and picks the candidate whose model family differs from the checked work's, read from model IDs, because a provider name alone does not identify the family and the parent-provider default would choose the parent's own family.

Three local rules keep cross-vendor panels usable. Shared `auxiliary_panel_members` names external members whose unresolved outcome is a reported gap rather than a blocker, so a supporting vendor's outage does not stop a review its primary members finished; upstream reaches the same availability by falling back within a family, which xagent entries never do. Native agent definitions own their effort: on Codex and Claude Code a worker's effort goes to a native agent type only when its definition sets none, so `reasoning_effort` mainly reaches `xagent:` workers, whose CLIs otherwise run at the user's interactive default. Shared `xagent_effort_caps` keeps that effort within what each external CLI's model accepts: an unsupported level is a `DROPOUT`, never a quiet downgrade, so a shared `xhigh` would otherwise drop every seat of a CLI whose model stops at `high`, as agy's `gemini-3.8-flash` does. setup-pstack proposes a cross-vendor layout for multi-party steps when another vendor's CLI is signed in, because this fork's `inherit-parent` defaults stay one family where upstream's defaults pair two.
Three local rules keep cross-vendor panels usable. Shared `auxiliary_panel_members` names external members whose unresolved outcome is a reported gap rather than a blocker, so a supporting vendor's outage does not stop a review its primary members finished; upstream reaches the same availability by falling back within a family, which xagent entries never do. Native agent definitions own their effort: on Codex and Claude Code a worker's effort goes to a native agent type only when its definition sets none, so `reasoning_effort` mainly reaches `xagent:` workers, whose CLIs otherwise run at the user's interactive default. Shared `xagent_effort_caps` keeps that effort within what each external CLI's model accepts: an unsupported level is a `DROPOUT`, never a quiet downgrade, so a shared `xhigh` would otherwise drop every seat of a CLI whose model stops at `high`, as agy's `gemini-3.8-flash` does. Shared `model_effort_caps` bounds cost instead: upstream raises the hardest work one step above the base effort, so a base of `xhigh` reaches `max` on exactly the models difficulty routing picks for that work, such as Fable and Astra. A cap keyed by model-name part stops only those models, native, T3 or external, while other models keep the raise and the model choice by difficulty stays; a single global ceiling would also have stopped subtle work on cheaper models. The T3 resolver enforces it and the native parents apply it from poteto-mode's effort rule. setup-pstack proposes a cross-vendor layout for multi-party steps when another vendor's CLI is signed in, because this fork's `inherit-parent` defaults stay one family where upstream's defaults pair two.

The orch store lock carries a local fix for three races that upstream still has at `e5136d82`: a writer killed between the exclusive open and its pid write left an empty lock that only `--force` could pass, two writers that saw the same dead holder could both take the lock, and a release could unlink the lock a `--force` writer had just put in its place. `acquireLock` now writes the pid to a private file and hard-links it into place, falling back to the exclusive open where hard links are unsupported. Every read-then-unlink of the lock, in a takeover or a release, runs behind a `.orch.lock.takeover` claim, and a takeover re-reads the holder before it unlinks. A writer that meets a live claim is refused with `is being released or replaced; retry`; a release that meets one retries every 10 ms, so `close` waits while a live claimant, even a stopped one, holds it. The fix and its multi-process race tests are adapted from michael-denyer/pstack-claude #219 (`eef8f7c6`) and #238 (`de804764`), MIT, Copyright (c) 2026 Michael Denyer, whose notice is added to `LICENSE`. That fork's TLA+ model and its retry when a lock vanishes before its holder is read are not taken. The lock lives in `skills/poteto-mode/scripts/orch/lock.ts`, with `UserError` and `errorCode` in `orch/errors.ts`, because this fork replaced upstream's lock wholesale and `store.ts` was already past 1,000 lines; its race tests live in `orch/lock.test.ts`, sharing `orch/store.test-helper.ts` with `orch.test.ts`. Each acquire also removes the private pid files and staged claims that a writer killed mid-claim leaves behind, judged by the pid in their names, which the kill-at-every-step test now checks. The frontier code stays in `store.ts` even though it is large, so upstream's edits there still merge file by file. Its GitHub remote parsing is `skills/poteto-mode/scripts/github-remote.ts`, moved out of upstream's `watch-pr/github.ts` so `orch` and `watch-pr` share one parser.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Marketplace registration is needed only once per host. Host caches are generated
- Claude Code entry: `/pstack:poteto-mode`.
- Optional setup: `$pstack-codex:setup-pstack` or `/pstack:setup-pstack`.

Preferences are shared in `~/.config/pstack/config.json`. Roles inherit the main conversation unless configured; workers receive the resolved policy and do not need setup. Concrete model choices are host-scoped so one vendor's model ID never leaks into another host. The exception is an external agent's own model, such as `xagent:claude/claude-opus-5-5`: xagent hands it to that agent's CLI from either host, so it can be shared. Shared reasoning and panel-size preferences apply to both when the actual runtime supports them. Shared `required_panel_members` adds external agents such as `xagent:grok` to every panel on both hosts, and those entries are never silently skipped. Shared `auxiliary_panel_members` marks external members, such as `xagent:gemini`, whose failure after a retry is reported as a gap instead of blocking a panel the other members finished. Shared `xagent_effort_caps`, such as `{"gemini": "high"}`, keeps an external agent's effort within the levels its CLI's model accepts. On Codex and Claude Code, an agent type whose definition sets its own effort keeps it; shared reasoning reaches the rest, including external agents. See [the runtime contract](./references/runtime.md).
Preferences are shared in `~/.config/pstack/config.json`. Roles inherit the main conversation unless configured; workers receive the resolved policy and do not need setup. Concrete model choices are host-scoped so one vendor's model ID never leaks into another host. The exception is an external agent's own model, such as `xagent:claude/claude-opus-5-5`: xagent hands it to that agent's CLI from either host, so it can be shared. Shared reasoning and panel-size preferences apply to both when the actual runtime supports them. Shared `required_panel_members` adds external agents such as `xagent:grok` to every panel on both hosts, and those entries are never silently skipped. Shared `auxiliary_panel_members` marks external members, such as `xagent:gemini`, whose failure after a retry is reported as a gap instead of blocking a panel the other members finished. Shared `xagent_effort_caps`, such as `{"gemini": "high"}`, keeps an external agent's effort within the levels its CLI's model accepts. Shared `model_effort_caps`, such as `{"fable": "xhigh", "astra": "xhigh"}`, is the highest effort a worker on that model receives on any host, so raising the effort for the hardest work stops there on expensive models while others can still reach `max`. On Codex and Claude Code, an agent type whose definition sets its own effort keeps it; shared reasoning reaches the rest, including external agents. See [the runtime contract](./references/runtime.md).

Examples below use `$skill` as a short reference to the bundled skill; in Claude Code invoke `/pstack:skill` or read its file through the host's skill interface.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pstack 本身不安装定时器;长任务流程也不保证会话退出或主

共享配置中的 `required_panel_members`(例如 `["xagent:grok", "xagent:gemini"]`)会把这些外部 Agent 追加到两个主控的每个评审组和默认评审组,单个评审组或主控覆盖都无法遗漏它们。评审组里的 `xagent:<agent>` 不会被替换成继承模型,也不会以 N-1 静默跳过;重试和失败报告规则见[运行时契约](./references/runtime.md)。

共享配置中的 `auxiliary_panel_members`(例如 `["xagent:gemini"]`)把这些外部 Agent 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。共享配置中的 `xagent_effort_caps`(例如 `{"gemini": "high"}`)给外部 Agent 设推理强度上限,适用于模型支持的档位比 `reasoning_effort` 少的 CLI;它只限制强度,不会换模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。
共享配置中的 `auxiliary_panel_members`(例如 `["xagent:gemini"]`)把这些外部 Agent 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。共享配置中的 `xagent_effort_caps`(例如 `{"gemini": "high"}`)给外部 Agent 设推理强度上限,适用于模型支持的档位比 `reasoning_effort` 少的 CLI;它只限制强度,不会换模型。共享配置中的 `model_effort_caps`(例如 `{"fable": "xhigh", "astra": "xhigh"}`)按模型名设推理强度上限:所有宿主上跑在这些模型上的 worker,最难工作的升档到此为止,其他模型照样可以升到 `max`;同样只限制强度,不换模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。

| 配置值 | 本机执行入口 |
|---|---|
Expand Down
20 changes: 13 additions & 7 deletions bin/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ const AUXILIARY_MEMBERS = "auxiliary_panel_members";
const MEMBER_LISTS = [REQUIRED_MEMBERS, AUXILIARY_MEMBERS];
// The highest effort each external agent's CLI accepts; the parent passes it the lower of the job's effort and this cap.
const EFFORT_CAPS = "xagent_effort_caps";
// The highest effort a worker on an expensive model receives, keyed by a dash-separated part of the model name
// (fable, astra), so every host's ID or alias for that model shares one cost ceiling.
const MODEL_EFFORT_CAPS = "model_effort_caps";
const XAGENT_NAME = /^[a-z][a-z0-9-]*$/;
// Fields that only common policy may set, because xagent runs the same way from every host.
const SHARED_FIELDS = [...MEMBER_LISTS, EFFORT_CAPS];
const MODEL_NAME_PART = /^[a-z][a-z0-9]*$/;
// Fields that only common policy may set, because xagent and each model's cost are the same from every host.
const SHARED_FIELDS = [...MEMBER_LISTS, EFFORT_CAPS, MODEL_EFFORT_CAPS];

export function getDefaultConfigPath(homeDir = homedir()) {
return path.join(homeDir, ".config", "pstack", "config.json");
Expand Down Expand Up @@ -136,7 +140,8 @@ function validateSharedFields(policy, scope, label) {
if (scope !== "common") throw new Error(`${field} is shared policy and cannot be set per host`);
const value = policy[key];
if (value === null) continue;
if (key === EFFORT_CAPS) validateEffortCaps(value, field);
if (key === EFFORT_CAPS) validateEffortCaps(value, field, XAGENT_NAME, "xagent agent names");
else if (key === MODEL_EFFORT_CAPS) validateEffortCaps(value, field, MODEL_NAME_PART, "single model-name parts such as fable");
else validateMemberList(value, field);
}
}
Expand All @@ -149,11 +154,11 @@ function validateMemberList(members, field) {
if (new Set(members).size !== members.length) throw new Error(`${field} entries must be unique`);
}

function validateEffortCaps(caps, field) {
function validateEffortCaps(caps, field, keyPattern, keyLabel) {
if (!isObject(caps) || Object.keys(caps).length === 0) throw new Error(`${field} must be a nonempty object or null`);
for (const [agent, effort] of Object.entries(caps)) {
if (!XAGENT_NAME.test(agent)) throw new Error(`${field} keys must be xagent agent names`);
if (!REASONING_EFFORTS.has(effort)) throw new Error(`${field}.${agent} must be one of ${[...REASONING_EFFORTS].join(", ")}`);
for (const [key, effort] of Object.entries(caps)) {
if (!keyPattern.test(key)) throw new Error(`${field} keys must be ${keyLabel}`);
if (!REASONING_EFFORTS.has(effort)) throw new Error(`${field}.${key} must be one of ${[...REASONING_EFFORTS].join(", ")}`);
}
}

Expand Down Expand Up @@ -353,6 +358,7 @@ export function showConfig({ configPath = getDefaultConfigPath(), host } = {}) {
[REQUIRED_MEMBERS]: [...required],
[AUXILIARY_MEMBERS]: [...(config[AUXILIARY_MEMBERS] ?? [])],
[EFFORT_CAPS]: { ...config[EFFORT_CAPS] },
[MODEL_EFFORT_CAPS]: { ...config[MODEL_EFFORT_CAPS] },
reasoning_effort: hostPolicy.reasoning_effort ?? config.reasoning_effort ?? null,
defaultModel: "inherit-parent",
defaultPanel: withRequired(DEFAULT_PANEL),
Expand Down
39 changes: 31 additions & 8 deletions bin/t3-targets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,37 @@ function effortFor(model, requested, label) {
return { effort: { requested, sent: requested, option: control.id }, warnings: [] };
}

function workerFor(entry, catalog, requestedEffort, effortCaps) {
// The requested effort lowered to a cap, with the warning that says so; a missing effort or cap changes nothing.
function capped(requested, cap, label, field) {
if (requested == null || cap == null || EFFORT_ORDER.indexOf(requested) <= EFFORT_ORDER.indexOf(cap)) {
return { effort: requested, warning: null };
}
return { effort: cap, warning: `${label}: requested effort ${requested} is above its ${field} entry, so ${cap} is sent` };
}

// The lowest model_effort_caps entry naming a dash-separated part of the model ID's last segment,
// so fable matches the fable alias and claude-fable-5-1, and astra matches gpt-6-astra.
function modelCapFor(modelId, modelCaps) {
if (typeof modelId !== "string") return undefined;
const parts = modelId.slice(modelId.lastIndexOf("/") + 1).toLowerCase().split("-");
return Object.entries(modelCaps)
.filter(([name]) => parts.includes(name))
.map(([, cap]) => cap)
.sort((a, b) => EFFORT_ORDER.indexOf(a) - EFFORT_ORDER.indexOf(b))[0];
}

function workerFor(entry, catalog, requestedEffort, caps) {
if (entry.startsWith("xagent:")) {
const spec = entry.slice("xagent:".length);
const slash = spec.indexOf("/");
const agent = slash < 0 ? spec : spec.slice(0, slash);
const cap = effortCaps[agent];
const lowered = requestedEffort !== null && cap !== undefined && EFFORT_ORDER.indexOf(requestedEffort) > EFFORT_ORDER.indexOf(cap);
const model = modelOf(entry, catalog);
const byModel = capped(requestedEffort, modelCapFor(model, caps.model), entry, "model_effort_caps");
const byAgent = capped(byModel.effort, caps.agent[agent], entry, "xagent_effort_caps");
return {
entry, transport: "xagent", agent, model, family: familyOf(model),
effort: lowered ? cap : requestedEffort,
warnings: lowered ? [`${entry}: requested effort ${requestedEffort} is above its xagent_effort_caps entry, so ${cap} is sent`] : [],
effort: byAgent.effort,
warnings: [byModel.warning, byAgent.warning].filter(Boolean),
};
}
const inherited = entry === "inherit-parent" || entry === "auto";
Expand All @@ -123,12 +142,15 @@ function workerFor(entry, catalog, requestedEffort, effortCaps) {
],
};
}
const { effort, warnings } = effortFor(model, requestedEffort, `${providerId}/${model.id}`);
const label = `${providerId}/${model.id}`;
const byModel = capped(requestedEffort, modelCapFor(model.id, caps.model), label, "model_effort_caps");
const { effort, warnings } = effortFor(model, byModel.effort, label);
const target = { providerInstanceId: providerId, model: model.id };
if (effort.sent !== null) target.options = { [effort.option]: effort.sent };
return {
entry, transport: "t3", target,
resolved: { providerInstanceId: providerId, model: model.id }, family: familyOf(model.id), effort, warnings,
resolved: { providerInstanceId: providerId, model: model.id }, family: familyOf(model.id), effort,
warnings: [byModel.warning, ...warnings].filter(Boolean),
};
}

Expand Down Expand Up @@ -186,11 +208,12 @@ export function resolveT3Targets(policy, catalog, { role, panel, effort, provide
entries = kind === "role" ? [value] : value;
}
const requestedEffort = effort ?? policy.reasoning_effort ?? null;
const caps = { agent: policy.xagent_effort_caps ?? {}, model: policy.model_effort_caps ?? {} };
const auxiliary = new Set(policy.auxiliary_panel_members ?? []);
return {
selection,
workers: entries.map(entry => {
const worker = workerFor(entry, catalog, requestedEffort, policy.xagent_effort_caps ?? {});
const worker = workerFor(entry, catalog, requestedEffort, caps);
// The auxiliary listing only relaxes panel seats; a role never becomes optional.
worker.auxiliary = kind === "panel" && auxiliary.has(entry);
if (checked !== undefined && !crossFamily(worker.family)) {
Expand Down
2 changes: 1 addition & 1 deletion references/antigravity-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Resolve `<plugin-root>` from this skill's installed path and run:
node <plugin-root>/bin/config.mjs show --host antigravity
```

The command reads `~/.config/pstack/config.json`. It returns the common `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, and `xagent_effort_caps` values. It ignores the Codex and Claude Code host overrides, because the stored schema defines no Antigravity-specific preferences. If the file is absent, it prints inherited defaults and creates neither the file nor its parent directory.
The command reads `~/.config/pstack/config.json`. It returns the common `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, `xagent_effort_caps`, and `model_effort_caps` values. It ignores the Codex and Claude Code host overrides, because the stored schema defines no Antigravity-specific preferences. If the file is absent, it prints inherited defaults and creates neither the file nor its parent directory.

`update --host antigravity` is unsupported and must fail without changing preferences. Use an existing supported setup or update path only for an explicitly requested configuration change.

Expand Down
Loading
Loading