diff --git a/.claude/skills/verify-open-pstack/features/grok.md b/.claude/skills/verify-open-pstack/features/grok.md index e7f932f0..47c7c2be 100644 --- a/.claude/skills/verify-open-pstack/features/grok.md +++ b/.claude/skills/verify-open-pstack/features/grok.md @@ -2,7 +2,7 @@ ## Sub-features -`grok:dispatch`: a Grok parent running the installed `poteto-mode` routes an `inherit-parent` lane natively through `spawn_subagent` with `pstack:poteto-agent`, an explicit `grok:` lane through `pstack-runner --parent grok --provider grok`, and a Codex lane through `pstack-runner --parent grok`, and Grok offers only the model-free pstack agents. `grok:setup`: `/pstack:setup-pstack` on a Grok parent writes `${GROK_HOME:-$HOME/.grok}/rules/pstack-models.md`. Select them for a change to the Grok manifest, `provider-dispatch.md`, `codex-tools.md`, `setup-pstack/SKILL.md`, `recipes.ts` or the Grok transport. A path that reaches them never adds them to every run; name them with `--feature grok:dispatch` or `--feature grok:setup`. +`grok:dispatch`: a Grok parent running the installed `poteto-mode` routes an `inherit-parent` lane natively through `spawn_subagent` with `pstack:poteto-agent`, an explicit `grok:` lane through `pstack-runner --parent grok --provider grok`, and a Codex lane through `pstack-runner --parent grok`, and Grok offers only the model-free pstack agents. `grok:setup`: `/pstack:setup-pstack` on a Grok parent writes `${GROK_HOME:-$HOME/.grok}/rules/pstack-models.md`. Select them for a change to the Grok manifest or its `grok-agents/` copies, `provider-dispatch.md`, `codex-tools.md`, `model-catalog.md`, poteto-mode's `SKILL.md`, the runner's `types.ts`, `registry/index.ts` or `registry/render.ts`, `setup-pstack/SKILL.md`, `recipes.ts` or the Grok transport. A path that reaches them never adds them to every run; name them with `--feature grok:dispatch` or `--feature grok:setup`. ## How to get to it (user POV) diff --git a/.claude/skills/verify-open-pstack/scripts/core.test.ts b/.claude/skills/verify-open-pstack/scripts/core.test.ts index 1112c580..995b6eb7 100644 --- a/.claude/skills/verify-open-pstack/scripts/core.test.ts +++ b/.claude/skills/verify-open-pstack/scripts/core.test.ts @@ -79,12 +79,15 @@ describe('registry and ownership', () => { test('only the Grok surfaces reach grok:dispatch and grok:setup, and self-test never adds a Grok session', () => { const reach = (filename: string) => classify([{ filename }], registry).features.filter(f => f.startsWith('grok:')); for (const filename of ['plugins/pstack/skills/poteto-mode/references/provider-dispatch.md', 'plugins/pstack/skills/poteto-mode/references/codex-tools.md', - 'plugins/pstack/.grok-plugin/plugin.json', '.claude/skills/verify-open-pstack/scripts/doctor.ts']) expect(reach(filename)).toEqual(['grok:dispatch']); + 'plugins/pstack/.grok-plugin/plugin.json', '.claude/skills/verify-open-pstack/scripts/doctor.ts', + 'plugins/pstack/grok-agents/poteto-agent.md', 'plugins/pstack/grok-agents/comment-sicko.md', + 'plugins/pstack/skills/poteto-mode/references/model-catalog.md', 'plugins/pstack/skills/poteto-mode/SKILL.md', + 'plugins/pstack/skills/poteto-mode/scripts/runner/types.ts', 'plugins/pstack/skills/poteto-mode/scripts/runner/registry/index.ts', + 'plugins/pstack/skills/poteto-mode/scripts/runner/registry/render.ts']) expect(reach(filename)).toEqual(['grok:dispatch']); expect(reach('.claude/skills/verify-open-pstack/scripts/isolation.ts')).toEqual(['grok:setup']); for (const filename of ['plugins/pstack/skills/setup-pstack/SKILL.md', '.claude/skills/verify-open-pstack/scripts/grok.ts', '.claude/skills/verify-open-pstack/scripts/harness.ts']) expect(reach(filename)).toEqual(['grok:dispatch', 'grok:setup']); - for (const filename of ['plugins/pstack/skills/poteto-mode/references/model-catalog.md', 'plugins/pstack/skills/poteto-mode/SKILL.md', - 'plugins/pstack/hooks/hooks.json', '.claude/skills/verify-open-pstack/scripts/verify.ts']) expect(reach(filename)).toEqual([]); + for (const filename of ['plugins/pstack/hooks/hooks.json', '.claude/skills/verify-open-pstack/scripts/verify.ts']) expect(reach(filename)).toEqual([]); const r = receipt(true); r.runFeatures = ['grok:dispatch', 'grok:setup']; expect(requiredHarnesses(r)).toEqual(['claude', 'codex', 'grok']); @@ -102,7 +105,7 @@ describe('registry and ownership', () => { }); test('poteto-mode SKILL.md also reaches the Claude agent that preloads it', () => { const s = classify([{ filename: 'plugins/pstack/skills/poteto-mode/SKILL.md' }], registry); - expect(s.features).toEqual([...both('skill-invocation:poteto-mode'), 'claude:agent-preload:poteto-agent'].sort()); + expect(s.features).toEqual([...both('skill-invocation:poteto-mode'), 'claude:agent-preload:poteto-agent', 'grok:dispatch'].sort()); }); const CATALOG = 'plugins/pstack/skills/poteto-mode/references/model-catalog.md'; test('catalog-only changes select catalog, setup and runner and no skill check', () => { @@ -111,7 +114,7 @@ describe('registry and ownership', () => { 'plugins/pstack/skills/poteto-mode/scripts/runner/registry/models.ts']) { const s = classify([{ filename }], registry); expect(s.skills).toEqual([]); - expect(s.features).toEqual(both('catalog', 'runner', 'setup')); + expect(s.features).toEqual(filename === CATALOG ? [...both('catalog', 'runner', 'setup'), 'grok:dispatch'].sort() : both('catalog', 'runner', 'setup')); } const PROVIDERS = 'plugins/pstack/skills/poteto-mode/scripts/runner/providers/'; for (const [filename, writers] of [ @@ -125,7 +128,8 @@ describe('registry and ownership', () => { }); test('a renamed or deleted catalog path keeps catalog ownership', () => { for (const previous_filename of [CATALOG, 'plugins/pstack/agents/pstack-opus-max.md']) { - expect(classify([{ filename: 'docs/moved.md', previous_filename }], registry).features).toEqual(both('catalog', 'runner', 'setup')); + expect(classify([{ filename: 'docs/moved.md', previous_filename }], registry).features) + .toEqual(previous_filename === CATALOG ? [...both('catalog', 'runner', 'setup'), 'grok:dispatch'].sort() : both('catalog', 'runner', 'setup')); } }); test('a catalog change mixed with a shared reference or a skill keeps their checks', () => { diff --git a/.claude/skills/verify-open-pstack/scripts/core.ts b/.claude/skills/verify-open-pstack/scripts/core.ts index f5dc3c9b..ec036dd4 100644 --- a/.claude/skills/verify-open-pstack/scripts/core.ts +++ b/.claude/skills/verify-open-pstack/scripts/core.ts @@ -24,7 +24,10 @@ const T3_DISPATCH_PATHS = [ ]; const GROK_TRANSPORT = '.claude/skills/verify-open-pstack/scripts/grok.ts'; const GROK_REACH: Readonly> = { - 'grok:dispatch': [...T3_DISPATCH_PATHS, GROK_TRANSPORT, 'plugins/pstack/.grok-plugin/plugin.json'], + 'grok:dispatch': [...T3_DISPATCH_PATHS, GROK_TRANSPORT, 'plugins/pstack/.grok-plugin/plugin.json', + 'plugins/pstack/grok-agents/poteto-agent.md', 'plugins/pstack/grok-agents/comment-sicko.md', 'plugins/pstack/skills/poteto-mode/SKILL.md', + 'plugins/pstack/skills/poteto-mode/references/model-catalog.md', 'plugins/pstack/skills/poteto-mode/scripts/runner/types.ts', + 'plugins/pstack/skills/poteto-mode/scripts/runner/registry/index.ts', 'plugins/pstack/skills/poteto-mode/scripts/runner/registry/render.ts'], 'grok:setup': ['plugins/pstack/skills/setup-pstack/SKILL.md', '.claude/skills/verify-open-pstack/features/recipes.ts', '.claude/skills/verify-open-pstack/scripts/harness.ts', '.claude/skills/verify-open-pstack/scripts/isolation.ts', GROK_TRANSPORT], }; diff --git a/.claude/skills/verify-open-pstack/scripts/grok.ts b/.claude/skills/verify-open-pstack/scripts/grok.ts index 80558a08..a8e86253 100644 --- a/.claude/skills/verify-open-pstack/scripts/grok.ts +++ b/.claude/skills/verify-open-pstack/scripts/grok.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import { copyFile, open, readdir, realpath } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; -import { track, treeHash } from './io.ts'; +import { grokHome, track, treeHash } from './io.ts'; import type { SessionRecord } from './types.ts'; export const GROK_PARENT = { model: 'grok-4.7', effort: 'low' } as const; @@ -88,7 +88,7 @@ export async function grokSession(options: Options): Promise<{ exitCode: number; child.stdin.end(); const exitCode = await child.exited; await reader; - const sessions = join(options.env.GROK_HOME ?? join(options.env.HOME!, '.grok'), 'sessions'); + const sessions = join(grokHome(options.env), 'sessions'); const folder = (await readdir(sessions)).find(cwd => existsSync(join(sessions, cwd, sessionId, 'summary.json'))); if (!folder) throw new Error('grok-session-record-missing'); const record = join(sessions, folder, sessionId); diff --git a/.claude/skills/verify-open-pstack/scripts/harness.ts b/.claude/skills/verify-open-pstack/scripts/harness.ts index 48e313f1..86b0c562 100644 --- a/.claude/skills/verify-open-pstack/scripts/harness.ts +++ b/.claude/skills/verify-open-pstack/scripts/harness.ts @@ -3,7 +3,7 @@ import { copyFile, mkdir, open, readdir, readFile, realpath, stat, writeFile } f import { basename, join } from 'node:path'; import { CLAUDE_TOOLS, recipe, type Fixture } from '../features/recipes.ts'; import { requiredFeatures, requiredHarnesses } from './core.ts'; -import { command, isolatedEnv, retainedFile, save, session, treeHash, type Command, type Session } from './io.ts'; +import { command, grokHome, isolatedEnv, retainedFile, save, session, treeHash, type Command, type Session } from './io.ts'; import { doctor, t3Binary } from './doctor.ts'; import { grokSession, parseGrok } from './grok.ts'; import { assertCodexAuth, linkCodexAuth, linkMuseConfig, removeCodexHome, restoreSetup, snapshotSetup, type SetupSnapshot } from './isolation.ts'; @@ -198,7 +198,7 @@ export class MacDriver implements Driver { const slug = feature.replace(/[^a-z0-9-]/g, '-'), dir = join(home, 'fixtures', slug), records = join(receipt.artifactRoot, 'sessions', harness, slug); for (const path of [dir, records]) await mkdir(path, { recursive: true, mode: 0o700 }); const fixture: Fixture = { harness, dir, workspace, location: installation.location, run: this.candidate(home), - configHome: harness === 'codex' ? env.CODEX_HOME! : harness === 'grok' ? env.GROK_HOME ?? join(env.HOME!, '.grok') : env.CLAUDE_CONFIG_DIR ?? join(env.HOME!, '.claude') }; + configHome: harness === 'codex' ? env.CODEX_HOME! : harness === 'grok' ? grokHome(env) : env.CLAUDE_CONFIG_DIR ?? join(env.HOME!, '.claude') }; const assertions: string[] = [], evidence: string[] = []; if (setupFeature(feature)) this.setup = await snapshotSetup(); try { diff --git a/.claude/skills/verify-open-pstack/scripts/io.ts b/.claude/skills/verify-open-pstack/scripts/io.ts index 9391337e..6cd6340e 100644 --- a/.claude/skills/verify-open-pstack/scripts/io.ts +++ b/.claude/skills/verify-open-pstack/scripts/io.ts @@ -57,6 +57,9 @@ export const session: Session = async (args, options) => { } finally { await stderr.close(); } } finally { await stdout.close(); } }; +export function grokHome(env: Readonly>): string { + return env.GROK_HOME || join(env.HOME!, '.grok'); +} export function isolatedEnv(state: string, _harness?: 'claude' | 'codex'): Record { if (!isAbsolute(state)) throw new Error('Candidate state must be an absolute run-owned directory'); const { HOME, USER, LOGNAME } = process.env; @@ -69,7 +72,7 @@ export function isolatedEnv(state: string, _harness?: 'claude' | 'codex'): Recor GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'credential.helper', GIT_CONFIG_VALUE_0: '', ...(process.env.CLAUDE_CONFIG_DIR !== undefined ? { CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR } : {}), - ...(process.env.GROK_HOME !== undefined ? { GROK_HOME: process.env.GROK_HOME } : {}), + ...(process.env.GROK_HOME ? { GROK_HOME: process.env.GROK_HOME } : {}), CODEX_HOME: join(state, '.codex') }; } export async function save(path: string, value: unknown): Promise { diff --git a/.claude/skills/verify-open-pstack/scripts/isolation.test.ts b/.claude/skills/verify-open-pstack/scripts/isolation.test.ts index ad185e9e..0359a4d4 100644 --- a/.claude/skills/verify-open-pstack/scripts/isolation.test.ts +++ b/.claude/skills/verify-open-pstack/scripts/isolation.test.ts @@ -126,6 +126,19 @@ describe('setup-only snapshot and restoration', () => { expect(await snapshotSetup()).toHaveLength(7); }); + test('an empty GROK_HOME snapshots the default Grok sheet, as setup resolves it', async () => { + const { root } = await fixture(); + process.env.GROK_HOME = ''; + const sheet = join(root, '.grok/rules/pstack-models.md'); + await mkdir(join(root, '.grok/rules'), { recursive: true }); + await writeFile(sheet, 'operator sheet'); + const snapshot = await snapshotSetup(); + expect(snapshot.map(entry => entry.path)).toContain(sheet); + await writeFile(sheet, 'setup mutation'); + await restoreSetup(snapshot); + expect(await readFile(sheet, 'utf8')).toBe('operator sheet'); + }); + test('restores the Grok sheet under GROK_HOME byte for byte', async () => { const { root } = await fixture(); process.env.GROK_HOME = join(root, 'daily-grok'); diff --git a/.claude/skills/verify-open-pstack/scripts/isolation.ts b/.claude/skills/verify-open-pstack/scripts/isolation.ts index f4397387..0556662c 100644 --- a/.claude/skills/verify-open-pstack/scripts/isolation.ts +++ b/.claude/skills/verify-open-pstack/scripts/isolation.ts @@ -1,5 +1,6 @@ import { lstat, mkdir, readFile, readlink, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises'; import { isAbsolute, join, resolve } from 'node:path'; +import { grokHome } from './io.ts'; function codexAuth(): string { return resolve(process.env.CODEX_HOME ?? join(process.env.HOME!, '.codex'), 'auth.json'); @@ -61,7 +62,7 @@ export async function snapshotSetup(): Promise { const directories = new Set([join(process.env.HOME!, '.claude'), join(process.env.HOME!, '.codex'), process.env.CLAUDE_CONFIG_DIR, process.env.CODEX_HOME].filter((directory): directory is string => !!directory).map(directory => resolve(directory))); const paths = [...directories].flatMap(directory => ['pstack-models.md', 'CLAUDE.md', 'AGENTS.md'].map(name => join(directory, name))); - paths.push(join(resolve(process.env.GROK_HOME ?? join(process.env.HOME!, '.grok')), 'rules/pstack-models.md')); + paths.push(join(resolve(grokHome(process.env)), 'rules/pstack-models.md')); const snapshot: SetupSnapshot = []; for (const path of paths) { const link = (await lstat(path).catch(() => undefined))?.isSymbolicLink() ? await readlink(path) : undefined; diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 1922df81..2494d4bf 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -46,6 +46,7 @@ Reflect never edits a skill inside the plugin cache, `/plugins/cach | --- | --- | --- | | Claude Code | `.claude/skills//` | `/skills//` | | Codex | `.agents/skills//` | `$HOME/.agents/skills//` | +| Grok | `.grok/skills//` | `/skills//` | The copy registers under its plain name beside the plugin's `pstack:`, and does not replace it. In the copy, rewrite each relative link that leaves the skill folder, such as `../poteto-mode/`, to the absolute path of that folder in the installed plugin.