From 701e361e2f26780b8942833f85b81d166334f7f1 Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:11:18 +0000 Subject: [PATCH 1/7] test: isolate config-home evidence collection [#120] Keep real HOME and USER while preparing redirected, credential-only profiles and checking setup artifacts without claiming a live run. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- tests/setup-config-home-repro.sh | 134 +++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/setup-config-home-repro.sh diff --git a/tests/setup-config-home-repro.sh b/tests/setup-config-home-repro.sh new file mode 100644 index 00000000..3b7a0d7a --- /dev/null +++ b/tests/setup-config-home-repro.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Prepare and inspect a real-harness run; never simulate setup's writes. +repo="$(cd "$(dirname "$0")/.." && pwd)" +usage() { + cat <<'EOF' +Usage: + bash tests/setup-config-home-repro.sh claude|codex redirected [--claude-config DIR] [--codex-home DIR] + bash tests/setup-config-home-repro.sh --verify RUN_DIR + +Preparation keeps HOME and USER unchanged, snapshots daily sheet/integration +files, and creates private redirected config directories (including spaces). +Only .credentials.json / auth.json are copied from explicitly supplied login +sources. Without a file-backed source, log in in the redirected directory. +No daily configuration or credentials are modified by this fixture. + +Run the printed commands in an operator terminal. Install the exact candidate, +invoke /setup-pstack through the real parent, complete probes and confirmation, +and refuse permission to write outside the redirected directories. Retain the +native transcripts and provider receipts. Then run --verify, rerun setup without +changes, and run --verify again to assert byte-identical output. Start a fresh +parent session and demonstrate the sheet is loaded. Verification below checks +artifacts, not live invocation or loaded-sheet proof; those require transcripts. + +Do not run old instructions that explicitly target daily paths without a native +sandbox denying those writes. Unset/empty defaults are tested statically only. +Run directories contain credentials and private snapshots: do not commit or +upload them. Retain only redacted transcripts/results, then delete the run dir. +EOF +} + +if [ "${1:-}" = "--help" ]; then usage; exit 0; fi +if [ "${1:-}" = "--verify" ]; then + [ "$#" = 2 ] || { usage >&2; exit 2; } + command -v node >/dev/null || { echo 'FAIL: node is required for artifact comparisons' >&2; exit 1; } + node - "$2" "$repo" <<'JS' +const fs = require('node:fs'); +const path = require('node:path'); +const cp = require('node:child_process'); +const run = fs.realpathSync(process.argv[2]); +const repo = process.argv[3]; +const meta = JSON.parse(fs.readFileSync(path.join(run, 'run.json'), 'utf8')); +function fail(message) { console.error(`FAIL: ${message}`); process.exit(1); } +if (meta.home !== process.env.HOME || meta.user !== (process.env.USER || '')) fail('HOME or USER changed'); +const sha = cp.execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], {encoding: 'utf8'}).trim(); +if (sha !== meta.sha || cp.execFileSync('git', ['-C', repo, 'status', '--porcelain'], {encoding: 'utf8'}).trim()) fail('candidate changed since preparation'); +for (const [i, target] of meta.targets.entries()) { + const before = path.join(run, 'daily', String(i)); + if (fs.existsSync(before) !== fs.existsSync(target) || (fs.existsSync(before) && !fs.readFileSync(before).equals(fs.readFileSync(target)))) fail(`daily config target changed: ${target}`); +} +const home = path.join(run, `${meta.harness} config`); +const sheet = path.join(home, 'pstack-models.md'); +const integration = path.join(home, meta.harness === 'claude' ? 'CLAUDE.md' : 'AGENTS.md'); +if (!fs.existsSync(sheet) || !fs.statSync(sheet).size) fail('redirected model sheet missing or empty'); +const bytes = fs.readFileSync(sheet); +const text = fs.readFileSync(integration, 'utf8'); +const sentinel = fs.readFileSync(path.join(run, 'sentinel'), 'utf8'); +if (!text.startsWith(sentinel)) fail('unrelated integration bytes changed'); +if (meta.harness === 'claude') { + const includes = text.split('\n').filter(line => line.startsWith('@') && line.includes('pstack-models.md')); + if (includes.length !== 1 || includes[0] !== `@${sheet.replaceAll(' ', '\\ ')}`) fail('Claude include is not the single resolved sheet path with escaped spaces'); +} else { + const begin = ''; + const end = ''; + if (text.split(begin).length !== 2 || text.split(end).length !== 2 || text.indexOf(end) <= text.indexOf(begin)) fail('Codex block markers missing, duplicated, or reversed'); + const block = text.slice(text.indexOf(begin) + begin.length + 1, text.indexOf(end)); + if (!Buffer.from(block).equals(bytes)) fail('Codex block differs from model sheet bytes'); +} +for (const [name, current] of [['sheet', sheet], ['integration', integration]]) { + const previous = path.join(run, `verified-${name}`); + if (fs.existsSync(previous) && !fs.readFileSync(previous).equals(fs.readFileSync(current))) fail(`unchanged rerun changed ${name}`); + fs.copyFileSync(current, previous); +} +console.log(`ok: ${meta.harness} redirected artifacts, unrelated bytes, daily targets and rerun snapshots; candidate=${sha}`); +console.log('Live invocation, probes, receipts, and fresh-session loading still require operator transcript evidence.'); +JS + exit 0 +fi + +harness="${1:-}" +case "$harness" in claude|codex) ;; *) usage >&2; exit 2 ;; esac +[ "${2:-}" = redirected ] || { echo 'FAIL: only redirected live writes are permitted; test defaults statically' >&2; exit 2; } +shift 2 +claude_source="" +codex_source="" +while [ "$#" -gt 0 ]; do + [ "$#" -ge 2 ] || { usage >&2; exit 2; } + case "$1" in + --claude-config) claude_source="$2" ;; + --codex-home) codex_source="$2" ;; + *) usage >&2; exit 2 ;; + esac + shift 2 +done +command -v node >/dev/null || { echo 'FAIL: node is required' >&2; exit 1; } +[ -z "$(git -C "$repo" status --porcelain)" ] || { echo 'FAIL: commit the exact candidate before preparing a live run' >&2; exit 1; } +for source in "$claude_source" "$codex_source"; do + [ -z "$source" ] || [ -d "$source" ] || { echo "FAIL: login source directory does not exist: $source" >&2; exit 1; } +done +[ -z "$claude_source" ] || [ -f "$claude_source/.credentials.json" ] || { echo 'FAIL: Claude source has no file-backed .credentials.json; use redirected login instead' >&2; exit 1; } +[ -z "$codex_source" ] || [ -f "$codex_source/auth.json" ] || { echo 'FAIL: Codex source has no file-backed auth.json; use redirected login instead' >&2; exit 1; } +umask 077 +run="$(mktemp -d "${TMPDIR:-/tmp}/pstack-config-home.XXXXXX")" +run="$(cd "$run" && pwd -P)" +mkdir "$run/claude config" "$run/codex config" "$run/daily" "$run/workspace" +[ -z "$claude_source" ] || cp "$claude_source/.credentials.json" "$run/claude config/.credentials.json" +[ -z "$codex_source" ] || cp "$codex_source/auth.json" "$run/codex config/auth.json" +printf '%s\n' '# Fixture instructions' 'Preserve this unrelated text exactly.' > "$run/sentinel" +cp "$run/sentinel" "$run/claude config/CLAUDE.md" +cp "$run/sentinel" "$run/codex config/AGENTS.md" +printf '[features]\nmulti_agent = true\n' > "$run/codex config/config.toml" +node - "$run" "$repo" "$harness" "$claude_source" "$codex_source" <<'JS' +const fs = require('node:fs'); +const path = require('node:path'); +const cp = require('node:child_process'); +const [run, repo, harness, claudeSource, codexSource] = process.argv.slice(2); +const dirs = [...new Set([path.join(process.env.HOME, '.claude'), path.join(process.env.HOME, '.codex'), process.env.CLAUDE_CONFIG_DIR, process.env.CODEX_HOME, claudeSource, codexSource].filter(Boolean))]; +const targets = dirs.flatMap(dir => ['pstack-models.md', 'CLAUDE.md', 'AGENTS.md'].map(file => path.resolve(dir, file))); +for (const [i, target] of targets.entries()) if (fs.existsSync(target)) fs.copyFileSync(target, path.join(run, 'daily', String(i))); +fs.writeFileSync(path.join(run, 'run.json'), JSON.stringify({harness, home: process.env.HOME, user: process.env.USER || '', sha: cp.execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], {encoding: 'utf8'}).trim(), targets}, null, 2)); +JS +printf 'Prepared private run: %s\n' "$run" +printf 'Keep HOME and USER unchanged. In an operator terminal:\ncd %q\n' "$run/workspace" +printf 'export CLAUDE_CONFIG_DIR=%q CODEX_HOME=%q\n' "$run/claude config" "$run/codex config" +if [ "$harness" = claude ]; then + printf 'claude --plugin-dir %q\n' "$repo/plugins/pstack" +else + printf 'codex plugin marketplace add ericlitman/open-pstack --ref %q\n' "$(git -C "$repo" rev-parse HEAD)" + printf 'codex plugin add pstack@open-pstack\ncodex\n' +fi +printf 'Invoke /setup-pstack; permit writes only under the redirected config dirs.\n' +printf 'Retain native transcripts/receipts; then verify, rerun setup unchanged, verify again, and prove fresh-session loading.\n' +printf 'bash %q --verify %q\n' "$repo/tests/setup-config-home-repro.sh" "$run" From bf2bd959d01468930853c397f087ba1a634d769f Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:14:26 +0000 Subject: [PATCH 2/7] fix: keep setup writes in the active harness config home [#120] Resolve nonempty config-home overrides at the existing harness mapping boundary and reuse that path throughout setup and integration rendering. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- .../skills/poteto-mode/references/codex-tools.md | 15 +++++++++++++-- plugins/pstack/skills/setup-pstack/SKILL.md | 12 +++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index c8a5a4cd..2db85481 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -2,6 +2,17 @@ pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuestion`) in shared prose. On Codex the files are the same; only those tool names resolve differently. Model execution is not translated here. Read [`provider-dispatch.md`](provider-dispatch.md) for the parent-owned Claude/Codex/Grok route table and provider-qualified descriptors. +## Harness config homes + +Resolve the current parent's config home once: Claude Code uses nonempty `CLAUDE_CONFIG_DIR`, otherwise `$HOME/.claude`; Codex uses nonempty `CODEX_HOME`, otherwise `$HOME/.codex`. The equivalent quoted shell expressions are: + +```bash +"${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +"${CODEX_HOME:-$HOME/.codex}" +``` + +Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's current-state reads, sheet and integration writes, snapshots, restoration, and readback. Render the Claude include with the actual resolved sheet path, escaping each space with a backslash for Claude's import syntax; never write a shell variable or `` placeholder into it. + ## Tool actions | pstack / Claude action | Codex equivalent | @@ -21,7 +32,7 @@ pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuesti | Track tasks (the todolist / `TodoWrite`) | `update_plan` | | Ask the human a fixed-choice question (`AskUserQuestion`) | Ask in plain text and let the user answer. Codex has no structured-choice tool. | -Subagent dispatch needs `multi_agent` enabled. Add to `~/.codex/config.toml`: +Subagent dispatch needs `multi_agent` enabled. Add to `/config.toml`, using Codex's [config-home rule](#harness-config-homes): ```toml [features] @@ -61,4 +72,4 @@ Some triggers name skills that ship with Claude Code, not pstack. They do not ex ## Instructions file -Where a pstack skill says "your instructions file", on Codex that is `AGENTS.md` (project root, plus `~/.codex/AGENTS.md` global). On Claude Code it is `CLAUDE.md`. +Where a pstack skill says "your instructions file", on Codex that is `AGENTS.md` (project root, plus `/AGENTS.md` global). On Claude Code it is `CLAUDE.md` (project root, plus `/CLAUDE.md` global). Resolve the global directory with the [config-home rule](#harness-config-homes). diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index a6af3b51..8674e626 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -7,17 +7,19 @@ description: Configure pstack's provider-qualified models, per-family requested Configure one portable model sheet for the current parent harness. Read [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md) before probing or writing anything. Its model matrix, descriptor grammar, and route table are the contract. Choose one requested effort per assigned matrix family. Do not add a second configuration file, a runtime resolver, or a weaker-model fallback. -Claude Code writes `~/.claude/pstack-models.md` and loads it from `~/.claude/CLAUDE.md` with: +Resolve `` once using the [harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes). Use it for all current-state reads, writes, snapshots, restoration, and readback. + +Claude Code writes `/pstack-models.md` and loads it from `/CLAUDE.md` with the actual resolved sheet path (backslash-escape spaces; the placeholder below is illustrative, not literal file content): ```text -@~/.claude/pstack-models.md +@ ``` -Codex writes `~/.codex/pstack-models.md`. Codex has no `@` include, so mirror the sheet's exact bytes inside one bounded block in `~/.codex/AGENTS.md` and retain the sheet as the editable source of truth: +Codex writes `/pstack-models.md`. Codex has no `@` include, so mirror the sheet's exact bytes inside one bounded block in `/AGENTS.md` and retain the sheet as the editable source of truth: ```text - + ``` @@ -105,7 +107,7 @@ interrogate reviewers: claude:opus@max, codex:gpt-5.6-sol@max, grok:grok-4.7@xhi ### 8. Wire it in -Render the parent integration in memory before either write. On Claude, the integration is the single `@~/.claude/pstack-models.md` include in `~/.claude/CLAUDE.md`. On Codex, it is the exact sheet bytes between one `` and `` pair in `~/.codex/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. +Render the parent integration in memory before either write. On Claude, the integration is the single `@` include of the actual resolved `/pstack-models.md` path (backslash-escape spaces, never leave a variable or placeholder) in `/CLAUDE.md`. On Codex, it is the exact sheet bytes between one `` and `` pair in `/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. Snapshot every target's current bytes. Write the sheet and parent integration only after every requested pair passes and the operator confirms. Read both targets back and compare them with the in-memory render. If either write or readback fails, restore every snapshot and report the failure. An unchanged rerun must produce byte-identical sheet and integration content after normalization. From f95670e51180d30d613eceaaa060f992034ce22c Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:17:35 +0000 Subject: [PATCH 3/7] test: prevent setup config-home regressions during upstream syncs [#120] Guard the canonical mapping and default/empty/spaced resolution without daily writes, and record the intentional port divergence for the patch ledger. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- CHANGES.md | 6 ++++++ UPSTREAM.md | 4 ++++ tests/skill-collision-repro.sh | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d84b2acc..955d3cde 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,12 @@ Mergify auto-queue is live: ready PRs to main require passing verify, Unfret and exact-head live-gate; workflow changes require operator queueing. This port applies the Cursor → Claude Code substitutions in skill bodies. Earlier drafts left them flagged; this revision resolves them. A later pass added a Codex build that shares the same skills; see [Codex port](#codex-port) below. +## Config-home port correction (#120) + +Setup resolves the active harness's config home through `poteto-mode/references/codex-tools.md`: nonempty `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, falling back to `$HOME/.claude` or `$HOME/.codex` for unset or empty values. The model sheet, global instruction integration, current-state reads, snapshots, restoration, and readback all use that directory. Claude imports render the resolved path with escaped spaces; Codex retains its exact-byte bounded block. Default-home behavior, role defaults, probes, and confirmation are unchanged. + +This is an intentional Claude Code/Codex port divergence from Cursor's `~/.cursor/rules/` destination, recorded in `UPSTREAM.md` for the port-patch ledger (#105). The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default-home paths in setup and checks unset, empty, and space-containing resolution without writing daily files. `tests/setup-config-home-repro.sh` prepares private redirected directories while preserving real `HOME` and `USER`, and checks artifacts and unchanged daily targets; it does not substitute for installed-harness invocation, receipts, or fresh-session loading proof. The operator supplies that evidence on the final PR head and posts the live gate. + ## 1.5.0 syncs to Cursor pstack 0.15.5 Open Pstack 1.5.0 tracks Cursor pstack 0.15.5 at `12d587dfb20741cafc376c42c696c5f6e2a64487`. The first-run panel is now three models: `claude:opus@max`, `codex:gpt-5.6-sol@max`, and `grok:grok-4.7@xhigh`. Opus max takes judgment and prose, hardest tasks, and the How explainer. Grok 4.7 xhigh takes feature and refactoring work, the How explorer, and Swarm workers. Bug-fix, perf-issue, and hillclimb stay on Sol max. Why and Reflect stay on `inherit-parent`. Fable stays in the model matrix with its native agents, but no first-run role uses it. Setup asks about roles first, then asks efforts for and probes only the assigned families (the ordering follows PR #73 by @arjitj2). It drops and lists retired-role rows such as `how critics`. diff --git a/UPSTREAM.md b/UPSTREAM.md index 28aab4d1..b6ee07c8 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -25,6 +25,10 @@ The table above is the current Cursor sync point. Open Pstack 1.5.0 imports this - The explicit Grok, Opus, and Sol defaults for the Why and Reflect roles are not applied. Those roles stay on `inherit-parent` because the external runner omits the parent's MCP servers. - The Claude manifest does not take the logo field from `efa2a53` because Claude Code has no schema for it. The shared asset is exposed through the Codex manifest instead. +## Local port-patch ledger + +- **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude imports escape spaces in the resolved path. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default paths in setup and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. + ## Check for changes The repository already names Cursor's repository as the `cursor` remote in the maintainer checkout. A fresh clone can add it once: diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 9a2ba488..cf590d04 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -77,6 +77,42 @@ fi # interrogate defaults copy it verbatim. setup="$repo/plugins/pstack/skills/setup-pstack/SKILL.md" dispatch="$repo/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md" + +# Config-home port invariant (#120): guard upstream merges against daily-home writes. +config_mapping="$repo/plugins/pstack/skills/poteto-mode/references/codex-tools.md" +config_home_bad="" +if grep -nE '~/(\.claude|\.codex)|\$HOME/(\.claude|\.codex)' "$setup"; then + config_home_bad="setup still names a literal default config path"$'\n' +fi +grep -Fq '[harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes)' "$setup" || config_home_bad="${config_home_bad}setup does not reference the canonical config-home rule"$'\n' +for expression in '"${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' '"${CODEX_HOME:-$HOME/.codex}"'; do + grep -Fxq "$expression" "$config_mapping" || config_home_bad="${config_home_bad}mapping lacks quoted nonempty/default resolution: $expression"$'\n' +done +for target in pstack-models.md CLAUDE.md AGENTS.md; do + grep -Fq "/$target" "$setup" || config_home_bad="${config_home_bad}setup does not resolve $target through config-home"$'\n' +done +# Resolution only: keep real HOME/USER and never write default or daily targets. +if ! ( + unset CLAUDE_CONFIG_DIR CODEX_HOME + [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "$HOME/.claude" ] && + [ "${CODEX_HOME:-$HOME/.codex}" = "$HOME/.codex" ] || exit 1 + CLAUDE_CONFIG_DIR="" CODEX_HOME="" + [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "$HOME/.claude" ] && + [ "${CODEX_HOME:-$HOME/.codex}" = "$HOME/.codex" ] || exit 1 + CLAUDE_CONFIG_DIR="/tmp/pstack claude config" CODEX_HOME="/tmp/pstack codex config" + [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "/tmp/pstack claude config" ] && + [ "${CODEX_HOME:-$HOME/.codex}" = "/tmp/pstack codex config" ] +); then + config_home_bad="${config_home_bad}unset, empty, or space-containing resolution changed"$'\n' +fi +if [ -n "$config_home_bad" ]; then + note "FAIL: setup config-home port invariant regressed:" + note "$config_home_bad" + fail=1 +else + note "ok: setup config-home port invariant; unset/empty defaults and spaced overrides resolve without writes" +fi + quad_of() { { grep -oE '(claude|codex|grok):[a-z0-9.-]+@(low|medium|high|xhigh|max)' || true; } | tr '\n' ' ' | sed 's/ $//'; } canon_panel="$( { grep -m1 '^arena runners:' "$setup" || true; } | quad_of)" panel_bad="" From b56d1876b8f210e92fc4788eab5f107ce71dc7eb Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:49:49 +0000 Subject: [PATCH 4/7] fix: preserve Claude sheet imports across setup reruns [#120] Keep default-home upgrades byte-identical and replace copied-profile imports instead of loading two model sheets. Require deterministic zero/one/many import handling in the config-home invariant. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- CHANGES.md | 4 ++-- UPSTREAM.md | 2 +- .../poteto-mode/references/codex-tools.md | 10 +++++++++- plugins/pstack/skills/setup-pstack/SKILL.md | 12 +++++++++--- tests/skill-collision-repro.sh | 19 +++++++++++++++++-- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 955d3cde..96a2579e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,9 +5,9 @@ This port applies the Cursor → Claude Code substitutions in skill bodies. Earl ## Config-home port correction (#120) -Setup resolves the active harness's config home through `poteto-mode/references/codex-tools.md`: nonempty `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, falling back to `$HOME/.claude` or `$HOME/.codex` for unset or empty values. The model sheet, global instruction integration, current-state reads, snapshots, restoration, and readback all use that directory. Claude imports render the resolved path with escaped spaces; Codex retains its exact-byte bounded block. Default-home behavior, role defaults, probes, and confirmation are unchanged. +Setup resolves the active harness's config home through `poteto-mode/references/codex-tools.md`: nonempty `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, falling back to `$HOME/.claude` or `$HOME/.codex` for unset or empty values. The model sheet, global instruction integration, current-state reads, snapshots, restoration, and readback all use that directory. Claude keeps the literal `@~/.claude/pstack-models.md` import at the default home and renders a space-escaped absolute path only for a redirected home. It identifies the existing import by the target basename `pstack-models.md`, replaces one matching line in place, appends when none exist, and stops before writing if more than one exists. Codex retains its exact-byte bounded block. Default-home behavior, role defaults, probes, and confirmation are unchanged. -This is an intentional Claude Code/Codex port divergence from Cursor's `~/.cursor/rules/` destination, recorded in `UPSTREAM.md` for the port-patch ledger (#105). The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default-home paths in setup and checks unset, empty, and space-containing resolution without writing daily files. `tests/setup-config-home-repro.sh` prepares private redirected directories while preserving real `HOME` and `USER`, and checks artifacts and unchanged daily targets; it does not substitute for installed-harness invocation, receipts, or fresh-session loading proof. The operator supplies that evidence on the final PR head and posts the live gate. +This is an intentional Claude Code/Codex port divergence from Cursor's `~/.cursor/rules/` destination, recorded in `UPSTREAM.md` for the port-patch ledger (#105). The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default-home destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and checks unset, empty, and space-containing resolution without writing daily files. `tests/setup-config-home-repro.sh` prepares private redirected directories while preserving real `HOME` and `USER`, and checks artifacts and unchanged daily targets; it does not substitute for installed-harness invocation, receipts, or fresh-session loading proof. The operator supplies that evidence on the final PR head and posts the live gate. ## 1.5.0 syncs to Cursor pstack 0.15.5 diff --git a/UPSTREAM.md b/UPSTREAM.md index b6ee07c8..a39f1dab 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -27,7 +27,7 @@ The table above is the current Cursor sync point. Open Pstack 1.5.0 imports this ## Local port-patch ledger -- **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude imports escape spaces in the resolved path. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default paths in setup and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. +- **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude preserves the literal legacy import at the default home and escapes spaces in the absolute redirected path; reruns replace the one import whose target basename is `pstack-models.md`, append if absent, and stop if duplicated. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. ## Check for changes diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 2db85481..9166525a 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -11,7 +11,15 @@ Resolve the current parent's config home once: Claude Code uses nonempty `CLAUDE "${CODEX_HOME:-$HOME/.codex}" ``` -Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's current-state reads, sheet and integration writes, snapshots, restoration, and readback. Render the Claude include with the actual resolved sheet path, escaping each space with a backslash for Claude's import syntax; never write a shell variable or `` placeholder into it. +Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's current-state reads, sheet and integration writes, snapshots, restoration, and readback. + +The Claude integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When `` is the default home, render exactly the legacy line: + +```text +@~/.claude/pstack-models.md +``` + +Only when `CLAUDE_CONFIG_DIR` redirects the home, render `@` followed by the absolute resolved sheet path with each space backslash-escaped, never a variable or placeholder. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. ## Tool actions diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 8674e626..f95925de 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -9,12 +9,14 @@ Configure one portable model sheet for the current parent harness. Read [`provid Resolve `` once using the [harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes). Use it for all current-state reads, writes, snapshots, restoration, and readback. -Claude Code writes `/pstack-models.md` and loads it from `/CLAUDE.md` with the actual resolved sheet path (backslash-escape spaces; the placeholder below is illustrative, not literal file content): +Claude Code writes `/pstack-models.md` and loads it from `/CLAUDE.md` using the import rule in step 8. When `` is the default home, render exactly the legacy line: ```text -@ +@~/.claude/pstack-models.md ``` +Only when `CLAUDE_CONFIG_DIR` redirects the home, render `@` followed by the absolute resolved sheet path with each space backslash-escaped, never a variable or placeholder. + Codex writes `/pstack-models.md`. Codex has no `@` include, so mirror the sheet's exact bytes inside one bounded block in `/AGENTS.md` and retain the sheet as the editable source of truth: ```text @@ -107,7 +109,11 @@ interrogate reviewers: claude:opus@max, codex:gpt-5.6-sol@max, grok:grok-4.7@xhi ### 8. Wire it in -Render the parent integration in memory before either write. On Claude, the integration is the single `@` include of the actual resolved `/pstack-models.md` path (backslash-escape spaces, never leave a variable or placeholder) in `/CLAUDE.md`. On Codex, it is the exact sheet bytes between one `` and `` pair in `/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. +Render the parent integration in memory before either write. + +On Claude, the integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When `` is the default home, render exactly `@~/.claude/pstack-models.md`, preserving the legacy text. Only when `CLAUDE_CONFIG_DIR` redirects the home, render `@` followed by the absolute resolved `/pstack-models.md` path with each space backslash-escaped, never a variable or placeholder. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. + +On Codex, the integration is the exact sheet bytes between one `` and `` pair in `/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. Snapshot every target's current bytes. Write the sheet and parent integration only after every requested pair passes and the operator confirms. Read both targets back and compare them with the in-memory render. If either write or readback fails, restore every snapshot and report the failure. An unchanged rerun must produce byte-identical sheet and integration content after normalization. diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index cf590d04..5e1aa618 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -81,9 +81,24 @@ dispatch="$repo/plugins/pstack/skills/poteto-mode/references/provider-dispatch.m # Config-home port invariant (#120): guard upstream merges against daily-home writes. config_mapping="$repo/plugins/pstack/skills/poteto-mode/references/codex-tools.md" config_home_bad="" -if grep -nE '~/(\.claude|\.codex)|\$HOME/(\.claude|\.codex)' "$setup"; then - config_home_bad="setup still names a literal default config path"$'\n' +# The legacy import is rendered text, not a hard-coded write destination. +if sed 's|@~/.claude/pstack-models.md||g' "$setup" | grep -nE '~/(\.claude|\.codex)|\$HOME/(\.claude|\.codex)'; then + config_home_bad="setup still names a literal default config path outside the legacy import"$'\n' fi +for source in "$setup" "$config_mapping"; do + grep -Fxq '@~/.claude/pstack-models.md' "$source" || config_home_bad="${config_home_bad}$source lacks the literal legacy default-home import"$'\n' + for rule in \ + 'When `` is the default home, render exactly' \ + 'Only when `CLAUDE_CONFIG_DIR` redirects the home' \ + 'absolute resolved' \ + 'each space backslash-escaped' \ + 'basename is `pstack-models.md`' \ + 'On a rerun, replace that one line in place, preserving all unrelated bytes.' \ + 'If zero matching import lines exist, append one.' \ + 'If more than one exists, stop and report inconsistent state before either write'; do + grep -Fq "$rule" "$source" || config_home_bad="${config_home_bad}$source lacks Claude import rule: $rule"$'\n' + done +done grep -Fq '[harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes)' "$setup" || config_home_bad="${config_home_bad}setup does not reference the canonical config-home rule"$'\n' for expression in '"${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' '"${CODEX_HOME:-$HOME/.codex}"'; do grep -Fxq "$expression" "$config_mapping" || config_home_bad="${config_home_bad}mapping lacks quoted nonempty/default resolution: $expression"$'\n' From 239a73c179582589cafe2f2bb4da2bb217abe5c1 Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:18:45 +0000 Subject: [PATCH 5/7] fix: keep redirected Claude imports independent of config paths [#120] Use a sibling-relative model-sheet import so directory characters cannot prevent loading, while preserving legacy default-home imports and deterministic reruns. Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- CHANGES.md | 2 +- UPSTREAM.md | 2 +- .../poteto-mode/references/codex-tools.md | 2 +- plugins/pstack/skills/setup-pstack/SKILL.md | 4 ++-- tests/setup-config-home-repro.sh | 20 +++++++++---------- tests/skill-collision-repro.sh | 12 ++++++----- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 96a2579e..4778c1fb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ This port applies the Cursor → Claude Code substitutions in skill bodies. Earl ## Config-home port correction (#120) -Setup resolves the active harness's config home through `poteto-mode/references/codex-tools.md`: nonempty `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, falling back to `$HOME/.claude` or `$HOME/.codex` for unset or empty values. The model sheet, global instruction integration, current-state reads, snapshots, restoration, and readback all use that directory. Claude keeps the literal `@~/.claude/pstack-models.md` import at the default home and renders a space-escaped absolute path only for a redirected home. It identifies the existing import by the target basename `pstack-models.md`, replaces one matching line in place, appends when none exist, and stops before writing if more than one exists. Codex retains its exact-byte bounded block. Default-home behavior, role defaults, probes, and confirmation are unchanged. +Setup resolves the active harness's config home through `poteto-mode/references/codex-tools.md`: nonempty `CLAUDE_CONFIG_DIR` or `CODEX_HOME`, falling back to `$HOME/.claude` or `$HOME/.codex` for unset or empty values. The model sheet, global instruction integration, current-state reads, snapshots, restoration, and readback all use that directory. Claude keeps the literal `@~/.claude/pstack-models.md` import at the default home and renders exactly `@./pstack-models.md` for a redirected home, keeping spaces and `#` in the config-directory name out of the import line. It identifies the existing import by the target basename `pstack-models.md`, replaces one matching line in place, appends when none exist, and stops before writing if more than one exists. Codex retains its exact-byte bounded block. Default-home behavior, role defaults, probes, and confirmation are unchanged. This is an intentional Claude Code/Codex port divergence from Cursor's `~/.cursor/rules/` destination, recorded in `UPSTREAM.md` for the port-patch ledger (#105). The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default-home destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and checks unset, empty, and space-containing resolution without writing daily files. `tests/setup-config-home-repro.sh` prepares private redirected directories while preserving real `HOME` and `USER`, and checks artifacts and unchanged daily targets; it does not substitute for installed-harness invocation, receipts, or fresh-session loading proof. The operator supplies that evidence on the final PR head and posts the live gate. diff --git a/UPSTREAM.md b/UPSTREAM.md index a39f1dab..8ee14094 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -27,7 +27,7 @@ The table above is the current Cursor sync point. Open Pstack 1.5.0 imports this ## Local port-patch ledger -- **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude preserves the literal legacy import at the default home and escapes spaces in the absolute redirected path; reruns replace the one import whose target basename is `pstack-models.md`, append if absent, and stop if duplicated. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. +- **#120 — harness config homes** (tracked for #105): Cursor's setup destination is `~/.cursor/rules/`. The shared port resolves nonempty `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, otherwise `$HOME/.claude` / `$HOME/.codex`, once at `plugins/pstack/skills/poteto-mode/references/codex-tools.md#harness-config-homes`. `setup-pstack/SKILL.md` reuses that home for reads, sheet/integration writes, snapshots, restoration, and readback; Claude preserves the literal legacy import at the default home and renders exactly `@./pstack-models.md` for redirected homes, keeping spaces and `#` in the config-directory name out of the import line; reruns replace the one import whose target basename is `pstack-models.md`, append if absent, and stop if duplicated. Preserve this intentional divergence during upstream syncs. The named config-home invariant in `tests/skill-collision-repro.sh` rejects literal default destinations outside the legacy import, requires the literal default-home rendering and zero/one/many import rules, and verifies unset/empty/space-containing resolution without daily writes. See `CHANGES.md` for the port correction and `tests/setup-config-home-repro.sh` for redirected live-evidence preparation. The Cursor sync point is unchanged. ## Check for changes diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 9166525a..b27ffcba 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -19,7 +19,7 @@ The Claude integration is the one `@` import line in `/CLAUDE.md` w @~/.claude/pstack-models.md ``` -Only when `CLAUDE_CONFIG_DIR` redirects the home, render `@` followed by the absolute resolved sheet path with each space backslash-escaped, never a variable or placeholder. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. +Only when `CLAUDE_CONFIG_DIR` redirects the home, render exactly `@./pstack-models.md`. This relative import resolves from the importing file's directory, where the sheet also lives, so the import line contains no config-directory characters. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. ## Tool actions diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index f95925de..f0eb28f5 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -15,7 +15,7 @@ Claude Code writes `/pstack-models.md` and loads it from `/pstack-models.md`. Codex has no `@` include, so mirror the sheet's exact bytes inside one bounded block in `/AGENTS.md` and retain the sheet as the editable source of truth: @@ -111,7 +111,7 @@ interrogate reviewers: claude:opus@max, codex:gpt-5.6-sol@max, grok:grok-4.7@xhi Render the parent integration in memory before either write. -On Claude, the integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When `` is the default home, render exactly `@~/.claude/pstack-models.md`, preserving the legacy text. Only when `CLAUDE_CONFIG_DIR` redirects the home, render `@` followed by the absolute resolved `/pstack-models.md` path with each space backslash-escaped, never a variable or placeholder. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. +On Claude, the integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When `` is the default home, render exactly `@~/.claude/pstack-models.md`, preserving the legacy text. Only when `CLAUDE_CONFIG_DIR` redirects the home, render exactly `@./pstack-models.md`. This relative import resolves from the importing file's directory, where the sheet also lives, so the import line contains no config-directory characters. On a rerun, replace that one line in place, preserving all unrelated bytes. If zero matching import lines exist, append one. If more than one exists, stop and report inconsistent state before either write; do not append another import or guess which one to replace. On Codex, the integration is the exact sheet bytes between one `` and `` pair in `/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. diff --git a/tests/setup-config-home-repro.sh b/tests/setup-config-home-repro.sh index 3b7a0d7a..5bf07dfa 100644 --- a/tests/setup-config-home-repro.sh +++ b/tests/setup-config-home-repro.sh @@ -10,7 +10,7 @@ Usage: bash tests/setup-config-home-repro.sh --verify RUN_DIR Preparation keeps HOME and USER unchanged, snapshots daily sheet/integration -files, and creates private redirected config directories (including spaces). +files, and creates private redirected config directories (including spaces and #). Only .credentials.json / auth.json are copied from explicitly supplied login sources. Without a file-backed source, log in in the redirected directory. No daily configuration or credentials are modified by this fixture. @@ -49,7 +49,7 @@ for (const [i, target] of meta.targets.entries()) { const before = path.join(run, 'daily', String(i)); if (fs.existsSync(before) !== fs.existsSync(target) || (fs.existsSync(before) && !fs.readFileSync(before).equals(fs.readFileSync(target)))) fail(`daily config target changed: ${target}`); } -const home = path.join(run, `${meta.harness} config`); +const home = path.join(run, `${meta.harness} # config`); const sheet = path.join(home, 'pstack-models.md'); const integration = path.join(home, meta.harness === 'claude' ? 'CLAUDE.md' : 'AGENTS.md'); if (!fs.existsSync(sheet) || !fs.statSync(sheet).size) fail('redirected model sheet missing or empty'); @@ -59,7 +59,7 @@ const sentinel = fs.readFileSync(path.join(run, 'sentinel'), 'utf8'); if (!text.startsWith(sentinel)) fail('unrelated integration bytes changed'); if (meta.harness === 'claude') { const includes = text.split('\n').filter(line => line.startsWith('@') && line.includes('pstack-models.md')); - if (includes.length !== 1 || includes[0] !== `@${sheet.replaceAll(' ', '\\ ')}`) fail('Claude include is not the single resolved sheet path with escaped spaces'); + if (includes.length !== 1 || includes[0] !== '@./pstack-models.md') fail('Claude include is not the single relative sheet import'); } else { const begin = ''; const end = ''; @@ -103,13 +103,13 @@ done umask 077 run="$(mktemp -d "${TMPDIR:-/tmp}/pstack-config-home.XXXXXX")" run="$(cd "$run" && pwd -P)" -mkdir "$run/claude config" "$run/codex config" "$run/daily" "$run/workspace" -[ -z "$claude_source" ] || cp "$claude_source/.credentials.json" "$run/claude config/.credentials.json" -[ -z "$codex_source" ] || cp "$codex_source/auth.json" "$run/codex config/auth.json" +mkdir "$run/claude # config" "$run/codex # config" "$run/daily" "$run/workspace" +[ -z "$claude_source" ] || cp "$claude_source/.credentials.json" "$run/claude # config/.credentials.json" +[ -z "$codex_source" ] || cp "$codex_source/auth.json" "$run/codex # config/auth.json" printf '%s\n' '# Fixture instructions' 'Preserve this unrelated text exactly.' > "$run/sentinel" -cp "$run/sentinel" "$run/claude config/CLAUDE.md" -cp "$run/sentinel" "$run/codex config/AGENTS.md" -printf '[features]\nmulti_agent = true\n' > "$run/codex config/config.toml" +cp "$run/sentinel" "$run/claude # config/CLAUDE.md" +cp "$run/sentinel" "$run/codex # config/AGENTS.md" +printf '[features]\nmulti_agent = true\n' > "$run/codex # config/config.toml" node - "$run" "$repo" "$harness" "$claude_source" "$codex_source" <<'JS' const fs = require('node:fs'); const path = require('node:path'); @@ -122,7 +122,7 @@ fs.writeFileSync(path.join(run, 'run.json'), JSON.stringify({harness, home: proc JS printf 'Prepared private run: %s\n' "$run" printf 'Keep HOME and USER unchanged. In an operator terminal:\ncd %q\n' "$run/workspace" -printf 'export CLAUDE_CONFIG_DIR=%q CODEX_HOME=%q\n' "$run/claude config" "$run/codex config" +printf 'export CLAUDE_CONFIG_DIR=%q CODEX_HOME=%q\n' "$run/claude # config" "$run/codex # config" if [ "$harness" = claude ]; then printf 'claude --plugin-dir %q\n' "$repo/plugins/pstack" else diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 5e1aa618..6f0a0194 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -90,14 +90,16 @@ for source in "$setup" "$config_mapping"; do for rule in \ 'When `` is the default home, render exactly' \ 'Only when `CLAUDE_CONFIG_DIR` redirects the home' \ - 'absolute resolved' \ - 'each space backslash-escaped' \ + 'render exactly `@./pstack-models.md`' \ 'basename is `pstack-models.md`' \ 'On a rerun, replace that one line in place, preserving all unrelated bytes.' \ 'If zero matching import lines exist, append one.' \ 'If more than one exists, stop and report inconsistent state before either write'; do grep -Fq "$rule" "$source" || config_home_bad="${config_home_bad}$source lacks Claude import rule: $rule"$'\n' done + if grep -nE 'backslash|space-escaped|absolute resolved' "$source"; then + config_home_bad="${config_home_bad}$source still specifies absolute or escaped Claude imports"$'\n' + fi done grep -Fq '[harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes)' "$setup" || config_home_bad="${config_home_bad}setup does not reference the canonical config-home rule"$'\n' for expression in '"${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' '"${CODEX_HOME:-$HOME/.codex}"'; do @@ -114,9 +116,9 @@ if ! ( CLAUDE_CONFIG_DIR="" CODEX_HOME="" [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "$HOME/.claude" ] && [ "${CODEX_HOME:-$HOME/.codex}" = "$HOME/.codex" ] || exit 1 - CLAUDE_CONFIG_DIR="/tmp/pstack claude config" CODEX_HOME="/tmp/pstack codex config" - [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "/tmp/pstack claude config" ] && - [ "${CODEX_HOME:-$HOME/.codex}" = "/tmp/pstack codex config" ] + CLAUDE_CONFIG_DIR="/tmp/pstack claude # config" CODEX_HOME="/tmp/pstack codex # config" + [ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" = "/tmp/pstack claude # config" ] && + [ "${CODEX_HOME:-$HOME/.codex}" = "/tmp/pstack codex # config" ] ); then config_home_bad="${config_home_bad}unset, empty, or space-containing resolution changed"$'\n' fi From e826c0a931a75e5b01096c548f1ca08535e7c396 Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:44:41 +0000 Subject: [PATCH 6/7] fix: preserve the assignments loaded by copied Claude profiles [#120] Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- .../poteto-mode/references/codex-tools.md | 4 +- plugins/pstack/skills/setup-pstack/SKILL.md | 4 +- tests/setup-config-home-repro.sh | 68 ++++++++++++++++++- tests/skill-collision-repro.sh | 1 + 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index b27ffcba..08a0560c 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -11,9 +11,9 @@ Resolve the current parent's config home once: Claude Code uses nonempty `CLAUDE "${CODEX_HOME:-$HOME/.codex}" ``` -Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's current-state reads, sheet and integration writes, snapshots, restoration, and readback. +Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's sheet and integration writes, snapshots, restoration, and readback; current-state reads follow the existing Claude import when it points elsewhere. -The Claude integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When `` is the default home, render exactly the legacy line: +The Claude integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When that import points elsewhere, load current state from its target (relative to the importing file's directory, expanding `~` to the user's home), name the source and new destination at confirmation, and carry the loaded assignments into `/pstack-models.md`; if the imported file is missing, report that and use the normal first-run path. When `` is the default home, render exactly the legacy line: ```text @~/.claude/pstack-models.md diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 01dfd561..6e939ce6 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -33,7 +33,7 @@ Use the harness and tool surface running this skill: Claude Code or Codex. Envir ### 2. Load current state -Read the current parent-specific sheet when it exists. Before matrix validation, normalize only the rolling-alias predecessors that earlier pstack releases generated. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-` or `claude-opus-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable` or `opus`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 7. This migration is valid loaded state and does not require a separate operator choice. +On Claude, if the single existing model-sheet import in `/CLAUDE.md` points elsewhere, resolve its target relative to the importing file's directory (expanding `~` to the user's home), read current state from that imported sheet, and name the source file; if it is missing, report that and use the normal first-run path. Otherwise, read the current parent-specific sheet when it exists. Before matrix validation, normalize only the rolling-alias predecessors that earlier pstack releases generated. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-` or `claude-opus-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable` or `opus`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 7. This migration is valid loaded state and does not require a separate operator choice. Treat the normalized values as current role-to-family assignments. Overlay those rows on the complete first-run role map in step 7. Materialize any missing documented role row from that map on the next successful write. A duplicate role row is inconsistent state; report it and resolve it before probing. A row whose role is not in the step 7 role map, such as `how critics`, is from a retired role. Drop it and list it at confirmation. A bare host-native slug from an older sheet is also invalid because it does not say which provider owns it. A versioned Claude model outside the two migration families remains inconsistent state. If the sheet is missing, use the complete first-run role map and the model matrix's Default effort cells. @@ -88,7 +88,7 @@ Rewrite every matrix-family descriptor to `provider:model@/pstack-models.md`, the route table for this parent, and every rendered role and descriptor. Carry the loaded assignments into the new destination; only explicit role or effort changes and the documented normalization may change them. Ask for confirmation before writing. Why and Reflect require the parent's live MCP surface. Keep their investigator, reviewer, and synthesizer roles on `inherit-parent` or `auto`; the bounded external runner deliberately omits ambient MCPs. `inherit-parent` and `auto` always validate, but say when they reduce a panel's provider diversity. For panel roles, one lane runs per entry. The list length is the fan-out count. `arena cross-judge pool` is a list from which Arena chooses a provider different from the parent and base candidate when possible. `swarm workers` is the default for every worker unless a race explicitly assigns another descriptor. diff --git a/tests/setup-config-home-repro.sh b/tests/setup-config-home-repro.sh index 5bf07dfa..c369b519 100644 --- a/tests/setup-config-home-repro.sh +++ b/tests/setup-config-home-repro.sh @@ -1,13 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -# Prepare and inspect a real-harness run; never simulate setup's writes. +# Prepare and inspect a real-harness run; static checks test the verifier only. repo="$(cd "$(dirname "$0")/.." && pwd)" usage() { cat <<'EOF' Usage: bash tests/setup-config-home-repro.sh claude|codex redirected [--claude-config DIR] [--codex-home DIR] + bash tests/setup-config-home-repro.sh claude imported-sheet [--claude-config DIR] [--codex-home DIR] bash tests/setup-config-home-repro.sh --verify RUN_DIR + bash tests/setup-config-home-repro.sh --static + +The imported-sheet case seeds the legacy import and snapshots the existing +~/.claude/pstack-models.md read-only; keep its assignments unchanged during setup. +Static mode tests artifact comparisons without invoking a harness or claiming +live acceptance. Preparation keeps HOME and USER unchanged, snapshots daily sheet/integration files, and creates private redirected config directories (including spaces and #). @@ -31,6 +38,49 @@ EOF } if [ "${1:-}" = "--help" ]; then usage; exit 0; fi +if [ "${1:-}" = "--static" ]; then + node - "$repo" <<'JS' +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const cp = require('node:child_process'); +const assert = require('node:assert/strict'); +const repo = process.argv[2]; +const run = fs.mkdtempSync(path.join(os.tmpdir(), 'pstack-import-static-')); +try { + const home = path.join(run, 'claude # config'); + fs.mkdirSync(home); + const sentinel = '# Fixture instructions\nPreserve this unrelated text exactly.\n'; + const rows = 'how explorer: claude:opus@max\narena runners: codex:gpt-5.6-sol@max, claude:opus@max\n'; + fs.writeFileSync(path.join(run, 'sentinel'), sentinel); + fs.writeFileSync(path.join(run, 'imported-sheet'), rows); + fs.writeFileSync(path.join(run, 'run.json'), JSON.stringify({harness: 'claude', home: process.env.HOME, user: process.env.USER || '', sha: cp.execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], {encoding: 'utf8'}).trim(), targets: []})); + const sheet = path.join(home, 'pstack-models.md'); + const integration = path.join(home, 'CLAUDE.md'); + const verify = () => cp.spawnSync('bash', [path.join(repo, 'tests/setup-config-home-repro.sh'), '--verify', run], {encoding: 'utf8'}); + fs.writeFileSync(sheet, rows); + fs.writeFileSync(integration, sentinel + '@~/.claude/pstack-models.md\n'); + let result = verify(); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stderr, /single relative sheet import/); + fs.writeFileSync(integration, sentinel + '@./pstack-models.md\n'); + for (const changed of [rows.replace('claude:opus@max', 'auto'), rows.replace('@max', '@high')]) { + fs.writeFileSync(sheet, changed); + result = verify(); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stderr, /imported sheet assignments were not preserved/); + } + fs.writeFileSync(sheet, rows); + for (let i = 0; i < 2; i++) { + result = verify(); + assert.equal(result.status, 0, result.stdout + result.stderr); + } + console.log('ok: static imported-sheet fixture rejects stale assignments and legacy redirected imports; preserved assignments and unchanged rerun pass'); + console.log('No live harness was invoked; HOME and USER and daily configuration were unchanged.'); +} finally { fs.rmSync(run, {recursive: true, force: true}); } +JS + exit 0 +fi if [ "${1:-}" = "--verify" ]; then [ "$#" = 2 ] || { usage >&2; exit 2; } command -v node >/dev/null || { echo 'FAIL: node is required for artifact comparisons' >&2; exit 1; } @@ -54,6 +104,12 @@ const sheet = path.join(home, 'pstack-models.md'); const integration = path.join(home, meta.harness === 'claude' ? 'CLAUDE.md' : 'AGENTS.md'); if (!fs.existsSync(sheet) || !fs.statSync(sheet).size) fail('redirected model sheet missing or empty'); const bytes = fs.readFileSync(sheet); +const imported = path.join(run, 'imported-sheet'); +if (fs.existsSync(imported)) { + const assignments = text => text.split('\n').filter(line => /^[^:#]+:\s*(?:claude:|codex:|grok:|inherit-parent\s*$|auto\s*$)/.test(line)).map(line => line.trim()).sort(); + const before = assignments(fs.readFileSync(imported, 'utf8')); + if (!before.length || JSON.stringify(before) !== JSON.stringify(assignments(bytes.toString('utf8')))) fail('imported sheet assignments were not preserved'); +} const text = fs.readFileSync(integration, 'utf8'); const sentinel = fs.readFileSync(path.join(run, 'sentinel'), 'utf8'); if (!text.startsWith(sentinel)) fail('unrelated integration bytes changed'); @@ -80,7 +136,11 @@ fi harness="${1:-}" case "$harness" in claude|codex) ;; *) usage >&2; exit 2 ;; esac -[ "${2:-}" = redirected ] || { echo 'FAIL: only redirected live writes are permitted; test defaults statically' >&2; exit 2; } +mode="${2:-}" +case "$harness:$mode" in claude:redirected|codex:redirected|claude:imported-sheet) ;; *) echo 'FAIL: only redirected live writes are permitted; test defaults statically' >&2; exit 2 ;; esac +if [ "$mode" = imported-sheet ]; then + [ -s "$HOME/.claude/pstack-models.md" ] || { echo 'FAIL: imported-sheet case requires an existing ~/.claude/pstack-models.md' >&2; exit 1; } +fi shift 2 claude_source="" codex_source="" @@ -108,6 +168,10 @@ mkdir "$run/claude # config" "$run/codex # config" "$run/daily" "$run/workspace" [ -z "$codex_source" ] || cp "$codex_source/auth.json" "$run/codex # config/auth.json" printf '%s\n' '# Fixture instructions' 'Preserve this unrelated text exactly.' > "$run/sentinel" cp "$run/sentinel" "$run/claude # config/CLAUDE.md" +if [ "$mode" = imported-sheet ]; then + cp "$HOME/.claude/pstack-models.md" "$run/imported-sheet" + printf '@~/.claude/pstack-models.md\n' >> "$run/claude # config/CLAUDE.md" +fi cp "$run/sentinel" "$run/codex # config/AGENTS.md" printf '[features]\nmulti_agent = true\n' > "$run/codex # config/config.toml" node - "$run" "$repo" "$harness" "$claude_source" "$codex_source" <<'JS' diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 6f0a0194..ba281887 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -101,6 +101,7 @@ for source in "$setup" "$config_mapping"; do config_home_bad="${config_home_bad}$source still specifies absolute or escaped Claude imports"$'\n' fi done +grep -Fq "On Claude, if the single existing model-sheet import in \`/CLAUDE.md\` points elsewhere, resolve its target relative to the importing file's directory (expanding \`~\` to the user's home), read current state from that imported sheet, and name the source file; if it is missing, report that and use the normal first-run path." "$setup" || config_home_bad="${config_home_bad}setup does not load the imported sheet as current state"$'\n' grep -Fq '[harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes)' "$setup" || config_home_bad="${config_home_bad}setup does not reference the canonical config-home rule"$'\n' for expression in '"${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' '"${CODEX_HOME:-$HOME/.codex}"'; do grep -Fxq "$expression" "$config_mapping" || config_home_bad="${config_home_bad}mapping lacks quoted nonempty/default resolution: $expression"$'\n' From 6ef25f56618c36526383e471c28065a571bbeb4e Mon Sep 17 00:00:00 2001 From: "mastra-platform[bot]" <284800079+mastra-platform[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:55:13 +0000 Subject: [PATCH 7/7] fix: select recoverable setup state before normalization [#120] Co-Authored-By: mastra-platform[bot] <284800079+mastra-platform[bot]@users.noreply.github.com> --- plugins/pstack/skills/poteto-mode/references/codex-tools.md | 4 ++-- plugins/pstack/skills/setup-pstack/SKILL.md | 2 +- tests/skill-collision-repro.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 08a0560c..1d375920 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -11,9 +11,9 @@ Resolve the current parent's config home once: Claude Code uses nonempty `CLAUDE "${CODEX_HOME:-$HOME/.codex}" ``` -Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's sheet and integration writes, snapshots, restoration, and readback; current-state reads follow the existing Claude import when it points elsewhere. +Empty and unset variables both select the default. Preserve spaces in the resolved path by quoting shell file operands. In shared instructions, `` means this resolved parent-specific directory, not literal text to write. Use that same home for setup's sheet and integration writes, snapshots, restoration, and readback. -The Claude integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. When that import points elsewhere, load current state from its target (relative to the importing file's directory, expanding `~` to the user's home), name the source and new destination at confirmation, and carry the loaded assignments into `/pstack-models.md`; if the imported file is missing, report that and use the normal first-run path. When `` is the default home, render exactly the legacy line: +The Claude integration is the one `@` import line in `/CLAUDE.md` whose target's basename is `pstack-models.md`, regardless of its existing directory or path spelling. Select current state using [setup-pstack step 2](../../setup-pstack/SKILL.md#2-load-current-state). When `` is the default home, render exactly the legacy line: ```text @~/.claude/pstack-models.md diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 6e939ce6..dba5aafc 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -33,7 +33,7 @@ Use the harness and tool surface running this skill: Claude Code or Codex. Envir ### 2. Load current state -On Claude, if the single existing model-sheet import in `/CLAUDE.md` points elsewhere, resolve its target relative to the importing file's directory (expanding `~` to the user's home), read current state from that imported sheet, and name the source file; if it is missing, report that and use the normal first-run path. Otherwise, read the current parent-specific sheet when it exists. Before matrix validation, normalize only the rolling-alias predecessors that earlier pstack releases generated. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-` or `claude-opus-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable` or `opus`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 7. This migration is valid loaded state and does not require a separate operator choice. +Inspect `/pstack-models.md` and the existing integration: resolve Claude's single model-sheet import, or read Codex's single bounded model block, reporting missing targets and stopping on unreadable, malformed, or ambiguous state. Use the sole surviving configuration, or the editable sheet when both configurations agree; on Codex, the sheet remains authoritative and the block is its mirror, recoverable when the sheet is absent. If surviving configurations differ, show both sources and their differing assignments and require an explicit source choice before normalization or probing, without merging them automatically. Use first-run defaults only when neither source contains recoverable configuration, then apply the existing normalization, role-completion, and validation rules to the selected state. Before matrix validation, normalize only the rolling-alias predecessors that earlier pstack releases generated. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-` or `claude-opus-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable` or `opus`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 7. This migration is valid loaded state and does not require a separate operator choice. Treat the normalized values as current role-to-family assignments. Overlay those rows on the complete first-run role map in step 7. Materialize any missing documented role row from that map on the next successful write. A duplicate role row is inconsistent state; report it and resolve it before probing. A row whose role is not in the step 7 role map, such as `how critics`, is from a retired role. Drop it and list it at confirmation. A bare host-native slug from an older sheet is also invalid because it does not say which provider owns it. A versioned Claude model outside the two migration families remains inconsistent state. If the sheet is missing, use the complete first-run role map and the model matrix's Default effort cells. diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index ba281887..9ca752b2 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -101,7 +101,7 @@ for source in "$setup" "$config_mapping"; do config_home_bad="${config_home_bad}$source still specifies absolute or escaped Claude imports"$'\n' fi done -grep -Fq "On Claude, if the single existing model-sheet import in \`/CLAUDE.md\` points elsewhere, resolve its target relative to the importing file's directory (expanding \`~\` to the user's home), read current state from that imported sheet, and name the source file; if it is missing, report that and use the normal first-run path." "$setup" || config_home_bad="${config_home_bad}setup does not load the imported sheet as current state"$'\n' +grep -Fq 'require an explicit source choice before normalization or probing' "$setup" || config_home_bad="${config_home_bad}setup does not require explicit source selection before normalization or probing"$'\n' grep -Fq '[harness config-home rule](../poteto-mode/references/codex-tools.md#harness-config-homes)' "$setup" || config_home_bad="${config_home_bad}setup does not reference the canonical config-home rule"$'\n' for expression in '"${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' '"${CODEX_HOME:-$HOME/.codex}"'; do grep -Fxq "$expression" "$config_mapping" || config_home_bad="${config_home_bad}mapping lacks quoted nonempty/default resolution: $expression"$'\n'