projectstore-claude 0.30.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ const HARNESS = "claude-code";
24
24
  // scripts/cli.mjs; the packaging test pins it). Any other verb — doctor,
25
25
  // status, search, --version — passes through untouched: the core refuses an
26
26
  // option a verb does not declare, so a blanket insert would break `doctor`.
27
- const HARNESS_VERBS = new Set(["setup","plan","install","uninstall","upgrade","agents"]);
27
+ const HARNESS_VERBS = new Set(["setup","configure","plan","install","uninstall","upgrade","agents"]);
28
28
 
29
29
  // The bundled core, by path — never by package resolution: a hoisted or global
30
30
  // copy at another version is exactly the pairing the pin exists to prevent,
@@ -12,7 +12,7 @@
12
12
  "name": "projectstore",
13
13
  "displayName": "projectstore",
14
14
  "description": "📚 Your agent runs the project through a verified loop: task → artifact (ADR · spec · epic · story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in an Obsidian-friendly vault, every write approved by you — and any model can pick the project up tomorrow.",
15
- "version": "0.30.0",
15
+ "version": "0.30.1",
16
16
  "author": {
17
17
  "name": "Evgenii Konev",
18
18
  "email": "ekonev@smartandpoint.com",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "projectstore",
3
3
  "displayName": "projectstore",
4
- "version": "0.30.0",
4
+ "version": "0.30.1",
5
5
  "description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
6
6
  "author": {
7
7
  "name": "Evgenii Konev @ SmartAndPoint",
@@ -85,6 +85,30 @@ npx projectstore setup
85
85
 
86
86
  Setup finds the coding agents the project uses — Claude Code, Codex or both — and installs ProjectStore for each, with a vault for the project's notes. At a terminal it asks a few questions: which folder is the project, which agents to install for, where the vault lives (an existing folder is bound; a missing or empty one becomes a new vault, with its own git repository and the layout's folders) and whether to show ProjectStore in the status line. Then it prints one plan — every file it writes and every host command, verbatim — and asks once before it writes anything. Restart the agent afterwards; Codex asks you to approve the ProjectStore hooks once.
87
87
 
88
+ **What it needs:**
89
+
90
+ - Node.js 20 or later, always. The plugin's hooks, its MCP server and the status line run as `node` inside the agent session.
91
+ - npm, for Codex only: setup fetches the Codex plugin through npm. Claude Code alone needs no npm.
92
+ - The agent's own CLI on PATH (`claude`, `codex`), and git for a new vault.
93
+ - A terminal outside every Claude Code and Codex session: inside one, the registration waits and the plan says so.
94
+ - macOS or Linux. Windows is not verified yet.
95
+
96
+ Other package runners start setup the same way. Each was observed running it with Claude Code and Codex:
97
+
98
+ ```
99
+ bunx projectstore@latest setup
100
+ ```
101
+
102
+ ```
103
+ pnpm dlx projectstore@latest setup
104
+ ```
105
+
106
+ ```
107
+ deno run -A npm:projectstore@latest setup
108
+ ```
109
+
110
+ `pnpm dlx` needs pnpm 6.13 or later. Without npm on PATH, a runner sets up Claude Code and refuses Codex, naming npm. The runner only fetches the package: setup copies the plugin into the agents' own directories, so clearing the runner's cache later breaks nothing.
111
+
88
112
  `npx projectstore setup --dry-run` shows the questions and the plan and writes nothing. Without a terminal (a script, CI, an agent's tool) nothing is asked: `--harness <id>` names an agent and is the confirmation, and `--vault <path>` names the vault. `npx projectstore setup --help` lists the rest.
89
113
 
90
114
  If you prefer a global command:
@@ -176,6 +200,37 @@ npx projectstore scaffold --write
176
200
  `projectstore-claude`, `projectstore-codex` and `projectstore-opencode` are release artifacts: per-harness packages that carry the core, pinned at the same version and bundled, with the harness fixed. Nobody needs to type their names. Setup fetches the Codex one, which carries the Codex plugin; the Claude Code one is published at the core's version too; the opencode one publishes after its plugin root is rendered. The other `projectstore-*` names are reserved placeholders pointing back here. One source package, one version, N tarballs. How a release builds and publishes them, and how to run a checkout's own build: [`packaging/README.md`](./packaging/README.md).
177
201
  </details>
178
202
 
203
+ ## Configure
204
+
205
+ Setup asks only what a first install needs. Everything after it is one command, from a terminal in your project:
206
+
207
+ ```
208
+ npx projectstore configure
209
+ ```
210
+
211
+ At a terminal it shows every setting with its current value and where it lives:
212
+
213
+ - each agent's model, per harness;
214
+ - the status line and the band above the prompt (Claude Code);
215
+ - the vault's template language, the default author and tags;
216
+ - the vault's spec policy and lifecycle gates.
217
+
218
+ It asks which to change, then shows one plan and asks once. Without a terminal it prints the same view and writes nothing. To change a setting there, name it with `--set`; naming it is the confirmation:
219
+
220
+ ```
221
+ npx projectstore configure --set agents.critic=fable --set statusline=on
222
+ ```
223
+
224
+ `--dry-run` shows the plan and writes nothing, and `--json` returns the settings as one envelope. In a project that uses two harnesses, an agent's model names its overlay with `--harness <id>`.
225
+
226
+ Each setting has exactly one writer, and the plan says which file it changes:
227
+
228
+ - **The agents' models** live in `.projectstore/harness/<harness>.json`. That file is committed, so a change there is a team change.
229
+ - **The status line, the band, the language and the other binding keys** live in `.projectstore/projectstore.json`, which stays on this machine.
230
+ - **The spec policy and the lifecycle gates** live in the vault's `.projectstore.json`, which commits with the vault. Setting `spec_policy=required` records when it took effect; stories closed before then are exempt.
231
+
232
+ The vault itself is changed by `bind --rebind`, which configure names when asked. In a Claude Code session, `/projectstore:statusline` and `/projectstore:agents configure` run the same writers.
233
+
179
234
  ## Upgrading
180
235
 
181
236
  Installed with setup or from npm: from a terminal in your project, name the version you want to run (`latest` is the newest):
@@ -346,8 +401,45 @@ A Claude Code install from the git marketplace is removed in the session:
346
401
  /plugin uninstall projectstore@SmartAndPoint
347
402
  ```
348
403
 
404
+ **What a removal keeps:**
405
+
406
+ - the vault;
407
+ - the binding `.projectstore/projectstore.json`, with the status-line choice, so a later setup wires the line again;
408
+ - the agents' models in `.projectstore/harness/`, which is committed team configuration;
409
+ - Codex's user-wide registration, until `--global` removes it.
410
+
411
+ `--global` removes the Codex plugin for every other project on this machine too. Their `AGENTS.md` blocks stay and name agents that no longer load: uninstall Codex in each of them as well, or set them up again.
412
+
349
413
  Your vault is yours — plain markdown, untouched. One leftover of a host-managed plugin path can be the agents block in `CLAUDE.md`/`AGENTS.md`; remove it with the harness's agents unregister skill, or delete everything between `<!-- projectstore:agents … -->` and `<!-- /projectstore:agents -->` by hand. A one-harness `uninstall`, with `--harness <id>`, leaves a block that lives in `AGENTS.md`, because that file is read by other coding agents too. Claude Code's then removes the `CLAUDE.md` import when that file holds nothing else; Codex's keeps it, so Claude Code still sees the block. `uninstall --surface agents_block` removes the block as well. Once every harness that can read the block's file is among those uninstalled (for `AGENTS.md`, Claude Code and Codex), the block goes too, and the `CLAUDE.md` import with it unless your own text keeps both files.
350
414
 
415
+ ### Reinstalling clean
416
+
417
+ Keeping the vault and the binding, uninstall and set up again. The second setup finds both:
418
+
419
+ ```
420
+ npx projectstore uninstall
421
+ ```
422
+
423
+ ```
424
+ npx projectstore setup
425
+ ```
426
+
427
+ To start as if the project was never bound, remove only the machine-local half after the uninstall. `.projectstore/harness/` and `.projectstore/.gitignore` are committed; keep them. The binding also carries the template language, the default author and tags, the skills switch, the band and the status-line choice, and removing it drops them. Setup then asks for the vault again.
428
+
429
+ ```
430
+ rm -f .projectstore/projectstore.json
431
+ ```
432
+
433
+ ```
434
+ rm -rf .projectstore/state
435
+ ```
436
+
437
+ To point the project at another vault, no reinstall is needed. From a terminal in your project:
438
+
439
+ ```
440
+ npx projectstore bind ~/vaults/other --rebind
441
+ ```
442
+
351
443
  ## Extending
352
444
 
353
445
  See [`docs/extending.md`](./docs/extending.md) for adding layouts, templates, and skills, and [`docs/harnesses.md`](./docs/harnesses.md) for which coding agents projectstore runs on — what "experimental" means there, and what adding one takes.
@@ -3,7 +3,7 @@ description: Bind this project to an Obsidian vault (or any markdown directory)
3
3
  argument-hint: <vault-path> | --inherit [--layout engineering] [--lang en|ru|es|de|fr|zh]
4
4
  ---
5
5
 
6
- You are binding the current project to a markdown vault for projectstore. The config is written by the core's `bind` / `init` verbs (roadmap A8); this interview decides the values, previews them, and asks — it never writes `projectstore.json` itself, except on the inherit path (step 0a), which copies the parent's file verbatim, and the one later stamp in step 11 (`statusline`), which `Edit`s the file the verb wrote. The interview's `--lang` is the verb's `--language`.
6
+ You are binding the current project to a markdown vault for projectstore. The config is written by the core's `bind` / `init` verbs (roadmap A8); this interview decides the values, previews them, and asks — it never writes `projectstore.json` itself, except on the inherit path (step 0a), which copies the parent's file verbatim. The settings later steps change — the vault policy (step 7.5) and the status line (step 11) — go through `configure --set`, each key's one writer. The interview's `--lang` is the verb's `--language`.
7
7
 
8
8
  Parse `$ARGUMENTS`:
9
9
  - First positional arg: vault path. Expand `~` if present.
@@ -81,7 +81,7 @@ Steps:
81
81
  node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" bind "<vault-path>" [--layout <name>] [--language <code>] [--rebind]
82
82
  ```
83
83
 
84
- `init "<vault-path>" …` instead when step 1 chose to create the vault — it creates the whole vault, so step 7 has nothing to offer after it; `--rebind` only when step 0 ended on **Replace bind**. Naming the vault is the verb's confirmation (there is no `--yes`); the interview's AskUserQuestion in step 4 is the in-session gate. Print the verb's output. A non-zero exit is a refusal or a usage error — relay it and stop — except an `init` whose output reports `git init failed` or `Scaffold failed`: the binding is written, so relay the remedy line it prints and continue. Step 11 below `Edit`s the file this step wrote; keep it after it.
84
+ `init "<vault-path>" …` instead when step 1 chose to create the vault — it creates the whole vault, so step 7 has nothing to offer after it; `--rebind` only when step 0 ended on **Replace bind**. Naming the vault is the verb's confirmation (there is no `--yes`); the interview's AskUserQuestion in step 4 is the in-session gate. Print the verb's output. A non-zero exit is a refusal or a usage error — relay it and stop — except an `init` whose output reports `git init failed` or `Scaffold failed`: the binding is written, so relay the remedy line it prints and continue.
85
85
 
86
86
  6. **Check `.gitignore`**: read `<project>/.gitignore` if it exists. Our own files are self-ignored inside `.projectstore/` — the verb writes that `.gitignore` itself, carrying `projectstore.json` and `state/`, with `harness/` committed on purpose (the layout ADR); on the inherit path of step 0a, which bypasses the verb, you wrote both by hand. Unless `.claude/` is ignored wholesale, the one machine-specific entry left is the host's `.claude/settings.local.json`. If it is missing, offer (AskUserQuestion) to append it. If the user declines, skip silently.
87
87
 
@@ -97,7 +97,7 @@ Steps:
97
97
  - If it already exists with a `spec_policy` key — respect it, print the current policy, do not re-ask.
98
98
  - **New bind into a vault with no artifacts** (a vault `init` just made counts: its folders and README indexes are not artifacts): ask via AskUserQuestion — "Enable spec-first policy for this vault (every story must be covered by a spec; doctor enforces it)?" with options **Yes, `spec_policy: required` (Recommended)** / **Not yet, `optional`**. Second question: "Enable lifecycle gates (plan/close sections + evidence checks on stories)?" — **Yes, `lifecycle_gates: on` (Recommended)** / **Off for now**.
99
99
  - **Bind to an existing vault with artifacts**: default to `spec_policy: optional`, `lifecycle_gates: off` and say doctor will suggest enabling once specs appear. Do not impose the gate on an existing backlog.
100
- - On any choice, write `<vault>/.projectstore.json` (vault ROOT — deliberately not inside `<vault>/.projectstore/`, whose .gitignore would keep the policy out of git):
100
+ - On any choice, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set spec_policy=<required|optional> --set lifecycle_gates=<on|off> --dry-run --project "${CLAUDE_PROJECT_DIR}"` and show its PLAN, then ask via AskUserQuestion: "Record this policy in the vault? [Yes / No]". On **Yes**, run the same command without `--dry-run`; a non-zero exit is a refusal — relay it and go on to step 8 (the binding is already written). It writes `<vault>/.projectstore.json` — vault ROOT, deliberately not inside `<vault>/.projectstore/`, whose .gitignore would keep the policy out of git — and keeps any other key in it:
101
101
 
102
102
  ```json
103
103
  {
@@ -107,7 +107,7 @@ Steps:
107
107
  }
108
108
  ```
109
109
 
110
- `spec_policy_since` is stamped ONLY when spec_policy is set to `required` — it anchors the legacy exemption (stories done before it stay exempt; stories in progress/review at enable time are in scope).
110
+ configure stamps `spec_policy_since` whenever spec_policy becomes `required` — it anchors the legacy exemption (stories done before it stay exempt; stories in progress/review at enable time are in scope).
111
111
 
112
112
  8. **Agent registration** (v0.13, ADR-002): ask via AskUserQuestion — "Register projectstore's agents in CLAUDE.md/AGENTS.md so every session routes to them (critic after authoring artifacts, planner before implementing, reviewer before commit)? [Yes (Recommended) / No]". On Yes, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" install --harness claude-code --surface agents_block --project "${CLAUDE_PROJECT_DIR}"` and print its output (it renders from the layout's roster, migrates rather than duplicates, previews every write, and applies because the harness is named). On a rebind where a block already exists the verb reports it current or replaces it in place — do not re-ask blindly.
113
113
 
@@ -121,4 +121,4 @@ Steps:
121
121
 
122
122
  (for `ru`: `[PS#<version>] 📚 Супер-фича в супер-продукте › Ручка для туалетной бумаги (in-progress)`; every bundled language ships its own example pair)
123
123
 
124
- Ask via AskUserQuestion: "Show your current epic/story in the status line, composed above any existing HUD? [Yes / No]". On Yes: Edit `projectstore.json` → `"statusline": { "enabled": true }` (approval-gated), then run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" install --harness claude-code --surface statusline --project "${CLAUDE_PROJECT_DIR}"` and print its output (it writes the `settings.local.json` entry and the launcher, previewed), and report: "Enabled — restart Claude Code in this project to apply. A fresh session shows: `[PS#<version>] 📚 <statusline_no_work>`."
124
+ First run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=on --dry-run --project "${CLAUDE_PROJECT_DIR}"` and show its PLAN; a non-zero exit is a refusal (a foreign status line, an unparseable settings file) — relay it and skip this step. Then ask via AskUserQuestion: "Show your current epic/story in the status line, composed above any existing HUD? [Yes / No]". On Yes, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=on --project "${CLAUDE_PROJECT_DIR}"` and print its output — a non-zero exit is a refusal to relay (it records `statusline.enabled: true` and writes the `settings.local.json` entry and the launcher, in one plan), and report: "Enabled — restart Claude Code in this project to apply. A fresh session shows: `[PS#<version>] 📚 <statusline_no_work>`."
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Enable/disable the projectstore status line (current epic & story in the HUD). Flips a flag and runs the install verb, which wires it behind a preview; the SessionStart hook keeps an existing entry current across plugin updates.
2
+ description: Enable/disable the projectstore status line (current epic & story in the HUD). Runs `configure --set statusline=on|off`, which records the flag and wires or removes the entry behind a preview; the SessionStart hook keeps an existing entry current across plugin updates.
3
3
  argument-hint: "on | off | status"
4
4
  ---
5
5
 
@@ -7,7 +7,7 @@ You are toggling the projectstore status line for THIS project. When enabled, `i
7
7
 
8
8
  Resolution is **per-session with zero cross-session and zero vault reads** (ADR-006): the 📚 segment comes from this session's pointer (`.projectstore/state/sessions/<session_id>.json`, maintained by the tool hooks — a write's patch after the write — by `story close`, and at session start; a done, closed or parked story leaves the segment); a fresh session shows an explicit localized cold-start line ("No epic or story in this session yet" / «Эпик и стори ещё не в работе в этой сессии») — never a silent blank; a corrupt pointer shows an error-marked string. Strings localize via `templates/<lang>/strings.json` (en fallback). The version badge is controlled by `statusline.show_version` (default true).
9
9
 
10
- This command flips the flag in `.projectstore/projectstore.json` and then runs the install verb, which writes `settings.local.json` and the launcher behind a preview. The SessionStart hook only refreshes an entry that is already ours and removes it when the flag is off — it never creates one.
10
+ This command runs `configure --set statusline=on|off` (the configure spec, contracts 6 and 9): install's own items record `statusline.enabled` and write or remove the `settings.local.json` entry and the launcher, in one plan. The command never writes `projectstore.json` itself. The SessionStart hook only refreshes an entry that is already ours and removes it when the flag is off — it never creates one.
11
11
 
12
12
  ## 1. Require a bound project
13
13
 
@@ -21,15 +21,15 @@ Read `.projectstore/projectstore.json`. If missing → "No vault bound. Run `/pr
21
21
 
22
22
  ## 3. `on`
23
23
 
24
- 1. **Foreign status line check** (read-only): read `.claude/settings.local.json` if present. If it has a `statusLine` whose `command` is **not** ours (ours contains either `.projectstore/state/<harness>/statusline.mjs` — the launcher; a not-yet-migrated project's legacy launcher is `.claude/.projectstore/statusline.mjs`, also ours — or `scripts/statusline.mjs` — an older or dev wiring), 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` or `~/.claude/settings.json`, never 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.
25
- 2. **AskUserQuestion** (Yes / No): "Wire the status line for this project?" On Yes, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" install --harness claude-code --surface statusline --project "${CLAUDE_PROJECT_DIR}"` and print its output verbatim — it previews and writes the `statusLine` entry and the launcher (`.projectstore/state/claude-code/statusline.mjs`, provenance-stamped). A non-zero exit is a refusal (a foreign status line, an unparseable settings file) — relay it and stop, leaving the flag as it was.
26
- 3. On success, read `.projectstore/projectstore.json`, set `statusline.enabled = true`. **Preserve `statusline.position`** if present (don't drop it); keep all other keys. Preview the change, then Write.
24
+ 1. **Foreign status line check** (read-only): read `.claude/settings.local.json` if present. If it has a `statusLine` whose `command` is **not** ours (ours contains either `.projectstore/state/<harness>/statusline.mjs` — the launcher; a not-yet-migrated project's legacy launcher is `.claude/.projectstore/statusline.mjs`, also ours — or `scripts/statusline.mjs` — an older or dev wiring), warn — configure refuses to enable over a foreign local status line (and `statusline.mjs` composes only over a base in `.claude/settings.json` or `~/.claude/settings.json`, never one in `settings.local.json`). AskUserQuestion: **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.
25
+ 2. **Preview**: run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=on --dry-run --project "${CLAUDE_PROJECT_DIR}"` and print its PLAN. A non-zero exit is a refusal — relay it and stop; nothing is asked or written.
26
+ 3. **AskUserQuestion** (Yes / No): "Wire the status line for this project?" On Yes, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=on --project "${CLAUDE_PROJECT_DIR}"` and print its output verbatim — it records `statusline.enabled: true` (keeping `statusline.position` and every other key) and writes the `statusLine` entry and the launcher (`.projectstore/state/claude-code/statusline.mjs`, provenance-stamped), in one plan. A non-zero exit is a refusal (a foreign status line, an unparseable settings file, a layout still to move) — relay it and stop; nothing was written.
27
27
  4. Report: "Enabled. `📚 <epic> › <story>` renders 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 entry carries a machine-specific absolute path."
28
28
 
29
29
  ## 4. `off`
30
30
 
31
- 1. **AskUserQuestion** (Yes / No): "Remove the projectstore status line from this project?" On Yes, read `.projectstore/projectstore.json` and set `statusline.enabled = false` (keep `statusline.position` + other keys) — write it **even if the flag was absent**, so the hook removes any managed `statusLine` entry it previously wrote. Preview, then Write.
32
- 2. Run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" uninstall --harness claude-code --surface statusline --project "${CLAUDE_PROJECT_DIR}"` and print its output — it removes our `statusLine` entry and the launcher, previewed, and leaves a foreign entry alone.
31
+ 1. **Preview**: run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=off --dry-run --project "${CLAUDE_PROJECT_DIR}"` and print its PLAN. A non-zero exit is a refusal — relay it and stop.
32
+ 2. **AskUserQuestion** (Yes / No): "Remove the projectstore status line from this project?" On Yes, run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" configure --set statusline=off --project "${CLAUDE_PROJECT_DIR}"` and print its output — it records `statusline.enabled: false` (even if the flag was absent, so the hook keeps it off) and removes our `statusLine` entry and the launcher, and leaves a foreign entry alone.
33
33
  3. Report: "Disabled; restart to apply."
34
34
 
35
35
  ## 5. `status`
@@ -42,5 +42,5 @@ Report, read-only:
42
42
  ## Notes
43
43
 
44
44
  - Every write goes through AskUserQuestion. Never write without approval.
45
- - The command writes `.projectstore/projectstore.json` and runs the install verb for the `statusline` surface; the SessionStart hook only refreshes an entry that is already ours (and removes it when the flag is off) — it never creates one, and never touches a foreign status line.
45
+ - The command runs `configure --set statusline=on|off`, whose install items are the flag's one writer; the SessionStart hook only refreshes an entry that is already ours (and removes it when the flag is off) — it never creates one, and never touches a foreign status line.
46
46
  - Config shape: `.projectstore/projectstore.json` → `"statusline": { "enabled": true, "position": "above", "show_version": true }`. `position` is `above` (default) or `below` — the side our 📚 line sits relative to the base HUD; `show_version` toggles the `[PS#…]` badge.
@@ -8,13 +8,13 @@ From a terminal in your project:
8
8
  npx projectstore setup
9
9
  ```
10
10
 
11
- Setup finds the coding agents the project uses (Claude Code, Codex or both), asks a few questions — among them where the vault lives — then shows one plan and asks once before it writes. It binds the vault, or creates one when the folder is missing or empty. The README's [Install](../README.md#install) section has the details, the global form and the registry-mirror form.
11
+ Setup finds the coding agents the project uses (Claude Code, Codex or both), asks a few questions — among them where the vault lives — then shows one plan and asks once before it writes. It binds the vault, or creates one when the folder is missing or empty. The README's [Install](../README.md#install) section has what setup needs (Node.js 20 or later; npm only for Codex), the other package runners, the global form and the registry-mirror form. Setup binds the vault itself, so after it the project is ready: restart the agent and go on to the daily flow.
12
12
 
13
- The next two sections are the other ways in: a checkout, and the git marketplace inside a Claude Code session. After either of them, the first-time setup below binds the vault.
13
+ The next two sections are the other ways in: a checkout, for contributors, and the git marketplace inside a Claude Code session. After either of them, the first-time setup below binds the vault from inside a session.
14
14
 
15
- ## Install (local dev)
15
+ ## From a checkout
16
16
 
17
- Until projectstore is published to a public marketplace, install it from a local clone.
17
+ For contributors working on projectstore itself. Clone it; [Running a checkout's build](../packaging/README.md#running-a-checkouts-build) covers the packed form.
18
18
 
19
19
  ```bash
20
20
  git clone https://github.com/SmartAndPoint/ProjectStore.git ~/Projects/SmartAndPoint/ProjectStore
@@ -40,7 +40,7 @@ Verify the plugin appears:
40
40
 
41
41
  You should see `projectstore` (displayName) with prefix `projectstore`.
42
42
 
43
- ## Install (via marketplace)
43
+ ## In a Claude Code session, from the git marketplace
44
44
 
45
45
  ```
46
46
  /plugin marketplace add SmartAndPoint/ProjectStore
@@ -49,6 +49,8 @@ You should see `projectstore` (displayName) with prefix `projectstore`.
49
49
 
50
50
  ## First-time setup
51
51
 
52
+ `npx projectstore setup` binds a vault or creates one, adds the agents block, and offers the status line, in one plan. The steps below do that from inside a Claude Code session, for the two ways in above, and also ask for the vault's policy and the agents' models; after setup, those two are [Configure](#configure)'s.
53
+
52
54
  1. **Pick a vault directory** — any folder where you want your project artifacts to live. Obsidian opens it natively. Git tracks it cleanly. There is nothing to create by hand: step 2 creates the vault when the folder does not exist yet or is empty.
53
55
 
54
56
  2. **Bind your current project to that vault**:
@@ -87,6 +89,22 @@ You should see `projectstore` (displayName) with prefix `projectstore`.
87
89
  npx projectstore scaffold --write
88
90
  ```
89
91
 
92
+ ## Configure
93
+
94
+ After setup, every setting is one command away, from a terminal in your project:
95
+
96
+ ```
97
+ npx projectstore configure
98
+ ```
99
+
100
+ It shows each agent's model, the status line and the band, the template language, and the vault's spec policy and lifecycle gates — with their values and the file each one lives in. Then it asks which to change, shows one plan and asks once. From a script or an agent, name the change; naming it is the confirmation:
101
+
102
+ ```
103
+ npx projectstore configure --set language=de --set lifecycle_gates=on
104
+ ```
105
+
106
+ The README's [Configure](../README.md#configure) section lists what each setting writes and which files are committed.
107
+
90
108
  ## Daily flow
91
109
 
92
110
  ```
@@ -122,12 +140,10 @@ Skills (`projectstore-decision-detector`, `projectstore-story-completion`) are p
122
140
 
123
141
  ## Disabling skills
124
142
 
125
- Edit `.projectstore/projectstore.json`:
143
+ From a terminal in your project:
126
144
 
127
- ```jsonc
128
- {
129
- "active_skills": false
130
- }
145
+ ```
146
+ npx projectstore configure --set active_skills=off
131
147
  ```
132
148
 
133
149
  ## Multi-language templates
@@ -138,7 +154,13 @@ Default is English (`en`). Also bundled: Russian (`ru`), Spanish (`es`), German
138
154
  /projectstore:bind <path> --lang de
139
155
  ```
140
156
 
141
- Or edit `language: "de"` in `.projectstore/projectstore.json` (templates must exist at `templates/de/`). The language also localizes the status line strings (e.g. the "no epic or story in this session yet" line).
157
+ A bound project changes it with `configure` (the value must be a bundled language). From a terminal in your project:
158
+
159
+ ```
160
+ npx projectstore configure --set language=de
161
+ ```
162
+
163
+ New artifacts take the new language's templates. The language also localizes the status line strings (e.g. the "no epic or story in this session yet" line).
142
164
 
143
165
  What the language does and does not change: section headings, table labels and prose are translated; frontmatter keys and their values (`status: planned`, `priority: p2`) stay English, because they are machine-read. Section headings are registered in `scaffold/headings.json`, so doctor, reconcile and the story lifecycle gates recognize every bundled language — a Russian-headed file lints in a French-bound vault, and mixed-language vaults reconcile.
144
166
 
@@ -96,6 +96,12 @@ project-local. Ordinary uninstall leaves the global plugin in place; add
96
96
  build runs through the same flow: [`packaging/README.md`](../packaging/README.md#running-a-checkouts-build)
97
97
  has the tarball loop.
98
98
 
99
+ `npx projectstore configure` changes Codex's settings as it does Claude Code's.
100
+ In a project that uses only Codex, it offers no status line and no band: Codex
101
+ has neither surface. An agent's model goes to `.projectstore/harness/codex.json`:
102
+ name it in Codex's own vocabulary with `--set agents.<name>=<model>`. The
103
+ terminal's model presets are Claude Code's, so they are not offered for Codex.
104
+
99
105
  What is known, and how:
100
106
 
101
107
  - **Hooks load from Codex's default file, so the shell carries no root
@@ -494,9 +494,30 @@
494
494
  ],
495
495
  "ui_vocabulary_reason": "This harness's UI affordances and tool names: words that mean nothing on another harness. The generator refuses them in another harness's rendered tree, and the runtime vocabulary lint refuses them in what the core prints for another harness (generation spec, contracts 6 and 18). A word here must be followed by a non-word character to count, so `/plugins/` in a path is not `/plugin`.",
496
496
  "update_hint": {
497
- "line": "See /plugin → Marketplaces to enable auto-update.",
498
- "welcome": ["**About future updates**: Claude Code does NOT auto-update third-party marketplaces by default. To get notified of new releases (v0.7+):", "1. Open `/plugin` → **Marketplaces** tab.", "2. Find **SmartAndPoint**.", "3. Toggle **auto-update** on.", "", "Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details."],
499
- "reason": "What the first-run welcome and its one-line notice say about updates (generation spec, contract 18): `line` ends the notice, `welcome` is the welcome's update block. update_instructions is the long-form guidance; this is the part a session prints, kept here so no hook names a harness's update path."
497
+ "line": "Update from a terminal in this project:\n{upgrade}",
498
+ "welcome": [
499
+ "**About future updates**: from a terminal in this project, run:",
500
+ "",
501
+ "```",
502
+ "{upgrade}",
503
+ "```",
504
+ "",
505
+ "Then restart Claude Code. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details."
506
+ ],
507
+ "by_channel": {
508
+ "marketplace": {
509
+ "line": "See /plugin → Marketplaces to enable auto-update.",
510
+ "welcome": [
511
+ "**About future updates**: Claude Code does NOT auto-update third-party marketplaces by default. To get notified of new releases (v0.7+):",
512
+ "1. Open `/plugin` → **Marketplaces** tab.",
513
+ "2. Find **SmartAndPoint**.",
514
+ "3. Toggle **auto-update** on.",
515
+ "",
516
+ "Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details."
517
+ ]
518
+ }
519
+ },
520
+ "reason": "What the first-run welcome and its one-line notice say about updates (generation spec, contract 18): `line` ends the notice, `welcome` is the welcome's update block, and `{upgrade}` is filled with the core's upgrade for this harness at latest, so what is printed runs as printed. `by_channel` holds the text for a copy that loads from another channel, by installChannel's answer: a copy from the git marketplace is updated by Claude Code itself, so its welcome keeps the auto-update toggle; the npm registration and a checkout get the npm form. update_instructions is the long-form guidance; this is the part a session prints, kept here so no hook names a harness's update path."
500
521
  },
501
522
  "update_instructions": [
502
523
  "**About future updates**: installed from the git marketplace, Claude Code does NOT auto-update third-party marketplaces by default; an npm registration updates with `npx projectstore@<version> upgrade` from a terminal in the project. To get notified of new git-marketplace releases (v0.7+):",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectstore",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
5
5
  "keywords": [
6
6
  "project-management",
@@ -0,0 +1,84 @@
1
+ // The change to a harness overlay's agents block (the configure spec,
2
+ // contract 8): planned once, here, and called by `agents configure` and by
3
+ // `configure`, so the clerk pin, the precedence and the overlay's other keys
4
+ // behave the same whichever verb asks. The gate stays the caller's.
5
+ import { readOverlayAt, writeOverlayAt } from "./lib.mjs";
6
+ import { harnessForOverlay } from "./harness.mjs";
7
+
8
+ // What a change would write. `before` is readOverlayAt's read; `roster` the
9
+ // layout's agent names (null when no layout loads); `agents` the
10
+ // `<name>=<model>` specs. A malformed spec or a name outside the roster comes
11
+ // back as `usage`, with nothing planned.
12
+ export function planAgentsChange({ before, harness, layout, roster, defaultModel, agents = [], reset = false }) {
13
+ // --reset empties the block first; --default and --agent then apply on top of
14
+ // the emptied block — "reset, then set" is one call, and nothing given is
15
+ // silently dropped.
16
+ const base = reset ? { default: null, per_agent: {} } : before.agents;
17
+ const next = {
18
+ default: defaultModel !== undefined ? defaultModel || null : base.default,
19
+ per_agent: { ...base.per_agent },
20
+ };
21
+ for (const spec of agents) {
22
+ const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(spec);
23
+ if (!m) return { usage: `--agent takes <name>=<model>, got ${spec}` };
24
+ // A name outside the roster would be written, listed, and never run.
25
+ if (roster && !roster.includes(m[1]))
26
+ return { usage: `no agent named ${m[1]} in the ${layout} roster — known: ${roster.join(", ")}` };
27
+ if (m[2]) next.per_agent[m[1]] = m[2];
28
+ else delete next.per_agent[m[1]];
29
+ }
30
+ // The clerk transcribes; a strong default must not lift it (commands/agents.md,
31
+ // ADR-008). WHICH model it is pinned to is the harness's vocabulary, so the
32
+ // manifest names it and this file does not: pinning every harness to
33
+ // "sonnet" wrote an Anthropic model name into a Codex overlay, which is a
34
+ // model that does not exist there. A manifest with no cheap model declared
35
+ // does not pin, and the preview says so rather than inventing one.
36
+ // No fallback model here, deliberately, and it is not the same call as
37
+ // SOURCE_WRITE_TOOLS_FALLBACK: a missing manifest cannot reach this line.
38
+ // harnessIds() derives from the manifests, so with none loadable a named
39
+ // harness is refused as unknown before any caller plans a change (`agents
40
+ // configure` and `configure` both check it first). A literal here would be
41
+ // the deleted `"sonnet"` restored for a case that cannot happen.
42
+ let pinnedClerk = false;
43
+ let clerkPinUnavailable = false;
44
+ const clerkAsked = agents.some((s) => s.startsWith("clerk="));
45
+ if (next.default && !next.per_agent.clerk && !clerkAsked) {
46
+ const cheap = harnessForOverlay(harness)?.agent_translation?.cheap_model || null;
47
+ if (cheap) {
48
+ next.per_agent.clerk = cheap;
49
+ pinnedClerk = true;
50
+ } else clerkPinUnavailable = true;
51
+ }
52
+ // Keys inside the agents block that the allowlist rejected (an `effort`, a
53
+ // stray shape) are rewritten out by the write and announced here; keys
54
+ // outside the block are kept, and stay doctor's to name.
55
+ const dropped = before.rejected.filter((k) => k.startsWith("agents."));
56
+ const same =
57
+ !dropped.length &&
58
+ JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) ===
59
+ JSON.stringify({ d: next.default, p: next.per_agent });
60
+ const preview = [
61
+ `agents configure — ${harness} — ${before.path}`,
62
+ ` default: ${before.agents.default || "—"} → ${next.default || "—"}`,
63
+ ];
64
+ const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(next.per_agent)]);
65
+ for (const n of [...names].sort())
66
+ preview.push(
67
+ ` ${n}: ${before.agents.per_agent[n] || "—"} → ${next.per_agent[n] || "—"}${pinnedClerk && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`,
68
+ );
69
+ // Silence here would read as "the clerk is fine", and it is not: it takes the
70
+ // strong default, which is the one thing ADR-008 says must not happen to it.
71
+ if (clerkPinUnavailable)
72
+ preview.push(
73
+ ` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`,
74
+ );
75
+ if (dropped.length)
76
+ preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
77
+ return { next, pinnedClerk, clerkPinUnavailable, dropped, same, preview };
78
+ }
79
+
80
+ // Writes a planned change through the overlay's one writer and reads it back.
81
+ export function applyAgentsChange(projectDir, harness, next) {
82
+ const path = writeOverlayAt(projectDir, harness, next);
83
+ return { path, after: readOverlayAt(projectDir, harness) };
84
+ }
@@ -135,6 +135,46 @@ function readExisting(configPath) {
135
135
  }
136
136
  }
137
137
 
138
+ // The binding's plain settings and their one writer (the configure spec,
139
+ // contract 5). Every other key of the binding has a writer of its own: the
140
+ // vault path is rebind's, the band the band verb's, the status line's flag
141
+ // install's.
142
+ export const BINDING_SETTING_KEYS = Object.freeze([
143
+ "language",
144
+ "active_skills",
145
+ "auto_inject",
146
+ "default_author",
147
+ "tags",
148
+ ]);
149
+
150
+ // What a change to named plain keys would write: never a binding that does
151
+ // not exist (a bare file of settings would read as bound), never over one
152
+ // that does not parse.
153
+ export function planBindingKeys(projectDir, changes, { env = process.env } = {}) {
154
+ const path = harnessConfigPath(projectDir, env);
155
+ const unknown = Object.keys(changes).filter((k) => !BINDING_SETTING_KEYS.includes(k));
156
+ if (unknown.length) throw new Error(`planBindingKeys: not a binding setting: ${unknown.join(", ")}`);
157
+ const { cfg, corrupt } = readExisting(path);
158
+ if (corrupt || (cfg && (typeof cfg !== "object" || Array.isArray(cfg))))
159
+ return { path, refusal: { code: "UNREADABLE", message: `${path} is not valid JSON — fix or remove it first` } };
160
+ if (!cfg) return { path, refusal: { code: "UNBOUND", message: `${projectDir} is not bound to a vault` } };
161
+ const keys = Object.keys(changes).filter((k) => JSON.stringify(cfg[k]) !== JSON.stringify(changes[k]));
162
+ const before = Object.fromEntries(keys.map((k) => [k, cfg[k]]));
163
+ const after = Object.fromEntries(keys.map((k) => [k, changes[k]]));
164
+ return { path, keys, before, after, changed: keys.length > 0 };
165
+ }
166
+
167
+ // Applies a plan: reads the binding again and sets only the planned keys, so
168
+ // a write another writer made since the plan (the band, the flag) is kept.
169
+ export function applyBindingKeys(plan) {
170
+ if (plan.refusal || !plan.changed) return { wrote: false };
171
+ const { cfg, corrupt } = readExisting(plan.path);
172
+ if (corrupt || !cfg || typeof cfg !== "object" || Array.isArray(cfg))
173
+ throw new Error(`${plan.path} changed since the plan and no longer reads as a binding`);
174
+ writeFileAtomic(plan.path, JSON.stringify({ ...cfg, ...plan.after }, null, 2) + "\n", { sweep: false });
175
+ return { wrote: true };
176
+ }
177
+
138
178
  export const shellQuote = (s) => (/^[A-Za-z0-9_/.~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`);
139
179
 
140
180
  // A directory with no entries at all. A `.DS_Store` makes it not empty: that