projectstore-claude 0.29.0 โ 0.29.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.
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/commands/review.md +1 -1
- package/node_modules/projectstore/harnesses/claude-code.json +17 -2
- package/node_modules/projectstore/harnesses/codex.json +20 -4
- package/node_modules/projectstore/hooks/pre-compact.mjs +2 -1
- package/node_modules/projectstore/hooks/session-rules.mjs +19 -7
- package/node_modules/projectstore/hooks/session-start.mjs +20 -18
- package/node_modules/projectstore/package.json +2 -2
- package/node_modules/projectstore/scripts/binding.mjs +3 -3
- package/node_modules/projectstore/scripts/build-adapters.mjs +43 -13
- package/node_modules/projectstore/scripts/cli.mjs +3 -3
- package/node_modules/projectstore/scripts/codemap.mjs +2 -2
- package/node_modules/projectstore/scripts/doctor.mjs +110 -58
- package/node_modules/projectstore/scripts/draft.mjs +4 -3
- package/node_modules/projectstore/scripts/graph.mjs +3 -2
- package/node_modules/projectstore/scripts/harness.mjs +89 -0
- package/node_modules/projectstore/scripts/kanban.mjs +3 -2
- package/node_modules/projectstore/scripts/lib.mjs +69 -8
- package/node_modules/projectstore/scripts/mcp.mjs +2 -2
- package/node_modules/projectstore/scripts/query.mjs +2 -1
- package/node_modules/projectstore/scripts/reconcile.mjs +4 -3
- package/node_modules/projectstore/scripts/story-section.mjs +2 -1
- package/node_modules/projectstore/scripts/surfaces.mjs +2 -1
- package/node_modules/projectstore/scripts/term.mjs +3 -1
- package/node_modules/projectstore/scripts/touch-session.mjs +6 -1
- package/node_modules/projectstore/scripts/worktree.mjs +12 -7
- package/package.json +2 -2
|
@@ -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.29.
|
|
15
|
+
"version": "0.29.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.29.
|
|
4
|
+
"version": "0.29.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",
|
|
@@ -23,7 +23,7 @@ You are running a peer review on a projectstore artifact.
|
|
|
23
23
|
|
|
24
24
|
5. **Gather domain context**: read the vault's top-level `README.md` and the folder README of the artifact's parent (e.g. `adr/README.md`). Keep both short โ they're context for the critic, not the focus.
|
|
25
25
|
|
|
26
|
-
6. **Spawn the critic agent**. Prefer this plugin's own `projectstore:critic` (purpose-built fresh-context critic, no sycophancy
|
|
26
|
+
6. **Spawn the critic agent**. Prefer this plugin's own `projectstore:critic` (purpose-built fresh-context critic, no sycophancy). If unavailable, fall back to `oh-my-claudecode:critic`, then `general-purpose`. Use this exact prompt template:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
You are a critic-mode reviewer. You have ONLY the artifact and the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Harness capability manifest โ Claude Code. This is the SOURCE layout (generation spec, contract 2): the plugin's own commands/, agents/, skills/ and hooks/hooks.json are already in Claude Code's shapes, so nothing is emitted for it (emit: false). The manifest still exists in full because the portability lint reads every harness's vocabulary, and a token only Claude Code understands is only detectable as unmapped in another harness's tree if Claude Code's own names are written down somewhere. It carries no `lint` block and an empty `rewrites` because linting runs over emitted trees only. Shape contributed by Maxim Podreshetnikov (PR #13); `verified`, `output_channels`, `scope_reason`, `root_placeholder_literal` and `agent_overrides` are the spec's additions.",
|
|
2
|
+
"_comment": "Harness capability manifest โ Claude Code. This is the SOURCE layout (generation spec, contract 2): the plugin's own commands/, agents/, skills/ and hooks/hooks.json are already in Claude Code's shapes, so nothing is emitted for it (emit: false). The manifest still exists in full because the portability lint reads every harness's vocabulary, and a token only Claude Code understands is only detectable as unmapped in another harness's tree if Claude Code's own names are written down somewhere. It carries no `lint` block and an empty `rewrites` because linting runs over emitted trees only; its `ui_vocabulary` lists the UI affordances that mean nothing elsewhere, read by the generator's leak check and by the runtime vocabulary lint over what the core prints for another harness (generation spec, contracts 2 and 18). Shape contributed by Maxim Podreshetnikov (PR #13); `verified`, `output_channels`, `scope_reason`, `root_placeholder_literal` and `agent_overrides` are the spec's additions.",
|
|
3
3
|
"id": "claude-code",
|
|
4
4
|
"display_name": "Claude Code",
|
|
5
5
|
"emit": false,
|
|
@@ -464,9 +464,24 @@
|
|
|
464
464
|
"interactive_approval_tool": true,
|
|
465
465
|
"permission_decision_hook": "PreToolUse",
|
|
466
466
|
"subagent_tool_allowlist": true,
|
|
467
|
-
"subagent_model_per_invocation": true
|
|
467
|
+
"subagent_model_per_invocation": true,
|
|
468
|
+
"session_rename": "/rename <name>",
|
|
469
|
+
"session_rename_reason": "Claude Code's own command for naming a session; the session-name offer prints it filled (generation spec, contract 18). A harness without one gets no offer.",
|
|
470
|
+
"bind_inherit": true,
|
|
471
|
+
"bind_inherit_reason": "commands/bind.md adopts the binding of the checkout a worktree was forked from (`bind --inherit`), so an unbound worktree is told to run that. A harness whose bind cannot inherit is told the explicit vault instead (generation spec, contract 18)."
|
|
468
472
|
},
|
|
469
473
|
"rewrites": [],
|
|
474
|
+
"ui_vocabulary": [
|
|
475
|
+
"/plugin",
|
|
476
|
+
"/reload-plugins",
|
|
477
|
+
"AskUserQuestion"
|
|
478
|
+
],
|
|
479
|
+
"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`.",
|
|
480
|
+
"update_hint": {
|
|
481
|
+
"line": "See /plugin โ Marketplaces to enable auto-update.",
|
|
482
|
+
"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#updates for details."],
|
|
483
|
+
"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."
|
|
484
|
+
},
|
|
470
485
|
"update_instructions": [
|
|
471
486
|
"**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-claude upgrade --project <dir>`). To get notified of new releases (v0.7+):",
|
|
472
487
|
"1. Open `/plugin` โ **Marketplaces** tab.",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"shared_env_reason": "Codex sets Claude Code's plugin-root names too, for compatibility โ measured in every one of the 759 captured hook payloads. Without declaring them, a Codex session scores a strong detection hit for claude-code, ties with codex, and loses the tie to manifest filename order. A variable two harnesses both set identifies neither, so these are demoted to weak for every manifest (generation spec, contract 1, amended 2026-09-08).",
|
|
31
31
|
"session_env": [],
|
|
32
|
-
"session_env_reason": "
|
|
32
|
+
"session_env_reason": "Deliberately empty, though measured. Codex's exec tool sets CODEX_THREAD_ID and CODEX_SESSION_ID in every command (measured 2026-10-05, codex-cli 0.160.0; CODEX_SANDBOX* depend on the sandbox mode), and CODEX_HOME never appeared in a hook environment. Declaring them would identify a session, but session_env also defers the registration and the layout move inside a session (insideHostSession), which would stop the doctor skill's upgrade --json fix from running there. Splitting identity from deferral is the question of *Codex hook contract measured in a live session* (PS-HARNESS); until then a core run from a rendered skill is identified by the shell that bundles it (generation spec, contract 18)."
|
|
33
33
|
},
|
|
34
34
|
"tools": {
|
|
35
35
|
"write_tools": [
|
|
@@ -161,7 +161,10 @@
|
|
|
161
161
|
"kind": "host",
|
|
162
162
|
"scope": "user",
|
|
163
163
|
"scope_reason": "Codex spawns subagents through a tool (collaborationspawn_agent), not by loading a plugin's agents/ directory.",
|
|
164
|
-
"why_unsupported": "Measured: told to run projectstore:critic, Codex spawned six of its own generic subagents named after our roster and briefed them itself; none of our agents/*.md prose reached them. No route from a plugin's agents/ entry to that mechanism has been found (roadmap B4)."
|
|
164
|
+
"why_unsupported": "Measured: told to run projectstore:critic, Codex spawned six of its own generic subagents named after our roster and briefed them itself; none of our agents/*.md prose reached them. No route from a plugin's agents/ entry to that mechanism has been found (roadmap B4).",
|
|
165
|
+
"rendered_as": "skill",
|
|
166
|
+
"rendered_name": "projectstore-<name>",
|
|
167
|
+
"rendered_reason": "Each role is rendered as a skill of this name (the generator reads this key), so a role is called through skills.invocation: $projectstore-critic. Messages name roles this way (generation spec, contract 18)."
|
|
165
168
|
},
|
|
166
169
|
"agents_block": {
|
|
167
170
|
"supported": true,
|
|
@@ -186,7 +189,10 @@
|
|
|
186
189
|
"kind": "host",
|
|
187
190
|
"scope": "user",
|
|
188
191
|
"scope_reason": "Codex has no registrable root slash command; the /prompts: mechanism is deprecated.",
|
|
189
|
-
"why_unsupported": "Shipping commands/ is actively harmful here: Codex rewrites them into skills itself and keeps ${CLAUDE_PLUGIN_ROOT} in the body, producing entry points that exit 1 with Cannot find module '/bin/projectstore.mjs' (reproduced). The generator renders them as skills instead (roadmap B5)."
|
|
192
|
+
"why_unsupported": "Shipping commands/ is actively harmful here: Codex rewrites them into skills itself and keeps ${CLAUDE_PLUGIN_ROOT} in the body, producing entry points that exit 1 with Cannot find module '/bin/projectstore.mjs' (reproduced). The generator renders them as skills instead (roadmap B5).",
|
|
193
|
+
"rendered_as": "skill",
|
|
194
|
+
"rendered_name": "projectstore-<name>",
|
|
195
|
+
"rendered_reason": "Each command is rendered as a skill of this name (the generator reads this key), so a command is called through skills.invocation: $projectstore-doctor. Messages name commands this way (generation spec, contract 18)."
|
|
190
196
|
},
|
|
191
197
|
"hooks": {
|
|
192
198
|
"supported": true,
|
|
@@ -286,7 +292,12 @@
|
|
|
286
292
|
}
|
|
287
293
|
}
|
|
288
294
|
},
|
|
289
|
-
"capabilities": {
|
|
295
|
+
"capabilities": {
|
|
296
|
+
"session_rename": null,
|
|
297
|
+
"session_rename_reason": "No session-rename command is known for Codex, so it gets no session-name offer (generation spec, contract 18).",
|
|
298
|
+
"bind_inherit": false,
|
|
299
|
+
"bind_inherit_reason": "The rendered bind skill and the CLI's bind verb have no inherit path; only Claude Code's command does. An unbound worktree is told to bind the parent checkout's vault explicitly. A follow-up story gives the core an inherit path and flips this."
|
|
300
|
+
},
|
|
290
301
|
"rewrites": [],
|
|
291
302
|
"install": {
|
|
292
303
|
"mechanism": "the package's portable plugin root, registered through its distribution shell and Codex's own marketplace CLI",
|
|
@@ -310,6 +321,11 @@
|
|
|
310
321
|
],
|
|
311
322
|
"docs": "https://github.com/SmartAndPoint/ProjectStore#install--one-message"
|
|
312
323
|
},
|
|
324
|
+
"update_hint": {
|
|
325
|
+
"line": "Update with `{upgrade}`.",
|
|
326
|
+
"welcome": ["**About future updates**: run `{upgrade}` from a terminal, then restart Codex and approve any hook it asks about.", "", "See https://github.com/SmartAndPoint/ProjectStore#updates for details."],
|
|
327
|
+
"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 this harness's shell command at latest, so what is printed runs as printed. 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."
|
|
328
|
+
},
|
|
313
329
|
"update_instructions": "Run `npx projectstore-codex@<version> upgrade --project <dir>`. The installer atomically replaces its stable source and `codex plugin add projectstore@projectstore-npx` materialises the new version; this was measured across two dev builds on codex-cli 0.153.4. Restart Codex afterwards so the new skills and hooks are loaded.",
|
|
314
330
|
"agent_translation": {
|
|
315
331
|
"carries_model": false,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
readActivityAsync,
|
|
32
32
|
resolveInFlightArtifact,
|
|
33
33
|
pathCell,
|
|
34
|
+
commandForm,
|
|
34
35
|
} from "../scripts/lib.mjs";
|
|
35
36
|
|
|
36
37
|
// Exits after the flush, never before: process.exit does not drain a pending
|
|
@@ -110,7 +111,7 @@ async function main() {
|
|
|
110
111
|
} else {
|
|
111
112
|
// True on every path: plugin commands are installation-scoped and not
|
|
112
113
|
// gated by auto_inject.
|
|
113
|
-
parts.push(
|
|
114
|
+
parts.push(`run ${commandForm("status")} to reorient`);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
emit(`projectstore: compacting โ ${parts.join(", ")}`);
|
|
@@ -16,22 +16,34 @@
|
|
|
16
16
|
// Spec: "Entry-rule detection: the score, the open-story predicate, and the
|
|
17
17
|
// delivery seams", contract 17.
|
|
18
18
|
|
|
19
|
-
import { readConfig, readStdinJson, adoptHookInput } from "../scripts/lib.mjs";
|
|
19
|
+
import { readConfig, readStdinJson, adoptHookInput, roleForm, sharedRoleForm } from "../scripts/lib.mjs";
|
|
20
20
|
|
|
21
|
-
// Kept well under the cap; asserted by a test rather than by intention.
|
|
22
|
-
|
|
21
|
+
// Kept well under the cap; asserted by a test rather than by intention. The
|
|
22
|
+
// roles are named the way the session's harness calls them (generation spec,
|
|
23
|
+
// contract 18). The shared AGENTS.md block names them in the source harness's
|
|
24
|
+
// form until that block is per-harness, so a session whose form differs is
|
|
25
|
+
// told once that both name one role โ otherwise the second rule below would
|
|
26
|
+
// have it report the block as a contradiction every session.
|
|
27
|
+
function rules() {
|
|
28
|
+
const critic = roleForm("critic"), reviewer = roleForm("reviewer");
|
|
29
|
+
const shared = sharedRoleForm("critic");
|
|
30
|
+
const bridge = shared === critic
|
|
31
|
+
? ""
|
|
32
|
+
: `\n\nThe \`AGENTS.md\` block may name roles as \`${shared}\`; that is the same role, called \`${critic}\` here.`;
|
|
33
|
+
return `# projectstore โ standing rules for this session
|
|
23
34
|
|
|
24
35
|
**Artifact-first order.** A feature-sized request opens a vault artifact before
|
|
25
36
|
it opens an editor: analysis โ placement (which epic, which story) โ an ADR
|
|
26
|
-
and/or spec when the "how" is non-trivial โ
|
|
27
|
-
implementation โ
|
|
37
|
+
and/or spec when the "how" is non-trivial โ \`${critic}\` โ only then
|
|
38
|
+
implementation โ \`${reviewer}\`. "Feature-sized" is not a judgement
|
|
28
39
|
call about how the request was phrased โ it is about what the work touches. If
|
|
29
40
|
you are about to write across several source files, open the story first.
|
|
30
41
|
|
|
31
42
|
**Report instruction conflicts; do not arbitrate them.** If a session-level or
|
|
32
43
|
harness-level instruction contradicts these rules or the \`AGENTS.md\`
|
|
33
44
|
registration block, say so and ask which wins. Resolving it silently is how the
|
|
34
|
-
contradiction becomes invisible to the person who could have settled it
|
|
45
|
+
contradiction becomes invisible to the person who could have settled it.${bridge}`;
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
function emit(additionalContext) {
|
|
37
49
|
process.stdout.write(JSON.stringify({
|
|
@@ -55,7 +67,7 @@ function main() {
|
|
|
55
67
|
// Honour auto_inject: a session that opted out of context injection is
|
|
56
68
|
// exactly the case for which the AGENTS.md block remains the durable copy.
|
|
57
69
|
if (cfg.auto_inject === false) return;
|
|
58
|
-
emit(
|
|
70
|
+
emit(rules());
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
try { main(); } catch {
|
|
@@ -36,6 +36,11 @@ import {
|
|
|
36
36
|
PATH_CELL,
|
|
37
37
|
ERROR_CELL,
|
|
38
38
|
TITLE_CELL,
|
|
39
|
+
commandForm,
|
|
40
|
+
updateHint,
|
|
41
|
+
speakingDisplayName,
|
|
42
|
+
inheritForm,
|
|
43
|
+
bindInherits,
|
|
39
44
|
} from "../scripts/lib.mjs";
|
|
40
45
|
import { runStartupChecks } from "../scripts/doctor.mjs";
|
|
41
46
|
import { resolveBinding, bindingOfferText } from "../scripts/worktree.mjs";
|
|
@@ -60,8 +65,8 @@ function welcomedMarkerWritePath(proj) {
|
|
|
60
65
|
// the same message.
|
|
61
66
|
function buildWelcome(cfg = null) {
|
|
62
67
|
const start = cfg && cfg.vault_path
|
|
63
|
-
? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want โ the agent picks up commands like
|
|
64
|
-
:
|
|
68
|
+
? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want โ the agent picks up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation, and you approve every write. If the vault has no folders yet, \`${commandForm("scaffold")}\` lays them out.`
|
|
69
|
+
: `**To start using it**: run \`${commandForm("bind", { args: "<vault-path>" })}\` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation; you only approve the writes.`;
|
|
65
70
|
return [
|
|
66
71
|
"# ๐ projectstore is loaded for the first time in this project",
|
|
67
72
|
"",
|
|
@@ -69,12 +74,9 @@ function buildWelcome(cfg = null) {
|
|
|
69
74
|
"",
|
|
70
75
|
start,
|
|
71
76
|
"",
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"3. Toggle **auto-update** on.",
|
|
76
|
-
"",
|
|
77
|
-
"Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#updates for details.",
|
|
77
|
+
// The update advice is the harness's own (its manifest's update_hint):
|
|
78
|
+
// a git marketplace's toggle means nothing to a harness installed from npm.
|
|
79
|
+
...updateHint().welcome,
|
|
78
80
|
"",
|
|
79
81
|
"_This message appears once per project._",
|
|
80
82
|
"",
|
|
@@ -128,7 +130,7 @@ function buildOthersWarning(others) {
|
|
|
128
130
|
"",
|
|
129
131
|
`## โ ๏ธ Multi-session warning โ ${others.length} other projectstore session(s) active on this vault`,
|
|
130
132
|
"",
|
|
131
|
-
|
|
133
|
+
`Another ${speakingDisplayName()} session is currently working on the same vault.`,
|
|
132
134
|
"Active session(s):",
|
|
133
135
|
"",
|
|
134
136
|
];
|
|
@@ -143,13 +145,13 @@ function buildOthersWarning(others) {
|
|
|
143
145
|
);
|
|
144
146
|
}
|
|
145
147
|
if (others.length > SIBLING_CAP) {
|
|
146
|
-
lines.push(`- โฆand ${others.length - SIBLING_CAP} more โ run
|
|
148
|
+
lines.push(`- โฆand ${others.length - SIBLING_CAP} more โ run \`${commandForm("status")}\``);
|
|
147
149
|
}
|
|
148
150
|
lines.push(
|
|
149
151
|
"",
|
|
150
152
|
"**Before creating new ADRs / epics / stories / research:**",
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
`1. Run \`${commandForm("search", { args: "<topic-keywords>" })}\` to check for in-flight artifacts on the same topic.`,
|
|
154
|
+
`2. Run \`${commandForm("status")}\` to see what artifacts have been touched recently.`,
|
|
153
155
|
"3. After creation, the plugin re-checks file existence right before write โ collisions are detected, but topic / number reservation across sessions is on you and the other agent to coordinate.",
|
|
154
156
|
"",
|
|
155
157
|
);
|
|
@@ -179,8 +181,8 @@ async function main() {
|
|
|
179
181
|
// one: a bound project is told what it is bound to, not to bind again.
|
|
180
182
|
const welcomeSystemMessage = welcome
|
|
181
183
|
? (cfg && cfg.vault_path
|
|
182
|
-
? `๐ projectstore: first-run welcome shown. Bound to ${cfg.vault_path}.
|
|
183
|
-
:
|
|
184
|
+
? `๐ projectstore: first-run welcome shown. Bound to ${cfg.vault_path}. ${updateHint().line}`
|
|
185
|
+
: `๐ projectstore: first-run welcome shown. Start with ${commandForm("bind", { args: "<vault-path>" })}. ${updateHint().line}`)
|
|
184
186
|
: null;
|
|
185
187
|
|
|
186
188
|
if (!cfg) {
|
|
@@ -191,7 +193,7 @@ async function main() {
|
|
|
191
193
|
// redundancy is accepted rather than papered over with copy that will rot.
|
|
192
194
|
const offer = binding && binding.state === "inheritable" ? bindingOfferText(binding) : "";
|
|
193
195
|
const offerSystemMessage = offer
|
|
194
|
-
?
|
|
196
|
+
? `projectstore: this worktree is unbound โ ${inheritForm(binding.vaultPath, { layout: binding.layout, language: binding.language })} ${bindInherits() ? "adopts the binding of" : "binds it to the vault, layout and language of"} the checkout it was forked from.`
|
|
195
197
|
: null;
|
|
196
198
|
const body = offer + (welcome || "");
|
|
197
199
|
if (body) {
|
|
@@ -273,9 +275,9 @@ async function main() {
|
|
|
273
275
|
try {
|
|
274
276
|
const r = runStartupChecks(cfg, proj);
|
|
275
277
|
if (r.skipped) {
|
|
276
|
-
doctorMsg =
|
|
278
|
+
doctorMsg = `projectstore doctor: startup checks skipped โ run ${commandForm("doctor")}`;
|
|
277
279
|
} else if (r.count > 0) {
|
|
278
|
-
doctorMsg = `projectstore doctor: ${r.count} install issue(s) โ run
|
|
280
|
+
doctorMsg = `projectstore doctor: ${r.count} install issue(s) โ run ${commandForm("doctor")}`;
|
|
279
281
|
}
|
|
280
282
|
// Offers (doctor's OFFER_CHECKS): one-time steps a user should see once,
|
|
281
283
|
// e.g. the re-stamp after a plugin update โ not issues, not silent.
|
|
@@ -287,7 +289,7 @@ async function main() {
|
|
|
287
289
|
if (gatherError) {
|
|
288
290
|
emit(
|
|
289
291
|
welcome +
|
|
290
|
-
`# projectstore: vault load failed\n\n${truncEnd(String(gatherError.message), ERROR_CELL)}\n\nFix \`.projectstore/projectstore.json\` or run
|
|
292
|
+
`# projectstore: vault load failed\n\n${truncEnd(String(gatherError.message), ERROR_CELL)}\n\nFix \`.projectstore/projectstore.json\` or run \`${commandForm("bind", { args: "<path>" })}\` again.`,
|
|
291
293
|
systemMessage,
|
|
292
294
|
);
|
|
293
295
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"test": "node --test tests/*.test.mjs",
|
|
60
|
+
"test": "node --test --import ./tests/fixtures/hermetic.mjs tests/*.test.mjs",
|
|
61
61
|
"guard": "node scripts/version-guard.mjs",
|
|
62
62
|
"packlist": "node scripts/version-guard.mjs --write-packlist",
|
|
63
63
|
"adapters": "node scripts/build-adapters.mjs --write",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, realpathSync } from "node:fs";
|
|
28
28
|
import { join, resolve, isAbsolute, dirname } from "node:path";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
|
-
import { writeFileAtomic, pluginRoot, ensureRuntimeDir, layoutPaths } from "./lib.mjs";
|
|
30
|
+
import { writeFileAtomic, pluginRoot, ensureRuntimeDir, layoutPaths, commandForm } from "./lib.mjs";
|
|
31
31
|
import { configPath as harnessConfigPath } from "./harness.mjs";
|
|
32
32
|
|
|
33
33
|
export const DEFAULT_LAYOUT = "engineering";
|
|
@@ -154,12 +154,12 @@ export function renderBindPlan(p, done = null) {
|
|
|
154
154
|
const lines = [];
|
|
155
155
|
if (!p.ok) { for (const r of p.refusals) lines.push(r.message); return lines.join("\n") + "\n"; }
|
|
156
156
|
if (p.state === "same") {
|
|
157
|
-
lines.push(`Already bound to ${p.vault}${p.ignored.length ? ` โ --${p.ignored.join(" and --")} ignored: a change of ${p.ignored.join("/")} is not a rebind (edit the config, or rebind to another vault)` : ""}. Run
|
|
157
|
+
lines.push(`Already bound to ${p.vault}${p.ignored.length ? ` โ --${p.ignored.join(" and --")} ignored: a change of ${p.ignored.join("/")} is not a rebind (edit the config, or rebind to another vault)` : ""}. Run ${commandForm("scaffold")} to (re)create the layout, or \`projectstore status\` to inspect it.`);
|
|
158
158
|
return lines.join("\n") + "\n";
|
|
159
159
|
}
|
|
160
160
|
if (done && done.created_vault) lines.push(`Created ${p.vault}`);
|
|
161
161
|
lines.push(`Wrote ${p.configPath}${p.state === "different" ? ` (rebind from ${p.before.vault_path}; kept: ${p.keptKeys.join(", ") || "nothing else"})` : ""}`);
|
|
162
162
|
lines.push(` vault_path: ${p.vault}`, ` layout: ${p.layout}`, ` language: ${p.language}`);
|
|
163
|
-
if (done) lines.push("", p.vaultExists && !done.created_vault ?
|
|
163
|
+
if (done) lines.push("", p.vaultExists && !done.created_vault ? `Next: \`projectstore status\`, or ${commandForm("scaffold")} if the vault has no layout folders yet.` : `Next: ${commandForm("scaffold")} in a session creates the layout's folders and READMEs.`);
|
|
164
164
|
return lines.join("\n") + "\n";
|
|
165
165
|
}
|
|
@@ -6,11 +6,28 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
7
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { emittingHarnesses, sourceHarness } from "./harness.mjs";
|
|
9
|
+
import { emittingHarnesses, sourceHarness, invocation, uiWordPatterns } from "./harness.mjs";
|
|
10
10
|
import { writeFileAtomic } from "./lib.mjs";
|
|
11
11
|
|
|
12
12
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
13
|
const SOURCE = sourceHarness();
|
|
14
|
+
// The source harness's UI affordances, from its manifest (ui_vocabulary): the
|
|
15
|
+
// same list the runtime vocabulary lint reads (generation spec, contract 18).
|
|
16
|
+
// The source's command form and the target's skill namespace, from the
|
|
17
|
+
// manifests (generation spec, contract 18): the body rewrite, the passive-skill
|
|
18
|
+
// name rule and the leak check read these, so a renamed form cannot leave
|
|
19
|
+
// directory names and in-body references disagreeing.
|
|
20
|
+
const escapeRe = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
const SOURCE_COMMAND_PREFIX = SOURCE.surfaces.commands.invocation.split("<name>")[0];
|
|
22
|
+
const SOURCE_COMMAND = new RegExp(`${escapeRe(SOURCE_COMMAND_PREFIX)}([a-z0-9-]+|\\*)`, "g");
|
|
23
|
+
const SOURCE_UI = uiWordPatterns(SOURCE);
|
|
24
|
+
// The source's role form (`projectstore:<name>`) for each of its roles, by file
|
|
25
|
+
// name: a rendered skill that says "spawn projectstore:critic" names a role the
|
|
26
|
+
// way only the source harness calls it. Never inside another form
|
|
27
|
+
// (`/projectstore:`, `$projectstore-`), never a longer name.
|
|
28
|
+
const ROLE_SUFFIX = SOURCE.surfaces.agents.file.replace("<name>", "");
|
|
29
|
+
const SOURCE_ROLES = readdirSync(join(ROOT, SOURCE.surfaces.agents.dir)).filter((f) => f.endsWith(ROLE_SUFFIX)).map((f) => f.slice(0, -ROLE_SUFFIX.length)).sort();
|
|
30
|
+
const SOURCE_ROLE = new RegExp(`(?<![\\w/$:-])${escapeRe(SOURCE.surfaces.agents.invocation.split("<name>")[0])}(${SOURCE_ROLES.map(escapeRe).join("|")})(?![\\w-])`, "g");
|
|
14
31
|
const TARGETS = emittingHarnesses();
|
|
15
32
|
if (TARGETS.length !== 1) throw new Error(`this renderer currently requires exactly one emitting harness; found ${TARGETS.length}`);
|
|
16
33
|
const TARGET = TARGETS[0];
|
|
@@ -61,8 +78,8 @@ function rewriteBody(body) {
|
|
|
61
78
|
.replaceAll(sourceWord, TARGET.id)
|
|
62
79
|
.replaceAll(SOURCE.surfaces.agents_block.reads_natively, TARGET.surfaces.agents_block.reads_natively)
|
|
63
80
|
.replaceAll(SOURCE.runtime.harness_dir + "/", TARGET.runtime.harness_dir + "/")
|
|
64
|
-
.
|
|
65
|
-
.replace(
|
|
81
|
+
.replace(SOURCE_COMMAND, (_, name) => invocation(TARGET, name, { kind: "commands" }))
|
|
82
|
+
.replace(SOURCE_ROLE, (_, name) => invocation(TARGET, name, { kind: "agents" }))
|
|
66
83
|
.replaceAll("AskUserQuestion", "the harness's user-input mechanism")
|
|
67
84
|
.replaceAll("Read tool", "file-reading tool")
|
|
68
85
|
.replaceAll("Write tool", "file-writing tool")
|
|
@@ -105,13 +122,15 @@ contract. A configured model is consumed by the role-orchestration skills on
|
|
|
105
122
|
their next spawn; no restart is needed.`,
|
|
106
123
|
},
|
|
107
124
|
bind: {
|
|
108
|
-
description: "Bind this project to an existing ProjectStore vault, or initialize and bind a new vault, using the harness-neutral core. Arguments: [vault-path].",
|
|
125
|
+
description: "Bind this project to an existing ProjectStore vault, or initialize and bind a new vault, using the harness-neutral core. Arguments: [vault-path] [--layout <name>] [--language <code>].",
|
|
109
126
|
body: `Bind the current project through the core; never write the binding by
|
|
110
127
|
hand.
|
|
111
128
|
|
|
112
|
-
1. Resolve the requested vault path
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
1. Resolve the requested vault path, and the layout and language when the
|
|
130
|
+
request passes \`--layout <name>\` or \`--language <code>\`. If the vault
|
|
131
|
+
exists, use \`bind\`; if the user explicitly asks to create it, use
|
|
132
|
+
\`init\`. Ask for layout and language only when the request has not
|
|
133
|
+
supplied them.
|
|
115
134
|
2. Show the resolved project, vault, verb, layout and language. Ask for explicit
|
|
116
135
|
approval. Naming the vault is the core's non-interactive confirmation.
|
|
117
136
|
3. Run \`node "\${PROJECTSTORE_CORE_ROOT}/bin/projectstore.mjs" <bind|init>
|
|
@@ -151,10 +170,21 @@ Codex application's own task and terminal UI.`,
|
|
|
151
170
|
},
|
|
152
171
|
};
|
|
153
172
|
|
|
173
|
+
// The rendered skills' namespace: the part of rendered_name before the name.
|
|
174
|
+
const NAMESPACE = (TARGET.surfaces?.commands?.rendered_name || "").split("<name>")[0];
|
|
175
|
+
|
|
176
|
+
// A command or role becomes the skill its manifest names (rendered_name), the
|
|
177
|
+
// same name every message calls it by (generation spec, contract 18).
|
|
178
|
+
const renderedName = (kind, name) => {
|
|
179
|
+
const tpl = TARGET.surfaces?.[kind]?.rendered_name;
|
|
180
|
+
if (!tpl) throw new Error(`${TARGET.id}: surfaces.${kind}.rendered_name is required to render ${kind} as skills`);
|
|
181
|
+
return tpl.split("<name>").join(name);
|
|
182
|
+
};
|
|
183
|
+
|
|
154
184
|
function renderCommand(file) {
|
|
155
185
|
const { data, body } = frontmatter(read(file), file);
|
|
156
186
|
const verb = file.split("/").pop().replace(/\.md$/, "");
|
|
157
|
-
const name =
|
|
187
|
+
const name = renderedName("commands", verb);
|
|
158
188
|
const override = COMMAND_OVERRIDES[verb];
|
|
159
189
|
if (override) return [join("skills", name, "SKILL.md"), skill(name, override.description, override.body)];
|
|
160
190
|
const hint = data["argument-hint"] ? ` Arguments: ${data["argument-hint"]}.` : "";
|
|
@@ -165,14 +195,14 @@ function renderPassive(file) {
|
|
|
165
195
|
const { data, body } = frontmatter(read(file), file);
|
|
166
196
|
const dir = file.split("/").at(-2);
|
|
167
197
|
const name = data.name || dir;
|
|
168
|
-
if (!name.startsWith(
|
|
198
|
+
if (!name.startsWith(NAMESPACE)) throw new Error(`${relative(ROOT, file)}: skill name must be ${NAMESPACE}*`);
|
|
169
199
|
return [join("skills", name, "SKILL.md"), skill(name, data.description || name, body)];
|
|
170
200
|
}
|
|
171
201
|
|
|
172
202
|
function renderRole(file) {
|
|
173
203
|
const { data, body } = frontmatter(read(file), file);
|
|
174
204
|
const role = file.split("/").pop().replace(/\.md$/, "");
|
|
175
|
-
const name =
|
|
205
|
+
const name = renderedName("agents", role);
|
|
176
206
|
const orchestration = `## Codex orchestration\n\nThis is a role-orchestration skill, not a native agent registration. Resolve the\nrole model by running:\n\n\`\`\`bash\nnode "\${PROJECTSTORE_CORE_ROOT}/bin/projectstore.mjs" agents model ${role} --json --project "$PWD"\n\`\`\`\n\nSpawn a collaboration agent for the bounded task. If the result names a model,\npass that model and use an empty or bounded context fork; otherwise inherit the\ncurrent model. Do not pass a reasoning-effort override: per-role effort belongs\nto a separate accepted story. Give the spawned agent the role contract below\nand the exact artifact/diff it must inspect. Wait for its final result.\n\n## Role contract\n\n`;
|
|
177
207
|
const description = (data.description || `Run the ProjectStore ${role} role in a fresh collaboration agent.`)
|
|
178
208
|
.replace(/^(?:Opus|Sonnet) \(max-effort\)\s+/, "");
|
|
@@ -203,7 +233,7 @@ export function renderCodexAdapter(root = ROOT) {
|
|
|
203
233
|
...files(join(root, "commands")).map(renderCommand),
|
|
204
234
|
...files(join(root, "agents")).map(renderRole),
|
|
205
235
|
...readdirSync(join(root, "skills"), { withFileTypes: true })
|
|
206
|
-
.filter((e) => e.isDirectory() && e.name.startsWith(
|
|
236
|
+
.filter((e) => e.isDirectory() && e.name.startsWith(NAMESPACE))
|
|
207
237
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
208
238
|
.map((e) => renderPassive(join(root, "skills", e.name, "SKILL.md"))),
|
|
209
239
|
renderHooks(manifest, root),
|
|
@@ -217,8 +247,8 @@ export function renderCodexAdapter(root = ROOT) {
|
|
|
217
247
|
for (const [rel, text] of out) {
|
|
218
248
|
const sourceTokens = [SOURCE.runtime.plugin_root_env, SOURCE.runtime.project_dir_env, SOURCE.display_name, SOURCE.runtime.harness_dir + "/"].filter(Boolean);
|
|
219
249
|
const sourceAgentEnvs = (SOURCE.runtime.agent_overrides || []).map((row) => row.env).filter(Boolean);
|
|
220
|
-
if (sourceTokens.some((token) => text.includes(token)) || sourceAgentEnvs.some((token) => text.includes(token)) || /\$ARGUMENTS
|
|
221
|
-
if (
|
|
250
|
+
if (sourceTokens.some((token) => text.includes(token)) || sourceAgentEnvs.some((token) => text.includes(token)) || text.includes(SOURCE_COMMAND_PREFIX) || new RegExp(SOURCE_ROLE.source).test(text) || /\$ARGUMENTS/.test(text)) throw new Error(`${rel}: source-harness vocabulary leaked into ${TARGET.display_name} adapter`);
|
|
251
|
+
if (SOURCE_UI.some(({ re }) => new RegExp(re.source).test(text)) || /harness\/codex-code\.json|\b(?:opus|sonnet|fable)\b/i.test(text)) throw new Error(`${rel}: source-harness model, environment or UI semantics leaked into ${TARGET.display_name} adapter`);
|
|
222
252
|
}
|
|
223
253
|
return out;
|
|
224
254
|
}
|
|
@@ -43,7 +43,7 @@ import { fileURLToPath } from "node:url";
|
|
|
43
43
|
import { parseArgs } from "node:util";
|
|
44
44
|
import * as term from "./term.mjs";
|
|
45
45
|
import { projectRootDeclared, childEnv, harnessIds, harnessForOverlay, pinPluginRoot } from "./harness.mjs";
|
|
46
|
-
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster } from "./lib.mjs";
|
|
46
|
+
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster, commandForm } from "./lib.mjs";
|
|
47
47
|
import { READ_OPERATIONS, LINEAGE_KINDS, LINEAGE_DEFAULT_DEPTH, SEARCH_DEFAULT_LIMIT, GRAPH_EDGE_CAP, DIRECTIONS } from "./query.mjs";
|
|
48
48
|
// binding.mjs is a write module imported statically where the install family
|
|
49
49
|
// is lazy: it is a dependency-free leaf with no side effects, so the MCP
|
|
@@ -173,7 +173,7 @@ export const VERBS = Object.freeze([
|
|
|
173
173
|
run: runBind(false),
|
|
174
174
|
}),
|
|
175
175
|
Object.freeze({
|
|
176
|
-
verb: "init", summary:
|
|
176
|
+
verb: "init", summary: `init <vault> โ create the vault directory and bind to it; the layout's folders come from ${commandForm("scaffold")}.`,
|
|
177
177
|
module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
178
178
|
options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "an already bound project: create the new vault and point the project at it; every other setting is kept"), JSON_OPT],
|
|
179
179
|
run: runBind(true),
|
|
@@ -401,7 +401,7 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
401
401
|
}
|
|
402
402
|
const project = resolveProject({ project: values.project, env, cwd });
|
|
403
403
|
const cfg = readConfigAt(project);
|
|
404
|
-
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault โ run
|
|
404
|
+
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault โ run ${commandForm("bind", { args: "<vault>", env })} in a session, or \`projectstore bind <vault>\` (\`projectstore init <vault>\` also creates the vault).`, 3);
|
|
405
405
|
try {
|
|
406
406
|
return await row.run({ row, values, positionals: positionals.slice(1), cfg, project, env, cwd, stdin, stdout, stderr, ask });
|
|
407
407
|
} catch (e) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { readConfig, loadLayout, folderByKind, parseFrontmatter, nowIso, isMain } from "./lib.mjs";
|
|
13
|
+
import { readConfig, loadLayout, folderByKind, parseFrontmatter, nowIso, isMain, commandForm } from "./lib.mjs";
|
|
14
14
|
|
|
15
15
|
function die(msg) {
|
|
16
16
|
process.stderr.write(`projectstore/codemap: ${msg}\n`);
|
|
@@ -29,7 +29,7 @@ function parseRefs(raw) {
|
|
|
29
29
|
|
|
30
30
|
function main() {
|
|
31
31
|
const cfg = readConfig();
|
|
32
|
-
if (!cfg) die(
|
|
32
|
+
if (!cfg) die(`No projectstore config. Run ${commandForm("bind")} first.`);
|
|
33
33
|
const layout = loadLayout(cfg.layout);
|
|
34
34
|
const folder = folderByKind(layout, "epic");
|
|
35
35
|
if (!folder) die("Layout has no epic folder โ nothing to map.");
|