+ (item) => {
+ const isSuccess = item.exit_code === 0;
+ const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)";
+ const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)";
+ const durationStr = item.duration_ms != null ? (item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s`) : "";
+
+ return `
+
+ ` : ""}
+ `;
+ },
);
}
@@ -1088,40 +1178,123 @@ function loadTechDebt(): void {
loadGenericList(
"/api/tech_debts",
"techdebt-container",
- (item) => `
-
-
Git Commit: ${item.git_commit || "None"}
-
Branch: ${item.git_branch || "None"}
+ (item) => {
+ const sev = (item.severity || "medium").toLowerCase();
+ let sevBg = "rgba(241, 196, 15, 0.15)";
+ let sevColor = "#f1c40f";
+ let sevBorder = "rgba(241, 196, 15, 0.4)";
+ if (sev === "critical" || sev === "blocker") {
+ sevBg = "rgba(231, 76, 60, 0.18)";
+ sevColor = "#e74c3c";
+ sevBorder = "rgba(231, 76, 60, 0.5)";
+ } else if (sev === "high") {
+ sevBg = "rgba(230, 126, 34, 0.18)";
+ sevColor = "#e67e22";
+ sevBorder = "rgba(230, 126, 34, 0.5)";
+ } else if (sev === "low" || sev === "trivial") {
+ sevBg = "rgba(52, 152, 219, 0.15)";
+ sevColor = "#3498db";
+ sevBorder = "rgba(52, 152, 219, 0.4)";
+ }
+
+ const statusBadge = item.is_resolved
+ ? '
✓ Resolved'
+ : '
● Open';
+
+ const fileLoc = item.file_path
+ ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}`
+ : "";
+
+ return `
+
+
+ ${escapeHtml(item.id)}
+ ${escapeHtml(item.severity || "medium")}
+ ${item.effort_estimate ? `⏳ ${escapeHtml(item.effort_estimate)}` : ""}
+
+
${statusBadge}
- `,
+ ${fileLoc ? `
📄 ${escapeHtml(fileLoc)}
` : ""}
+
Description: ${escapeHtml(item.description || "")}
+ ${item.workaround ? `
Workaround: ${escapeHtml(item.workaround)}
` : ""}
+
Ideal Solution: ${escapeHtml(item.ideal_solution || "")}
+
+ ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""}
+ Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}
+ 🌿 ${escapeHtml(item.git_branch || "None")}
+ 🏷️ ${escapeHtml(item.namespace || "global")}
+
+ `;
+ },
);
loadGenericList(
"/api/error_fixes",
"errorfixes-container",
(item) => `
-
${item.signature || "Error"} (Resolved)
-
Solution: ${item.solution || ""}
-
Commit: ${item.git_commit || "None"}
+
+
+ ${escapeHtml(item.signature || "Error")}
+ ${item.error_category ? `${escapeHtml(item.error_category)}` : ""}
+ ${item.toolchain ? `🔧 ${escapeHtml(item.toolchain)}` : ""}
+
+
✓ Resolved
+
+
Solution: ${escapeHtml(item.solution || "")}
+ ${item.stack_trace ? `
+
+ Stack Trace
+ ${escapeHtml(item.stack_trace)}
+
+ ` : ""}
+
+ ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""}
+ Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}
+ ${item.git_branch ? `🌿 ${escapeHtml(item.git_branch)}` : ""}
+
`,
);
}
-
function loadADRs(): void {
loadGenericList(
"/api/adrs",
"adrs-container",
- (item) => `
-
${item.id} | ${item.title}
-
${item.status}
-
Context: ${item.context || ""}
-
Decision: ${item.decision || ""}
-
Consequence: ${item.consequence || ""}
- ${item.supersedes ? `
Supersedes: ${item.supersedes}
` : ""}
- `,
+ (item) => {
+ const statusLower = (item.status || "").toLowerCase();
+ let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);";
+ let badgeText = `● ${escapeHtml(item.status || "accepted")}`;
+
+ if (statusLower === "implemented" || statusLower === "resolved") {
+ badgeStyle = "color:var(--success-color); border-color:rgba(46, 160, 67, 0.4); background:rgba(46, 160, 67, 0.15); font-weight:600;";
+ badgeText = `✓ ${escapeHtml(item.status)}`;
+ } else if (statusLower === "superseded") {
+ badgeStyle = "text-decoration: line-through; color:var(--error-color); border-color:rgba(248, 81, 73, 0.4); background:rgba(248, 81, 73, 0.12);";
+ badgeText = `✕ ${escapeHtml(item.status)}`;
+ } else if (statusLower === "proposed" || statusLower === "draft") {
+ badgeStyle = "color:var(--warning-color); border-color:rgba(210, 153, 34, 0.4); background:rgba(210, 153, 34, 0.12);";
+ badgeText = `○ ${escapeHtml(item.status)}`;
+ }
+
+ const resolvedDate = item.resolved_at ? new Date(item.resolved_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
+
+ return `
+
+ ${escapeHtml(item.id)} | ${escapeHtml(item.title)}
+ ${badgeText}
+
+ ${(item.git_commit || item.git_branch || item.task_id || item.resolved_at) ? `
+
+ ${item.git_branch || item.git_commit ? `📦 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}` : ""}
+ ${item.task_id ? `🎯 Task: ${escapeHtml(item.task_id)}` : ""}
+ ${resolvedDate ? `✓ Resolved: ${escapeHtml(resolvedDate)}` : ""}
+
+ ` : ""}
+
Context: ${escapeHtml(item.context || "")}
+
Decision: ${escapeHtml(item.decision || "")}
+
Consequence: ${escapeHtml(item.consequence || "")}
+ ${item.supersedes ? `
Supersedes: ${escapeHtml(item.supersedes)}
` : ""}
+ `;
+ },
);
}
@@ -1129,10 +1302,41 @@ function loadMemos(): void {
loadGenericList(
"/api/handoff_memos",
"memos-container",
- (item) => `
-
Memo from ${item.author || "System"}
-
${item.content || item.summary || ""}
- `,
+ (item) => {
+ const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : [];
+ const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : [];
+
+ return `
+
+ 👤 Memo from ${escapeHtml(item.author || "System")}
+ ${escapeHtml(item.namespace || "global")}
+
+ ${(item.git_branch || item.vcs_revision || item.session_id) ? `
+
+ ${item.git_branch ? `🌿 ${escapeHtml(item.git_branch)}` : ""}
+ ${item.vcs_revision ? `Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}` : ""}
+ ${item.session_id ? `Session: ${escapeHtml(item.session_id)}` : ""}
+
+ ` : ""}
+
${escapeHtml(item.content || item.summary || "")}
+ ${blockers.length > 0 ? `
+
+
🚫 Blockers:
+
+ ${blockers.map((b: string) => `- ${escapeHtml(b)}
`).join("")}
+
+
+ ` : ""}
+ ${actionItems.length > 0 ? `
+
+
📋 Action Items:
+
+ ${actionItems.map((a: string) => `- ${escapeHtml(a)}
`).join("")}
+
+
+ ` : ""}
+ `;
+ },
);
loadGenericList(
@@ -1153,10 +1357,54 @@ function loadMemos(): void {
loadGenericList(
"/api/milestones",
"milestones-container",
- (item) => `
-
${item.title || item.name}
-
${item.description || ""}
- `,
+ (item) => {
+ const statusLower = (item.status || "pending").toLowerCase();
+ let statusBg = "rgba(52, 152, 219, 0.15)";
+ let statusColor = "var(--accent-color)";
+ let statusLabel = item.status || "Pending";
+ if (statusLower === "completed" || statusLower === "done") {
+ statusBg = "rgba(46, 204, 113, 0.15)";
+ statusColor = "var(--success-color)";
+ statusLabel = "✓ Completed";
+ } else if (statusLower === "in_progress" || statusLower === "active") {
+ statusBg = "rgba(230, 126, 34, 0.15)";
+ statusColor = "var(--open-color)";
+ statusLabel = "● In Progress";
+ }
+
+ const targetDate = item.target_date ? new Date(item.target_date * 1000).toLocaleDateString() : "";
+ const completedDate = item.completed_at ? new Date(item.completed_at * 1000).toLocaleDateString() : "";
+ const deliverables = Array.isArray(item.deliverables) && item.deliverables.length > 0 ? item.deliverables : [];
+ const taskIds = Array.isArray(item.task_ids) && item.task_ids.length > 0 ? item.task_ids : [];
+
+ return `
+
+ ${escapeHtml(item.title || item.name || "Milestone")}
+ ${escapeHtml(statusLabel)}
+
+ ${targetDate || completedDate ? `
+
+ ${targetDate ? `🎯 Target: ${escapeHtml(targetDate)}` : ""}
+ ${completedDate ? `✓ Finished: ${escapeHtml(completedDate)}` : ""}
+
+ ` : ""}
+
${escapeHtml(item.description || "")}
+ ${deliverables.length > 0 ? `
+
+
Deliverables:
+
+ ${deliverables.map((d: string) => `- ${escapeHtml(d)}
`).join("")}
+
+
+ ` : ""}
+ ${taskIds.length > 0 ? `
+
+ Linked Tasks:
+ ${taskIds.map((tid: string) => `${escapeHtml(tid)}`).join("")}
+
+ ` : ""}
+ `;
+ },
);
}
@@ -1164,14 +1412,30 @@ function loadSnippets(): void {
loadGenericList(
"/api/snippets",
"snippets-container",
- (item) => `
-
${item.description || "Snippet"}
-
Language: ${item.language || "txt"} | Tags: ${(item.tags || []).join(", ")}
-
${item.content || item.code || ""}
- `,
+ (item) => {
+ const code = item.code || item.content || "";
+ const tags = Array.isArray(item.tags) ? item.tags : [];
+ const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : "";
+
+ return `
+
+
+ ${escapeHtml(item.name || item.description || "Snippet")}
+ ${item.language ? `${escapeHtml(item.language)}` : ""}
+
+
+
+ ${item.description && item.description !== item.name ? `
${escapeHtml(item.description)}
` : ""}
+ ${origin ? `
📄 ${escapeHtml(origin)}
` : ""}
+
${escapeHtml(code)}
+
+ ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""}
+ ${tags.map((t: string) => `#${escapeHtml(t)}`).join(" ")}
+
+ `;
+ },
);
}
-
function loadAllExtras(): void {
loadTerminal();
loadTechDebt();
diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs
index 52ebf4b..2ce8aa2 100644
--- a/server/src/handlers/meta.rs
+++ b/server/src/handlers/meta.rs
@@ -378,7 +378,7 @@ impl McpTool for DecisionsHandler {
fn schema(&self) -> Value {
crate::mcp::tool_def::
(
"decisions",
- "Consolidated Architectural Decision Records (ADRs) management (log, query, delete)",
+ "Consolidated Architectural Decision Records (ADRs) management (log, query, update, delete)",
)
}
@@ -396,6 +396,12 @@ impl McpTool for DecisionsHandler {
let context = req.context.unwrap_or_default();
let decision = req.decision.unwrap_or_default();
let consequence = req.consequences.unwrap_or_default();
+ let status_lower = status.to_ascii_lowercase();
+ let resolved_at = if status_lower == "implemented" || status_lower == "resolved" {
+ Some(crate::handlers::utils::now_secs())
+ } else {
+ None
+ };
let idx = state.get_search_index().await;
let mut final_id = String::new();
@@ -404,7 +410,7 @@ impl McpTool for DecisionsHandler {
state.code.adrs.modify(|adrs| {
if let Some(superseded_id) = &req.supersedes {
for old_adr in adrs.iter_mut() {
- if old_adr.id == *superseded_id {
+ if old_adr.id.eq_ignore_ascii_case(superseded_id) {
old_adr.status = "superseded".to_string();
adrs_to_index.push(old_adr.clone());
break;
@@ -427,6 +433,10 @@ impl McpTool for DecisionsHandler {
alternatives_considered: req.alternatives_considered.unwrap_or_default(),
affected_components: req.affected_components.unwrap_or_default(),
author: req.author,
+ git_commit: req.git_commit,
+ git_branch: req.git_branch,
+ resolved_at,
+ task_id: req.task_id,
};
adrs_to_index.push(a.clone());
@@ -444,6 +454,94 @@ impl McpTool for DecisionsHandler {
);
Ok(format!("Logged decision {}: {}", final_id, title))
}
+ DecisionAction::Update => {
+ let id = req.id.ok_or_else(|| {
+ crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string())
+ })?;
+ let mut updated_adr = None;
+ let mut adrs_to_index = Vec::new();
+
+ state.code.adrs.modify(|adrs| {
+ let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
+ if let Some(pos) = target_pos {
+ if let Some(superseded_id) = &req.supersedes {
+ if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) {
+ if s_pos != pos {
+ adrs[s_pos].status = "superseded".to_string();
+ adrs_to_index.push(adrs[s_pos].clone());
+ }
+ }
+ }
+
+ let a = &mut adrs[pos];
+ if let Some(t) = req.title {
+ a.title = t;
+ }
+ if let Some(c) = req.context {
+ a.context = c;
+ }
+ if let Some(d) = req.decision {
+ a.decision = d;
+ }
+ if let Some(cons) = req.consequences {
+ a.consequence = cons;
+ }
+ if let Some(s) = req.status {
+ let s_lower = s.to_ascii_lowercase();
+ if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() {
+ a.resolved_at = Some(crate::handlers::utils::now_secs());
+ } else if s_lower != "implemented" && s_lower != "resolved" {
+ a.resolved_at = None;
+ }
+ a.status = s;
+ }
+ if req.supersedes.is_some() {
+ a.supersedes = req.supersedes;
+ }
+ if req.repo_name.is_some() {
+ a.repo_name = req.repo_name;
+ }
+ if let Some(alts) = req.alternatives_considered {
+ a.alternatives_considered = alts;
+ }
+ if let Some(aff) = req.affected_components {
+ a.affected_components = aff;
+ }
+ if req.author.is_some() {
+ a.author = req.author;
+ }
+ if req.git_commit.is_some() {
+ a.git_commit = req.git_commit;
+ }
+ if req.git_branch.is_some() {
+ a.git_branch = req.git_branch;
+ }
+ if req.task_id.is_some() {
+ a.task_id = req.task_id;
+ }
+
+ adrs_to_index.push(a.clone());
+ updated_adr = Some(a.clone());
+ }
+ });
+
+ if let Some(adr) = updated_adr {
+ let idx = state.get_search_index().await;
+ for a in &adrs_to_index {
+ drop(idx.index_adr(a));
+ }
+ state.record_activity(
+ "decision",
+ &format!("Updated {}: {}", adr.id, adr.title),
+ Some(&adr.status),
+ );
+ Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status))
+ } else {
+ Err(crate::error::AppError::Internal(
+ format!("Decision with id '{}' not found", id),
+ ))
+ }
+ }
DecisionAction::Query => {
let limit = req.limit.unwrap_or(20);
let include_body = req.include_body.unwrap_or(true);
@@ -458,6 +556,7 @@ impl McpTool for DecisionsHandler {
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.context, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q)
+ || crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q)
} else {
true
}
@@ -476,6 +575,10 @@ impl McpTool for DecisionsHandler {
"title": a.title,
"status": a.status,
"timestamp": a.timestamp,
+ "git_commit": a.git_commit,
+ "git_branch": a.git_branch,
+ "resolved_at": a.resolved_at,
+ "task_id": a.task_id,
})
})
.collect();
@@ -1787,6 +1890,11 @@ mod tests {
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res1, "Logged decision ADR-0001: Architecture");
+ let q_dec = decision_handler
+ .execute(json!({"action": "query"}), state.clone())
+ .await
+ .unwrap();
+ assert!(q_dec.contains("Simple"));
let debt_handler = TechDebtHandler;
let args_debt = json!({
@@ -2000,6 +2108,40 @@ mod tests {
.await
.unwrap();
assert!(q_dec_res.contains("Use Axum"));
+ assert!(q_dec_res.contains("Fast async API routing"));
+
+ let q_by_consequence = handler_dec
+ .execute(serde_json::json!({"action": "query", "query": "Fast async"}), state.clone())
+ .await
+ .unwrap();
+ assert!(q_by_consequence.contains("Use Axum"));
+
+ let update_res = handler_dec
+ .execute(
+ serde_json::json!({
+ "action": "update",
+ "id": "ADR-0001",
+ "status": "implemented",
+ "git_commit": "abc1234",
+ "git_branch": "master",
+ "task_id": "TASK-123"
+ }),
+ state.clone(),
+ )
+ .await
+ .unwrap();
+ assert!(update_res.contains("Updated decision ADR-0001"));
+ assert!(update_res.contains("implemented"));
+
+ let q_after_update = handler_dec
+ .execute(serde_json::json!({"action": "query", "include_body": false}), state.clone())
+ .await
+ .unwrap();
+ assert!(q_after_update.contains("implemented"));
+ assert!(q_after_update.contains("abc1234"));
+ assert!(q_after_update.contains("master"));
+ assert!(q_after_update.contains("TASK-123"));
+ assert!(q_after_update.contains("resolved_at"));
let del_dec_res = handler_dec
.execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone())
diff --git a/server/src/models.rs b/server/src/models.rs
index 9226fd2..8f0def5 100644
--- a/server/src/models.rs
+++ b/server/src/models.rs
@@ -367,6 +367,7 @@ pub struct Adr {
pub title: String,
pub context: String,
pub decision: String,
+ #[serde(alias = "consequences")]
pub consequence: String,
#[serde(default = "default_adr_status")]
pub status: String,
@@ -383,6 +384,14 @@ pub struct Adr {
pub affected_components: Vec,
#[serde(default)]
pub author: Option,
+ #[serde(default)]
+ pub git_commit: Option,
+ #[serde(default)]
+ pub git_branch: Option,
+ #[serde(default)]
+ pub resolved_at: Option,
+ #[serde(default)]
+ pub task_id: Option,
}
fn default_adr_status() -> String {
diff --git a/server/src/tools.rs b/server/src/tools.rs
index 35a983e..951e416 100644
--- a/server/src/tools.rs
+++ b/server/src/tools.rs
@@ -792,7 +792,7 @@ pub struct SnippetsTool {
pub line_range: Option,
}
-#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
+#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DecisionAction {
#[serde(alias = "log", alias = "LOG", alias = "Log")]
@@ -801,15 +801,18 @@ pub enum DecisionAction {
Query,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete,
+ #[serde(alias = "update", alias = "UPDATE", alias = "Update")]
+ Update,
}
-/// Architectural Decision Records (ADRs) log (log, query, delete).
+/// Architectural Decision Records (ADRs) log (log, query, update, delete).
/// Actions:
/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace.
/// - 'query': Search ADRs. Optional: query, namespace.
+/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title.
/// - 'delete': Remove ADR. Required: id.
-/// Next steps on error: Provide title for 'log' or id for 'delete'.
-#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'.
+#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
pub struct DecisionsTool {
/// Action to perform: 'log', 'query', or 'delete'.
pub action: DecisionAction,
@@ -824,6 +827,7 @@ pub struct DecisionsTool {
/// Decision made (for 'log').
pub decision: Option,
/// Consequences & tradeoffs (for 'log').
+ #[serde(alias = "consequence")]
pub consequences: Option,
/// Search query string (for 'query').
pub query: Option,
@@ -843,6 +847,12 @@ pub struct DecisionsTool {
pub limit: Option,
/// Include the full body of the matched decisions. Defaults to true.
pub include_body: Option,
+ /// Optional Git commit where decision was implemented.
+ pub git_commit: Option,
+ /// Optional Git branch where decision was implemented.
+ pub git_branch: Option,
+ /// Optional task ID linked to this decision.
+ pub task_id: Option,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]