Skip to content

Implement useful SmalltalkGenie features. #347

Description

@ericwinger

I asked Claude to Analyze the SmalltalkGenie work and report back on what might be useful to a GemStone mcp server & Jasper. We may or may not want to implement these but it's a good feature list for future ideas.

Ideas worth borrowing from SmalltalkGenie for Jasper's MCP surface

Provenance

Three repositories, one lineage, decreasing maturity along it.

Repo Smalltalk Tools Commits Tracked lines Tests
KentBeck/SmalltalkGenie Pharo 72 30 6366 1903 lines
riverdusty/SmalltalkGenie-VAST VA Smalltalk 53 31, of which 11 are the porter's 1999 none
riverdusty/SmalltalkGenie-Gemstone GemStone/S 64 27 2 1640 none

Tool counts are grep -c "mcpTool: '" over each repo's server class; VAST's own README agrees at README.md:99 ("53 wishes over MCP"). Pharo's test total is GenieActionTest 1355 lines plus GenieProtocolTest 548.

VAST is a fork of the Pharo repo, not an independent sibling. 20 of its 31 commits are Kent Beck's; the port begins at 5ca28ff (2026-06-28) and the porter's 11 commits run to 50593ce (2026-07-21) — roughly three weeks. The port commit deleted the parent's entire test suite (GenieActionTest, GenieProtocolTest) along with its CLAUDE.md, AGENTS.md and all five agent skills, and never replaced any of it. VAST's .gitignore records the stance: "Agent / local working files — not part of the project" (.gitignore:16-19).

The GemStone port is a port of that port, and is the least-proven of the three: 7 tracked files, 2 commits both on 2026-07-30 fifty minutes apart, both co-authored by an agent, no tests, no CI, no branches or tags. It also carries five self-flagged VERIFY: markers on kernel selectors the author says should be sanity-checked against a real image (genie.gs:21-23) — one of them in the HTTP read path, on the critical path for every request. Its own status section still defers all 28 refactorings, "GemStone's server image ships no Refactoring Browser engine" (README.md:31-33).

VAST is the closest analogue to Jasper's situation and therefore the better precedent: a mature commercial Smalltalk with a full IDE — browsers, inspectors, debuggers, ENVY edition dialogs — where the question is not "can an agent drive a Smalltalk" but "which part of a working IDE's capability do you hand it." VAST answered that question under real use and had to revise its answer once (see the next section). The GemStone port never got that far.

What to trust as design signal: the shape of the tool surface across all three — which operations were judged worth handing to an agent, and where the boundary was drawn. What not to trust: any implementation detail or "this works" claim from either port. Only the Pharo original is executed by tests.


The architectural contrast

Jasper's MCP server sits outside the stone and talks to it over GCI: client/src/mcpTools.ts:1 registers tools against the VS Code window's active session, and mcp-server/src/index.ts:1 runs the same catalogue as a standalone process with its own GCI login. All three genies are the image. GemStone's opens a raw GsSocket, hand-writes HTTP/1.1, and hand-rolls a JSON parser to avoid dependencies; VAST subclasses SstHttpServlet and inherits SST's threading and parsing.

Being inside buys full access to image internals with no marshalling cost — a refactoring engine, an AST, and the compiler are all objects in scope. For GemStone the cost is severe: the accept loop blocks the session and serves connections strictly one at a time (genie.gs:406-419); the server instance must be stashed in SessionTemps because a live GsSocket cannot be committed (genie.gs:290-294, GemStone error 2407); the server's own methods are live on the call stack while it serves a request, so eval has to compile onto a throwaway scratch class (genie.gs:53); and installing the server means filing Smalltalk into the production image.

Jasper's outside-the-stone position is the right one for an IDE, and nothing below proposes changing it. The point of the contrast is narrower: being outside means every capability Jasper wants to give an agent has to be deliberately wired through GCI, so capabilities that already exist server-side can sit unexposed indefinitely. That is exactly what has happened with the refactoring engine, and it frames the whole list — most of what follows is wiring, not new machinery.


Where the genies draw the tool boundary — and what moved it

This is the most transferable thing in the family, and it is not a tool list.

The stated policy is headless and code-only. VAST: "Headless, code-only. No Morphic/Spec, no screen inspection" (README.md:232). The Pharo parent states it prescriptively and forecloses a whole tool family: "Genie is fully headless — no UI, ever. The server is code-only. Do NOT add UI-inspection tools (read_screen) or any Morphic / Spec / Roassal code. If a task seems to need the screen, it's the wrong approach here" (CLAUDE.md:27-29). So: no debugger, no inspector, no object-graph browsing, no breakpoints, no transcript streaming, no edition dialogs. Exposed is everything expressible as text in, structured text out against the code base.

The boundary that actually mattered was a different one, and it moved under real use. VAST shipped its 28 refactorings on 2026-07-20 at 14:40 (db18841) with a deliberate, argued decision not to protect core classes:

"ensureEditableForChanges: obtains editable class editions first, so refactoring a versioned/core class … applies cleanly instead of popping ENVY's make-new-edition modal and freezing the single-threaded server. The genie intentionally does NOT refuse core classes — a developer driving it over MCP may want them, as eval/define_method already allow."

Fifty-eight minutes and two commits later, f6cb060 reversed it:

"runRefactoring: now REFUSES, before applying anything, any change that would modify the definition or an existing method of a class the genie does not own … This replaces the earlier behaviour of auto-obtaining editions for any touched class, which on an image-wide sender rewrite of a common selector (e.g. rename_method on label) tried to edition thousands of core sub-applications and flooded the image with modal dialogs."

The scar is preserved in the code: "The genie must never edit core/versioned classes (that once tried to edition thousands of core sub-applications and had to be reverted)" (GenieServer.class.st:262-266).

Three details of the fix transfer directly:

  1. The guard runs on the computed change set, not on the tool arguments. runRefactoring: executes primitiveExecute to compute the CompositeChange, calls guardChanges: on the whole thing, and only then applies (GenieServer.class.st:1274-1280). A rename whose sender rewrite reaches one base-class method is refused wholesale, before anything is written. Argument-level checking could not have caught that case — the arguments named a user class.
  2. The refusal text is written for an agent to act on, naming the class, the owning application, the reason and the remedy: 'refused: this refactoring would modify <Class> in application <App>, which the genie does not own and is not an open edition. … Open an edition of that class/application yourself first if you really intend to change it.' (GenieServer.class.st:282-286).
  3. The escalation path hands back to the human IDE. "To refactor such a class, open an edition of it yourself first" (README.md:124-126, COMPARISON.md:106-110). The agent gets the whole code surface within the code it owns; the human retains the authority to widen that scope.

One honest caveat: the guard covers refactorings only. eval, define_method and remove_class never reach guardChanges: — it has exactly one sender, at GenieServer.class.st:1277. An agent can still eval its way into a base class. This is policy on the structured path, not a sandbox. (inferred, from the single-sender grep.)

For Jasper. Jasper faces the identical question against a live, multi-user stone, where the blast radius of an image-wide sender rewrite is larger than VAST's and the recovery story is a transaction abort rather than a modal storm. Jasper already has the ingredients — a server-side change set (gs-src/refactoring/engine/GsRefactoringChangeSet.class.st), a token-based non-committing preview, and a kernel-class discriminator (client/src/refactoring/queries/isKernelClass.ts:11) — and currently uses none of them on the MCP path. VAST's evidence says the guard is not a nice-to-have you add later: it is the same commit as the refactoring tools, or you ship the outage first. Proposals 1 and 2 below should land together.


Proposals

1. Expose the refactoring engine as MCP tools, with a dry run first

What the family does: Pharo drives the Refactoring Browser as MCP tools — 34 methods categorised method refactoring and 24 variable refactoring in GenieServer.class.st. VAST ports 28 of them and runs them headlessly against a commercial Smalltalk's classic RB engine. Between them the ports prove the whole family is drivable without a UI, including the two hard cases: GenieExtractMethodRefactoring>>requestMethodNameFor: injects the caller's selector instead of opening a modal, and GeniePullUpInstanceVariableRefactoring answers confirm: with true while making request: error out rather than guess — two different answers to two different kinds of prompt. The GemStone port has none of them, and dropped them purely for lack of an engine (README.md:31-33) — a constraint Jasper does not have.

What Jasper has today: the engine already exists and is better than what the genies drive. gs-src/refactoring/engine/ holds 17 Smalltalk classes covering 13 refactorings, with 16 SUnit test classes in gs-src/refactoring/tests/. It is UI-only: 18 commands (package.json:11481249), 71 files under client/src/refactoring/, and code actions registered for {scheme: 'gemstone'} editors only (client/src/refactoring/renameRefactorCodeActions.ts:21). There are zero refactoring MCP tools; an agent driving Jasper cannot rename, extract, or move anything.

Proposed change: this is wiring, and the codebase is already shaped for it. client/src/queries/types.ts:1-5 states the pattern outright: "Callers supply their own executor — the client wraps browserQueries.executeFetchString … the MCP server wraps McpSession.executeFetchString." All 19 modules in client/src/refactoring/queries/ are written against that QueryExecutor and none imports vscode. The engine protocol is already dry-run-shaped: startRenameMethodPreview builds a non-committing change set, stashes it in SessionTemps under a caller-supplied token, and returns {"token":..,"total":N,"outOfScope":{..},"skippedMethods":[..],"page":{..}} (client/src/refactoring/queries/previewRenameMethod.ts:36), with applyRenameMethod (:83) and clearRenameMethodPreview (:96) as separate steps and a scope selector of class / hierarchy / wholeSystem / dictionary (:10). That maps directly onto a preview_* / apply_* tool pair per refactoring. New code lands in client/src/mcpTools.ts and mcp-server/src/tools.ts — the two files carry an identical 33-tool catalogue, so every tool proposal here is a two-file edit. No new stone-side Smalltalk is required.

Three concrete conventions VAST supplies, which Jasper needs before extract/inline are callable at all:

  • Source-range addressing. VAST's six extract/inline tools take a 1-based inclusive character range start..stop, parsed in one line by intervalFrom: (GenieServer.class.st:952), and the README tells the agent where to get the text to index into: "use get_method_source to get the canonical text" (README.md:141-143). Jasper has no such convention because its extract/inline are selection-driven only — extractMethodCommand.ts:64 reads editor.selection, and the code action only offers Extract when !range.isEmpty (renameRefactorCodeActions.ts:37-40). get_method_source (client/src/mcpTools.ts:493) is the natural anchor.
  • Computed defaults for the hard argument. VAST's change_method_signature makes permutation optional and derives one from the two arities when it is omitted, marking surplus arguments as new (GenieServer.class.st:1113-1125). Jasper's changeSignatureCommand.ts gets the permutation from a webview panel; an MCP version needs this fallback or agents will get it wrong.
  • Batch selectors. pull_up_method / push_down_method accept method_names as a list as well as method_name (GenieServer.class.st:1304, schemas :1194-:1195) — a round-trip saver for the operation an agent most often wants over a whole protocol. Jasper has exactly one list-taking argument in each catalogue today, classNames on list_failing_tests (client/src/mcpTools.ts:580-581), so the convention exists and just isn't used twice.

Open question: one generic apply_refactoring(kind, args) tool versus ~13 typed tools. Typed tools give the model real JSON Schema per refactoring, which is how all three genies do it and how Jasper's existing 33 tools are written; a generic tool keeps the catalogue small. Related: whether preview/apply are two tools or one tool with dryRun: true. Also unanswered anywhere in Jasper: which Rowan package does an extracted method join? VAST's default is defensible — new methods a refactoring introduces are filed into the agent's own application as class extensions, leaving the target's package untouched (README.md:119-122).

2. Guard the computed change set, not the tool arguments

What Genie does: see the boundary section above. The whole of guardChanges: is 25 lines and it is the port's headline safety property.

What Jasper has today: the discriminator exists and is barely used. client/src/refactoring/queries/isKernelClass.ts:11 resolves whether a name is bound in the base Globals dictionary, with a careful comment on why isModifiable cannot be used instead. Outside its own unit test it has one re-export wrapper (client/src/browserQueries.ts:1387) and exactly one consumer: client/src/gemstoneExplorer.ts:1891, a warning before rename-class in the UI. The MCP write tools do not consult it. compile_method (client/src/mcpTools.ts:224), delete_class (:271) and delete_method (:286) will let an agent edit Object with only a DESTRUCTIVE: string in the tool description (:272, :657) standing between it and the kernel.

Proposed change: two parts, and the second is the one VAST's evidence is actually about.

  1. Call isKernelClass in the write-tool path in both MCP files and return an error naming the class and stating how to override. Roughly four call sites plus a shared helper.
  2. When proposal 1 lands, run the same check over the previewed change set, not the named target. Jasper's preview already returns every change it intends to make, and previewRenameMethod.ts:36 already reports outOfScope — so the guard is a filter over data the engine hands back, not new stone-side work. This is the only construction that catches a whole-system rename whose sender rewrite reaches a kernel method, which is precisely the failure VAST hit.

Copy the refusal message shape too: class, owner, reason, remedy. Jasper's remedy line is different from VAST's — abort and do it from the browser, or re-run with a narrower scope (class / hierarchy / dictionary instead of wholeSystem).

Open question: refuse outright, or refuse-unless-allowKernelEdits: true? Unconditional is cleaner, but Jasper's users do legitimately extend kernel classes. Note VAST's answer was unconditional-with-a-human-escalation-path, after trying the permissive version first.

3. Format agent-generated source on compile

What Genie does: Pharo compiles self formattedSource: source rather than the raw string (GenieServer.class.st:391); formattedSource: pretty-prints via RBParser … formattedCode and falls back to the raw source if it does not parse, so a genuine syntax error still surfaces on compile (:266). It was added deliberately and it is tested (GenieActionTest:136, testDefineMethodFormatsSourceOnCompile). The payoff is that read-back after write is stable: the agent's "confirm it took" step compares like with like instead of diffing its own whitespace.

What Jasper has today: compile_method (client/src/mcpTools.ts:224-251) passes the source string straight to queries.compileMethod. Meanwhile Jasper owns a 702-line formatter with 11 configuration knobs — formatDocument at server/src/services/formatting.ts:28, settings under gemstoneSmalltalk.formatter.* at package.json:274-329.

Proposed change: format on the MCP write path. DocumentManager.update(uri, version, source, 'smalltalk') already parses bare method source into the ParsedDocument that formatDocument consumes, and formatDocument iterates regions generically, so no formatter change is needed (inferred — this path is not currently exercised, because the LSP formatting handler declines bare Smalltalk at server/src/server.ts:326). Cross-workspace imports are already precedented: mcp-server/src/tools.ts:8-15 imports from client/src. Keep Pharo's fallback: if the source does not parse, compile it unformatted so the compile error is the error the agent sees.

This is the cheapest high-leverage item in the document — one helper, two call sites, an existing formatter, and it makes every agent write-then-verify loop deterministic.

Open question: respect the user's gemstoneSmalltalk.formatter.* settings, or format to a fixed canonical style on the MCP path? User settings keep agent-written code consistent with hand-written code, which is the point.

4. Close the tool schemas so a hallucinated argument is an error

What the family does: Pharo declares additionalProperties: false in every tool schema (GenieServer.class.st:1496) and asserts it in a test that also checks array item types (GenieProtocolTest:323). VAST enforces it at dispatch: toolArgumentValidationErrorFor:spec: rejects missing-required, unknown ('unknown argument: ') and mistyped arguments as JSON-RPC -32602 (GenieServer.class.st:1374-1375). The GemStone port checks only for missing required keys (genie.gs:619-620).

What Jasper has today: zod object shapes with no .strict() anywhere — grep -c "strict()" is 0 in both client/src/mcpTools.ts and mcp-server/src/tools.ts. Zod strips unknown keys by default, so a hallucinated argument is silently dropped and the tool runs with its defaults. That is silent wrong behaviour where an error would be correctable. Jasper is already halfway there: client/src/mcpZodErrorMap.ts:82 attaches an actionable error map per schema at registration (mcpTools.ts:133, mcp-server/src/tools.ts:141), so the reporting machinery exists and only the strictness is missing.

Proposed change: make the shapes strict at the same registration point that already wraps them, so it is one change in withMcpErrorMap rather than 33 per file. This matters more as proposals 1 and 2 land: a refactoring tool that silently drops scope and defaults to wholeSystem is a different class of problem from a search tool that ignores a typo'd limit.

5. Test the MCP surface through the envelope

What Genie does: Pharo's 1903 test lines are the family's real asset, and they are split by layer.

  • GenieProtocolTest (33 tests) drives the JSON-RPC boundary with no image mutation: malformed JSON, bad jsonrpc version, non-object params, notifications, unknown method, unknown tool, origin allow/reject including hostname spoofing, auth across all four token states, token redaction. Two of them are property tests over the tool registry rather than per-tool tests. testDangerousToolGateBlocksAllNonReadOnlyTools (:33) enumerates server dangerousToolNames and asserts every one returns an error when the gate is off, with a per-tool failure description — so a newly added tool that isn't allow-listed is caught by construction. testRefactoringToolsUseDedicatedHandlers (:184) asserts every refactoring tool has its own handler.
  • GenieActionTest (31 tests) drives real fixtures against the live image through the MCP envelope. invokeMcpTool:with: (:25) posts a genuine tools/call message to handleMcpMessage:, unwraps result.content.first.text, asserts isError is false with the error text in the description, then parses the JSON — so every action test also covers dispatch, schema validation, envelope and rendering. Assertions are on source read back out of the image: testMethodRefactoringToolsExtractAndInline (:246) extracts 3 + 4 into sum, asserts the class now implements #sum and that the original source contains 'self sum', then inlines it back and asserts the source is restored. The helper that computes the start..stop range does it by searching live source (:10) — exactly how an agent must.

What Jasper has today: 1946 lines across the two MCP catalogue test files (client/src/__tests__/mcpTools.test.ts 872, mcp-server/src/__tests__/tools.test.ts 1074), and they are delegation checks. Both build a mock server that captures registrations, mock the session's executeFetchString, call the captured handler directly, and assert on the generated Smalltalk string — e.g. mcp-server/src/__tests__/tools.test.ts:160 asserts executeFetchString was called with 'Array definition'. Nothing goes through tools/call, so dispatch, zod validation, isError and content rendering are untested on both paths.

Proposed change: three transplants, in descending value.

  1. Test through the envelope on the existing test:gci tier. Jasper already has 23 live-stone GCI test files (client/src/__tests__/gci/, run by package.json:2289), two of which — gciPushMethod.e2e.test.ts, gciMoveMethod.e2e.test.ts — already drive real refactoring query builders against a stone and roll everything back. What is missing is not live-stone infrastructure but the MCP layer above it: construct the real McpServer, issue tools/call, assert on source read back out of the stone.
  2. Property tests over the tool registry. Jasper registers the same 33-tool catalogue twice, and each test file hard-codes its own expected name list ("registers the expected tools in alphabetical order", client/src/__tests__/mcpTools.test.ts:117; mcp-server/src/__tests__/tools.test.ts:49) — there is no test that the two catalogues agree. A registry-level test that they match, that every schema is closed (proposal 4), and that every mutating tool is gated (proposal 7) is worth more than 33 more delegation tests, and it is the only construction that covers tools added later.
  3. Fixture teardown as a first-class concern. Pharo needed a dedicated commit to clear fixtures' Undeclared bindings and another to keep fixtures out of the change set; its tearDown removes thirteen named classes (GenieActionTest:65). Jasper's equivalent is a fixture SymbolDictionary plus an abort. This one is the weakest of the three for Jasper, because the existing e2e GCI tests already establish rollback discipline — worth reading them before inventing a second pattern.

6. An agent can commit only if the tests are green

What Genie does: both ports gate persistence on tests. GemStone's commit_transaction optionally takes test_class / test_package, runs them, and commits only when clean — shouldCommit := ((report at: 'failed') + (report at: 'errored')) = 0 (genie.gs:1217) — returning {committed:, tests:}. VAST's save_image carries the identical gate. Pharo tests the invariant (GenieActionTest:918, testSaveImageGateBlocksWhenTestsFail). It is the closest thing any of them has to a built-in repair loop: a bad generation never reaches durable state.

What Jasper has today: nothing couples them. commit (client/src/mcpTools.ts:191) takes an empty argument schema and evaluates a bare System commitTransaction. The SUnit tools are excellent and entirely separate — list_failing_tests (:565), run_test_class (:667), run_test_method (:697), describe_test_failure (:327). An agent can run tests then commit, but nothing makes the commit conditional, so a partial or interrupted turn can persist a red image.

Proposed change: add optional classNames / classNamePattern to the commit tool in client/src/mcpTools.ts:191 and mcp-server/src/tools.ts:184, reusing sunit.runFailingTests — the same call list_failing_tests already makes at client/src/mcpTools.ts:599, and the same argument names, so the convention is already established. Return a structured {committed, testsRun, failures} rather than the current bare string. Jasper's version would be strictly better than either genie's: runFailingTests captures the real exception class and messageText, so a refused commit can say why.

Open question: whether a refused commit should also abort, leave the transaction dirty for the agent to repair, or be configurable. Dirty is more useful for a repair loop; aborting is safer against an agent that stops paying attention.

7. A user can run Jasper's MCP server in read-only mode

What Genie does: allowDangerousTools gates every non-read-only tool against a 14-name allow-list, with the dangerous set computed by subtraction (genie.gs:361-377) and enforcement returning JSON-RPC -32600 before dispatch (genie.gs:616-617). The classification is a first-class property of the registry, not prose — which is what makes Pharo's property test over it possible.

What Jasper has today: no gating of any kind. The only destructive/safe distinction is two occurrences of the literal string DESTRUCTIVE: inside tool descriptions (client/src/mcpTools.ts:272, :657) — advisory text the model may ignore. There is no setting either: the MCP settings are jasper.mcp.registerWithClaudeDesktop and jasper.mcp.httpPort (package.json:162, :167), and nothing else.

Proposed change: add a jasper.mcp.readOnly setting checked once in the shared wrap helper (client/src/mcpTools.ts:147) rather than in 33 handlers. Copy the subtract-from-an-allow-list construction, not a per-tool mutates: boolean flag: subtraction fails closed for tools added later, an explicit flag fails open and will rot. Touches both MCP files plus package.json, and pairs with the registry property test in proposal 5.

Open question: hide gated tools from tools/list entirely, or list them and error on call? Hiding gives the model a cleaner surface; erroring tells it why, which is more useful mid-task.

8. An agent can see Rowan packages, not just dictionaries

What Genie does: all three treat the package as the primary unit — list_packages, list_classes by package, list_methods by package, list_extended_classes (classes a package extends but does not define), export_package / import_package. The GemStone port adds create_package, which builds an in-image repoType: #none Rowan project+package as a writable place to define classes, and both ports let run_test and the test gate take a test_package.

What Jasper has today: zero Rowan MCP tools — grep -i rowan over both MCP files returns nothing. Meanwhile the extension has 15 Rowan commands (package.json:836913) and a ready-made query layer: client/src/queries/rowan/ contains 8 modules (listRowanProjects, diffRowanProject, findRowanClassOwners, listAllRowanClasses, exportRowanProject, loadRowanProject, unloadRowanProject, getGemCacheKB), all QueryExecutor-based and none importing vscode. An agent working through Jasper is package-blind: it can create a class but cannot say which Rowan package owns it, or whether the image has drifted from disk.

Proposed change: register a read-first subset — list_rowan_projects, find_class_package (findRowanClassOwners), diff_rowan_project — in both MCP files. Same wiring pattern as proposal 1; the formatters already produce agent-readable text. ROADMAP.md:84 names this explicitly: "As each theme ships, its operations should also land on the MCP/AI surface (session-admin tools, Rowan-audit tools, …)", and Rowan audit is a tracked theme (#311, ROADMAP.md:73).

Open question: whether write-side Rowan operations (load / unload / export) belong on the MCP surface at all, or whether an agent should be confined to reads and hand loads back to the user. Load and unload restructure a live image far more drastically than any single-class edit.

9. An agent driving Jasper starts from a GemStone-correct workflow instead of inventing one

What Genie does — stated precisely, because the earlier reading of this was too generous: no genie implements MCP prompts or resources. All three declare exactly one capability key, tools, with an empty object — VAST :1035-1036, Pharo :1474, GemStone genie.gs:582-583 — and no dispatcher has a prompts/* or resources/* branch (VAST :627-634, Pharo :783-791, GemStone genie.gs:569-575).

What Pharo ships instead is a client-side workflow layer: five .claude/skills/*/SKILL.md files (pharo-tdd, pharo-refactor, pharo-rename-method, representation-envy-refactor, pharo-live-image), a 96-line CLAUDE.md working contract, and a 226-line scripts/genie-init that scaffolds a user project with a .genie/project.ston ownership file so the agent is told which packages it owns before it starts. The rules are the interesting part: "Prefer specific MCP tools over eval"; "After every change, read the method or class back to confirm it installed" (agents/common/live-image.md:31); and pharo-refactor's step 1, "Green baseline. … If the code you're about to touch has NO test coverage, STOP and write characterization tests first" (SKILL.md:15-19). VAST's port deleted all of it.

So this proposal is ours, informed by the gap — not something to credit the genies with. They demonstrate that the workflow layer is real and load-bearing, and that nobody has put it in the protocol.

What Jasper has today: nothing agent-facing. Jasper ships .claude/skills/jasper-issue/SKILL.md, but that is for agents working on Jasper. CLAUDE.md is likewise contributor guidance for the monorepo. Neither tells an agent working through Jasper's MCP server anything about GemStone. No prompts or resources are registered: server.tool( appears 33 times in each MCP file, server.prompt( and server.resource( zero times, and docs/mcp-server.md (234 lines) mentions neither primitive.

Proposed change: three workflows that earn their place because they encode things GemStone gets wrong by default. gemstone-tdd — locate via list_classes/list_methods, red via compile_method + run_test_class, green, then list_failing_tests across the dictionary, then commit (gated, if proposal 6 lands). gemstone-safe-refactor — green baseline, impact map via find_senders/find_implementors/find_references_to, preview, one change, re-test. gemstone-transaction-hygienestatusrefresh → work → commit/abort, the loop that keeps a long agent turn from silently working against a stale read view; Jasper already has the refreshIfClean machinery (client/src/mcpTools.ts:29, mcp-server/src/tools.ts:127) but nothing tells the model when it matters.

Open question: MCP prompts, or a shipped skill file? Prompts travel with the server and work for any MCP client; a skill file is richer, versionable, and loads without the user picking it, but only helps Claude Code. Pharo chose the skill file, but it had no choice — it never implemented prompts. Jasper does have the choice, and could ship both from the same prose.

10. Tool failures come back as structured data the agent can branch on

What Genie does: every handler runs inside returnResultDo:, producing a uniform {success, result} / {success, error:{description, type, error_id}} envelope rendered as JSON with isError set from success. The type field carries the exception class name, so a model can distinguish a compile error from a missing class without parsing prose.

There is a sharp detail underneath. exposeDebugErrors defaults to false in both ports (VAST GenieSettings.class.st:42, GemStone genie.gs:328), so by default the agent receives 'Tool execution failed', an exception class name, and an error_id — the real messageText is withheld (GenieServer.class.st:1240-1250). With exactly one exception: run_test's per-test details entries are built on the success path and always carry sig messageText (GenieServer.class.st:200-207), bypassing the redaction. So in a default deployment, tests are the only self-diagnosable path an agent has: every other failure is an opaque id, and no log anywhere in either repo is keyed by that id. That is a strong argument for the proposal, and a decent explanation for why test-gated persistence became the family's de facto repair loop.

What Jasper has today: the shared wrap helper returns `Error: ${(err as Error).message}` with isError: true (client/src/mcpTools.ts:160; mcp-server/src/tools.ts inlines the same try/catch per tool). The message content is good — better than either genie's default, which redacts it — but it is unstructured text, so an agent must pattern-match English to decide whether to retry, repair, or give up.

Proposed change: have wrap emit a small JSON object — {error: {message, kind}} — with kind from a short enum (compileError, notFound, ambiguous, sessionBusy, other). Jasper already models exactly this distinction internally: resolveTestDictionary returns a tagged union of resolved / notFound / ambiguous (client/src/mcpTools.ts:45-48). One helper, two files. Whatever else changes, keep returning the real message — the redaction default is the family's worst call.

Open question: changing the error shape is visible to every existing agent transcript. Version it behind a flag, or just change it?


Already covered — no action

Genie capabilities Jasper already has, with the proof. Line numbers are client/src/mcpTools.ts.

Genie tool Jasper equivalent Note
eval execute_code :366 both block-wrap multi-statement code
define_class compile_class_definition :206
define_method compile_method :224 formatting is proposal 3
remove_class / remove_method delete_class :271 / delete_method :286 Jasper requires dictionaryName; Genie resolves globally
get_class_source describe_class :305, export_class_source :387 describe_class is one round trip for definition + comment + methods
get_method_source get_method_source :493
get_class_comment describe_class :305; write side set_class_comment :742 no genie has a comment setter
search_implementors find_implementors :406 Jasper caps at 500 and falls back env-0 → env-1
search_references find_senders :451
search_references_to_class find_references_to :429
search_methods_like search_method_source :728 Jasper searches source text, capped at 500
list_classes list_classes :527, list_all_classes :514
list_methods list_methods :611 Jasper groups by category
run_test run_test_class :667, run_test_method :697, list_failing_tests :565, describe_test_failure :327 Jasper is materially richer — glob selection, real exception text, single-test re-run with stack report
commit_transaction / save_image (ungated part) commit :191, abort :170, refresh :639, status :760 gating is proposal 6
list_namespaces list_dictionaries :539, list_dictionary_entries :549 resolution order comes free from System myUserProfile symbolList names, client/src/queries/getDictionaryNames.ts:7; list_dictionary_entries returns classes and non-class globals
create_namespace add_dictionary :180; remove_dictionary :656 no genie has a remove
Bearer auth, Origin check, loopback bind client/src/tlsCert.ts:1, client/src/mcpSocketServer.ts:1, client/src/mcpOwnerSidecar.ts:1 Jasper uses TLS + a local socket + cross-window ownership arbitration
GenieServer start/stop jasper.claimMcpServer package.json:631, jasperMcpServer view package.json:442

Namespaces specifically: the GemStone port's entire second commit was namespace support, and it is the one area where Jasper is already clearly ahead. Its own file admits the feature is half-wired — remove_method, remove_class and rename_class resolve through the namespace as an existence check, then hand the bare string name to Rowan, which re-resolves globally (genie.gs:1125, :1135, :1147 respectively). Jasper threads dictionaryName through the actual operation.


Deliberately not proposed

  • MCP resources for class source. Considered and dropped. No genie implements resources, so there is no evidence to borrow; describe_class (client/src/mcpTools.ts:305) already does definition + comment + methods in one round trip; and a full GemStone image has thousands of classes, so the enumeration story is bad. Revisit if a client materially rewards resource-shaped reads.
  • Moving Jasper's MCP server into the Gem. Not the ask, and the costs are real: a blocking single-connection accept loop, a socket that cannot be committed, and filing server code into a production image. Jasper's GCI-from-outside position is correct.
  • Hand-rolled HTTP and JSON. The GemStone port writes both by hand to avoid dependencies (genie.gs:66, :452); VAST uses NeoJSON and inherits HTTP from SstHttpServletEngine. Jasper uses the official MCP SDK. Nothing to learn.
  • allowDangerousTools defaulting to true (VAST GenieSettings.class.st:41, GemStone genie.gs:327) — the gating mechanism is good and proposal 7 borrows it, but the default makes hardening opt-in, so the safe configuration is the one nobody selects. If Jasper adds the mechanism, the interesting question is whether readOnly should default on for a freshly claimed server.
  • exposeDebugErrors defaulting to false — the more damaging default, discussed under proposal 10. Jasper already returns the real message; keep it that way.
  • apply_settings as a runtime tool (genie.gs:1237) — it cannot actually rebind a running server (the socket was bound at start, genie.gs:384-390), and it is itself a mutating tool, so an agent with write access can reach the switch that governs write access. Configuration belongs in VS Code settings.
  • create_configuration_map — VAST-only, and the GemStone port dropped it for the right reason: in Rowan the project is the configuration unit.
  • compile_method_from_file / save_to_file — already considered and rejected in TODO.md:50-59, on the grounds that fileInManager.ts auto-files-in .gs saves and a second write path would race the editor's save handler. export_package / import_package are the same idea and fall to the same argument, with the extra wrinkle that they operate on stone-host paths, not the client's.
  • Result-size discipline. GemStone's port truncates in exactly one place (search_methods_like, cap 2000, genie.gs:1013); VAST caps the same tool at 1000; get_class_source and list_methods emit everything in both. Jasper already caps consistently at 500.
  • UI-inspection tools. Both ports foreclose them explicitly and Jasper's IDE already owns that surface. The MCP question for Jasper's debugger and inspector is a separate one, not a genie-derived one.

If you only do one thing

Expose the refactoring engine as MCP tools, with the change-set guard in the same change (proposals 1 and 2). It is still first, and the deeper read makes the case sharper rather than softer.

The capability-per-diff is the largest on the list: 17 engine classes and 16 SUnit test classes already exist server-side, all 19 driver modules in client/src/refactoring/queries/ are already vscode-free and written against the exact QueryExecutor interface the MCP server was designed to supply (client/src/queries/types.ts:1-5), and the token-based preview/apply protocol gives you a dry run for free. It closes the single widest gap between what a Jasper user can do and what an agent driving Jasper can do. And it is the one thing the GemStone port's author wanted and could not have, for a reason that does not apply to Jasper.

VAST also supplies the cautionary tale, which is why the guard is not a follow-up. It shipped 28 refactorings with an explicit decision not to protect core classes, and reversed that decision fifty-eight minutes later after rename_method on a common selector tried to edition thousands of core sub-applications. The fix that stuck inspects the computed change set before applying anything. Jasper's engine already computes that change set and already hands it to the client as a preview; the guard is a filter over data you already have.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions