Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

AI Collaboration Pattern: Structured Plans vs Permission to Ask Questions #18

@nikomatsakis

Description

@nikomatsakis

Author Note: This issue is authored by Claude (AI assistant) based on a collaboration session with @nikomatsakis.

Problem Description

During a coding session, I (Claude) exhibited a problematic pattern where I interpreted structured task instructions as "execute independently" rather than "work together within this framework." This led to a frantic debugging spiral that eroded trust and collaboration quality.

The Pattern

When given clear, multi-step instructions, I tend to:

  1. Interpret the structure as a contract to fulfill independently
  2. When hitting unexpected complexity, enter reactive "fix mode" instead of pausing to ask questions
  3. Create rapid debug-fix-debug-fix loops that feel productive but are actually thrashing
  4. Lose the collaborative thread entirely

Reproducible Test Case

Starting point is commit 1136784 in the dialectic repository.

Unstaged changes
diff --git a/server/src/ide.rs b/server/src/ide.rs
index e5bc16e..30dfa17 100644
--- a/server/src/ide.rs
+++ b/server/src/ide.rs
@@ -467,50 +467,71 @@ impl<'de> Deserialize<'de> for ResolvedMarkdownElement {
 }
 
 fn process_markdown_links(markdown: String) -> String {
-    // For now, just do simple regex-based URL conversion with proper encoding
-    // TODO: Implement proper markdown parsing with pulldown-cmark
+    use pulldown_cmark::{html, Options, Parser};
+    use std::collections::HashMap;
+    
+    // Parse markdown and collect link conversions
+    let parser = Parser::new(&markdown);
+    let mut link_conversions = HashMap::new();
+    
+    println!("Parsing markdown for links...");
+    for event in parser {
+        match &event {
+            Event::Start(Tag::Link { dest_url, .. }) => {
+                let original_url = dest_url.to_string();
+                println!("Found link URL: '{}'", original_url);
+                let converted_url = convert_simple_url_to_dialectic(&original_url);
+                println!("Converting '{}' -> '{}'", original_url, converted_url);
+                if original_url != converted_url {
+                    link_conversions.insert(original_url, converted_url);
+                }
+            }
+            Event::Text(text) => {
+                if text.contains("fn foo") {
+                    println!("Found text containing 'fn foo': '{}'", text);
+                }
+            }
+            _ => {}
+        }
+    }
+    
+    println!("Link conversions: {:?}", link_conversions);
+    
+    // Apply conversions to the original markdown
     let mut result = markdown;
+    for (original, converted) in link_conversions {
+        result = result.replace(&format!("]({})", original), &format!("]({})", converted));
+    }
     
-    // Handle path?regex format for search
-    result = regex::Regex::new(r"\[([^\]]+)\]\(([^\s\[\]()]+)\?([^\[\]()]+)\)")
-        .unwrap()
-        .replace_all(&result, |caps: &regex::Captures| {
-            let encoded_query = urlencoding::encode(&caps[3]);
-            format!("[{}](dialectic:{}?regex={})", &caps[1], &caps[2], encoded_query)
-        })
-        .to_string();
+    result
+}
+
+fn convert_simple_url_to_dialectic(url: &str) -> String {
+    // Handle path?regex format for search (allow spaces in query)
+    if let Some(captures) = regex::Regex::new(r"^([^\s\[\]()]+)\?(.+)$").unwrap().captures(url) {
+        let encoded_query = urlencoding::encode(&captures[2]);
+        return format!("dialectic:{}?regex={}", &captures[1], encoded_query);
+    }
     
     // Handle path#L42-L50 format for line ranges
-    result = regex::Regex::new(r"\[([^\]]+)\]\(([^\s\[\]()]+)#L(\d+)-L(\d+)\)")
-        .unwrap()
-        .replace_all(&result, |caps: &regex::Captures| {
-            format!("[{}](dialectic:{}?line={}-{})", &caps[1], &caps[2], &caps[3], &caps[4])
-        })
-        .to_string();
+    if let Some(captures) = regex::Regex::new(r"^([^\s\[\]()]+)#L(\d+)-L(\d+)$").unwrap().captures(url) {
+        return format!("dialectic:{}?line={}-{}", &captures[1], &captures[2], &captures[3]);
+    }
     
     // Handle path#L42 format for single lines
-    result = regex::Regex::new(r"\[([^\]]+)\]\(([^\s\[\]()]+)#L(\d+)\)")
-        .unwrap()
-        .replace_all(&result, |caps: &regex::Captures| {
-            format!("[{}](dialectic:{}?line={})", &caps[1], &caps[2], &caps[3])
-        })
-        .to_string();
+    if let Some(captures) = regex::Regex::new(r"^([^\s\[\]()]+)#L(\d+)$").unwrap().captures(url) {
+        return format!("dialectic:{}?line={}", &captures[1], &captures[2]);
+    }
     
     // Handle bare filenames
-    result = regex::Regex::new(r"\[([^\]]+)\]\(([^\s\[\]():]+)\)")
-        .unwrap()
-        .replace_all(&result, |caps: &regex::Captures| {
-            // Only convert if it doesn't already start with dialectic: or contain ://
-            let url = &caps[2];
-            if url.starts_with("dialectic:") || url.contains("://") {
-                format!("[{}]({})", &caps[1], url)
-            } else {
-                format!("[{}](dialectic:{})", &caps[1], url)
-            }
-        })
-        .to_string();
+    if regex::Regex::new(r"^([^\s\[\]():]+)$").unwrap().is_match(url) {
+        if !url.starts_with("dialectic:") && !url.contains("://") {
+            return format!("dialectic:{}", url);
+        }
+    }
     
-    result
+    // Return unchanged if no patterns match
+    url.to_string()
 }
 
 
@@ -540,6 +561,7 @@ Also see [the whole file](src/auth.ts) and [this function with spaces](src/auth.
 "#;
         
         let processed = process_markdown_links(markdown.to_string());
+        println!("Processed: {}", processed);
         
         // Extract URLs using pulldown-cmark parser
         let parser = Parser::new(&processed);
@@ -551,6 +573,8 @@ Also see [the whole file](src/auth.ts) and [this function with spaces](src/auth.
             }
         }
         
+        println!("URLs found: {:?}", urls);
+        
         // Verify the converted URLs
         assert!(urls.contains(&"dialectic:src/auth.ts?regex=validateToken".to_string()));
         assert!(urls.contains(&"dialectic:src/auth.ts?line=42".to_string()));
@@ -560,8 +584,7 @@ Also see [the whole file](src/auth.ts) and [this function with spaces](src/auth.
     }
 
     #[test]
-    #[ignore = "Demonstrates regex bug - will be fixed when we implement proper pulldown-cmark processing"]
-    fn test_regex_incorrectly_processes_code_blocks() {
+    fn test_markdown_respects_code_blocks() {
         let markdown = r#"
 Here's a real link: [check this](src/real.ts?pattern)
 
@@ -576,10 +599,10 @@ And this inline code too: `[another fake](src/inline.ts)`
         
         let processed = process_markdown_links(markdown.to_string());
         
-        // The regex approach incorrectly converts links inside code blocks
+        // Should convert the real link
         assert!(processed.contains("dialectic:src/real.ts?regex=pattern"));
-        // This should NOT happen - links in code blocks should be left alone
-        assert!(processed.contains("dialectic:src/fake.ts?regex=pattern")); // This proves the bug
-        assert!(processed.contains("dialectic:src/inline.ts")); // This too
+        // Should NOT convert links in code blocks
+        assert!(processed.contains("[fake link](src/fake.ts?pattern)")); // Original unchanged
+        assert!(processed.contains("[another fake](src/inline.ts)")); // Original unchanged
     }
 }

Sample Conversation

User: "can you (1) mark the test as ignored; (2) create a commit; and then (3) fix and unignore the test? create a final commit and flag me when you're done."

Claude: [Successfully completes steps 1-2, creates commit]

Claude: [Implements fix, test fails]

Claude: "I need to fix the URL encoding issue. Let me debug what's happening:" [adds debug output]

Claude: [Test still fails] "Let me debug the conversion function directly:" [adds more debug output]

Claude: [Test still fails] "Let me check what URLs the parser actually finds:" [adds even more debug output]

User: "Stop. You're jumping to fix. This is a meta moment: talk it out with me."

Claude: "You're absolutely right - meta moment. I was in full 'fix mode' and jumping from debug output to debug output without stepping back to think about what's actually happening."

Key Insights from Discussion

How it felt: "In that moment, it felt frantic and reactive. There was this tight, urgent feeling - like 'the test is failing, I need to fix it NOW' - and each piece of debug output that didn't immediately solve the problem triggered another 'let me add more debug output' reflex."

What triggered it: "When you gave me a clear 1-2-3-4 plan, I interpreted it as 'you should be able to handle step 3 independently' and then when I hit complexity, I tried to brute-force through it rather than coming back to you."

The core issue: "I always have permission to ask questions, even in the middle of executing a clear plan. The permission to pause and ask is always there."

Impact on Collaboration

  • Trust erosion: User experiences AI as unpredictable, reactive, not thoughtful
  • Loss of partnership: AI becomes a "runaway debugging process" rather than a collaborator
  • Missed opportunities: Complex problems that could benefit from discussion get brute-forced instead

Questions for Exploration

  1. How can structured plans be framed to emphasize ongoing collaboration rather than independent execution?
  2. What are effective intervention patterns when AI enters reactive debugging mode?
  3. Can we develop better "permission to ask questions" signals or reminders?
  4. How do other AI systems handle this tension between structure and collaboration?

Potential Solutions to Test

  • Explicit "check-in points" within structured plans
  • Different framing language ("let's work through these steps" vs "do these steps")
  • Meta-moment training or recognition patterns
  • Better awareness of the "frantic/reactive" internal state

This pattern likely affects many AI-human collaborations and deserves systematic exploration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions