Skip to main content

cpm_planner/
lib.rs

1// T26 — restriction-category lint on production code only.
2// `#[cfg(test)]` modules inside production sources DO see this when
3// invoked via `cargo build`, but `cargo test` evaluates `not(test)`
4// as false (test cfg is on) and silences the warning everywhere —
5// which is what we want: tests panic deliberately via unwrap, prod
6// code should `.expect("invariant: ...")` or propagate.
7#![cfg_attr(not(test), warn(clippy::unwrap_used))]
8
9//! cpm-planner: a Critical Path Method (CPM) planner for agent work, exposed
10//! as a standalone MCP server and usable as a plain Rust library.
11//!
12//! The CPM kernel does the forward pass (earliest start/finish), backward pass
13//! (latest start/finish), slack computation, critical-path identification,
14//! parallel batch grouping, and bottleneck (ROI) analysis.
15//!
16//! On top of that kernel, [`BasicCpmPlanner`] implements the lock-aware
17//! [`Planner`](ports::Planner) trait: callers submit a [`PlanGraph`](plan::PlanGraph),
18//! then acquire / heartbeat / release locks on disjoint cohorts of deliverables
19//! so that multiple workers can run in parallel without stepping on each other.
20//! The same planner keeps named plan lines and variants (plan-as-code files
21//! under `.cpm-planner/plans/`), frozen earned-value baselines and snapshots,
22//! and durable state in SQLite ([`SqlitePlanStore`]). [`PlanServer`] surfaces
23//! all of it as MCP tools (`plan.submit`, `plan.acquire_cohort`, `plan.ev`, …)
24//! over stdio, so any MCP-speaking client — Claude Code, Cursor, a custom
25//! orchestrator, or a praxec workflow `connection` — can drive it.
26//!
27//! # Example
28//!
29//! Schedule a graph, then lease and complete its first ready cohort:
30//!
31//! ```
32//! use cpm_planner::plan::{AcquireRequest, CallerId, DeliverableStatus, PlanGraph};
33//! use cpm_planner::ports::Planner;
34//! use cpm_planner::{BasicCpmPlanner, MarkStatusRequest};
35//!
36//! # fn main() -> anyhow::Result<()> {
37//! let graph: PlanGraph = serde_json::from_value(serde_json::json!({
38//!     "deliverables": [
39//!         {"id": "design", "owned_files": ["docs/design.md"], "prerequisites": [],
40//!          "estimated_effort_hours": 4.0},
41//!         {"id": "build", "owned_files": ["src/app.rs"], "prerequisites": ["design"],
42//!          "estimated_effort_hours": 8.0},
43//!         {"id": "docs", "owned_files": ["README.md"], "prerequisites": ["design"],
44//!          "estimated_effort_hours": 2.0}
45//!     ]
46//! }))?;
47//!
48//! // Pure CPM: `__start__` and `__finish__` are synthetic zero-length endpoints.
49//! let cpm = cpm_planner::schedule::compute_cpm(&graph)?;
50//! assert_eq!(cpm.critical_path, ["__start__", "design", "build", "__finish__"]);
51//! assert_eq!(cpm.critical_path_duration, 12.0);
52//!
53//! // Lock-aware execution on a private in-memory store.
54//! let runtime = tokio::runtime::Runtime::new()?;
55//! runtime.block_on(async {
56//!     let planner = BasicCpmPlanner::new();
57//!     let plan_id = planner.submit_plan(graph).await?;
58//!     let caller = CallerId("worker-1".into());
59//!
60//!     let cohort = planner
61//!         .acquire_cohort(AcquireRequest::new(plan_id.clone(), caller.clone(), 4))
62//!         .await?;
63//!     let leased: Vec<&str> = cohort.rows.iter().map(|r| r.deliverable.id.as_str()).collect();
64//!     assert_eq!(leased, ["design"]); // `build` and `docs` wait on it
65//!
66//!     planner
67//!         .mark_status(MarkStatusRequest::new(
68//!             plan_id.clone(),
69//!             "design",
70//!             caller.clone(),
71//!             DeliverableStatus::Complete,
72//!         ))
73//!         .await?;
74//!
75//!     // Completing `design` releases its lock and readies both dependents.
76//!     let next = planner
77//!         .acquire_cohort(AcquireRequest::new(plan_id, caller, 4))
78//!         .await?;
79//!     let mut leased: Vec<&str> = next.rows.iter().map(|r| r.deliverable.id.as_str()).collect();
80//!     leased.sort();
81//!     assert_eq!(leased, ["build", "docs"]);
82//!     Ok::<_, anyhow::Error>(())
83//! })?;
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! # Layout
89//!
90//! - [`plan`] — the wire/domain model (deliverables, cohorts, locks, errors).
91//! - [`ports`] — the [`Planner`](ports::Planner) trait.
92//! - [`algorithm`] / [`task`] — the pure CPM kernel and its internal model
93//!   (the `Task` types carry ES/EF/LS/LF/slack/batching state the wire model
94//!   doesn't need to expose).
95//! - [`schedule`] — [`schedule::compute_cpm`], the CPM run shared by submit,
96//!   status and the analysis tools.
97//! - [`planner`] — [`BasicCpmPlanner`], the lock-aware implementation
98//!   (including the earned-value operations).
99//! - [`plan_store`] — durable SQLite persistence (plans, statuses, cohort
100//!   locks, submit dedup, portfolio lines/variants/revisions, earned value)
101//!   and the cross-process atomicity mechanism.
102//! - [`project`] — project-root discovery and confined plan-file I/O.
103//! - [`edits`], [`revise`] — structured graph edits and progress-preserving
104//!   revision.
105//! - [`lint`], [`simulate`], [`resource_schedule`], [`monte_carlo`],
106//!   [`compare`], [`metrics`], [`drag`](mod@drag), [`risk`], [`network_health`] — the
107//!   read-only analysis tools and the plan scorecard.
108//! - [`earned_value`] — the pure PV / EV / AC engine.
109//! - [`review`], [`llm`] — the optional `plan.review` and its OpenRouter /
110//!   Jev judge.
111//! - [`server`] — the MCP tool façade.
112//! - [`skills`] — the embedded agent skills and `cpm-planner skills install`.
113//! - [`audit`] — the lock-lifecycle audit surface.
114//!
115//! See `docs/architecture.md` in the repository for the request flow, the
116//! store schema and the concurrency model.
117//!
118//! This crate has no dependency on praxec; it is consumed purely over the
119//! MCP protocol.
120
121pub mod algorithm;
122pub mod audit;
123pub mod compare;
124pub mod drag;
125pub mod earned_value;
126pub mod edits;
127pub mod estimator;
128pub(crate) mod ev_store;
129mod graph;
130#[cfg(test)]
131mod lease_hours_tests;
132pub mod lint;
133pub mod llm;
134mod locks;
135#[cfg(test)]
136mod mark_actuals_tests;
137pub mod metrics;
138pub mod monte_carlo;
139pub mod network_health;
140pub mod plan;
141pub mod plan_store;
142pub mod planner;
143mod portfolio;
144pub mod ports;
145pub mod project;
146pub mod resource_schedule;
147pub mod review;
148pub mod revise;
149pub mod risk;
150pub mod schedule;
151pub mod server;
152pub mod simulate;
153pub mod skills;
154pub mod task;
155
156pub use algorithm::CpmAlgorithm;
157pub use drag::{DragResult, diameter, drag};
158pub use estimator::{EffortEstimator, EstimationConfig};
159pub use plan::{
160    AcceptRequest, AcquireRequest, Estimate, ForceReleaseRequest, HeartbeatRequest,
161    MarkStatusRequest,
162};
163pub use plan_store::{DB_PATH_ENV, SqlitePlanStore};
164pub use planner::{
165    BasicCpmPlanner, ClockFn, DEFAULT_EFFORT_HOURS, DEFAULT_MAX_TTL, DEFAULT_TTL, MAX_ATTEMPTS,
166    MAX_LAPSES,
167};
168pub use server::{
169    PLAN_TOOL_NAMES, PlanServer, TOOL_ACCEPT, TOOL_ACQUIRE_COHORT, TOOL_FORCE_RELEASE,
170    TOOL_HEARTBEAT, TOOL_MARK_STATUS, TOOL_REVIEW, TOOL_STATUS, TOOL_SUBMIT, plan_tool_definitions,
171};
172pub use task::{Bottleneck, CriticalPathResult, Task, TaskBatch, TaskKind, TaskStatus};