mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 23:14:33 +01:00
v1.92.57.0 fix(hermes): plain skill-start fence that Tirith can resolve, so Hermes can start gstack skills (GST-882) (#3825)
* fix: plain skill-start fence that a static resolver can follow (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: checked-in renders keep the lookup as plain lines; regenerate goldens, lower size pins (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: Codex preamble expectation follows the plain start lookup (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (runtime-root header documents the plain skill-start fence, GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: Step 11 review fixes: troubleshooting names the skill-start exception; parity test pins the start lookup to the later-fence root (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: type the parity test's env so the test typecheck ratchet stays clean (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog fragment for GST-882 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: writing-style Codex fence expectation follows the plain start lookup (GST-882) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
f5cb6ec1eb
commit
e2d9c798b2
15 files changed
+225
-80
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.92.57.0] - 2026-11-02
|
||||
|
||||
**Hermes can start gstack skills again (GST-882).** On stock Hermes, the Tirith scanner flagged every gstack skill's first command as HIGH ("Nested executable body could not be resolved"), so `gstack-skill-start` was denied in 8 of 8 trials. Tirith-only findings can't go on Hermes's permanent allowlist, so there was no workaround. The start line Tirith couldn't follow was a grouped fallback that picked the script through a variable.
|
||||
|
||||
### Fixed
|
||||
- **The skill-start line on Codex, Hermes and the other env-var hosts is plain shell.** A skill installed for one folder (Hermes, and the Codex install `./setup` writes) now checks for that folder's `bin/gstack-skill-start` and then runs it by its literal path. Tirith 0.4.2 allows that line where it blocked the old one. A checked-in render keeps the same lookup order (an exported `GSTACK_ROOT`, then the repo's own install, then the global one) as plain lines, without the `{ …; exit 1;}` group. When no install exists, both print the same "no install found" line with the setup command, and the start command then fails.
|
||||
- **An install-root render's first block no longer honors an exported `GSTACK_ROOT`.** Tirith blocks any start command picked through a variable. Later blocks still honor it. `docs/troubleshooting.md` names this exception.
|
||||
|
||||
### Tests
|
||||
- `test/skill-start-plain.test.ts`: on every host and both render kinds, the start fence has no grouped fallback, and a missing install fails loudly under `bash -u`. The install-root start command is a literal path that runs, and a checked-in render finds the global install. On each env-var host, the start lookup picks the same root as later blocks for global, repo-local and exported installs.
|
||||
|
||||
## [1.92.56.0] - 2026-11-02
|
||||
|
||||
**`gstack-ship-receipt write` records the session kind on Windows.** Without `--session-kind` or `GSTACK_SESSION_KIND`, it asks the `gstack-session-kind` bash helper. It used to spawn that helper by path, which never starts on Windows, so every /ship receipt there carried no `session_kind`. It now starts the helper through `lib/script-invocation.ts`, which uses Git Bash on Windows. This is the /ship half of #3541.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.92.56.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.92.57.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -880,7 +880,11 @@ pick a new `--output`), then copy the recovery file to where you want it.
|
||||
**Meaning.** On Codex, Factory, OpenCode, Cursor, Copilot and Kiro every bash
|
||||
block finds gstack on its own: an exported `GSTACK_ROOT` (with `bin/` and
|
||||
`lib/`), then a repo-local install, then the host's global install
|
||||
(`$CODEX_HOME/skills/gstack` on Codex). None existed.
|
||||
(`$CODEX_HOME/skills/gstack` on Codex). None existed. A skill installed for one
|
||||
folder (Hermes, or the Codex install `./setup` writes) is the exception for its
|
||||
first block: that block runs the folder's own `bin/gstack-skill-start` and
|
||||
ignores an exported `GSTACK_ROOT`. When the folder is missing, the block prints
|
||||
this line and its start command then fails.
|
||||
|
||||
**Fix.**
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.92.56",
|
||||
"version": "1.92.57",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TemplateContext } from '../types';
|
||||
import { getHostConfig } from '../../../hosts/index';
|
||||
import { runtimeRootPrelude } from '../runtime-root';
|
||||
import { skillStartLines } from '../runtime-root';
|
||||
|
||||
/**
|
||||
* Preamble bootstrap (token-reduction Phase 1).
|
||||
@@ -25,11 +25,10 @@ import { runtimeRootPrelude } from '../runtime-root';
|
||||
*/
|
||||
export function generatePreambleBash(ctx: TemplateContext): string {
|
||||
const hostConfig = getHostConfig(ctx.host);
|
||||
const runtimeRoot = hostConfig.usesEnvVars ? `${runtimeRootPrelude(ctx)}\n` : '';
|
||||
const start = hostConfig.usesEnvVars ? skillStartLines(ctx) : null;
|
||||
const runtimeRoot = start ? `${start.lookup}\n` : '';
|
||||
const brainHealthFlag = ctx.host === 'gbrain' || ctx.host === 'hermes' ? ' --brain-health' : '';
|
||||
const startCommand = hostConfig.usesEnvVars
|
||||
? `"${ctx.paths.binDir}/gstack-skill-start"`
|
||||
: `${ctx.paths.binDir}/gstack-skill-start`;
|
||||
const startCommand = start ? start.command : `${ctx.paths.binDir}/gstack-skill-start`;
|
||||
|
||||
const entry = ['plan-design-review', 'plan-eng-review'].includes(ctx.skillName)
|
||||
? `## Preamble (after scope gate)
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* bash finds gstack on Codex and every other `usesEnvVars` host.
|
||||
*
|
||||
* Those hosts run each fenced block in a fresh shell, so `GSTACK_ROOT`, `B`,
|
||||
* `D` and `P` set by an earlier block are gone. The preamble's root resolution and
|
||||
* the per-fence prelude come from `runtimeRootPrelude()`; `insertRuntimePreludes()`
|
||||
* `D` and `P` set by an earlier block are gone. The preamble's skill-start fence
|
||||
* comes from `skillStartLines()` and the per-fence prelude from
|
||||
* `runtimeRootPrelude()`; `insertRuntimePreludes()`
|
||||
* is the single post-render pass (gen-skill-docs, before rewriteInstallRoot)
|
||||
* that puts it into every fence that uses these variables without assigning
|
||||
* them. Claude output is returned unchanged.
|
||||
@@ -13,6 +14,9 @@
|
||||
* on a per-install render, the literal install root (no git call); otherwise the
|
||||
* repo-local install, then the host's global root (`CODEX_HOME` on Codex). No
|
||||
* root prints one line naming what was tried and the setup command, and exits.
|
||||
* The skill-start fence differs (GST-882): an install-root render calls the
|
||||
* literal install root's start script and ignores an exported `GSTACK_ROOT`, and
|
||||
* with no root either render prints that line and lets the call fail.
|
||||
*/
|
||||
import { getHostConfig } from '../../hosts/index';
|
||||
import { toShellPath, type TemplateContext } from './types';
|
||||
@@ -37,13 +41,45 @@ const uses = (name: string) => new RegExp(`\\$\\{?${name}\\b`);
|
||||
*/
|
||||
function resolveRoot(ctx: TemplateContext): string {
|
||||
const host = getHostConfig(ctx.host);
|
||||
const fix = `Fix: ./setup --host ${host.name} from your gstack checkout; ./setup --status shows it.`;
|
||||
const fix = installFix(ctx);
|
||||
const exported = '[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||';
|
||||
if (ctx.installRoot) {
|
||||
return `${exported}{ GSTACK_ROOT="${ctx.installRoot.replace(/\/+$/, '')}";[ -d "$GSTACK_ROOT/bin" ]||{ echo "gstack: no install found (tried $GSTACK_ROOT). ${fix}">&2;exit 1;};}`;
|
||||
}
|
||||
const global = host.name === 'codex' ? '${CODEX_HOME:-~/.codex}/skills/gstack' : `~/${host.globalRoot}`;
|
||||
return `${exported}{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/${host.localSkillRoot};[ -d "$_r/bin" ]||_r=${global};[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). ${fix}">&2;exit 1;};GSTACK_ROOT=$_r;}`;
|
||||
return `${exported}{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/${host.localSkillRoot};[ -d "$_r/bin" ]||_r=${globalRoot(ctx)};[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). ${fix}">&2;exit 1;};GSTACK_ROOT=$_r;}`;
|
||||
}
|
||||
|
||||
const installFix = (ctx: TemplateContext) => `Fix: ./setup --host ${getHostConfig(ctx.host).name} from your gstack checkout; ./setup --status shows it.`;
|
||||
const globalRoot = (ctx: TemplateContext) => {
|
||||
const host = getHostConfig(ctx.host);
|
||||
return host.name === 'codex' ? '${CODEX_HOME:-~/.codex}/skills/gstack' : `~/${host.globalRoot}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* The skill-start fence on env-var hosts (GST-882). Hermes's Tirith blocks a
|
||||
* start command it cannot resolve statically, a grouped fallback or a
|
||||
* variable-selected path alike, and no allowlist entry clears that finding. An
|
||||
* install-root render therefore names the literal start path; a checked-in
|
||||
* render keeps resolveRoot's lookup order as plain lines. Either way a missing
|
||||
* install prints the fix, and the call then fails.
|
||||
*/
|
||||
export function skillStartLines(ctx: TemplateContext): { lookup: string; command: string } {
|
||||
const fix = installFix(ctx);
|
||||
if (ctx.installRoot) {
|
||||
const root = ctx.installRoot.replace(/\/+$/, '');
|
||||
return {
|
||||
lookup: `[ -x "${root}/bin/gstack-skill-start" ]||echo "gstack: no install found (tried ${root}). ${fix}">&2`,
|
||||
command: `"${root}/bin/gstack-skill-start"`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
lookup: [
|
||||
`[ -d "\${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/${getHostConfig(ctx.host).localSkillRoot}`,
|
||||
`[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=${globalRoot(ctx)}`,
|
||||
`[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). ${fix}">&2`,
|
||||
].join('\n'),
|
||||
command: '"$GSTACK_ROOT/bin/gstack-skill-start"',
|
||||
};
|
||||
}
|
||||
|
||||
const DERIVED: Record<string, string> = { BIN: 'bin', BROWSE: 'browse/dist', DESIGN: 'design/dist', MAKE_PDF: 'make-pdf/dist' };
|
||||
|
||||
+4
-3
@@ -17,9 +17,10 @@ This skill follows gstack shared preamble `6872f1ec4eb9` (AskUserQuestion format
|
||||
## Preamble (run first)
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=~/.gemini/antigravity-cli/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host agy from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
GSTACK_BIN=$GSTACK_ROOT/bin
|
||||
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "gemini"
|
||||
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=~/.gemini/antigravity-cli/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host agy from your gstack checkout; ./setup --status shows it.">&2
|
||||
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "gemini"
|
||||
```
|
||||
|
||||
**Antigravity tool names:** `AskUserQuestion` means your `ask_question` tool (options as choices); there is no `mcp__*__AskUserQuestion` variant. The Bash tool means `run_command`, the Read tool `view_file`, the Write tool `write_to_file`, the Edit tool `replace_file_content` (`multi_replace_file_content` for several edits in one file), Grep `grep_search`, Glob `find_by_name`, and the Agent tool `invoke_subagent`. There is no Skill tool: load a skill by reading its SKILL.md with `view_file`. If a step says to call `ExitPlanMode`, tell the user the plan is ready and wait. `view_file` can return only part of a long file: when a SKILL.md or a file it tells you to read ends before its last line, read the remaining line ranges before acting on it.
|
||||
|
||||
+4
-3
@@ -17,9 +17,10 @@ This skill follows gstack shared preamble `2af24c2d75e4` (AskUserQuestion format
|
||||
## Preamble (run first)
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
GSTACK_BIN=$GSTACK_ROOT/bin
|
||||
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "gpt"
|
||||
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=${CODEX_HOME:-~/.codex}/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2
|
||||
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "gpt"
|
||||
```
|
||||
|
||||
Read the echoed `KEY: value` STATUS lines — they drive every preamble rule
|
||||
|
||||
+4
-3
@@ -19,9 +19,10 @@ This skill follows gstack shared preamble `99cd6cb5d373` (AskUserQuestion format
|
||||
## Preamble (run first)
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
GSTACK_BIN=$GSTACK_ROOT/bin
|
||||
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "claude"
|
||||
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=~/.factory/skills/gstack
|
||||
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2
|
||||
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "claude"
|
||||
```
|
||||
|
||||
Read the echoed `KEY: value` STATUS lines — they drive every preamble rule
|
||||
|
||||
+51
-51
@@ -2,56 +2,56 @@
|
||||
"_comment": "Generated SKILL.md files over the limit, pinned at their exact character count (#3199). test/skill-md-size-guard.test.ts fails when a pinned skill grows past its pin or shrinks below it: lower the pin when a skill shrinks and delete it once the skill is within the limit. Raising a pin is an owner decision. The shared-preamble hoist (GSTA-96) moved every tier 2+ skill's shared preamble into sections/preamble.md; the pins below are what remains.",
|
||||
"limit": 100000,
|
||||
"pins": {
|
||||
"agy/gstack-autoplan": 109666,
|
||||
"agy/gstack-plan-design-review": 109116,
|
||||
"agy/gstack-plan-devex-review": 114957,
|
||||
"agy/gstack-plan-eng-review": 143590,
|
||||
"agy/gstack-review": 104276,
|
||||
"codex/gstack-autoplan": 108409,
|
||||
"codex/gstack-plan-design-review": 110473,
|
||||
"codex/gstack-plan-devex-review": 116465,
|
||||
"codex/gstack-plan-eng-review": 144755,
|
||||
"codex/gstack-review": 103799,
|
||||
"copilot/gstack-autoplan": 109109,
|
||||
"copilot/gstack-plan-design-review": 109382,
|
||||
"copilot/gstack-plan-devex-review": 114419,
|
||||
"copilot/gstack-plan-eng-review": 143755,
|
||||
"copilot/gstack-review": 103857,
|
||||
"cursor/gstack-autoplan": 108341,
|
||||
"cursor/gstack-plan-design-review": 107924,
|
||||
"cursor/gstack-plan-devex-review": 113689,
|
||||
"cursor/gstack-plan-eng-review": 142345,
|
||||
"cursor/gstack-review": 122147,
|
||||
"factory/gstack-autoplan": 110619,
|
||||
"factory/gstack-plan-design-review": 111022,
|
||||
"factory/gstack-plan-devex-review": 117052,
|
||||
"factory/gstack-plan-eng-review": 145453,
|
||||
"factory/gstack-review": 125920,
|
||||
"gbrain/gstack-autoplan": 108748,
|
||||
"gbrain/gstack-plan-eng-review": 124972,
|
||||
"gemini/gstack-autoplan": 108899,
|
||||
"gemini/gstack-plan-design-review": 109210,
|
||||
"gemini/gstack-plan-devex-review": 114247,
|
||||
"gemini/gstack-plan-eng-review": 143588,
|
||||
"gemini/gstack-review": 103679,
|
||||
"hermes/gstack-autoplan": 108751,
|
||||
"hermes/gstack-plan-eng-review": 125008,
|
||||
"kiro/gstack-autoplan": 108115,
|
||||
"kiro/gstack-plan-design-review": 107770,
|
||||
"kiro/gstack-plan-devex-review": 113541,
|
||||
"kiro/gstack-plan-eng-review": 142207,
|
||||
"kiro/gstack-review": 121971,
|
||||
"openclaw/gstack-autoplan": 108571,
|
||||
"openclaw/gstack-plan-eng-review": 123487,
|
||||
"opencode/gstack-autoplan": 109090,
|
||||
"opencode/gstack-plan-design-review": 109238,
|
||||
"opencode/gstack-plan-devex-review": 114297,
|
||||
"opencode/gstack-plan-eng-review": 143614,
|
||||
"opencode/gstack-review": 122769,
|
||||
"slate/gstack-autoplan": 108228,
|
||||
"slate/gstack-plan-design-review": 107847,
|
||||
"slate/gstack-plan-devex-review": 113615,
|
||||
"slate/gstack-plan-eng-review": 142276,
|
||||
"slate/gstack-review": 122059
|
||||
"agy/gstack-autoplan": 109657,
|
||||
"agy/gstack-plan-design-review": 109107,
|
||||
"agy/gstack-plan-devex-review": 114948,
|
||||
"agy/gstack-plan-eng-review": 143581,
|
||||
"agy/gstack-review": 104267,
|
||||
"codex/gstack-autoplan": 108400,
|
||||
"codex/gstack-plan-design-review": 110464,
|
||||
"codex/gstack-plan-devex-review": 116456,
|
||||
"codex/gstack-plan-eng-review": 144746,
|
||||
"codex/gstack-review": 103790,
|
||||
"copilot/gstack-autoplan": 109100,
|
||||
"copilot/gstack-plan-design-review": 109373,
|
||||
"copilot/gstack-plan-devex-review": 114410,
|
||||
"copilot/gstack-plan-eng-review": 143746,
|
||||
"copilot/gstack-review": 103848,
|
||||
"cursor/gstack-autoplan": 108332,
|
||||
"cursor/gstack-plan-design-review": 107915,
|
||||
"cursor/gstack-plan-devex-review": 113680,
|
||||
"cursor/gstack-plan-eng-review": 142336,
|
||||
"cursor/gstack-review": 122138,
|
||||
"factory/gstack-autoplan": 110610,
|
||||
"factory/gstack-plan-design-review": 111013,
|
||||
"factory/gstack-plan-devex-review": 117043,
|
||||
"factory/gstack-plan-eng-review": 145444,
|
||||
"factory/gstack-review": 125911,
|
||||
"gbrain/gstack-autoplan": 108739,
|
||||
"gbrain/gstack-plan-eng-review": 124963,
|
||||
"gemini/gstack-autoplan": 108890,
|
||||
"gemini/gstack-plan-design-review": 109201,
|
||||
"gemini/gstack-plan-devex-review": 114238,
|
||||
"gemini/gstack-plan-eng-review": 143579,
|
||||
"gemini/gstack-review": 103670,
|
||||
"hermes/gstack-autoplan": 108742,
|
||||
"hermes/gstack-plan-eng-review": 124999,
|
||||
"kiro/gstack-autoplan": 108106,
|
||||
"kiro/gstack-plan-design-review": 107761,
|
||||
"kiro/gstack-plan-devex-review": 113532,
|
||||
"kiro/gstack-plan-eng-review": 142198,
|
||||
"kiro/gstack-review": 121962,
|
||||
"openclaw/gstack-autoplan": 108562,
|
||||
"openclaw/gstack-plan-eng-review": 123478,
|
||||
"opencode/gstack-autoplan": 109081,
|
||||
"opencode/gstack-plan-design-review": 109229,
|
||||
"opencode/gstack-plan-devex-review": 114288,
|
||||
"opencode/gstack-plan-eng-review": 143605,
|
||||
"opencode/gstack-review": 122760,
|
||||
"slate/gstack-autoplan": 108219,
|
||||
"slate/gstack-plan-design-review": 107838,
|
||||
"slate/gstack-plan-devex-review": 113606,
|
||||
"slate/gstack-plan-eng-review": 142267,
|
||||
"slate/gstack-review": 122050
|
||||
}
|
||||
}
|
||||
@@ -3125,9 +3125,11 @@ describe('Codex generation (--host codex)', () => {
|
||||
expect(content).toContain('_r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack');
|
||||
expect(content).toContain('_r=${CODEX_HOME:-~/.codex}/skills/gstack');
|
||||
// Phase 1/2: config reads moved into gstack-skill-start — the fence itself
|
||||
// is the bin asset the preamble must resolve through $GSTACK_BIN, and the
|
||||
// question-preference runtime call still resolves the same way.
|
||||
expect(content).toContain('$GSTACK_BIN/gstack-skill-start');
|
||||
// is the bin asset the preamble resolves through the same roots, as plain
|
||||
// lines (GST-882), and the question-preference runtime call resolves
|
||||
// through $GSTACK_BIN.
|
||||
expect(content).toContain('[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=${CODEX_HOME:-~/.codex}/skills/gstack');
|
||||
expect(content).toContain('"$GSTACK_ROOT/bin/gstack-skill-start"');
|
||||
expect(content).toContain('$GSTACK_BIN/gstack-question-preference');
|
||||
// The upgrade-skill doc reference moved into the script's upgrade-flow
|
||||
// block, resolved $0-relative ($_ROOT_DIR) — host-neutral by construction,
|
||||
|
||||
@@ -118,7 +118,8 @@ describe('gstack-skill-start contract', () => {
|
||||
// Worktree-isolated Claude Code sessions refuse a start command named by a
|
||||
// variable, a `[ -x ] ||` fallback, or a trailing `|| echo` (reproduced
|
||||
// with the pinned CLI). The Claude render is the literal install path;
|
||||
// env-var hosts reach the script through $GSTACK_BIN. A missing helper is
|
||||
// checked-in env-var host renders reach it through $GSTACK_ROOT/bin (an
|
||||
// install-root render names the literal path, GST-882). A missing helper is
|
||||
// covered by the degraded-mode prose, not a shell fallback.
|
||||
const fence = (text: string) => {
|
||||
const at = text.search(/^## Preamble \((?:run first|after scope gate)\)/m);
|
||||
@@ -130,7 +131,7 @@ describe('gstack-skill-start contract', () => {
|
||||
}
|
||||
for (const host of ['codex', 'gbrain']) {
|
||||
const lines = fence(generatePreamble({ skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', host, paths: HOST_PATHS[host], preambleTier: 4 } as TemplateContext)).split('\n');
|
||||
expect(lines.at(-1)).toMatch(/^"\$GSTACK_BIN\/gstack-skill-start" --skill "ship" --model "[^"]+"( --brain-health)?$/);
|
||||
expect(lines.at(-1)).toMatch(/^"\$GSTACK_ROOT\/bin\/gstack-skill-start" --skill "ship" --model "[^"]+"( --brain-health)?$/);
|
||||
expect(lines.join('\n')).not.toMatch(/_SS|\$PPID|\|\| echo|\[ -x/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* GST-882: the skill-start fence must be plain shell a static resolver can
|
||||
* follow. Hermes's Tirith 0.4.2 blocks a grouped fallback
|
||||
* (`[ -d … ]||{ …; exit 1;}`) and any variable-selected command with HIGH
|
||||
* "Nested executable body could not be resolved", which no allowlist entry can
|
||||
* clear, so every gstack skill start was denied. An install-root render names
|
||||
* the literal start path; every render keeps a loud failure when gstack is
|
||||
* missing.
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { ALL_HOST_CONFIGS } from '../hosts';
|
||||
import { generatePreambleBash } from '../scripts/resolvers/preamble/generate-preamble-bash';
|
||||
import { insertRuntimePreludes, runtimeRootPrelude } from '../scripts/resolvers/runtime-root';
|
||||
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gst882-'));
|
||||
const ctx = (host: string, installRoot: string | null) =>
|
||||
({ host, paths: HOST_PATHS[host], skillName: 'ship', tmplPath: '', model: 'claude', installRoot }) as TemplateContext;
|
||||
const startFence = (c: TemplateContext) => insertRuntimePreludes(generatePreambleBash(c), c).match(/```bash\n([\s\S]*?)\n```/)![1];
|
||||
const GROUPED = /(?:^|[;&|]\s*)\{\s|exit 1;\s*\}/m;
|
||||
const sh = (body: string, env: Record<string, string>, cwd = tmp) =>
|
||||
spawnSync('bash', ['-uc', body], { cwd, encoding: 'utf8', timeout: 10_000, env: { PATH: '/usr/bin:/bin', ...env } });
|
||||
function stubStart(root: string) {
|
||||
fs.mkdirSync(path.join(root, 'bin'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'bin', 'gstack-skill-start'), '#!/bin/sh\necho "STARTED $0 $*"\n', { mode: 0o755 });
|
||||
}
|
||||
|
||||
describe('skill-start fence is plain shell on every host (GST-882)', () => {
|
||||
for (const host of ALL_HOST_CONFIGS) {
|
||||
for (const installRoot of [null, path.join(tmp, `${host.name}-install`)]) {
|
||||
const label = `${host.name} ${installRoot ? 'install-root' : 'checked-in'} render`;
|
||||
test(`${label}: no grouped fallback`, () => {
|
||||
expect(startFence(ctx(host.name, installRoot))).not.toMatch(GROUPED);
|
||||
});
|
||||
if (!host.usesEnvVars) continue;
|
||||
test(`${label}: a missing install fails loudly`, () => {
|
||||
const r = sh(startFence(ctx(host.name, installRoot)), { HOME: path.join(tmp, 'empty-home') });
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('gstack: no install found');
|
||||
expect(r.stderr).toContain(`./setup --host ${host.name}`);
|
||||
});
|
||||
}
|
||||
if (!host.usesEnvVars) continue;
|
||||
test(`${host.name} install-root render: the start command is a literal path that runs`, () => {
|
||||
const root = path.join(tmp, `${host.name}-literal`);
|
||||
const fence = startFence(ctx(host.name, root));
|
||||
const command = fence.split('\n').at(-1)!;
|
||||
expect(command).toStartWith(`"${root}/bin/gstack-skill-start" --skill "ship"`);
|
||||
expect(fence).not.toContain('$');
|
||||
stubStart(root);
|
||||
const r = sh(fence, { HOME: path.join(tmp, 'empty-home') });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`STARTED ${root}/bin/gstack-skill-start --skill ship`);
|
||||
});
|
||||
test(`${host.name} checked-in render: finds the global install`, () => {
|
||||
const home = path.join(tmp, `${host.name}-home`);
|
||||
const global = path.join(home, host.name === 'codex' ? '.codex/skills/gstack' : host.globalRoot);
|
||||
stubStart(global);
|
||||
const r = sh(startFence(ctx(host.name, null)), { HOME: home });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('STARTED ');
|
||||
expect(r.stdout).toContain('--skill ship');
|
||||
});
|
||||
test(`${host.name} checked-in render: the start lookup picks the root later fences pick`, () => {
|
||||
const base = path.join(tmp, `${host.name}-parity`);
|
||||
const home = path.join(base, 'home');
|
||||
const repo = path.join(base, 'repo');
|
||||
const exported = path.join(base, 'exported');
|
||||
stubStart(path.join(home, host.name === 'codex' ? '.codex/skills/gstack' : host.globalRoot));
|
||||
fs.mkdirSync(repo, { recursive: true });
|
||||
spawnSync('git', ['init', '-q', repo], { timeout: 10_000 });
|
||||
const c = ctx(host.name, null);
|
||||
const lookup = startFence(c).split('\n').slice(0, -1).join('\n');
|
||||
const root = (body: string, env: Record<string, string>) => sh(`${body}\necho "ROOT=$GSTACK_ROOT"`, env, repo).stdout.trim().split('\n').at(-1);
|
||||
for (const layout of ['global', 'repo-local', 'exported']) {
|
||||
if (layout === 'repo-local') stubStart(path.join(repo, host.localSkillRoot));
|
||||
if (layout === 'exported') stubStart(exported);
|
||||
const env: Record<string, string> = layout === 'exported' ? { HOME: home, GSTACK_ROOT: exported } : { HOME: home };
|
||||
expect(root(lookup, env)).toBe(root(runtimeRootPrelude(c), env));
|
||||
if (layout === 'exported') expect(root(lookup, env)).toBe(`ROOT=${exported}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('Writing Style preamble section', () => {
|
||||
const fenceLine = out.split('\n').find(l => l.includes('gstack-skill-start" --skill'));
|
||||
expect(fenceLine).toBeDefined();
|
||||
expect(fenceLine).not.toMatch(/~\/\.claude\//);
|
||||
expect(fenceLine).toContain('$GSTACK_BIN');
|
||||
expect(fenceLine).toContain('$GSTACK_ROOT/bin/');
|
||||
});
|
||||
|
||||
test('tier 1 preamble does NOT include Writing Style section', () => {
|
||||
|
||||
Reference in new issue
Block a user