From 5474bb6578fa03abaa6b95b9be161e46a743bebb Mon Sep 17 00:00:00 2001 From: main Date: Fri, 20 Mar 2026 23:59:48 -0400 Subject: Move issues into state dir --- README.md | 4 ++-- assets/codex-skills/jira-at-home/SKILL.md | 4 ++-- crates/jira-at-home/src/mcp/catalog.rs | 4 ++-- crates/jira-at-home/src/mcp/host/runtime.rs | 2 +- crates/jira-at-home/src/mcp/service.rs | 26 ++++++++++++-------------- crates/jira-at-home/src/store.rs | 4 ++-- crates/jira-at-home/tests/mcp_hardening.rs | 5 +++-- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index cf982cc..aa852ad 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The domain is mercilessly small: -- `issues/.md` stores the actual note body +- `~/.local/state/jira_at_home/projects/.../issues/.md` stores the actual note body - `issue.save` overwrites or creates one note - `issue.list` enumerates the existing slugs - `issue.read` returns the note body for one slug @@ -15,6 +15,6 @@ The feature set stays primitive, but the transport posture is not: - explicit replay contracts - porcelain-by-default tool output - hot host reexec through `libmcp` session snapshots -- append-only JSONL telemetry outside the repo under the platform state dir +- issue bodies and append-only JSONL telemetry outside the repo under the platform state dir Use `cargo run -- mcp serve --project .` to launch it against the current repo. diff --git a/assets/codex-skills/jira-at-home/SKILL.md b/assets/codex-skills/jira-at-home/SKILL.md index 541c7f7..be759d1 100644 --- a/assets/codex-skills/jira-at-home/SKILL.md +++ b/assets/codex-skills/jira-at-home/SKILL.md @@ -1,6 +1,6 @@ --- name: jira-at-home -description: Use when you want brutally basic per-project issue parking in this repository: bind a project, save a freeform note to `issues/.md`, list existing issue slugs, or read one issue back. Keep the workflow primitive; there are no issue types, statuses, or schema beyond slug plus Markdown body. +description: Use when you want brutally basic per-project issue parking without touching the worktree: bind a project, save a freeform note to the project’s state-backed `issues/.md`, list existing issue slugs, or read one issue back. Keep the workflow primitive; there are no issue types, statuses, or schema beyond slug plus Markdown body. --- -Bind the target project with `project.bind` before issue work unless the MCP was started with `--project`; then use `issue.save` to create or overwrite `issues/.md`, `issue.list` to enumerate the currently open issue files, and `issue.read` to recover one note body by slug. Treat the store as a tiny parked-ideas notebook, not a tracker: every issue is just freeform Markdown in the canonical `issues/` directory, and the only operational tools worth touching are `system.health` and `system.telemetry` when the transport itself looks suspect. +Bind the target project with `project.bind` before issue work unless the MCP was started with `--project`; then use `issue.save` to create or overwrite `issues/.md` under the project’s external state root, `issue.list` to enumerate the currently open issue files, and `issue.read` to recover one note body by slug. Treat the store as a tiny parked-ideas notebook, not a tracker: every issue is just freeform Markdown outside the repo in the canonical `issues/` directory for that project, and the only operational tools worth touching are `system.health` and `system.telemetry` when the transport itself looks suspect. diff --git a/crates/jira-at-home/src/mcp/catalog.rs b/crates/jira-at-home/src/mcp/catalog.rs index 2ff8e81..d6ce29b 100644 --- a/crates/jira-at-home/src/mcp/catalog.rs +++ b/crates/jira-at-home/src/mcp/catalog.rs @@ -47,7 +47,7 @@ const TOOL_SPECS: &[ToolSpec] = &[ }, ToolSpec { name: "issue.save", - description: "Create or overwrite one issue note at `issues/.md`.", + description: "Create or overwrite one issue note at `issues/.md` under the bound project's external state root.", dispatch: DispatchTarget::Worker, replay: ReplayContract::NeverReplay, }, @@ -112,7 +112,7 @@ fn tool_schema(name: &str) -> Value { "properties": { "slug": { "type": "string", - "description": "Stable slug. Stored at `issues/.md`." + "description": "Stable slug. Stored at `issues/.md` under the bound project's external state root." }, "body": { "type": "string", diff --git a/crates/jira-at-home/src/mcp/host/runtime.rs b/crates/jira-at-home/src/mcp/host/runtime.rs index b2049a0..1994632 100644 --- a/crates/jira-at-home/src/mcp/host/runtime.rs +++ b/crates/jira-at-home/src/mcp/host/runtime.rs @@ -230,7 +230,7 @@ impl HostRuntime { "name": SERVER_NAME, "version": env!("CARGO_PKG_VERSION") }, - "instructions": "Bind the session with project.bind, then use issue.save to park ideas in issues/.md. issue.list enumerates every existing issue file because there is no closed state." + "instructions": "Bind the session with project.bind, then use issue.save to park ideas in the bound state directory under issues/.md. issue.list enumerates every existing issue file because there is no closed state." }))), "notifications/initialized" => { if !self.seed_captured() { diff --git a/crates/jira-at-home/src/mcp/service.rs b/crates/jira-at-home/src/mcp/service.rs index fc9dbf0..2a98829 100644 --- a/crates/jira-at-home/src/mcp/service.rs +++ b/crates/jira-at-home/src/mcp/service.rs @@ -84,7 +84,7 @@ impl WorkerService { .map_err(store_fault(self.generation, &operation))?; issue_save_output( &receipt, - self.store.layout().project_root.as_path(), + self.store.layout().state_root.as_path(), self.generation, &operation, )? @@ -96,7 +96,7 @@ impl WorkerService { .map_err(store_fault(self.generation, &operation))?; issue_list_output( &issues, - self.store.layout().project_root.as_path(), + self.store.layout().state_root.as_path(), self.generation, &operation, )? @@ -111,7 +111,7 @@ impl WorkerService { .map_err(store_fault(self.generation, &operation))?; issue_read_output( &record, - self.store.layout().project_root.as_path(), + self.store.layout().state_root.as_path(), self.generation, &operation, )? @@ -189,11 +189,11 @@ fn store_fault( fn issue_save_output( receipt: &SaveReceipt, - project_root: &Path, + state_root: &Path, generation: Generation, operation: &str, ) -> Result { - let relative_path = relative_issue_path(&receipt.path, project_root); + let relative_path = relative_issue_path(&receipt.path, state_root); let status = if receipt.created { "created" } else { @@ -218,7 +218,7 @@ fn issue_save_output( [ format!("saved issue {}", receipt.slug), format!("status: {status}"), - format!("path: {}", relative_issue_path(&receipt.path, project_root)), + format!("path: {}", relative_issue_path(&receipt.path, state_root)), format!("updated: {}", format_timestamp(receipt.updated_at)), ] .join("\n"), @@ -232,7 +232,7 @@ fn issue_save_output( fn issue_list_output( issues: &[crate::store::IssueSummary], - project_root: &Path, + state_root: &Path, generation: Generation, operation: &str, ) -> Result { @@ -249,10 +249,8 @@ fn issue_list_output( .iter() .map(|issue| { let path = relative_issue_path( - &project_root - .join("issues") - .join(format!("{}.md", issue.slug)), - project_root, + &state_root.join("issues").join(format!("{}.md", issue.slug)), + state_root, ); json!({ "slug": issue.slug, @@ -277,11 +275,11 @@ fn issue_list_output( fn issue_read_output( record: &IssueRecord, - project_root: &Path, + state_root: &Path, generation: Generation, operation: &str, ) -> Result { - let relative_path = relative_issue_path(&record.path, project_root); + let relative_path = relative_issue_path(&record.path, state_root); let concise = json!({ "slug": record.slug, "updated_at": format_timestamp(record.updated_at), @@ -303,7 +301,7 @@ fn issue_read_output( let full_text = Some(format!( "issue {}\npath: {}\nupdated: {}\nbytes: {}\n\n{}", record.slug, - relative_issue_path(&record.path, project_root), + relative_issue_path(&record.path, state_root), format_timestamp(record.updated_at), record.bytes, record.body, diff --git a/crates/jira-at-home/src/store.rs b/crates/jira-at-home/src/store.rs index faf2c9a..238477a 100644 --- a/crates/jira-at-home/src/store.rs +++ b/crates/jira-at-home/src/store.rs @@ -102,9 +102,9 @@ impl ProjectLayout { pub(crate) fn bind(requested_path: impl Into) -> Result { let requested_path = requested_path.into(); let project_root = resolve_project_root(&requested_path)?; - let issues_root = project_root.join(ISSUES_DIR_NAME); - fs::create_dir_all(&issues_root)?; let state_root = external_state_root(&project_root)?; + let issues_root = state_root.join(ISSUES_DIR_NAME); + fs::create_dir_all(&issues_root)?; fs::create_dir_all(state_root.join("mcp"))?; Ok(Self { requested_path, diff --git a/crates/jira-at-home/tests/mcp_hardening.rs b/crates/jira-at-home/tests/mcp_hardening.rs index 02f4fda..52d9a35 100644 --- a/crates/jira-at-home/tests/mcp_hardening.rs +++ b/crates/jira-at-home/tests/mcp_hardening.rs @@ -231,7 +231,7 @@ fn cold_start_exposes_basic_toolset_and_binding_surface() -> TestResult { } #[test] -fn save_list_and_read_roundtrip_through_canonical_issue_dir() -> TestResult { +fn save_list_and_read_roundtrip_through_state_backed_issue_dir() -> TestResult { let project_root = temp_project_root("roundtrip")?; let state_home = project_root.join("state-home"); must(fs::create_dir_all(&state_home), "create state home")?; @@ -263,11 +263,12 @@ fn save_list_and_read_roundtrip_through_canonical_issue_dir() -> TestResult { Some("issues/feral-machine.md") ); - let saved_path = project_root.join("issues").join("feral-machine.md"); + let saved_path = state_root.join("issues").join("feral-machine.md"); assert_eq!( must(fs::read_to_string(&saved_path), "read saved issue")?, body ); + assert!(!project_root.join("issues").exists()); let list = harness.call_tool(4, "issue.list", json!({}))?; assert_tool_ok(&list); -- cgit v1.2.3