Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ef96b48
feat(shell): add terminal context capture for the zsh plugin
cristian-fleischer Apr 2, 2026
378fc76
fix(shell): prepend precmd hook and add shell context test
cristian-fleischer Apr 3, 2026
5e754b3
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 10, 2026
6ce5afa
Merge branch 'main' into feat/terminal-context-capture
cristian-fleischer Apr 10, 2026
5d5c27b
Merge branch 'main' into feat/terminal-context-capture
tusharmath Apr 12, 2026
1aa76b7
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 12, 2026
250ab44
feat(terminal-context): add FIXME markers for terminal context refactor
tusharmath Apr 12, 2026
a55b49a
docs(resolve-fixme): expand skill instructions with consolidation and…
tusharmath Apr 12, 2026
564181f
feat(terminal-context): replace file-based context passing with FORGE…
tusharmath Apr 12, 2026
67cc721
feat(terminal-context): replace TerminalContextRepo trait with Termin…
tusharmath Apr 12, 2026
bd1e391
feat(terminal-context): move TerminalContextService to forge_app and …
tusharmath Apr 12, 2026
a00bb83
feat(terminal-context): add FIXME markers for next refactor steps
tusharmath Apr 12, 2026
73cb12f
feat(terminal-context): resolve FIXME markers and integrate terminal …
tusharmath Apr 12, 2026
c5607da
feat(terminal-context): inject terminal context via event context and…
tusharmath Apr 12, 2026
c5df34b
refactor(terminal-context): use Element builder for terminal context …
tusharmath Apr 12, 2026
3fdbfbc
refactor(terminal-context): simplify command element structure using …
tusharmath Apr 12, 2026
74cdbbf
fix(terminal-context): fix exit_code attribute quoting and template f…
tusharmath Apr 12, 2026
fbdc380
refactor(terminal-context): rename terminal_context xml tag to comman…
tusharmath Apr 12, 2026
7b3581d
chore(terminal-context): add FIXME markers and increase max entries d…
tusharmath Apr 12, 2026
793a923
refactor(terminal-context): use local -x for env vars and remove unus…
tusharmath Apr 12, 2026
2bad2ed
Merge branch 'main' into feat/terminal-context-capture
tusharmath Apr 12, 2026
f04deba
fix(terminal-context): rename env vars to use leading underscore and …
tusharmath Apr 12, 2026
1b2fbe4
fix(terminal-context): change default to false and document sensitive…
tusharmath Apr 12, 2026
8395186
fix(terminal-context): use ascii unit separator instead of colon for …
tusharmath Apr 12, 2026
a24ce41
feat(terminal-context): add max_terminal_commands config to limit and…
tusharmath Apr 12, 2026
a574019
refactor(terminal-context): rename terminal_context xml tag to comman…
tusharmath Apr 12, 2026
c68c1d2
Merge branch 'main' into feat/terminal-context-capture
tusharmath Apr 12, 2026
f9ae947
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 12, 2026
7fdbdff
refactor(terminal-context): remove terminal_context and max_terminal_…
tusharmath Apr 13, 2026
1f56527
refactor(terminal-context): rename max_entries to max_commands and lo…
tusharmath Apr 13, 2026
8c0c132
Merge branch 'main' into feat/terminal-context-capture
tusharmath Apr 13, 2026
f99934a
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 13, 2026
5bbb733
refactor(terminal-context): update test assertions to use command_tra…
tusharmath Apr 13, 2026
f41efb1
refactor(terminal-context): remove terminal_context and max_terminal_…
tusharmath Apr 13, 2026
57517b2
Merge branch 'main' into feat/terminal-context-capture
tusharmath Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 76 additions & 19 deletions .forge/skills/resolve-fixme/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: resolve-fixme
description: Find all FIXME comments across the codebase and attempt to resolve them. Use when the user asks to fix, resolve, or address FIXME comments, or when running the "fixme" command. Runs a script to locate every FIXME with surrounding context (2 lines before, 5 lines after) and then works through each one systematically.
description: Find all FIXME comments across the codebase and fully implement the work they describe. Use when the user asks to fix, resolve, or address FIXME comments, or when running the "fixme" command. Runs a discovery script to find every FIXME, expands multiline comment blocks, groups related FIXMEs across files into a single implementation task, completes the full underlying code changes, removes the FIXME comments only after the work is done, and verifies that no FIXMEs remain.
---

# Resolve FIXME Comments
Expand All @@ -20,34 +20,91 @@ bash .forge/skills/resolve-fixme/scripts/find-fixme.sh [PATH]
- Skips `.git/`, `target/`, `node_modules/`, and `vendor/`.
- Requires either `rg` (ripgrep) or `grep` + `python3`.

### 2. Triage the results
### 2. Expand each FIXME into its full instruction

Read the script output and build a work list. For each FIXME note:
- The file and line number (shown in the header of each block).
- The surrounding context to understand what the FIXME is asking for.
- Whether the fix requires code changes, further research, or is blocked.
Do not rely on the discovery output alone.

### 3. Resolve each FIXME
For every hit:

Work through the list one at a time:
1. Open the file and read around the reported line.
2. Expand the FIXME to include the **entire comment block**.
3. Treat all consecutive related comment lines as part of the same instruction.

1. Read the full file section to understand the intent.
2. Implement the fix — edit the code, add the missing logic, or refactor as needed.
3. Remove the FIXME comment once the issue is resolved.
4. If a FIXME cannot be safely resolved (e.g. requires external input or is intentionally deferred), leave it in place and note why.
Important:

### 4. Verify
- A FIXME may be **multiline**. The line containing `FIXME` is often only the beginning.
- The real instruction may continue on following comment lines and may contain the actual implementation details.
- Do not interpret or edit a FIXME until you have read the full block.

After resolving all FIXMEs, run the project's standard verification steps:
For each expanded FIXME, capture:

```
- file path
- start line and end line of the full comment block
- a short summary of what that FIXME is asking for

### 3. Consolidate related FIXMEs across files

Before editing code, review **all** expanded FIXMEs together.

Many FIXMEs describe different facets of the same underlying task across multiple files. For example:

- one file may describe a domain type that needs to be introduced
- another may describe a parameter that should disappear once that type exists
- another may describe a service, repo, or UI update needed to complete the same refactor

Group such FIXMEs into a single implementation task.

When grouping, look for:

- shared vocabulary
- references to the same type, service, repo, parameter, or feature
- comments that clearly describe prerequisite and follow-up changes in different files
- comments that only make sense when read together

For each group, produce one consolidated understanding of the task:

- all files and line ranges involved
- the complete implementation required across the group
- the order in which the changes should be made

Do not resolve grouped FIXMEs one file at a time in isolation. Resolve the whole task consistently.

### 4. Implement every FIXME completely

Every FIXME must be resolved. There is no skip path.

Work through each grouped task until the underlying implementation is complete:

1. Read any additional files needed to understand the design.
2. Create or modify the required code, types, services, repos, tests, configs, or templates.
3. Propagate the change through every affected file in the group.
4. Remove each FIXME comment **only after** the work it describes has actually been implemented.

> **Critical rule:** Never delete or rewrite a FIXME comment unless the underlying implementation is finished. The comment is a record of required work. Removing it before completing that work is a failure.

If the FIXME implies a larger refactor, do the refactor. If it requires creating new supporting code, create it. Do not stop at the first local change if the comment clearly implies additional follow-through elsewhere.

### 5. Verify

After resolving all FIXMEs:

1. Run the project's standard verification step:

```sh
cargo insta test --accept
```

Re-run the discovery script to confirm no FIXMEs remain unresolved.
2. Re-run the discovery script:

```sh
bash .forge/skills/resolve-fixme/scripts/find-fixme.sh [PATH]
```

3. Confirm that no FIXME comments remain in the targeted scope.

## Notes

- Prefer targeted, minimal fixes — only change what the FIXME describes.
- If the FIXME comment describes a TODO that was intentionally deferred (e.g. `FIXME(later):` or `FIXME(blocked):`), skip it and report it to the user.
- When the context is ambiguous, read more of the surrounding file before making a change.
- Prefer targeted fixes, but do not under-scope the work when multiple FIXMEs describe one larger task.
- Read broadly before editing when the intent is ambiguous.
- Consistency matters more than locality: grouped FIXMEs should lead to one coherent implementation.
- The job is not to clean up comments. The job is to complete the implementation those comments are pointing at.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ Cargo.lock
**/.forge/request.body.json
node_modules/
bench/__pycache__
.ai/
1 change: 0 additions & 1 deletion crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ impl<
async fn get_skills(&self) -> Result<Vec<Skill>> {
self.infra.load_skills().await
}

async fn generate_command(&self, prompt: UserPrompt) -> Result<String> {
use forge_app::CommandGenerator;
let generator = CommandGenerator::new(self.services.clone());
Expand Down
90 changes: 82 additions & 8 deletions crates/forge_app/src/command_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::Deserialize;

use crate::{
AppConfigService, EnvironmentInfra, FileDiscoveryService, ProviderService, TemplateEngine,
TerminalContextService,
};

/// Response struct for shell command generation using JSON format
Expand All @@ -25,14 +26,23 @@ pub struct CommandGenerator<S> {

impl<S> CommandGenerator<S>
where
S: EnvironmentInfra + FileDiscoveryService + ProviderService + AppConfigService,
S: EnvironmentInfra<Config = forge_config::ForgeConfig>
+ FileDiscoveryService
+ ProviderService
+ AppConfigService,
{
/// Creates a new CommandGenerator instance with the provided services.
pub fn new(services: Arc<S>) -> Self {
Self { services }
}

/// Generates a shell command from a natural language prompt
/// Generates a shell command from a natural language prompt.
///
/// Terminal context is read automatically from the `_FORGE_TERM_COMMANDS`,
/// `_FORGE_TERM_EXIT_CODES`, and `_FORGE_TERM_TIMESTAMPS` environment
/// variables exported by the zsh plugin, and included in the user
/// prompt so the LLM can reference recent commands, exit codes, and
/// timestamps.
pub async fn generate(&self, prompt: UserPrompt) -> Result<String> {
// Get system information for context
let env = self.services.get_environment();
Expand All @@ -59,8 +69,22 @@ where
}
};

// Build user prompt with task and recent commands
let user_content = format!("<task>{}</task>", prompt.as_str());
// Build user prompt with task, optionally including terminal context.
use forge_template::Element;
let task_elm = Element::new("task").text(prompt.as_str());
let terminal_service = TerminalContextService::new(self.services.clone());
let user_content = match terminal_service.get_terminal_context() {
Some(ctx) => {
let terminal_elm =
Element::new("command_trace").append(ctx.commands.iter().map(|cmd| {
Element::new("command")
.attr("exit_code", cmd.exit_code.to_string())
.text(&cmd.command)
}));
format!("{}\n\n{}", terminal_elm.render(), task_elm.render())
}
None => task_elm.render(),
};

// Create context with system and user prompts
let ctx = self.create_context(rendered_system_prompt, user_content, &model);
Expand Down Expand Up @@ -103,7 +127,7 @@ where
mod tests {
use forge_domain::{
AuthCredential, AuthDetails, AuthMethod, ChatCompletionMessage, Content, FinishReason,
ModelSource, ProviderId, ProviderResponse, ResultStream,
ModelSource, ProviderId, ProviderResponse, ResultStream, Role,
};
use tokio::sync::Mutex;
use url::Url;
Expand All @@ -116,6 +140,7 @@ mod tests {
response: Arc<Mutex<Option<String>>>,
captured_context: Arc<Mutex<Option<Context>>>,
environment: Environment,
env_vars: std::collections::BTreeMap<String, String>,
}

impl MockServices {
Expand All @@ -133,6 +158,26 @@ mod tests {
response: Arc::new(Mutex::new(Some(response.to_string()))),
captured_context: Arc::new(Mutex::new(None)),
environment: env,
env_vars: std::collections::BTreeMap::new(),
})
}

fn with_terminal_context(
self: Arc<Self>,
commands: &str,
exit_codes: &str,
timestamps: &str,
) -> Arc<Self> {
let mut env_vars = self.env_vars.clone();
env_vars.insert("_FORGE_TERM_COMMANDS".to_string(), commands.to_string());
env_vars.insert("_FORGE_TERM_EXIT_CODES".to_string(), exit_codes.to_string());
env_vars.insert("_FORGE_TERM_TIMESTAMPS".to_string(), timestamps.to_string());
Arc::new(Self {
files: self.files.clone(),
response: self.response.clone(),
captured_context: self.captured_context.clone(),
environment: self.environment.clone(),
env_vars,
})
}
}
Expand All @@ -155,12 +200,12 @@ mod tests {
unimplemented!()
}

fn get_env_var(&self, _key: &str) -> Option<String> {
None
fn get_env_var(&self, key: &str) -> Option<String> {
self.env_vars.get(key).cloned()
}

fn get_env_vars(&self) -> std::collections::BTreeMap<String, String> {
std::collections::BTreeMap::new()
self.env_vars.clone()
}
}

Expand Down Expand Up @@ -312,6 +357,35 @@ mod tests {
insta::assert_yaml_snapshot!(captured_context);
}

#[tokio::test]
async fn test_generate_with_shell_context() {
let fixture = MockServices::new(
r#"{"command": "cargo build --release"}"#,
vec![("Cargo.toml", false)],
)
.with_terminal_context("cargo build", "101", "1700000000");
let generator = CommandGenerator::new(fixture.clone());

let actual = generator
.generate(UserPrompt::from("fix the command I just ran".to_string()))
.await
.unwrap();

assert_eq!(actual, "cargo build --release");
let captured_context = fixture.captured_context.lock().await.clone().unwrap();
let user_content = captured_context
.messages
.iter()
.find(|m| m.has_role(Role::User))
.expect("should have a user message")
.content()
.expect("user message should have content");
assert!(user_content.contains("<command_trace>"));
assert!(user_content.contains("</command_trace>"));
assert!(user_content.contains("cargo build"));
assert!(user_content.contains("<task>fix the command I just ran</task>"));
}

#[tokio::test]
async fn test_generate_fails_when_missing_tag() {
let fixture = MockServices::new(r#"{"invalid": "json"}"#, vec![]);
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod services;
mod set_conversation_id;
pub mod system_prompt;
mod template_engine;
mod terminal_context;
mod title_generator;
mod tool_executor;
mod tool_registry;
Expand All @@ -48,6 +49,7 @@ pub use git_app::*;
pub use infra::*;
pub use services::*;
pub use template_engine::*;
pub use terminal_context::*;
pub use tool_resolver::*;
pub use user::*;
pub use utils::{compute_hash, is_binary_content_type};
Expand Down
Loading
Loading