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 "" --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> {
+ tasks::project_scope(&self.conn, &self.owner_id, name)
+ }
+
fn health(&self) -> Result {
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> {
+ 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 {
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>;
+ /// Resolve a project NAME to its scope ids (the project + its subtree), for
+ /// `heph list --project `. Errors if the name names no project.
+ fn project_scope(&self, name: &str) -> Result>;
+
/// Working-set health — orange/active/on-deck/conflict counts (tech-spec §7).
fn health(&self) -> Result;
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,
+ /// Restrict to a project by NAME (subtree-expanded). e.g. --project Hephaestus.
+ #[arg(long)]
+ project: Option,
/// Only this attention-state: white|orange|red|blue.
#[arg(short = 'a', long)]
attention: Option,
/// 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> {
+ self.call_as("project.scope", json!({ "name": name }))
+ }
+
fn health(&self) -> Result {
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,
+}
+
#[derive(Deserialize)]
struct GetNodeParams {
id: String,
@@ -387,13 +397,22 @@ pub fn dispatch(store: &mut dyn Store, method: &str, params: Value) -> Result {
- let filter: ListFilter = parse(params)?;
+ let p: ListParams = parse(params)?;
+ let mut filter = p.filter;
+ // `--project ` 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 ` + `--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.