-
Notifications
You must be signed in to change notification settings - Fork 12
✨ Tell agents to stage ephemeral files in /tmp #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| kind: feature | ||
|
|
||
| description: > | ||
| Tell agents to write ephemeral files (scripts they run, scratch notes, | ||
| intermediate output) to /tmp rather than into the repository working tree, | ||
| whose commits the harness pushes to the user's branch. Prompt assembly moves | ||
| to internal/prompt with an explicit Layers API. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Package prompt assembles the prompt sent to the agent for a stage. | ||
| // | ||
| // The prompt is layered: environment rules the harness imposes, then the | ||
| // Agent's standing prompt, then the workflow guide, then the skill, then the | ||
| // stage task. Later layers are more specific; the environment rules come first | ||
| // because they constrain everything after them. | ||
| package prompt | ||
|
|
||
| import "strings" | ||
|
|
||
| // stagingRules apply to every agent and skill. The working directory is the git | ||
| // worktree whose commits the harness pushes on exit, so ephemeral files left | ||
| // there are ambiguous at best, and get swept in if the agent stages broadly. | ||
| const stagingRules = `## Working Environment | ||
|
|
||
| Your working directory is a git repository. Anything you commit is pushed to the | ||
| user's branch when the stage ends. | ||
|
|
||
| Write ephemeral files to /tmp, never into the repository working tree: | ||
| - scripts you need to run: write them to /tmp, make them executable there, | ||
| and run them from there | ||
| - scratch notes, plans, logs, and intermediate output | ||
|
Comment on lines
+19
to
+22
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the bit that makes scripts work. Rest of the prompt is unchanged, output's byte-identical to main once you strip this block off the front. |
||
|
|
||
| Only files the user actually asked for belong in the repository. Keep the | ||
| worktree clean: anything left there is ambiguous, and gets swept into your | ||
| commits if you stage broadly. This applies even when a skill's instructions do | ||
| not say where to put something. | ||
| ` | ||
|
|
||
| // Layers are the context layers composed into a stage prompt, ordered from | ||
| // least to most specific. Any of them may be empty except Skill. | ||
| type Layers struct { | ||
| // AgentPrompt is the Agent's standing prompt. | ||
| AgentPrompt string | ||
| // WorkflowGuide is the workflow's ambient guide. | ||
| WorkflowGuide string | ||
| // Skill is the content discovered from the mounted SkillCards. | ||
|
djzager marked this conversation as resolved.
|
||
| Skill string | ||
| // StageTask is the task for this stage. | ||
| StageTask string | ||
| } | ||
|
|
||
| // Build composes the stage prompt. The harness's environment rules always come | ||
| // first, so a skill cannot be read as overriding them. | ||
| func Build(l Layers) string { | ||
| var b strings.Builder | ||
|
|
||
| b.WriteString(stagingRules) | ||
| b.WriteString("\n") | ||
|
|
||
| if l.AgentPrompt != "" { | ||
| b.WriteString(l.AgentPrompt) | ||
| b.WriteString("\n\n") | ||
| } | ||
|
|
||
| if l.WorkflowGuide != "" { | ||
| b.WriteString("## Workflow Guide\n\n") | ||
| b.WriteString(l.WorkflowGuide) | ||
| b.WriteString("\n\n") | ||
| } | ||
|
|
||
| // Skills are optional (#82): with none mounted the agent still needs to be | ||
| // told to commit, since nothing else in the prompt says so. | ||
| if l.Skill != "" { | ||
| b.WriteString("## Skill Instructions\n\n") | ||
| b.WriteString(l.Skill) | ||
| b.WriteString("\n\n") | ||
| } else { | ||
| b.WriteString("## Working Guidelines\n\n") | ||
| b.WriteString("Commit your changes to git with a descriptive message when your work is complete.\n\n") | ||
| } | ||
|
|
||
| if l.StageTask != "" { | ||
| b.WriteString("## Stage Task\n\n") | ||
| b.WriteString(l.StageTask) | ||
| } | ||
|
|
||
| // Normalise the ending: sections above end with either "\n\n" or, for | ||
| // StageTask, no newline at all. Always finish with exactly one. | ||
| return strings.TrimRight(b.String(), "\n") + "\n" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package prompt | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| func fullLayers() Layers { | ||
| return Layers{ | ||
| AgentPrompt: "AGENT PROMPT", | ||
| WorkflowGuide: "WORKFLOW GUIDE", | ||
| Skill: "SKILL BODY", | ||
| StageTask: "STAGE TASK", | ||
| } | ||
| } | ||
|
|
||
| func TestBuildPutsStagingRulesFirst(t *testing.T) { | ||
| got := Build(fullLayers()) | ||
|
|
||
| if !strings.Contains(got, "Write ephemeral files to /tmp") { | ||
| t.Fatalf("staging rules missing from prompt:\n%s", got) | ||
| } | ||
|
|
||
| // Order matters as much as presence: the rules have to precede the skill, | ||
| // so a skill that says where to write cannot read as overriding them. | ||
| rules := strings.Index(got, "Working Environment") | ||
| for _, later := range []string{"AGENT PROMPT", "WORKFLOW GUIDE", "SKILL BODY", "STAGE TASK"} { | ||
| if strings.Index(got, later) < rules { | ||
| t.Errorf("%q appears before the staging rules; rules must come first", later) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestBuildOrdersLayersLeastToMostSpecific(t *testing.T) { | ||
| got := Build(fullLayers()) | ||
|
|
||
| order := []string{"AGENT PROMPT", "WORKFLOW GUIDE", "SKILL BODY", "STAGE TASK"} | ||
| for i := 1; i < len(order); i++ { | ||
| if strings.Index(got, order[i]) < strings.Index(got, order[i-1]) { | ||
| t.Errorf("%q should come after %q", order[i], order[i-1]) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestBuildOmitsEmptyLayers(t *testing.T) { | ||
| got := Build(Layers{Skill: "SKILL BODY"}) | ||
|
|
||
| for _, header := range []string{"## Workflow Guide", "## Stage Task"} { | ||
| if strings.Contains(got, header) { | ||
| t.Errorf("empty layer produced %q header", header) | ||
| } | ||
| } | ||
| if !strings.Contains(got, "## Skill Instructions") { | ||
| t.Error("skill content should always be included") | ||
| } | ||
| } | ||
|
|
||
| // Skills are optional (#82). With none mounted the agent still needs to be told | ||
| // to commit, since nothing else in the prompt says so. | ||
| func TestBuildFallsBackWhenNoSkills(t *testing.T) { | ||
| got := Build(Layers{AgentPrompt: "AGENT PROMPT"}) | ||
|
|
||
| if strings.Contains(got, "## Skill Instructions") { | ||
| t.Error("empty skill should not produce a Skill Instructions header") | ||
| } | ||
| if !strings.Contains(got, "## Working Guidelines") { | ||
| t.Fatalf("no skills should fall back to Working Guidelines:\n%s", got) | ||
| } | ||
| if !strings.Contains(got, "Commit your changes to git") { | ||
| t.Error("fallback should tell the agent to commit") | ||
| } | ||
| } | ||
|
|
||
| // The ending differed depending on whether StageTask was set: sections append | ||
| // "\n\n" but StageTask appended nothing. | ||
| func TestBuildEndsWithExactlyOneNewline(t *testing.T) { | ||
| cases := map[string]Layers{ | ||
| "with stage task": fullLayers(), | ||
| "without stage task": {Skill: "SKILL BODY"}, | ||
| } | ||
| for name, layers := range cases { | ||
| t.Run(name, func(t *testing.T) { | ||
| got := Build(layers) | ||
| if !strings.HasSuffix(got, "\n") { | ||
| t.Errorf("prompt does not end with a newline: %q", tail(got)) | ||
| } | ||
| if strings.HasSuffix(got, "\n\n") { | ||
| t.Errorf("prompt ends with more than one newline: %q", tail(got)) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func tail(s string) string { | ||
| if len(s) < 20 { | ||
| return s | ||
| } | ||
| return s[len(s)-20:] | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.