Skip to main content

oxicode_agent/issues/
mod.rs

1//! Local issue tracking system — GitHub-style issues stored as markdown files.
2//!
3//! Issues live in `.oxicode/issues/` at the project root (discovered by walking
4//! up from the current directory until `.oxicode/` is found, mirroring
5//! `Settings::find_project_settings`). Each issue is a single markdown file
6//! with a YAML frontmatter block holding structured metadata, followed by a
7//! free-form markdown body:
8//!
9//! ```markdown
10//! ---
11//! id: 12
12//! title: "Fix login bug"
13//! status: open
14//! priority: high
15//! labels: [bug, auth]
16//! assignee: null
17//! created_at: 2026-06-17T10:30:00Z
18//! updated_at: 2026-06-17T14:20:00Z
19//! closed_at: null
20//! sessions: [abc123, def456]
21//! assigned_to: null
22//! github: null
23//! ---
24//!
25//! Free-form markdown body...
26//! ```
27//!
28//! # Design decisions
29//!
30//! - **Why not a `StateStore` port?** Issues are *documents* that humans open
31//!   in `$EDITOR`, commit to git, and diff. `StateStore` is an opaque KV/append
32//!   blob. Different workload, different storage shape.
33//! - **Optimistic concurrency (content-hash CAS).** Mutations take an optional
34//!   `content_hash` captured at the last read. The write is rejected if the
35//!   on-disk content has changed since. This is the exact pattern used by the
36//!   `edit` tool (`oxicode-agent/src/tools/edit.rs`), so external edits (e.g.
37//!   someone editing the file in vim) are detected without any locking.
38//! - **Atomic writes** via temp+rename.
39//! - **Assignment is process-liveness based, not time based.** An assigned
40//!   issue records the owning session id. Whether that session is still alive
41//!   is determined by an OS-held advisory lock on
42//!   `.oxicode/issues/.alive/<session_id>` (see [`liveness`]). When the owning
43//!   process exits — including `kill -9`, crash, or terminal close — the OS
44//!   releases the lock and the assignment becomes stale and reclaimable.
45
46use std::io;
47use std::path::Path;
48pub mod error;
49pub mod filter;
50pub mod liveness;
51pub mod serialize;
52pub mod store;
53pub mod types;
54
55// Re-export the public surface so callers can keep using
56// `crate::issues::*` exactly as before the directory split.
57pub use error::IssueError;
58pub use filter::IssueFilter;
59pub use serialize::{content_hash, issue_filename, issues_dir, parse_issue, serialize_issue};
60pub use store::{FileIssueStore, IssueSummary};
61pub use types::{Assignment, GithubRef, Issue, IssueMeta, IssuePatch, Priority, Status};
62
63/// Atomically write a string to `path` (temp file + rename).
64///
65/// Moved from the CLI's `store::fs_util` when the issues module relocated to
66/// this crate; semantics preserved byte-for-byte so on-disk temp artifacts
67/// stay identical.
68pub(crate) fn atomic_write(path: &Path, content: &str) -> io::Result<()> {
69    let tmp = path.with_extension(format!(
70        "tmp.{}.{}",
71        std::process::id(),
72        uuid::Uuid::new_v4().simple()
73    ));
74    std::fs::write(&tmp, content)?;
75    match std::fs::rename(&tmp, path) {
76        Ok(()) => Ok(()),
77        Err(e) => {
78            // Best-effort cleanup; the rename error is the one we propagate.
79            let _ = std::fs::remove_file(&tmp);
80            Err(e)
81        }
82    }
83}