v1.91.188.0 fix(pregate): resolve extensionless .jsx, .mts, .cts and index.jsx/tsx/mjs imports (#3340) (#3371)

* fix(pregate): resolve extensionless .jsx, .mts, .cts and index.jsx/tsx/mjs imports (#3340)

RESOLVE_EXT had no .jsx and no index.jsx/index.tsx/index.mjs, so on a JSX repo
every extensionless component import resolved nowhere and lanes failed with
'has no test lane'. Existing entries keep their order (it decides precedence).

Fixes #3340

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.188.0 stamp

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:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-25 00:20:13 +00:00
1 parent 632bde77de
commit 1e3ee83c43
7 files changed
+70 -4

No files matched your search

+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [1.91.188.0] - 2026-10-24
**Pregate maps extensionless .jsx imports to their tests (#3340).** On a Next.js or webpack JSX repo, `gstack-pregate` failed `lanes` with "has no test lane" for every component imported without an extension, so /ship's receipt refused and the only way out was shipping without one. The import resolver tried `.ts`, `.tsx`, `.js`, `.mjs` and `.cjs` but never `.jsx`, and a directory import found `index.ts` or `index.js` but not `index.jsx`, `index.tsx` or `index.mjs` (it fell back to selecting on the whole directory). Reported by adam-balazs.
### Fixed
- **`lib/pregate/import-graph.ts`**: `RESOLVE_EXT` adds `.jsx`, `.mts`, `.cts`, `/index.tsx`, `/index.jsx` and `/index.mjs`. The existing entries keep their order, so a `.js` file still wins over a `.jsx` sibling. Relative, `paths` alias and `baseUrl` imports share the list, so all three resolve the new forms.
### Tests
- `test/pregate-selection.test.ts` has three new cases: an extensionless import of a `.jsx`, `.mts` and `.cts` file; a directory import that reaches `index.jsx`, `index.tsx` and `index.mjs` and leaves the directory's other files unselected; and a jsconfig `paths` alias to a `.jsx` file, with a `.js` sibling that still wins. All three failed before the change.
### Docs
- `docs/pregate.md` lists the extensions and index forms an extensionless import tries, in order.
## [1.91.187.0] - 2026-10-24
**Four docs now say what the code does: the unattended run contract, the gate reply grammar, the session-kind override and voice triggers.** All four were reported by readers who checked the docs against the code. `docs/unattended.md` promised that every workflow skill ends an unattended run with a run directory and a `GSTACK_RESULT` line, but only `/autoplan` has the steps and helpers that write one (#3281, thanks @exGeni). `gstack-gate --help` still taught the reply grammar from before 1.91.107.0, without `rec` or `as-is`, while the render it relays leads with `rec` (#3282, thanks @exGeni). `docs/OPENCLAW.md` said only `spawned` is honored as a `GSTACK_SESSION_KIND` override, but `unattended` is honored too (#3287, thanks @exGeni). The README said natural phrases activate the right skill, without saying that only `voice-triggers:` aliases reach a host and that the plain `triggers:` list does not (#2118, thanks @jinzheio).
+1 -1
View File
@@ -1 +1 @@
1.91.187.0
1.91.188.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.187.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.188.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,
+4
View File
@@ -137,6 +137,10 @@ Two stages with separate clocks, both printed:
included, are source code, so a `dependencies` `[]` declaration clears them and `empty_lanes`
does not), transitively through
non-test files; deleted and renamed files select the tests that named them.
An extensionless JS/TS import (relative, a tsconfig/jsconfig `paths` alias or
`baseUrl`) tries the exact path, then `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`,
`.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx`
and `index.mjs` inside the directory; the first file that exists wins.
A touched source file with no lane is `FAIL lanes — <file> has no test lane`,
never a pass. Markdown that no test depends on is a docs/no-tests empty lane
instead (`docs/no-tests: N documentation file(s)`), including when the base
+1 -1
View File
@@ -47,7 +47,7 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' });
// that matches more text only gains an edge when it resolves to a repo file.
const QUOTED = /['"`]([A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g;
const JOIN = /\bjoin\(([^()]*)\)/g;
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs', '/index.ts', '/index.js'];
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.js', '/index.jsx', '/index.mjs'];
// A statement starts a line or follows `;` or a compound statement's `:`. The module spec is
// non-empty and starts with a dot or a word character, so no two repetitions can share a dot or a space.
const PY_IMPORT = /(?:^|[;:])[ \t\f]*(?:from[ \t]+(\.+(?:\w[\w.]*)?|\w[\w.]*)[ \t]+import[ \t]+(\([^)]*\)|[^\n;]+)|import[ \t]+([^\n;]+))/gm;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.187",
"version": "1.91.188",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+49
View File
@@ -82,6 +82,55 @@ describe('import graph (static imports ∪ repo-path literals ∪ bin convention
expect(testsFor(g, 'notes/scratch.md')).toMatchObject({ tests: [], declaredNone: false });
});
test('#3340: an extensionless import of a .jsx, .mts or .cts file maps to that file', () => {
const repo = fixture();
commit(repo, {
'src/components/Foo.jsx': 'export default function Foo() { return null; }\n',
'src/lib/m.mts': 'export const m = 1;\n',
'src/lib/c.cts': 'export const c = 1;\n',
'test/foo.test.jsx': "import Foo from '../src/components/Foo';\nimport { m } from '../src/lib/m';\nimport { c } from '../src/lib/c';\n",
});
const g = buildGraph(repo.work);
for (const f of ['src/components/Foo.jsx', 'src/lib/m.mts', 'src/lib/c.cts']) {
expect(testsFor(g, f).tests).toEqual(['test/foo.test.jsx']);
expect(testsFor(g, f).provenance[0]).toContain('(import)');
}
});
test('#3340: a directory import resolves to its index.jsx, index.tsx or index.mjs, not only the dir/** fallback', () => {
const repo = fixture();
commit(repo, {
'src/Bar/index.jsx': 'export default 1;\n',
'src/Bar/other.js': 'export default 2;\n',
'src/Baz/index.tsx': 'export default 1;\n',
'src/Baz/other.js': 'export default 2;\n',
'src/Qux/index.mjs': 'export default 1;\n',
'src/Qux/other.js': 'export default 2;\n',
'test/dirs.test.js': "import Bar from '../src/Bar';\nimport Baz from '../src/Baz';\nimport Qux from '../src/Qux';\n",
});
const g = buildGraph(repo.work);
for (const f of ['src/Bar/index.jsx', 'src/Baz/index.tsx', 'src/Qux/index.mjs']) {
expect(testsFor(g, f).tests).toEqual(['test/dirs.test.js']);
expect(testsFor(g, f).provenance[0]).not.toContain('/**');
}
for (const f of ['src/Bar/other.js', 'src/Baz/other.js', 'src/Qux/other.js']) expect(testsFor(g, f).tests).toEqual([]);
});
test('#3340: an import through a jsconfig paths alias reaches a .jsx file; an existing .js sibling still wins', () => {
const repo = fixture();
commit(repo, {
'jsconfig.json': '{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }\n',
'src/ui/Button.jsx': 'export default function Button() { return null; }\n',
'src/ui/Card.js': 'export default 1;\n',
'src/ui/Card.jsx': 'export default 2;\n',
'test/ui.test.jsx': "import Button from '@/ui/Button';\nimport Card from '@/ui/Card';\n",
});
const g = buildGraph(repo.work);
expect(testsFor(g, 'src/ui/Button.jsx').tests).toEqual(['test/ui.test.jsx']);
expect(testsFor(g, 'src/ui/Card.js').tests).toEqual(['test/ui.test.jsx']);
expect(testsFor(g, 'src/ui/Card.jsx').tests).toEqual([]);
});
test('python: absolute, from-import submodule, relative and parenthesised imports and mock.patch targets map; stdlib, comments and docstrings do not', () => {
const repo = fixture();
commit(repo, {