Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "projectstore",
"displayName": "projectstore",
"description": "📚 Your project's knowledge base, written by your AI agent — ADRs · epics · stories · runbooks · research. An Obsidian-friendly markdown vault, agent-maintained, you approve every write. Like Karpathy's LLM Wiki, but for engineering project artifacts.",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "Evgenii Konev",
"email": "ekonev@smartandpoint.com",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "projectstore",
"displayName": "projectstore",
"version": "0.10.0",
"version": "0.11.0",
"description": "Opinionated project-management paradigms (ADR / epics / stories / kanban / runbooks) for agentic development. Markdown-first, git-portable, human-readable.",
"author": {
"name": "Evgenii Konev @ SmartAndPoint",
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ PreCompact [...pre-compact.mjs] completed successfully: {

The packet contains the vault path, the command list, the last 15 vault touches, and the newest in-flight ADR / epic / story / research. The post-compact agent picks up drafting from where the previous one left off, no manual rehydration.

## What's in the box (v0.10)
## What's in the box (v0.11)

- **14 commands** — `bind`, `scaffold`, `status`, `adr`, `epic`, `story`, `kanban`, `research`, `concept`, `meeting`, `runbook`, `search`, `review`, `statusline`
- **3 passive skills** — `decision-detector`, `story-completion`, `peer-reviewer`. They suggest commands; they never write directly.
Expand Down Expand Up @@ -208,21 +208,22 @@ See the epic and story the agent is working on this session, right in Claude Cod

![projectstore status line: the 📚 epic › story line sitting above an existing oh-my-claudecode HUD](docs/images/statusline-hud.png)

**It does not overwrite your current status-line settings.** `statusLine` is a single slot and not plugin-declarable, so projectstore ships `scripts/statusline.mjs` plus a `/projectstore:statusline on|off` command that wires it into the project's `.claude/settings.local.json` (local scope, this-project-only, reverts cleanly; conventionally git-ignored). Rather than clobber your HUD, the script **composes**: it re-runs whatever base `statusLine` command you already have — for example [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode)'s HUD, read from `~/.claude/settings.json` — prints its output verbatim, and adds the `📚 <epic> › <story> (<status>)` line above it (position via `projectstore.json` → `statusline.position`, default `above`). With no base command it renders a standalone line: `<model> · <dir> · ⎇ <branch> · 📚 …`.
**It does not overwrite your current status-line settings.** `statusLine` is a single slot and not plugin-declarable, so projectstore ships `scripts/statusline.mjs` plus a `/projectstore:statusline on|off` command. Enabling it just flips `statusline.enabled` in `.claude/projectstore.json`; the **SessionStart hook** then wires the project's `.claude/settings.local.json` (local scope, this-project-only, conventionally git-ignored) to the current plugin version's script — re-derived on every session start, so it keeps working across plugin updates with no path to maintain by hand. Rather than clobber your HUD, the script **composes**: it re-runs whatever base `statusLine` command you already have — for example [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode)'s HUD, read from `~/.claude/settings.json` — prints its output verbatim, and adds the `📚 <epic> › <story> (<status>)` line above it (position via `projectstore.json` → `statusline.position`, default `above`). With no base command it renders a standalone line: `<model> · <dir> · ⎇ <branch> · 📚 …`.

So in a projectstore project you keep your full [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode) context / rate-limit / session HUD **and** gain the current epic & story on top. The epic/story is derived from this session's `recent_activity` log — it appears once you touch an epic or story, and stays transparent (base HUD only) otherwise.

```
/projectstore:statusline on # wire it into this project (project-local)
/projectstore:statusline off # remove it
/projectstore:statusline on # enable (project-local); the hook wires + refreshes it
/projectstore:statusline off # disable it
/projectstore:statusline status # current state + the base HUD it composes over
```

## Roadmap

| Version | What ships | Status |
|---|---|---|
| **v0.10** | Status line — current epic & story in the HUD, composes with an existing status line | ✅ current |
| **v0.11** | Status line install simplified — opt-in flag + self-healing SessionStart wiring | ✅ current |
| v0.10 | Status line — current epic & story in the HUD, composes with an existing status line | ✅ |
| v0.9 | Bundled review agents (`projectstore-critic`, `code-planner`, `code-reviewer`) | ✅ |
| v0.8 | Russian (`ru`) templates | ✅ |
| v0.7 | First-run welcome (SessionStart one-shot), auto-update follow-up in `/projectstore:bind` | ✅ |
Expand Down
55 changes: 21 additions & 34 deletions commands/statusline.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,42 @@
---
description: Enable/disable the projectstore status line (current epic & story in the HUD). Composes above an existing status line (e.g. oh-my-claudecode) instead of replacing it.
description: Enable/disable the projectstore status line (current epic & story in the HUD). Flips a flag; the SessionStart hook does the wiring and keeps it current across plugin updates.
argument-hint: "on | off | status"
---

You are configuring the projectstore status line for THIS project. It shows the epic & story the agent is touching this session, composed **above** any existing status line: `scripts/statusline.mjs` delegates to the base `statusLine` command in your `settings.json`, prints its output verbatim, and adds one `📚 <epic> › <story>` line.
You are toggling the projectstore status line for THIS project. When enabled, the **SessionStart hook** keeps the project's `.claude/settings.local.json` pointed at the current plugin version's `scripts/statusline.mjs` (it self-heals across plugin updates — no machine/version-specific path to maintain by hand). The status line shows `📚 <epic> › <story> (<status>)` **composed above** any existing HUD (e.g. oh-my-claudecode), never replacing it.

`statusLine` is **not** plugin-declarable, so this writes a `statusLine` entry into the **project's `.claude/settings.local.json`** (local scope, highest precedence, conventionally git-ignored — it overrides a global HUD ONLY in this project, and reverts cleanly). If the project tracks `.claude/settings.local.json` in git, tell the user (the baked absolute path is machine-specific and shouldn't be committed).
This command itself only flips a flag in `.claude/projectstore.json` — it does **not** touch `settings.local.json`; the hook owns that file.

## 1. Parse `$ARGUMENTS`
## 1. Require a bound project

- `on` (or empty) — install / refresh the status line.
- `off` — remove it.
- `status` — report current state; modify nothing.
Read `.claude/projectstore.json`. If missing → "No vault bound. Run `/projectstore:bind <vault-path>` first." and stop. (The epic/story segment needs a vault.)

## 2. Resolve paths
## 2. Parse `$ARGUMENTS`

```bash
echo "SCRIPT=$CLAUDE_PLUGIN_ROOT/scripts/statusline.mjs"
echo "SETTINGS=${CLAUDE_PROJECT_DIR:-$PWD}/.claude/settings.local.json"
```

The `statusLine.command` MUST be a baked absolute path — `${CLAUDE_PLUGIN_ROOT}` is not expanded in `statusLine.command` (unlike in hooks). Build the command as `node "<SCRIPT>"` and **keep the inner quotes** — the plugin path may contain spaces, and `statusLine.command` runs in a shell.
- `on` (or empty) — enable.
- `off` — disable.
- `status` — report only; modify nothing.

## 3. `on`

1. Read `SETTINGS` if it exists and `JSON.parse` it; keep **all** existing keys. If absent, start from `{}`.
2. Inspect the existing `statusLine`:
- **Absent** → proceed.
- **Already ours** (`command` contains `scripts/statusline.mjs`) → tell the user it's already on; offer `off`. Stop.
- **A different local status line** → warn: the wrapper composes over a base command found in `~/.claude/settings.json` or `<project>/.claude/settings.json`, but NOT one already sitting in `settings.local.json`; replacing it here drops that local line. Ask via AskUserQuestion: **Replace / Cancel**.
3. Set (preserving other keys) — note the escaped inner quotes around the path:
```json
"statusLine": { "type": "command", "command": "node \"<SCRIPT>\"" }
```
4. **Preview** the full resulting file + path. **AskUserQuestion**: Yes / No.
5. On **Yes** → create `.claude/` if needed and Write the file.
6. Report: "Status line wired. `📚 <epic> › <story>` will appear above your existing HUD once this session touches an epic/story. statusLine loads at session start — if it doesn't show, restart Claude Code in this project."
1. **Foreign status line check** (read-only): read `.claude/settings.local.json` if present. If it has a `statusLine` whose `command` does **not** contain `scripts/statusline.mjs`, warn — the hook will **not** clobber a foreign local status line, so enabling would silently do nothing (and `statusline.mjs` composes only over a base in `~/.claude/settings.json`, not one in `settings.local.json`). AskUserQuestion: **Proceed anyway / Help me clear it / Cancel**. If a base HUD lives in `~/.claude/settings.json` (e.g. oh-my-claudecode), that's fine — we compose over it; no warning needed.
2. Read `.claude/projectstore.json`, set `statusline.enabled = true`. **Preserve `statusline.position`** if present (don't drop it); keep all other keys. Preview the change + **AskUserQuestion** (Yes / No), then Write.
3. Report: "Enabled. On the next session start the hook wires `📚 <epic> › <story>` above your existing HUD. **Restart Claude Code in this project** to apply now (statusLine loads at session start). If `.claude/settings.local.json` is tracked in git, add it to `.gitignore` — the hook bakes a machine-specific absolute path."

## 4. `off`

1. Read `SETTINGS`. No `statusLine` → say "already off"; stop.
2. If `statusLine` is **not** ours → AskUserQuestion before removing (**Remove / Cancel**).
3. Remove only the `statusLine` key; keep the rest. Preview + AskUserQuestion, then Write. (If the object becomes `{}`, leaving `{}` is fine.)
4. Confirm removed; note a restart may be needed.
1. Read `.claude/projectstore.json`. Set `statusline.enabled = false` (keep `statusline.position` + other keys) — write it **even if the flag was absent**, so the hook will remove any managed `statusLine` entry it previously wrote (e.g. an install carried over from an older projectstore version). Preview + AskUserQuestion, then Write.
2. Report: "Disabled. The hook removes its `statusLine` entry from `settings.local.json` on next session start; restart to apply."

## 5. `status`

Read `SETTINGS` and report: is `statusLine` present, and is it ours (`command` contains `scripts/statusline.mjs`)? Then read `~/.claude/settings.json` → `statusLine.command` and report the base HUD it will compose over (or "none — standalone line").
Report, read-only:
- `.claude/projectstore.json` → `statusline.enabled` and `statusline.position` (default `above`).
- `.claude/settings.local.json` → whether `statusLine` is present and ours (`command` contains `scripts/statusline.mjs`), foreign, or absent.
- `~/.claude/settings.json` → `statusLine.command` = the base HUD we compose over (or "none — standalone line").

## Notes

- Every settings write goes through AskUserQuestion. Never write without approval.
- The status line works without a bound vault (it shows the base line / passes the base HUD through); the `📚 epic › story` segment appears only in a projectstore-bound project after the session touches an epic/story.
- Position of our line (`above` / `below` the base HUD) is read from `.claude/projectstore.json` → `statusline.position` (default `above`).
- Every write goes through AskUserQuestion. Never write without approval.
- The command writes only `.claude/projectstore.json`; the SessionStart hook reconciles `settings.local.json` (create/refresh/remove our entry) idempotently and never touches a foreign status line.
- Config shape: `.claude/projectstore.json` → `"statusline": { "enabled": true, "position": "above" }`. `position` is `above` (default) or `below` — the side our 📚 line sits relative to the base HUD.
7 changes: 7 additions & 0 deletions hooks/session-start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
removeLegacySessionIdFile,
readStdinJson,
projectRoot,
syncStatusLine,
} from "../scripts/lib.mjs";

function welcomedMarkerPath(proj) {
Expand Down Expand Up @@ -110,6 +111,12 @@ function main() {
if (welcome) emit(welcome, welcomeSystemMessage);
process.exit(0);
}

// Opt-in status line: keep settings.local.json pointed at this plugin
// version's statusline.mjs (self-heals on update). Best-effort; a settings
// write must never break session-context injection.
try { syncStatusLine(cfg, proj); } catch {}

if (cfg.auto_inject === false) {
if (welcome) emit(welcome, welcomeSystemMessage);
process.exit(0);
Expand Down
55 changes: 55 additions & 0 deletions scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,61 @@ export function writeConfig(cfg) {
writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
}

// ─── Status line wiring (SessionStart-managed) ─────────────────────────
//
// The Claude Code statusLine slot is single and NOT plugin-declarable, so
// when a bound project opts in (projectstore.json → statusline.enabled=true)
// the SessionStart hook keeps <project>/.claude/settings.local.json pointing
// at THIS plugin version's scripts/statusline.mjs. The path is re-derived from
// pluginRoot() every session start, so it self-heals across plugin updates.
// Idempotent (writes only when the value changes); never clobbers a foreign
// statusLine; bails on an unparseable settings file. Returns a status string,
// never throws — the caller wraps it, and this must not break session start.
export function syncStatusLine(cfg, projectDir) {
const st = cfg && cfg.statusline;
if (!st || typeof st.enabled !== "boolean") return "no-flag"; // absent → leave manual installs alone

const p = join(projectDir, ".claude", "settings.local.json");
const desired = `node "${join(pluginRoot(), "scripts", "statusline.mjs")}"`;

let settings = {};
if (existsSync(p)) {
try {
settings = JSON.parse(readFileSync(p, "utf8"));
} catch {
return "skipped-unparseable"; // never clobber a file we cannot read
}
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
return "skipped-nonobject";
}
}

const cur = settings.statusLine;
const curCmd = cur && typeof cur.command === "string" ? cur.command : null;
const isOurs = curCmd ? curCmd.includes("scripts/statusline.mjs") : false;

let changed = false;
if (st.enabled) {
if (cur && !isOurs) return "foreign-present"; // any existing non-ours entry: leave the slot to its owner
if (!cur || curCmd !== desired) {
settings.statusLine = { type: "command", command: desired };
changed = true;
}
} else if (isOurs) {
delete settings.statusLine; // disabled: remove only our entry, keep the rest
changed = true;
}

if (!changed) return "unchanged";
try {
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, JSON.stringify(settings, null, 2) + "\n", "utf8");
} catch {
return "write-failed";
}
return st.enabled ? "enabled" : "disabled";
}

// ─── Layouts ───────────────────────────────────────────────────────────

export function loadLayout(name) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/statusline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function discoverBaseCommand(projectDir) {
typeof cmd === "string" &&
cmd.trim() &&
!cmd.includes(SELF) &&
!cmd.includes("statusline.mjs")
!cmd.includes("scripts/statusline.mjs")
) {
return cmd;
}
Expand Down