Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

Commit 79da59a

Browse files
committed
feat: add why investigation workflows
1 parent 2dc6c84 commit 79da59a

11 files changed

Lines changed: 402 additions & 3 deletions

File tree

.beads/issues.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{"id":"why-29z","title":"Benchmark coupling and outage performance","status":"closed","priority":3,"issue_type":"task","created_at":"2026-03-31T15:04:47.074853587Z","created_by":"quangdang","updated_at":"2026-03-31T15:12:53.200972486Z","closed_at":"2026-03-31T15:12:53.200930041Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0}
55
{"id":"why-2ky","title":"Adopt Claude Code investigation-runtime patterns in why","description":"Adopt Claude Code's strongest multi-surface investigation patterns in why: shared question-engine ownership, markdown-defined investigation workflows, and bounded result/policy handling for expensive git + LLM operations.","design":"Keep this epic focused on multi-surface investigation quality: a shared question engine, named investigation workflows, and bounded policy-aware handling for expensive git plus model operations.","acceptance_criteria":"Child beads cover engine extraction, workflow loading, and bounded or policy-aware output behavior. The epic stays tightly tied to history-investigation use cases.","notes":"Copy the runtime discipline, not the product-specific surface area.","status":"open","priority":1,"issue_type":"epic","created_at":"2026-03-31T16:34:21.250561562Z","created_by":"Codex","updated_at":"2026-03-31T16:42:07.575816255Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["architecture","claude-code-port","investigation"]}
66
{"id":"why-2ky.1","title":"Extract a shared question engine for why","description":"Extract a shared why question engine that owns history-target normalization, evidence gathering, prompt assembly, model invocation, and result shaping so CLI and future MCP/web surfaces share one investigative core.","design":"Extract a shared question engine that owns target normalization, git evidence collection policy, prompt assembly, model invocation, and bounded result shaping so CLI and future MCP or web surfaces share one investigative core.","acceptance_criteria":"At least two why-facing surfaces can share the question engine boundary, or the engine is clearly transport-neutral and covered by tests. Evidence and result shaping no longer live only in the CLI path. Tests cover engine behavior directly.","notes":"This bead is high leverage because the product thesis already separates evidence gathering from presentation.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-31T16:34:21.928038332Z","created_by":"Codex","updated_at":"2026-04-01T06:22:19.556317528Z","closed_at":"2026-04-01T06:22:19.556153427Z","close_reason":"Extracted why-question-engine for shared target normalization, archaeology, evidence, and report synthesis; rewired CLI default query and LSP hover to consume the shared engine with direct tests.","source_repo":".","compaction_level":0,"original_size":0,"labels":["architecture","claude-code-port","provider-runtime"],"dependencies":[{"issue_id":"why-2ky.1","depends_on_id":"why-2ky","type":"parent-child","created_at":"2026-03-31T16:34:21.928038332Z","created_by":"Codex","metadata":"{}","thread_id":""}]}
7-
{"id":"why-2ky.2","title":"Load markdown-defined investigation workflows in why","description":"Load markdown-defined investigation workflows for common why jobs such as root-cause archaeology, design-intent lookup, regression context gathering, and blame-to-commit narrative assembly.","design":"Load markdown-defined investigation workflows for jobs like root-cause archaeology, design-intent lookup, regression context gathering, and blame-to-commit narrative assembly so agents can call named investigation recipes instead of improvising every prompt.","acceptance_criteria":"Why can discover and load investigation workflows from markdown on disk. At least one named workflow is usable by an agent client or local scripted flow. Parsing, loading, and reload behavior are tested.","notes":"Package investigative intent rather than hardcoding prompt strings inside Rust match arms.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-31T16:34:22.519107188Z","created_by":"Codex","updated_at":"2026-03-31T16:42:39.502855636Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["agent-integration","claude-code-port","workflow"],"dependencies":[{"issue_id":"why-2ky.2","depends_on_id":"why-2ky","type":"parent-child","created_at":"2026-03-31T16:34:22.519107188Z","created_by":"Codex","metadata":"{}","thread_id":""},{"issue_id":"why-2ky.2","depends_on_id":"why-2ky.1","type":"blocks","created_at":"2026-03-31T16:34:23.070264679Z","created_by":"quangdang","metadata":"{}","thread_id":""}]}
7+
{"id":"why-2ky.2","title":"Load markdown-defined investigation workflows in why","description":"Load markdown-defined investigation workflows for common why jobs such as root-cause archaeology, design-intent lookup, regression context gathering, and blame-to-commit narrative assembly.","design":"Load markdown-defined investigation workflows for jobs like root-cause archaeology, design-intent lookup, regression context gathering, and blame-to-commit narrative assembly so agents can call named investigation recipes instead of improvising every prompt.","acceptance_criteria":"Why can discover and load investigation workflows from markdown on disk. At least one named workflow is usable by an agent client or local scripted flow. Parsing, loading, and reload behavior are tested.","notes":"Package investigative intent rather than hardcoding prompt strings inside Rust match arms.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-31T16:34:22.519107188Z","created_by":"Codex","updated_at":"2026-04-01T06:43:21.014208166Z","closed_at":"2026-04-01T06:43:21.013754766Z","close_reason":"Added markdown workflow files, a dedicated loader crate with reload tests, and MCP list/get workflow tools so agents can consume named why investigation recipes from disk.","source_repo":".","compaction_level":0,"original_size":0,"labels":["agent-integration","claude-code-port","workflow"],"dependencies":[{"issue_id":"why-2ky.2","depends_on_id":"why-2ky","type":"parent-child","created_at":"2026-03-31T16:34:22.519107188Z","created_by":"Codex","metadata":"{}","thread_id":""},{"issue_id":"why-2ky.2","depends_on_id":"why-2ky.1","type":"blocks","created_at":"2026-03-31T16:34:23.070264679Z","created_by":"quangdang","metadata":"{}","thread_id":""}]}
88
{"id":"why-2ky.3","title":"Add bounded evidence budgeting and policy gating in why","description":"Add bounded result budgeting, provider-aware policy checks, and operator-readable gate reasons for expensive why operations so deep git walks, large evidence sets, and costly model calls are explicit and controllable.","design":"Add bounded evidence budgeting, provider-aware policy checks, and operator-readable gate reasons for expensive why operations so deep git walks, large evidence sets, and costly model calls are explicit and controllable.","acceptance_criteria":"Why exposes bounded output and evidence policies for expensive investigations. Gate or truncation reasons appear in structured output or diagnostics. Tests cover representative budgeted and gated scenarios.","notes":"This bead should make why safer for agent automation and long-running usage, not merely stricter for humans.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-31T16:34:23.694266474Z","created_by":"Codex","updated_at":"2026-04-01T06:36:06.543249511Z","closed_at":"2026-04-01T06:36:06.542516911Z","close_reason":"Added bounded evidence commit budgets, provider-aware LLM gating, and structured policy/truncation reasons surfaced through why reports and shared question-engine tests.","source_repo":".","compaction_level":0,"original_size":0,"labels":["budgeting","claude-code-port","policy"],"dependencies":[{"issue_id":"why-2ky.3","depends_on_id":"why-2ky","type":"parent-child","created_at":"2026-03-31T16:34:23.694266474Z","created_by":"Codex","metadata":"{}","thread_id":""},{"issue_id":"why-2ky.3","depends_on_id":"why-2ky.1","type":"blocks","created_at":"2026-03-31T16:34:24.310467531Z","created_by":"quangdang","metadata":"{}","thread_id":""}]}
99
{"id":"why-2x9","title":"Reconcile docs with implemented symbol-query support","description":"Update README/POC/spec-style docs to match the actual Rust implementation, which already supports symbol queries across Rust, JavaScript, TypeScript, and Python. Clarify that symbol support is implemented even if richer synthesis/reporting phases remain future work, and align placeholder locator spec text accordingly.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-11T05:27:36.932742629Z","created_by":"quangdang","updated_at":"2026-03-11T05:29:32.161445314Z","closed_at":"2026-03-11T05:29:32.160132720Z","close_reason":"Completed","source_repo":".","compaction_level":0,"original_size":0,"labels":["docs","phase:2","product"]}
1010
{"id":"why-2xa","title":"temp parent blocked repro child","status":"closed","priority":4,"issue_type":"task","created_at":"2026-03-11T11:45:14.052458353Z","created_by":"quangdang","updated_at":"2026-03-11T11:46:16.492127341Z","closed_at":"2026-03-11T11:46:16.492091260Z","close_reason":"Repro child closes after parent unblocked","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"why-2xa","depends_on_id":"why-1hf","type":"parent-child","created_at":"2026-03-11T11:45:26.788940915Z","created_by":"quangdang","metadata":"{}","thread_id":""}]}

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"crates/hooks",
1717
"crates/cache",
1818
"crates/question-engine",
19+
"crates/workflows",
1920
"crates/test-support",
2021
]
2122
resolver = "2"

crates/mcp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ why-archaeologist = { path = "../archaeologist" }
1515
why-locator = { path = "../locator" }
1616
why-scanner = { path = "../scanner" }
1717
why-splitter = { path = "../splitter" }
18+
why-workflows = { path = "../workflows" }

crates/mcp/src/lib.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use why_archaeologist::analyze_target;
66
use why_locator::parse_target;
77
use why_scanner::{scan_coupling, scan_hotspots, scan_rename_safe, scan_time_bombs};
88
use why_splitter::suggest_split;
9+
use why_workflows::{load_builtin_workflow, load_builtin_workflows};
910

1011
const JSONRPC_VERSION: &str = "2.0";
1112
const DEFAULT_TIME_BOMB_AGE_DAYS: i64 = 30;
@@ -156,6 +157,27 @@ fn tools_list_result() -> Value {
156157
"required": ["target"],
157158
"additionalProperties": false
158159
})
160+
),
161+
tool_definition(
162+
"why_list_workflows",
163+
"List builtin why investigation workflows loaded from markdown on disk.",
164+
json!({
165+
"type": "object",
166+
"properties": {},
167+
"additionalProperties": false
168+
})
169+
),
170+
tool_definition(
171+
"why_get_workflow",
172+
"Load a named builtin why investigation workflow from markdown on disk.",
173+
json!({
174+
"type": "object",
175+
"properties": {
176+
"id": { "type": "string" }
177+
},
178+
"required": ["id"],
179+
"additionalProperties": false
180+
})
159181
)
160182
]
161183
})
@@ -294,6 +316,32 @@ fn call_tool(params: Option<Value>) -> std::result::Result<Value, McpError> {
294316
)
295317
})?
296318
}
319+
"why_list_workflows" => serde_json::to_value(
320+
load_builtin_workflows().map_err(|error| McpError::tool_error(error.to_string()))?,
321+
)
322+
.map_err(|error| {
323+
McpError::new(
324+
ErrorCode::InternalError,
325+
format!("failed to serialize why_list_workflows result: {error}"),
326+
)
327+
})?,
328+
"why_get_workflow" => {
329+
let args: WhyGetWorkflowArgs = deserialize_arguments(request.arguments)?;
330+
let workflow = load_builtin_workflow(&args.id)
331+
.map_err(|error| McpError::tool_error(error.to_string()))?
332+
.ok_or_else(|| {
333+
McpError::new(
334+
ErrorCode::InvalidParams,
335+
format!("unknown workflow: {}", args.id),
336+
)
337+
})?;
338+
serde_json::to_value(workflow).map_err(|error| {
339+
McpError::new(
340+
ErrorCode::InternalError,
341+
format!("failed to serialize why_get_workflow result: {error}"),
342+
)
343+
})?
344+
}
297345
other => {
298346
return Err(McpError::new(
299347
ErrorCode::InvalidParams,
@@ -360,6 +408,11 @@ struct WhyRenameSafeArgs {
360408
since_days: Option<u64>,
361409
}
362410

411+
#[derive(Debug, Clone, Deserialize)]
412+
struct WhyGetWorkflowArgs {
413+
id: String,
414+
}
415+
363416
#[derive(Debug, Clone, Deserialize)]
364417
struct ToolCallRequest {
365418
name: String,
@@ -496,13 +549,15 @@ mod tests {
496549
assert!(result["tools"].is_array(), "tools should be array");
497550
let empty_tools = Vec::new();
498551
let tools = result["tools"].as_array().unwrap_or(&empty_tools);
499-
assert_eq!(tools.len(), 6);
552+
assert_eq!(tools.len(), 8);
500553
assert_eq!(tools[0]["name"], "why_symbol");
501554
assert_eq!(tools[1]["name"], "why_split");
502555
assert_eq!(tools[2]["name"], "why_time_bombs");
503556
assert_eq!(tools[3]["name"], "why_hotspots");
504557
assert_eq!(tools[4]["name"], "why_coupling");
505558
assert_eq!(tools[5]["name"], "why_rename_safe");
559+
assert_eq!(tools[6]["name"], "why_list_workflows");
560+
assert_eq!(tools[7]["name"], "why_get_workflow");
506561
}
507562

508563
#[test]
@@ -596,4 +651,41 @@ mod tests {
596651
assert_eq!(error.code, ErrorCode::InvalidParams.as_i32());
597652
assert!(error.message.contains("limit must be greater than zero"));
598653
}
654+
655+
#[test]
656+
fn workflow_tools_return_builtin_markdown_workflows() {
657+
let list_response = handle_request(request(
658+
"tools/call",
659+
json!({
660+
"name": "why_list_workflows",
661+
"arguments": {}
662+
}),
663+
));
664+
assert!(list_response.error.is_none());
665+
let list_payload = &list_response.result.unwrap_or(Value::Null)["content"][0]["json"];
666+
let workflows = list_payload
667+
.as_array()
668+
.expect("workflow list should be array");
669+
assert!(
670+
workflows
671+
.iter()
672+
.any(|workflow| workflow["id"] == "root-cause-archaeology")
673+
);
674+
675+
let get_response = handle_request(request(
676+
"tools/call",
677+
json!({
678+
"name": "why_get_workflow",
679+
"arguments": { "id": "root-cause-archaeology" }
680+
}),
681+
));
682+
assert!(get_response.error.is_none());
683+
let workflow = &get_response.result.unwrap_or(Value::Null)["content"][0]["json"];
684+
assert_eq!(workflow["id"], "root-cause-archaeology");
685+
assert!(
686+
workflow["body"]
687+
.as_str()
688+
.is_some_and(|body| body.contains("Run the default `why` report first"))
689+
);
690+
}
599691
}

crates/workflows/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "why-workflows"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
publish = false
9+
10+
[dependencies]
11+
anyhow.workspace = true
12+
serde.workspace = true
13+
14+
[dev-dependencies]
15+
tempfile = "3"

0 commit comments

Comments
 (0)