From dce3519345b905d610f4747d8a090f1dac22b68e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 3 Jun 2026 20:38:57 -0700 Subject: [PATCH] feat: heph list --project + --json; thin AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `heph list --project ` lists a project's outstanding tasks by name (subtree-expanded, resolved server-side via a new project.scope path that reuses the view machinery; errors on unknown names). `--json` prints raw rows — node_id, canonical_context_id, attention/state/do_date/late_on/ recurrence/project_id — for scripting and agents. Store::project_scope on the trait + LocalStore + RemoteStore; new project.scope RPC and a flattened ListParams so `list` accepts an optional project name. Test covers resolve-by-name + unknown-name error. AGENTS.md thinned to tight command/pattern sections: dropped the historical parity narrative and the verbose roadmap section; added a "Working state" section documenting `heph list --project Hephaestus [--json]` as the way to inspect heph's self-hosted roadmap. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 17 ++++++------- crates/heph-core/src/sqlite/mod.rs | 27 ++++++++++++++++++--- crates/heph-core/src/sqlite/tasks.rs | 11 +++++++++ crates/heph-core/src/store.rs | 4 +++ crates/heph/src/main.rs | 23 +++++++++++++++--- crates/hephd/src/remote.rs | 4 +++ crates/hephd/src/rpc.rs | 21 +++++++++++++++- docs/changelog.d/v1-list-project.feature.md | 1 + 8 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 docs/changelog.d/v1-list-project.feature.md diff --git a/AGENTS.md b/AGENTS.md index 724eb15..39227d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ See [[agent-change-process]] for the full methodology. ## Project Structure -A Cargo workspace (`Cargo.toml` at the root) plus the Neovim plugin and repo tooling. **v1 reached Todoist feature-parity on 2026-06-03** — the **Rust backend is feature-complete** (all three runtime modes + sync + OIDC auth) and all three surfaces (`heph` CLI, **`heph-tui`**, **`heph.nvim`**) are installed daily-drivers. **Remaining/future work is now tracked in heph itself** (see *Planning* below), not in a doc; the **[[v1-prototype-tech-spec]]** is the historical build record. +A Cargo workspace (`Cargo.toml` at root) plus the Neovim plugin and repo tooling. Backend feature-complete (all three runtime modes + sync + OIDC); three daily-driver surfaces — `heph` (CLI), `heph-tui` (agenda/triage), `heph.nvim` (context/KB). ``` ./Cargo.toml # workspace manifest (shared deps + members) @@ -58,15 +58,14 @@ A Cargo workspace (`Cargo.toml` at the root) plus the Neovim plugin and repo too ./mise-tasks/ # repo automation via `mise run` ``` -**Development is TDD** (v1-prototype-tech-spec §2, §9): failing test first, implement to green, commit on green. `heph-core` is clock-injected — no ambient wall-clock reads; time is always passed in. The **historical v1 build spec** is [[v1-prototype-tech-spec]]; the **living rationale/decisions** are [[design]]. +**TDD:** failing test first → implement to green → commit on green. `heph-core` is clock-injected (no ambient wall-clock; time is passed in). Spec: [[v1-prototype-tech-spec]] (frozen v1 build record); rationale: [[design]] (living). Other doc paths via `mise run ai-docs`; `[[like-this]]` wiki-links refer to `docs/` cards. -Other doc paths are listed via `mise run ai-docs`. Wiki-links (`[[like-this]]`) refer to `docs/` cards. +## Working state (heph self-hosts its roadmap) -## Planning future work (heph self-hosts its roadmap) +Outstanding/future heph work lives as tasks in the **`Hephaestus` project** — inspect it before planning: -Since v1 parity, **heph tracks its own remaining/future work as tasks in the `Hephaestus` project inside the live store** (the "bootstrap lift" — heph plans heph). This replaces the old [[v1-prototype-tech-spec]] §14 tracker, which is now a historical build record. +- `heph list --project Hephaestus` — outstanding tasks (human-readable) +- `heph list --project Hephaestus --json` — JSON rows: `node_id`, `canonical_context_id`, attention/state/do_date/recurrence (for scripting/agents) +- `heph task "" --project Hephaestus -a blue` — capture new work (blue = on-deck backlog) -- **See the roadmap:** `heph view ondeck` (CLI) or `heph-tui` → the **On Deck** sidebar view (the backlog lives as blue/on-deck tasks); open the **Hephaestus** project in the sidebar to see all of it. -- **Capture new work:** `heph task "<title>" --project Hephaestus -a blue` (blue = on-deck backlog, kept out of `next`/ToM until pulled up). Add detail in the task's canonical-context doc via `heph.nvim`. -- **Don't reopen the §14 tracker** for new work — add a task instead. Update the spec only to correct historical record. -- Note the [[design]] doc remains the **living** design/decision log; the tech-spec is frozen as the v1 build description. +Triage in `heph-tui` (*On Deck* view). Add a task for new work — don't reopen the frozen §14 tracker. diff --git a/crates/heph-core/src/sqlite/mod.rs b/crates/heph-core/src/sqlite/mod.rs index d4b93f1..2a11ef1 100644 --- a/crates/heph-core/src/sqlite/mod.rs +++ b/crates/heph-core/src/sqlite/mod.rs @@ -282,6 +282,10 @@ impl Store for LocalStore { tasks::view(&self.conn, &self.owner_id, now, name) } + fn project_scope(&self, name: &str) -> Result<Vec<String>> { + tasks::project_scope(&self.conn, &self.owner_id, name) + } + fn health(&self) -> Result<Health> { tasks::health(&self.conn, &self.owner_id) } @@ -470,6 +474,21 @@ mod tests { assert_eq!(v, latest_version()); } + #[test] + fn project_scope_resolves_by_name_and_errors_on_unknown() { + use crate::model::{NewNode, NodeKind}; + let mut store = store_at(1); + let p = store + .create_node(NewNode { + kind: NodeKind::Project, + title: "Garden".into(), + body: None, + }) + .unwrap(); + assert_eq!(store.project_scope("Garden").unwrap(), vec![p.id]); + assert!(store.project_scope("Nope").is_err()); + } + #[test] fn delete_project_unfiles_its_tasks_then_tombstones_it() { use crate::filter::ListFilter; @@ -511,9 +530,11 @@ mod tests { // The project node is tombstoned… let ts: i64 = store .conn - .query_row("SELECT tombstoned FROM nodes WHERE id=?1", [&proj.id], |r| { - r.get(0) - }) + .query_row( + "SELECT tombstoned FROM nodes WHERE id=?1", + [&proj.id], + |r| r.get(0), + ) .unwrap(); assert_eq!(ts, 1); // …and the task survives, now unfiled (it shows in the Inbox), not orphaned. diff --git a/crates/heph-core/src/sqlite/tasks.rs b/crates/heph-core/src/sqlite/tasks.rs index 1d5e9c7..3df2a53 100644 --- a/crates/heph-core/src/sqlite/tasks.rs +++ b/crates/heph-core/src/sqlite/tasks.rs @@ -438,6 +438,17 @@ fn resolve_project_names(conn: &Connection, owner: &str, names: &[&str]) -> Resu Ok(ids) } +/// Resolve a single project NAME to its scope: the project id plus its subtree +/// (parent→child). Errors if the name names no project, so `--project Foo` fails +/// loudly rather than silently widening to "everything". +pub(super) fn project_scope(conn: &Connection, owner: &str, name: &str) -> Result<Vec<String>> { + let scope = resolve_project_names(conn, owner, &[name])?; + if scope.is_empty() { + return Err(Error::InvalidArg(format!("no project named {name:?}"))); + } + Ok(scope) +} + /// Working-set health counts (tech-spec §7) — surfaced honestly. pub(super) fn health(conn: &Connection, owner: &str) -> Result<Health> { let mut stmt = conn.prepare( diff --git a/crates/heph-core/src/store.rs b/crates/heph-core/src/store.rs index a582abd..cb0475d 100644 --- a/crates/heph-core/src/store.rs +++ b/crates/heph-core/src/store.rs @@ -128,6 +128,10 @@ pub trait Store { /// unknown view name. fn view(&self, name: &str) -> Result<Vec<RankedTask>>; + /// Resolve a project NAME to its scope ids (the project + its subtree), for + /// `heph list --project <name>`. Errors if the name names no project. + fn project_scope(&self, name: &str) -> Result<Vec<String>>; + /// Working-set health — orange/active/on-deck/conflict counts (tech-spec §7). fn health(&self) -> Result<Health>; diff --git a/crates/heph/src/main.rs b/crates/heph/src/main.rs index 8c2f8b0..505aa93 100644 --- a/crates/heph/src/main.rs +++ b/crates/heph/src/main.rs @@ -68,12 +68,18 @@ enum Command { /// Restrict to a project node id. #[arg(long)] scope: Option<String>, + /// Restrict to a project by NAME (subtree-expanded). e.g. --project Hephaestus. + #[arg(long)] + project: Option<String>, /// Only this attention-state: white|orange|red|blue. #[arg(short = 'a', long)] attention: Option<String>, /// Hide on-deck (blue) items. #[arg(long)] no_blue: bool, + /// Print raw JSON rows (node id, canonical-context id, scalars) for scripting/agents. + #[arg(long)] + json: bool, }, /// Run a built-in filter view (tech-spec §8.2); omit the name to list views. View { @@ -429,16 +435,21 @@ fn main() -> Result<()> { } Command::List { scope, + project, attention, no_blue, + json, } => { - // `list` takes a ListFilter (tech-spec §8.2). Map the legacy flags: - // a single `--scope` id, a single `--attention` whitelist, and - // `--no-blue` as an attention exclusion. + // `list` takes a ListFilter (tech-spec §8.2). Map the flags: a single + // `--scope` id or `--project` NAME (resolved + subtree-expanded by the + // daemon), a single `--attention` whitelist, and `--no-blue`. let mut filter = json!({}); if let Some(s) = scope { filter["scope"] = json!([s]); } + if let Some(p) = project { + filter["project"] = json!(p); + } if let Some(a) = attention { filter["attention_in"] = json!([a]); } @@ -446,7 +457,11 @@ fn main() -> Result<()> { filter["attention_not"] = json!(["blue"]); } let result = client.call("list", filter)?; - print_rows(result)?; + if json { + println!("{}", serde_json::to_string_pretty(&result)?); + } else { + print_rows(result)?; + } } Command::View { name } => match name { Some(name) => { diff --git a/crates/hephd/src/remote.rs b/crates/hephd/src/remote.rs index 55b12b8..9f405a8 100644 --- a/crates/hephd/src/remote.rs +++ b/crates/hephd/src/remote.rs @@ -209,6 +209,10 @@ impl Store for RemoteStore { self.call_as("view", json!({ "name": name })) } + fn project_scope(&self, name: &str) -> Result<Vec<String>> { + self.call_as("project.scope", json!({ "name": name })) + } + fn health(&self) -> Result<Health> { self.call_as("health", json!({})) } diff --git a/crates/hephd/src/rpc.rs b/crates/hephd/src/rpc.rs index 1b7a947..5f9a59c 100644 --- a/crates/hephd/src/rpc.rs +++ b/crates/hephd/src/rpc.rs @@ -112,6 +112,16 @@ struct IdParam { id: String, } +/// `list` params: a [`ListFilter`] plus an optional `project` NAME the daemon +/// resolves (subtree-expanded) into the filter's `scope`. +#[derive(Deserialize)] +struct ListParams { + #[serde(flatten)] + filter: ListFilter, + #[serde(default)] + project: Option<String>, +} + #[derive(Deserialize)] struct GetNodeParams { id: String, @@ -387,13 +397,22 @@ pub fn dispatch(store: &mut dyn Store, method: &str, params: Value) -> Result<Va json!(store.next(p.scope.as_deref(), p.limit.unwrap_or(DEFAULT_LIMIT))?) } "list" => { - let filter: ListFilter = parse(params)?; + let p: ListParams = parse(params)?; + let mut filter = p.filter; + // `--project <name>` resolves to its subtree scope server-side. + if let Some(name) = p.project { + filter.scope = store.project_scope(&name)?; + } json!(store.list(&filter)?) } "view" => { let p: ViewParams = parse(params)?; json!(store.view(&p.name)?) } + "project.scope" => { + let p: ViewParams = parse(params)?; + json!(store.project_scope(&p.name)?) + } "health" => json!(store.health()?), "search" => { let p: SearchParams = parse(params)?; diff --git a/docs/changelog.d/v1-list-project.feature.md b/docs/changelog.d/v1-list-project.feature.md new file mode 100644 index 0000000..a8d7f2a --- /dev/null +++ b/docs/changelog.d/v1-list-project.feature.md @@ -0,0 +1 @@ +- **`heph list --project <name>` + `--json`** (§8.2): list a project's outstanding tasks by **name** (subtree-expanded, resolved server-side via a new `project.scope` path that reuses the view machinery — errors loudly on an unknown name), and `--json` prints the raw rows (`node_id`, `canonical_context_id`, attention/state/do_date/late_on/recurrence/project_id) for scripting and agents. This is the canonical "show me a project's outstanding work" command — `AGENTS.md` documents it as how to inspect heph's own roadmap (the `Hephaestus` project), now that heph self-hosts it.