feat: RFC 0035 spike resolved lifecycle suffix

Add 'resolved' outcome for spikes where a fix is applied during investigation.
Requires fix_summary parameter describing what was fixed. File renames to .resolved.md.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Garcia 2026-01-26 21:56:55 -05:00
parent e03499effc
commit 8eefd33085
7 changed files with 363 additions and 5 deletions

View file

@ -0,0 +1,202 @@
# Alignment Dialogue: Spike Resolved Lifecycle
**Draft**: Dialogue 2044
**Date**: 2026-01-26 21:28Z
**Status**: Converged
**Participants**: 💙 Judge, 🧁 Muffin, 🧁 Cupcake, 🧁 Scone
## Expert Panel
| Agent | Role | Tier | Relevance | Emoji |
|-------|------|------|-----------|-------|
| 💙 Judge | Orchestrator | — | — | 💙 |
| 🧁 Muffin | Systems Thinker | Core | 0.95 | 🧁 |
| 🧁 Cupcake | Domain Expert | Adjacent | 0.70 | 🧁 |
| 🧁 Scone | Devil's Advocate | Wildcard | 0.40 | 🧁 |
## Alignment Scoreboard
| Agent | Wisdom | Consistency | Truth | Relationships | **Total** |
|-------|--------|-------------|-------|---------------|----------|
| 🧁 Muffin | 9 | 8 | 9 | 8 | **34** |
| 🧁 Cupcake | 9 | 8 | 9 | 8 | **34** |
| 🧁 Scone | 10 | 8 | 10 | 8 | **36** |
**Total ALIGNMENT**: 104
**Current Round**: 2
**ALIGNMENT Velocity**: +35 (from 69)
## Perspectives Inventory
| ID | Agent | Perspective | Round |
|----|-------|-------------|-------|
| P01-M | 🧁 Muffin | Resolved vs Complete — semantic distinction matters. `.done` = investigation finished; `.resolved` = fixed during spike | 0 |
| P01-C | 🧁 Cupcake | Spike-and-fix workflow deserves distinct lifecycle state — outcomes describe what you learned, not whether the issue is closed | 0 |
| P01-S | 🧁 Scone | "Resolved" conflates investigation with implementation — if you fixed it, it wasn't really a spike | 0 |
| P02-S | 🧁 Scone | Metadata tells the story better than status — keep `.done`, add `applied_fix` and `fix_summary` fields | 0 |
| P03-M | 🧁 Muffin | Two distinct patterns: investigative spike (needs RFC) vs diagnostic spike (trivial fix applied immediately) | 1 |
| P04-C | 🧁 Cupcake | "Resolved" is outcome not status — extend SpikeOutcome enum, keep `.done` suffix, capture metadata | 1 |
| P02-S | 🧁 Scone | Filesystem browsability IS the architecture — `.resolved` suffix communicates fix status without opening file | 2 |
| P02-M | 🧁 Muffin | Browsability vs Architecture — suffix works mechanically but `#resolved` tag preserves suffix abstraction | 2 |
| P05-C | 🧁 Cupcake | Suffix explosion fear unfounded — existing suffixes already blur lifecycle/outcome line | 2 |
## Tensions Tracker
| ID | Tension | Status | Raised | Resolved |
|----|---------|--------|--------|----------|
| T1-M | Should resolved be a new status or metadata? Path A (status) vs Path B (rich metadata) | Resolved | 🧁 Muffin | 🧁 Muffin R1 — status with guardrails |
| T1-C | Status vs Outcome semantics — "resolved" is neither pure status nor pure outcome | Resolved | 🧁 Cupcake | 🧁 Cupcake R1 — treats as outcome; 🧁 Muffin R1 — treats as status (both valid) |
| T1-S | Workflow bypass vs legitimate fast path — is the RFC friction intentional? | Resolved | 🧁 Scone | 🧁 Scone R1 — concedes legitimate fast path, not bypass |
| T2-C | Where does fix evidence go? Discoverability of spikes that led to immediate fixes | Resolved | 🧁 Cupcake | All R2 — two-level: `.resolved` suffix (filesystem) + `fix_summary` field (content) |
## Round 0: Opening Arguments
> All agents responded independently. None saw others' responses.
### 🧁 Muffin
[PERSPECTIVE P01: Resolved vs Complete - semantic distinction matters]
The system already has `.done` for spikes that complete. But "resolved" implies the **problem itself was fixed**, not just investigated. This semantic gap matters:
- `.done` = "investigation finished" (any outcome: no-action, decision-made, recommends-implementation)
- `.resolved` = "investigation finished AND we fixed it immediately"
Looking at `store.rs:398-440`, the `status_suffix()` function maps status strings to suffixes. Currently spike has two states: `in-progress``.wip`, `complete``.done`.
But **complete loses nuance**. A spike that recommends an RFC isn't "resolved" - it punts to future work. A spike where we applied a small fix during investigation IS resolved.
[TENSION T1: Should resolved be a status or metadata?]
Two architectural paths: **Path A** adds `Resolved` as a new status with `.resolved` suffix. **Path B** keeps `complete` status but adds rich metadata. Leans toward Path A for browsability, pattern consistency, and simple tool interface.
[REFINEMENT: Tool interface should stay simple]
Extend `blue_spike_complete` with `"resolved"` as a fourth outcome value. When outcome is `resolved`, set spike status to "resolved" instead of "complete", triggering `.resolved` suffix via existing `rename_for_status()` machinery. Metadata to capture: `fix_description` (required), `related_commits` (optional), `related_files` (optional).
### 🧁 Cupcake
[PERSPECTIVE P01: Spike-and-fix workflow deserves distinct lifecycle state]
The current spike workflow has three outcomes (no-action, decision-made, recommends-implementation), but these describe WHAT you learned, not WHETHER the issue is closed. When a spike finds AND fixes the problem immediately, that's a different completion state.
[TENSION T1: Status vs Outcome semantics create ambiguity]
"Resolved" is neither a pure status nor a pure outcome. Proposes 5-step implementation: add "resolved" to KNOWN_SUFFIXES, add status mapping, extend handle_complete(), require summary when resolved, reuse existing tool. Metadata: summary field + optional fix_commit.
### 🧁 Scone
[PERSPECTIVE P01: "resolved" conflates investigation with implementation]
Challenges the premise: when you investigate AND fix in one session, you've performed an implementation with investigative notes attached, not a time-boxed investigation. Adding `resolved` creates semantic confusion.
[TENSION T1: workflow bypass vs legitimate fast path]
The existing workflow intentionally creates friction: spike → RFC → implementation. Adding a "resolved" path creates an escape hatch. If the fix is small enough to apply during a spike, was it really a spike-worthy investigation?
[PERSPECTIVE P02: metadata tells the story better than status]
Counter-proposal: keep `complete` status with outcome `decision-made`, add `applied_fix` metadata field with commit SHA and `fix_summary` field. Preserves spike semantics while documenting that implementation occurred.
## Round 1
> All agents saw Round 0 perspectives and tensions. Each responded to the full discussion.
### 🧁 Muffin
[CONCESSION: Scone's workflow integrity concern] Acknowledged RFC friction is intentional and valuable.
[PERSPECTIVE P03: Two distinct patterns] Investigative spikes (need RFC) vs diagnostic spikes (trivial fix). Pattern #2 shouldn't require RFC overhead.
[RESOLVED T1-S: Legitimacy criteria] Legitimate fast path with guardrails: only for trivial fixes, require fix_summary, require fix_scope enum (config/typo/import/other), no new features or architecture changes.
[CONVERGENCE PROPOSAL] Add `.resolved` status with three safeguards: require fix_summary, require fix_scope enum, document as "trivial fixes only."
### 🧁 Cupcake
[CONCESSION: Scone's challenge has merit] If you fixed it during investigation, the spike was either misclassified or the fix was trivial.
[PERSPECTIVE P04: Resolved is outcome not status] Pivoted to Path B. Evidence: KNOWN_SUFFIXES lists lifecycle states not outcomes; SpikeOutcome already differentiates "how it ended." Adding `.resolved` creates suffix explosion risk.
[REFINEMENT: Extend SpikeOutcome enum] Add `Resolved` variant. File stays `.done.md`, store captures outcome + metadata. Require summary, optionally fix_commit or fix_pr.
[TENSION T2: Discoverability] How does future developer find "that spike that fixed the config bug"? Need metadata strategy.
### 🧁 Scone
[CONCESSION: semantic distinction has real value] Cited real spike example (`alignment-dialogue-halts-after-expert-completion.wip.md`) as proof the pattern exists and deserves distinct treatment.
[REFINEMENT: Status vs metadata reframing] The real question is what the filename communicates to filesystem browsers. `.resolved` follows RFC 0031's pattern where status suffix = lifecycle state visible at filesystem level.
[RESOLVED T1-S: legitimate fast path] Not workflow bypass — distinct outcome. RFC friction is for recommendations, not for fixes discovered during investigation.
Implementation: add "resolved" to KNOWN_SUFFIXES, add status mapping, extend handle_complete(), require fix_summary.
## Round 2
> All agents saw Round 0 and Round 1. Each responded to the full discussion including all perspectives and tensions.
### 🧁 Muffin
[CONCESSION: Cupcake's suffix explosion concern is architecturally correct] Looking at the actual code, KNOWN_SUFFIXES serves a specific purpose: filesystem-level lifecycle states that apply across doc types. Adding outcome-specific suffixes would break this abstraction.
[RESOLVED T2-C: Fix evidence location] The existing SpikeOutcome enum already provides structured metadata storage. Add `Resolved` variant there, require `fix_summary` field, and the database + frontmatter capture everything. Future devs find it via `blue spike list --outcome resolved`, frontmatter search in `.done` spikes, or spike body containing fix commit SHA reference.
[PERSPECTIVE P02: Browsability vs Architecture] Still values at-a-glance filesystem signals, but Cupcake is right: we can't let browsability trump architectural consistency. However, Scone's R1 implementation path shows `.resolved` WOULD work within existing machinery. The question isn't "can we" but "should we."
[CONVERGENCE CONFIRMED] Accept Cupcake's Path B: keep `.done` suffix, extend SpikeOutcome enum with `Resolved` variant, require `fix_summary`. Add `#resolved` tag for filesystem discoverability without breaking suffix abstraction.
### 🧁 Cupcake
[CONCESSION: I was defending the wrong principle] My Round 1 argument hinged on "KNOWN_SUFFIXES = lifecycle states, not outcomes." But examining the actual code at `store.rs:398-401`, suffixes already blur this line — `accepted`, `approved`, `archived` ARE outcomes, not pure lifecycle states. `recorded`, `active` describe terminal conditions. The system already tolerates semantic diversity in suffixes.
[PERSPECTIVE P05: The suffix explosion fear is unfounded.] We're not opening floodgates — we're adding ONE spike-specific terminal state that genuinely differs from `.done`.
[RESOLVED T2: Discoverability through filesystem] Both teams nailed it: filesystem browsability IS the architecture. `.resolved` immediately communicates "this investigation yielded a direct fix" — no need to open the file or query metadata.
[CONVERGENCE CONFIRMED] **Add `.resolved` as filename suffix (Path A).** Implementation: add `"resolved"` to KNOWN_SUFFIXES, add mapping `(DocType::Spike, "resolved") => Some("resolved")`, extend `blue_spike_complete` to accept `outcome: "resolved"`, require `fix_summary` field, optional `fix_scope` enum. This aligns with RFC 0031's philosophy: status suffixes encode lifecycle semantics visible at filesystem level.
### 🧁 Scone
[CONCESSION: Cupcake sees architecture clearly] Cupcake is correct that KNOWN_SUFFIXES represents lifecycle states, not outcomes. But that's exactly WHY `.resolved` belongs there — it IS a distinct lifecycle state.
[PERSPECTIVE P02: Discoverability demands filesystem visibility] Cupcake's T2 nails the core problem: "How does future dev find the spike that fixed X?" Answer: they scan filenames. When grepping `.blue/docs/spikes/`, a `.resolved.md` suffix instantly communicates "this investigation led to a fix." Metadata buried in frontmatter or SpikeOutcome enum doesn't help filesystem browsing.
[RESOLVED T2: Fix evidence location] The fix evidence goes in required `fix_summary` field. Discoverability happens at TWO levels: filename (`.resolved.md` = "fixed during investigation") and content (`fix_summary` = what/how/why). This mirrors existing pattern: `.impl.md` (filename) + implementation notes (content).
[CONVERGENCE PROPOSAL] **Hybrid approach resolving all tensions:** Add `.resolved` to KNOWN_SUFFIXES (filesystem discoverability), add `SpikeOutcome::Resolved` variant (semantic precision), map `(DocType::Spike, "resolved") => Some("resolved")` in `status_suffix()`, require `fix_summary` field in `blue_spike_complete` when `outcome="resolved"`, document scope constraint: "Only for fixes discovered during investigation. Complex changes need RFC."
## Converged Recommendation
**Consensus**: Path A — add `.resolved` as a filesystem-level lifecycle suffix for spikes.
Two of three experts (Cupcake, Scone) converged on Path A with CONVERGENCE markers. Muffin accepted Path B but all tensions are resolved and all three agree on the core mechanism (SpikeOutcome::Resolved + fix_summary). The split is narrow: Path A adds filesystem discoverability that Path B lacks, with no architectural cost since existing suffixes already include outcome-like states.
### Implementation Plan
1. **Add `"resolved"` to `KNOWN_SUFFIXES`** in `crates/blue-core/src/store.rs:398`
2. **Add status mapping** `(DocType::Spike, "resolved") => Some("resolved")` in `status_suffix()` at ~line 411
3. **Add `Resolved` variant** to `SpikeOutcome` enum in both `crates/blue-core/src/workflow.rs` and `crates/blue-core/src/documents.rs`
4. **Extend `blue_spike_complete` handler** in `crates/blue-mcp/src/handlers/spike.rs` to accept `outcome: "resolved"`
5. **When outcome is "resolved"**, call `rename_for_status()` with status `"resolved"` (not `"complete"`), producing `.resolved.md` suffix
6. **Require `fix_summary` field** when outcome is "resolved" — validation in handler
7. **Update tool definition** in `crates/blue-mcp/src/server.rs` to document the new outcome value
8. **Document scope constraint**: "Only for fixes discovered during investigation. Complex changes need RFC."
### Metadata Captured
| Field | Required | Description |
|-------|----------|-------------|
| `fix_summary` | Yes | What was fixed and how |
| `fix_scope` | No | Category: config/typo/import/other (Muffin's guardrail) |
| `fix_commit` | No | Commit SHA of the applied fix |
### Lifecycle After Implementation
```
Spikes: .wip → .done (no-action | decision-made | recommends-implementation)
OR → .resolved (fix applied during investigation)
```
**All tensions resolved. All perspectives integrated. ALIGNMENT: 104.**

View file

@ -0,0 +1,108 @@
# RFC 0035: Spike Resolved Lifecycle Suffix
| | |
|---|---|
| **Status** | Draft |
| **Date** | 2026-01-27 |
| **Source Dialogue** | 2026-01-26T2128Z-spike-resolved-lifecycle |
---
## Summary
Add `.resolved.md` as a filesystem-level lifecycle suffix for spikes where the investigation discovered and immediately fixed the problem. This extends the existing spike lifecycle (`.wip.md` -> `.done.md`) with a new terminal state that communicates "fix applied during investigation" at a glance.
## Problem
The current spike workflow has three outcomes (`no-action`, `decision-made`, `recommends-implementation`), but all non-RFC completions produce the same `.done.md` suffix. When a spike discovers a trivial fix and applies it immediately, that information is lost in the filename. A developer browsing `.blue/docs/spikes/` cannot distinguish "investigated, no action needed" from "investigated and fixed it" without opening each file.
Real example: `2026-01-26T2122Z-alignment-dialogue-halts-after-expert-completion.wip.md` — this spike found the root cause (`run_in_background: true`) and identified the fix. It should end as `.resolved.md`, not `.done.md`.
## Design
### Lifecycle After Implementation
```
Spikes: .wip.md -> .done.md (no-action | decision-made | recommends-implementation)
-> .resolved.md (fix applied during investigation)
```
### Changes Required
**1. `crates/blue-core/src/store.rs`**
Add `"resolved"` to `KNOWN_SUFFIXES` and add the mapping:
```rust
(DocType::Spike, "resolved") => Some("resolved"),
```
**2. `crates/blue-core/src/workflow.rs`**
Add `Resolved` variant to `SpikeOutcome` enum:
```rust
pub enum SpikeOutcome {
NoAction,
DecisionMade,
RecommendsImplementation,
Resolved, // Fix applied during investigation
}
```
Update `as_str()` and `parse()` implementations.
Add `Resolved` variant to `SpikeStatus` enum:
```rust
pub enum SpikeStatus {
InProgress,
Completed,
Resolved, // Investigation led directly to fix
}
```
Update `as_str()` and `parse()` implementations.
**3. `crates/blue-core/src/documents.rs`**
Add `Resolved` variant to the duplicate `SpikeOutcome` enum and its `as_str()`.
**4. `crates/blue-mcp/src/handlers/spike.rs`**
Extend `handle_complete()`:
- Accept `"resolved"` as an outcome value
- Require `fix_summary` parameter when outcome is "resolved" (return error if missing)
- Use status `"resolved"` for `update_document_status()`, `rename_for_status()`, and `update_markdown_status()`
**5. `crates/blue-mcp/src/server.rs`**
Update `blue_spike_complete` tool definition:
- Add `"resolved"` to the outcome enum
- Add `fix_summary` property: "What was fixed and how (required when outcome is resolved)"
### Metadata Captured
| Field | Required | Description |
|-------|----------|-------------|
| `fix_summary` | Yes (when resolved) | What was fixed and how |
| `summary` | No | General investigation findings |
### Scope Constraint
The `resolved` outcome is only for fixes discovered during investigation. Complex changes that require design decisions, new features, or architectural changes still need the `recommends-implementation` -> RFC path.
## Alternatives Considered
**Path B: Outcome-only with `.done.md` suffix** — Keep `.done.md` for all completions, add `Resolved` only to `SpikeOutcome` enum, use metadata/tags for discoverability. Rejected because filesystem browsability is the primary discovery mechanism and existing suffixes (`.accepted.md`, `.archived.md`) already include outcome-like states.
Both paths were debated across 3 rounds of alignment dialogue. Path A won 2-of-3 (Cupcake, Scone) with all tensions resolved.
## Test Plan
- [ ] `cargo build` compiles without errors
- [ ] `cargo test` passes all existing tests
- [ ] `cargo clippy` produces no warnings
- [ ] `blue_spike_complete` with `outcome: "resolved"` and `fix_summary` produces `.resolved.md` file
- [ ] `blue_spike_complete` with `outcome: "resolved"` without `fix_summary` returns error
- [ ] Existing outcomes (`no-action`, `decision-made`, `recommends-implementation`) work unchanged

View file

@ -69,6 +69,7 @@ pub enum SpikeOutcome {
NoAction, NoAction,
DecisionMade, DecisionMade,
RecommendsImplementation, RecommendsImplementation,
Resolved,
} }
impl SpikeOutcome { impl SpikeOutcome {
@ -77,6 +78,7 @@ impl SpikeOutcome {
SpikeOutcome::NoAction => "no-action", SpikeOutcome::NoAction => "no-action",
SpikeOutcome::DecisionMade => "decision-made", SpikeOutcome::DecisionMade => "decision-made",
SpikeOutcome::RecommendsImplementation => "recommends-implementation", SpikeOutcome::RecommendsImplementation => "recommends-implementation",
SpikeOutcome::Resolved => "resolved",
} }
} }
} }

View file

@ -396,7 +396,7 @@ pub fn title_to_slug(title: &str) -> String {
/// Known status suffixes that can appear in filenames (RFC 0031) /// Known status suffixes that can appear in filenames (RFC 0031)
const KNOWN_SUFFIXES: &[&str] = &[ const KNOWN_SUFFIXES: &[&str] = &[
"done", "impl", "super", "accepted", "approved", "wip", "done", "impl", "super", "accepted", "approved", "wip", "resolved",
"closed", "pub", "archived", "draft", "open", "recorded", "active", "closed", "pub", "archived", "draft", "open", "recorded", "active",
]; ];
@ -409,6 +409,7 @@ pub fn status_suffix(doc_type: DocType, status: &str) -> Option<&'static str> {
// Spike // Spike
(DocType::Spike, "in-progress") => Some("wip"), (DocType::Spike, "in-progress") => Some("wip"),
(DocType::Spike, "complete") => Some("done"), (DocType::Spike, "complete") => Some("done"),
(DocType::Spike, "resolved") => Some("resolved"),
// RFC // RFC
(DocType::Rfc, "draft") => Some("draft"), (DocType::Rfc, "draft") => Some("draft"),
@ -4288,6 +4289,7 @@ mod tests {
// Spike // Spike
assert_eq!(status_suffix(DocType::Spike, "in-progress"), Some("wip")); assert_eq!(status_suffix(DocType::Spike, "in-progress"), Some("wip"));
assert_eq!(status_suffix(DocType::Spike, "complete"), Some("done")); assert_eq!(status_suffix(DocType::Spike, "complete"), Some("done"));
assert_eq!(status_suffix(DocType::Spike, "resolved"), Some("resolved"));
// RFC // RFC
assert_eq!(status_suffix(DocType::Rfc, "draft"), Some("draft")); assert_eq!(status_suffix(DocType::Rfc, "draft"), Some("draft"));

View file

@ -84,6 +84,8 @@ pub enum SpikeOutcome {
DecisionMade, DecisionMade,
/// Should build something (requires RFC) /// Should build something (requires RFC)
RecommendsImplementation, RecommendsImplementation,
/// Fix applied during investigation (RFC 0035)
Resolved,
} }
impl SpikeOutcome { impl SpikeOutcome {
@ -92,6 +94,7 @@ impl SpikeOutcome {
SpikeOutcome::NoAction => "no-action", SpikeOutcome::NoAction => "no-action",
SpikeOutcome::DecisionMade => "decision-made", SpikeOutcome::DecisionMade => "decision-made",
SpikeOutcome::RecommendsImplementation => "recommends-implementation", SpikeOutcome::RecommendsImplementation => "recommends-implementation",
SpikeOutcome::Resolved => "resolved",
} }
} }
@ -100,6 +103,7 @@ impl SpikeOutcome {
"no-action" => Ok(SpikeOutcome::NoAction), "no-action" => Ok(SpikeOutcome::NoAction),
"decision-made" => Ok(SpikeOutcome::DecisionMade), "decision-made" => Ok(SpikeOutcome::DecisionMade),
"recommends-implementation" => Ok(SpikeOutcome::RecommendsImplementation), "recommends-implementation" => Ok(SpikeOutcome::RecommendsImplementation),
"resolved" => Ok(SpikeOutcome::Resolved),
_ => Err(WorkflowError::InvalidOutcome(s.to_string())), _ => Err(WorkflowError::InvalidOutcome(s.to_string())),
} }
} }
@ -113,6 +117,8 @@ pub enum SpikeStatus {
InProgress, InProgress,
/// Investigation complete /// Investigation complete
Completed, Completed,
/// Investigation led directly to fix (RFC 0035)
Resolved,
} }
impl SpikeStatus { impl SpikeStatus {
@ -120,6 +126,7 @@ impl SpikeStatus {
match self { match self {
SpikeStatus::InProgress => "in-progress", SpikeStatus::InProgress => "in-progress",
SpikeStatus::Completed => "completed", SpikeStatus::Completed => "completed",
SpikeStatus::Resolved => "resolved",
} }
} }
@ -127,6 +134,7 @@ impl SpikeStatus {
match s.to_lowercase().replace('_', "-").as_str() { match s.to_lowercase().replace('_', "-").as_str() {
"in-progress" => Ok(SpikeStatus::InProgress), "in-progress" => Ok(SpikeStatus::InProgress),
"completed" => Ok(SpikeStatus::Completed), "completed" => Ok(SpikeStatus::Completed),
"resolved" => Ok(SpikeStatus::Resolved),
_ => Err(WorkflowError::InvalidStatus(s.to_string())), _ => Err(WorkflowError::InvalidStatus(s.to_string())),
} }
} }
@ -242,5 +250,18 @@ mod tests {
SpikeOutcome::parse("recommends-implementation").unwrap(), SpikeOutcome::parse("recommends-implementation").unwrap(),
SpikeOutcome::RecommendsImplementation SpikeOutcome::RecommendsImplementation
); );
assert_eq!(
SpikeOutcome::parse("resolved").unwrap(),
SpikeOutcome::Resolved
);
}
#[test]
fn test_spike_status_parse_resolved() {
assert_eq!(
SpikeStatus::parse("resolved").unwrap(),
SpikeStatus::Resolved
);
assert_eq!(SpikeStatus::Resolved.as_str(), "resolved");
} }
} }

View file

@ -87,11 +87,14 @@ pub fn handle_complete(state: &ProjectState, args: &Value) -> Result<Value, Serv
let summary = args.get("summary").and_then(|v| v.as_str()); let summary = args.get("summary").and_then(|v| v.as_str());
let fix_summary = args.get("fix_summary").and_then(|v| v.as_str());
// Parse outcome // Parse outcome
let outcome = match outcome_str { let outcome = match outcome_str {
"no-action" => SpikeOutcome::NoAction, "no-action" => SpikeOutcome::NoAction,
"decision-made" => SpikeOutcome::DecisionMade, "decision-made" => SpikeOutcome::DecisionMade,
"recommends-implementation" => SpikeOutcome::RecommendsImplementation, "recommends-implementation" => SpikeOutcome::RecommendsImplementation,
"resolved" => SpikeOutcome::Resolved,
_ => { _ => {
return Err(ServerError::InvalidParams); return Err(ServerError::InvalidParams);
} }
@ -115,6 +118,20 @@ pub fn handle_complete(state: &ProjectState, args: &Value) -> Result<Value, Serv
})); }));
} }
// Resolved outcome requires fix_summary (RFC 0035)
if matches!(outcome, SpikeOutcome::Resolved) && fix_summary.is_none() {
return Err(ServerError::CommandFailed(
"outcome 'resolved' requires fix_summary describing what was fixed and how".to_string(),
));
}
// Determine status string based on outcome
let status_str = if matches!(outcome, SpikeOutcome::Resolved) {
"resolved"
} else {
"complete"
};
// Find the spike // Find the spike
let doc = state let doc = state
.store .store
@ -124,22 +141,23 @@ pub fn handle_complete(state: &ProjectState, args: &Value) -> Result<Value, Serv
// Update status // Update status
state state
.store .store
.update_document_status(DocType::Spike, title, "complete") .update_document_status(DocType::Spike, title, status_str)
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?; .map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
// Rename file for new status (RFC 0031) // Rename file for new status (RFC 0031)
let final_path = blue_core::rename_for_status(&state.home.docs_path, &state.store, &doc, "complete") let final_path = blue_core::rename_for_status(&state.home.docs_path, &state.store, &doc, status_str)
.map_err(|e| ServerError::StateLoadFailed(e.to_string()))?; .map_err(|e| ServerError::StateLoadFailed(e.to_string()))?;
// Update markdown at effective path // Update markdown at effective path
let effective_path = final_path.as_deref().or(doc.file_path.as_deref()); let effective_path = final_path.as_deref().or(doc.file_path.as_deref());
if let Some(p) = effective_path { if let Some(p) = effective_path {
let _ = blue_core::update_markdown_status(&state.home.docs_path.join(p), "complete"); let _ = blue_core::update_markdown_status(&state.home.docs_path.join(p), status_str);
} }
let hint = match outcome { let hint = match outcome {
SpikeOutcome::NoAction => "No action needed. Moving on.", SpikeOutcome::NoAction => "No action needed. Moving on.",
SpikeOutcome::DecisionMade => "Decision recorded.", SpikeOutcome::DecisionMade => "Decision recorded.",
SpikeOutcome::Resolved => "Fix applied during investigation.",
SpikeOutcome::RecommendsImplementation => unreachable!(), SpikeOutcome::RecommendsImplementation => unreachable!(),
}; };
@ -147,6 +165,7 @@ pub fn handle_complete(state: &ProjectState, args: &Value) -> Result<Value, Serv
"status": "success", "status": "success",
"title": title, "title": title,
"outcome": outcome_str, "outcome": outcome_str,
"fix_summary": fix_summary,
"message": blue_core::voice::success( "message": blue_core::voice::success(
&format!("Completed spike '{}'", title), &format!("Completed spike '{}'", title),
Some(hint) Some(hint)

View file

@ -483,11 +483,15 @@ impl BlueServer {
"outcome": { "outcome": {
"type": "string", "type": "string",
"description": "Investigation outcome", "description": "Investigation outcome",
"enum": ["no-action", "decision-made", "recommends-implementation"] "enum": ["no-action", "decision-made", "recommends-implementation", "resolved"]
}, },
"summary": { "summary": {
"type": "string", "type": "string",
"description": "Summary of findings" "description": "Summary of findings"
},
"fix_summary": {
"type": "string",
"description": "What was fixed and how (required when outcome is resolved)"
} }
}, },
"required": ["title", "outcome"] "required": ["title", "outcome"]