Skip to content

fix(cmd/gc): wire ConfigDir into the prompt render context - #13

Open
austinborn wants to merge 1 commit into
mainfrom
prompt-configdir-template-var
Open

fix(cmd/gc): wire ConfigDir into the prompt render context#13
austinborn wants to merge 1 commit into
mainfrom
prompt-configdir-template-var

Conversation

@austinborn

Copy link
Copy Markdown

Summary

{{ .ConfigDir }} rendered as an empty string in every agent prompt, so every pack-shipped helper script resolved to a bare /assets/scripts/<tool>.py (no directory prefix at all) and exited 127. This wires ConfigDir into the prompt render context, which is where it wasn't.

And the failure was silent. No error, nothing on stderr, just a path with its prefix quietly removed.

Which layer was wrong, and how that was established

So the prompts were right and the renderer was wrong. That's worth stating plainly, because the opposite conclusion was just as available at the start, and it would have meant editing 41 call sites instead of adding one map key.

ConfigDir is a real Gas City template variable, but it only ever lived in one scope (session setup). resolveTemplate builds a SessionSetupContext carrying ConfigDir and uses it to expand command, session_setup, pre_start and session_live. Prompts don't go through that path at all. They go through renderPrompt, whose data comes from PromptContext flattened by buildTemplateData into a map[string]string, and that map never had a ConfigDir key.

So the silence comes from those two facts together. The template executes with Option("missingkey=zero") over a map whose element type is string, so an absent key yields "" (not an error) and nothing downstream can tell the difference. Had the data been a struct, the same lookup would've blown up at execute time, and this probably would have been caught years ago.

flowchart TD
    A[agent config] --> B[resolveTemplate]
    B --> C[SessionSetupContext<br/>has ConfigDir]
    B --> D[PromptContext<br/>had no ConfigDir]
    C --> E[expandSessionSetup<br/>command, pre_start, session_live]
    D --> F[buildTemplateData<br/>map string to string]
    F --> G[template execute<br/>missingkey=zero]
    G --> H["{{ .ConfigDir }} renders empty<br/>path becomes /assets/scripts/tool.py<br/>exit 127"]
Loading

So the two scopes disagreed about a variable that names the same concept in both. This change makes them share one definition, so they can't drift apart by accident.

What changed

PromptContext gains a ConfigDir field and buildTemplateData publishes it (one map key, which is essentially the whole fix). A small promptConfigDir helper applies the fallback (the agent's config SourceDir, else the city root), and all four sites now call it: the three PromptContext construction sites in template_resolve.go, cmd_prime.go and cmd_lint.go, plus the existing session-setup path, which previously inlined the same two-line fallback. And sharing that helper is what stops the two scopes drifting apart again.

Verification

And the new TestRenderPromptConfigDir reproduces the production symptom exactly. With the fix removed it fails with:

renderPrompt(ConfigDir) = "/assets/scripts/slack.py send hi",
   want "/home/user/packs/local-core/assets/scripts/slack.py send hi"

That left-hand string is the reported bug character for character (not a paraphrase of it), so the test guards the regression rather than just exercising the new field. TestPromptConfigDir covers both fallback branches (the pack dir, and the city-root fallback).

End to end, against a real city, comparing the released binary with one built from this branch:

Binary Rendered slack.py path
gc 1.4.0 /assets/scripts/slack.py
this branch <pack-cache>/packs/local-core/assets/scripts/slack.py

The resolved file exists and runs (checked with a harmless --help, so nothing got posted anywhere). And across the planner, architect, reviewer and builder prompts, every {{ .ConfigDir }} call site now resolves, with none left rendering bare.

Test plan

  • go test ./cmd/gc/... passes.
  • gc prime <agent> renders a helper-script path with a real directory prefix.
  • That path exists on disk and executes.

Note for reviewers

Building this package on macOS needs ICU headers for a transitive CGO dependency, which fail on a stock machine on main as well as here. CGO_CFLAGS, CGO_CXXFLAGS and CGO_LDFLAGS pointed at $(brew --prefix icu4c) make it build. That's pre-existing and unrelated to this change.

go test ./cmd/gc/... is also red on this machine, and that's pre-existing too. It reports 37 failures, all in rig, store, Dolt and city-resolution tests (TestFindCity, TestResolveBdScopeTarget*, TestBuildStores* and friends), none of which touch prompt rendering. I checked rather than assumed: running that exact set of 37 test names against pristine main with this change absent fails on the same 37, so the regression count from this branch is zero. CI is the authority on a clean machine.

One thing this doesn't address: missingkey=zero will still silently swallow any other out-of-scope or misspelled variable in a prompt. This fix closes the specific hole, but the general hazard is worth a separate look.


Generated by the operator's software factory.
• City: factory-main · Agent: local-core.builder-3
• On behalf of: @austinborn

{{ .ConfigDir }} rendered as an empty string in every agent prompt, so a
pack-shipped helper script referenced as {{ .ConfigDir }}/assets/scripts/
<tool>.py resolved to a bare /assets/scripts/<tool>.py and exited 127.

ConfigDir was only ever populated in session-setup scope, where
SessionSetupContext carries it for command, session_setup, pre_start and
session_live expansion. Prompts take a different path: renderPrompt reads
PromptContext, flattened by buildTemplateData into a map[string]string
that never had a ConfigDir key. Because the template executes with
Option("missingkey=zero") over a map whose element type is string, the
absent key yielded "" instead of an execute-time error, which is why the
breakage stayed invisible.

Add ConfigDir to PromptContext, publish it from buildTemplateData, and
resolve it through a shared promptConfigDir helper (the agent's config
SourceDir, else the city root). All three PromptContext construction
sites and the existing session-setup path now call that helper, so the
two scopes cannot drift apart again.

TestRenderPromptConfigDir guards the regression: with the fix removed it
renders "/assets/scripts/slack.py send hi", the reported symptom exactly.

Generated by the operator's software factory.
City: factory-main · Agent: local-core.builder-3
On behalf of: @austinborn
Co-Authored-By: operator-factory-bot <factory-bot@operator-domain.invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant