@siddicky/oh-my-musecode 0.1.0
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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.muse-plugin/plugin.json +94 -0
- package/LICENSE +32 -0
- package/README.md +190 -0
- package/dist/mcp/state-server.d.ts +13 -0
- package/dist/mcp/state-server.js +109 -0
- package/dist/mcp/state-server.js.map +1 -0
- package/dist/paths.d.ts +58 -0
- package/dist/paths.js +167 -0
- package/dist/paths.js.map +1 -0
- package/dist/personas.d.ts +40 -0
- package/dist/personas.js +93 -0
- package/dist/personas.js.map +1 -0
- package/dist/state.d.ts +38 -0
- package/dist/state.js +59 -0
- package/dist/state.js.map +1 -0
- package/docs/recipe.md +253 -0
- package/hooks/hooks.json +34 -0
- package/hooks/lib.mjs +66 -0
- package/hooks/routing.mjs +99 -0
- package/hooks/session-start.mjs +34 -0
- package/hooks/stop.mjs +52 -0
- package/hooks/user-prompt-submit.mjs +16 -0
- package/package.json +57 -0
- package/personas/architect/SOUL.md +27 -0
- package/personas/code-reviewer/SOUL.md +30 -0
- package/personas/critic/SOUL.md +28 -0
- package/personas/debugger/SOUL.md +28 -0
- package/personas/executor/SOUL.md +25 -0
- package/personas/explore/SOUL.md +24 -0
- package/personas/manifest.json +119 -0
- package/personas/planner/SOUL.md +25 -0
- package/personas/test-engineer/SOUL.md +27 -0
- package/personas/verifier/SOUL.md +29 -0
- package/personas/writer/SOUL.md +27 -0
- package/scripts/install.mjs +303 -0
- package/scripts/preflight.mjs +121 -0
- package/scripts/settings-install.mjs +155 -0
- package/scripts/verify-manifest.mjs +211 -0
- package/scripts/verify-skills.mjs +78 -0
- package/skills/cancel/SKILL.md +76 -0
- package/skills/deep-dive/SKILL.md +73 -0
- package/skills/deep-interview/SKILL.md +101 -0
- package/skills/ralph/SKILL.md +111 -0
- package/skills/ralplan/SKILL.md +96 -0
- package/skills/team/SKILL.md +94 -0
- package/skills/trace/SKILL.md +75 -0
package/docs/recipe.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# oh-my-musecode: a gated delivery pipeline that self-hosts
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
|---|---|
|
|
5
|
+
| **Section** | Muse Code plugin |
|
|
6
|
+
| **Time to complete** | ~45 min |
|
|
7
|
+
| **Model** | `meta` provider (any Meta model muse is configured for) |
|
|
8
|
+
| **Harness** | Muse Code (the `muse` CLI, 1.0.3) |
|
|
9
|
+
| **Prerequisites** | Node.js >= 20, `muse` on `PATH`, a workspace you can trust |
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
oh-my-musecode ports oh-my-claudecode's Tier-0 delivery pipeline —
|
|
14
|
+
`deep-interview → deep-dive/trace → ralplan → ralph`, plus `team` and
|
|
15
|
+
`cancel` — onto the `muse` CLI. It ships seven skills and ten personas whose
|
|
16
|
+
`SOUL.md` text and tool allowlists are pulled in on demand via the bundled
|
|
17
|
+
`omm-state` MCP server's `persona_list`/`persona_render` tools, ready to
|
|
18
|
+
interpolate into a `subagent_spawn` objective. Everything is wired together
|
|
19
|
+
with hooks rather than frontmatter, because Muse's skill frontmatter carries
|
|
20
|
+
no `pipeline` or `next-skill` field to read. Each skill's own body tells the
|
|
21
|
+
user, in prose, what to run next.
|
|
22
|
+
|
|
23
|
+
The repo ships a native `.muse-plugin/plugin.json` manifest, correct per
|
|
24
|
+
muse's own documented plugin contract — but muse 1.0.3-R2198.1's plugins
|
|
25
|
+
subsystem is disabled outright (`muse plugins --help` answers "plugins are
|
|
26
|
+
not available in this build"), so on this build the manifest is inert and
|
|
27
|
+
delivery goes through `muse skills install`, a `hooks` entry, and an
|
|
28
|
+
`mcpServers` entry in `settings.json` instead. See Orchestration contract
|
|
29
|
+
and step 1 below for exactly which route runs today.
|
|
30
|
+
|
|
31
|
+
This recipe walks the repo's own acceptance run: installing it, then driving
|
|
32
|
+
`/deep-interview → /ralplan --critic codex → /ralph --critic codex` against
|
|
33
|
+
this same repository. The run is self-hosting — it builds one of the repo's
|
|
34
|
+
own seven skills — so a failure here is a real defect, not a toy demo. What
|
|
35
|
+
is actually verified as of this writing, and what is not, is stated plainly
|
|
36
|
+
in the Proof Point section below — the full pipeline run has not yet been
|
|
37
|
+
executed end to end in a muse session.
|
|
38
|
+
|
|
39
|
+
## When To Use
|
|
40
|
+
|
|
41
|
+
Use this recipe when you want a repeatable pipeline for turning a vague request
|
|
42
|
+
into an approved spec, a reviewed PRD, and verified working code, with an
|
|
43
|
+
optional cross-model adversarial review at the planning and verification
|
|
44
|
+
gates — and you want that pipeline to run on `muse` specifically, respecting
|
|
45
|
+
its approval/sandbox/trust/audit model instead of working around it.
|
|
46
|
+
|
|
47
|
+
Do not use it for a single small edit — the interview and planning stages exist
|
|
48
|
+
to gate genuinely ambiguous or multi-story work, and their overhead does not
|
|
49
|
+
pay for itself on a one-line fix. Do not reach for `--critic codex` casually:
|
|
50
|
+
it requires disabling the session sandbox for the whole run, not just the
|
|
51
|
+
critic call (see Proof Point and What Can Go Wrong below). If the work is
|
|
52
|
+
already well-specified, skip straight to `/ralplan` or even `/ralph` with a
|
|
53
|
+
hand-written `prd.json`.
|
|
54
|
+
|
|
55
|
+
## Orchestration contract
|
|
56
|
+
|
|
57
|
+
| Contract | Muse mechanism |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Skill catalog stays cheap | Skills load as summaries at session open; `read_skill`/invocation pulls the full `SKILL.md` for one turn only |
|
|
60
|
+
| Skills never self-trigger | Explicit-invocation only — no `triggers` field exists in Muse's skill frontmatter subset (`name`, `description`, `allowed-tools`); each `SKILL.md` body says so in prose |
|
|
61
|
+
| Pipeline sequencing | No `next-skill`/`handoff`/`pipeline` frontmatter field exists on Muse. Each skill's body tells the user in prose what to run next (`deep-interview` → "run `/ralplan`"; `ralplan` → "run `/ralph`") |
|
|
62
|
+
| Personas | Ten `SOUL.md` files, pulled in via the `omm-state` MCP server's `persona_list`/`persona_render` tools and interpolated into `subagent_spawn(role, objective, worktree_isolation)` prompt text — not muse Agent Definitions. A Claude-family plugin declaring `agents` gets `unsupported-agent-schema`/`agent-overlay-inactive`, so there is no route to register personas as first-class agent types |
|
|
63
|
+
| Parallel work isolation | `team` and multi-story `ralph` runs pass `worktree_isolation: true` to `subagent_spawn`, landing each child in its own `.muse/worktrees/` checkout |
|
|
64
|
+
| Runtime state | `.omm/` at the workspace root — `.agents/` and `.muse/` are muse-protected: a mediated `edit_file` write there is held for human review with no standing grant, and a shell write fails read-only at the sandbox |
|
|
65
|
+
| Keyword routing / bootstrap / verification gate | `UserPromptSubmit`, `SessionStart`, and `Stop` hooks (`hooks/hooks.json`, `hooks/*.mjs`) restore what inert frontmatter cannot do |
|
|
66
|
+
| Delivery on this build | `muse plugins` is disabled entirely on 1.0.3-R2198.1 ("plugins are not available in this build"), so `scripts/install.mjs` runs `muse skills install --scope user` per skill and merges `hooks`/`mcpServers` into `$CONFIG_DIR/muse/settings.json`. The repo also ships a native `.muse-plugin/plugin.json`, forward-looking for a build with plugins enabled — inert today; there is no `muse plugin install` command on any build |
|
|
67
|
+
|
|
68
|
+
## Walkthrough: the self-hosting acceptance run
|
|
69
|
+
|
|
70
|
+
### 1. Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone <this repo> oh-my-musecode && cd oh-my-musecode
|
|
74
|
+
npm install
|
|
75
|
+
npm run build
|
|
76
|
+
node scripts/install.mjs --workspace /path/to/oh-my-musecode --dry-run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`--dry-run` prints what the installer would do without touching disk. It
|
|
80
|
+
first probes `muse plugins --help`: on 1.0.3-R2198.1 that answers "plugins
|
|
81
|
+
are not available in this build", so the installer takes the settings route
|
|
82
|
+
instead of writing a plugin marketplace entry that muse would silently
|
|
83
|
+
never load. Drop `--dry-run` to install for real. For this self-hosting run,
|
|
84
|
+
the target workspace is this repo itself:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node scripts/install.mjs --workspace .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This runs three routes, each verified to work on 1.0.3-R2198.1:
|
|
91
|
+
|
|
92
|
+
1. `muse skills install <dir> --scope user --force` for each of the 7
|
|
93
|
+
skills, landing in `$CONFIG_DIR/skills/`.
|
|
94
|
+
2. A `hooks` entry merged into `$CONFIG_DIR/muse/settings.json` (a
|
|
95
|
+
SessionStart hook installed this way is what creates `.omm/`).
|
|
96
|
+
3. An `mcpServers` entry for `omm-state` in the same `settings.json`.
|
|
97
|
+
|
|
98
|
+
(`$CONFIG_DIR` is `~/.config/muse`, or `$XDG_CONFIG_HOME/muse` when set.)
|
|
99
|
+
|
|
100
|
+
The installer also runs an escalation preflight against the local `muse`
|
|
101
|
+
build and prints, plainly, whether named permission profiles are available.
|
|
102
|
+
On muse 1.0.3 they are not — read the "External critic posture" section it
|
|
103
|
+
prints; the rest of this walkthrough depends on it.
|
|
104
|
+
|
|
105
|
+
### 2. Confirm the install and start a session
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
muse skills list --source user
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
All seven skills (`deep-interview`, `deep-dive`, `trace`, `ralplan`,
|
|
112
|
+
`ralph`, `team`, `cancel`) should appear with `scope: "user"`. Unlike a
|
|
113
|
+
plugin-loaded skill, a user-scoped skill installed this way does not need
|
|
114
|
+
`--trust-workspace` to be visible — but the hooks and MCP server delivered
|
|
115
|
+
through `settings.json` still only take effect inside a real `muse` session,
|
|
116
|
+
so start one before continuing:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
muse --trust-workspace
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
(or `--yolo`, which also disables approval and sandboxing — do not use that
|
|
123
|
+
here yet; the external-critic stage below is the point where sandboxing
|
|
124
|
+
actually needs to come off, and that decision should be explicit, not a side
|
|
125
|
+
effect of an earlier `--yolo`.)
|
|
126
|
+
|
|
127
|
+
### 3. `/deep-interview`
|
|
128
|
+
|
|
129
|
+
Inside the trusted session, pick one of the plugin's own seven skills that has
|
|
130
|
+
room for a real design decision (for example, tightening the `team` skill's
|
|
131
|
+
worktree-slot-queueing behavior) and run:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
/deep-interview Improve <chosen skill>'s <specific rough edge>.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Answer the interview's questions until it reports ambiguity below threshold
|
|
138
|
+
and presents a spec. Approve it. The spec lands at `.omm/specs/<slug>.md`.
|
|
139
|
+
|
|
140
|
+
### 4. `/ralplan --critic codex`
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
/ralplan .omm/specs/<slug>.md --critic codex
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`ralplan` writes `.omm/state/prd.json`, then routes the consensus review
|
|
147
|
+
through an external `codex` CLI process instead of the in-process `critic`
|
|
148
|
+
persona. This is the point where the earlier preflight result matters: routing
|
|
149
|
+
through an external CLI requires the *whole session* to have been launched
|
|
150
|
+
with sandboxing disabled. If you started with plain `--trust-workspace`, stop
|
|
151
|
+
here, exit, and relaunch as:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
muse --disable-sandbox --trust-workspace
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This is **session-wide**, not scoped to the critic call — every tool call for
|
|
158
|
+
the rest of the session runs unsandboxed, not just the `codex exec` process
|
|
159
|
+
`ralplan` shells out to. It also means the external critic's reasoning and any
|
|
160
|
+
files it touches are **not captured in Muse's append-only session log** the
|
|
161
|
+
way an in-session `subagent_spawn` result is; that portion of the review
|
|
162
|
+
happens outside the audit trail this recipe otherwise relies on as evidence.
|
|
163
|
+
Re-run `/ralplan .omm/specs/<slug>.md --critic codex` in the relaunched
|
|
164
|
+
session. Review the PRD and its critic findings, then approve.
|
|
165
|
+
|
|
166
|
+
### 5. `/ralph --critic codex`
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
/ralph --critic codex
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`ralph` iterates `.omm/state/prd.json` story by story, implementing each one
|
|
173
|
+
(directly or via `subagent_spawn("executor", ..., worktree_isolation: true)`
|
|
174
|
+
for larger stories) and checking every acceptance criterion against real
|
|
175
|
+
command output. When every story is `done` or `blocked`, it runs a final
|
|
176
|
+
verification pass — here, through the same external `codex` CLI, under the
|
|
177
|
+
same session-wide `--disable-sandbox` already in effect. Same two costs as
|
|
178
|
+
step 4 apply again to this pass; `ralph`'s own completion report states them
|
|
179
|
+
again rather than assuming step 4 already covered it.
|
|
180
|
+
|
|
181
|
+
## Proof Point
|
|
182
|
+
|
|
183
|
+
The run is verified done when all of the following hold. Some of these are
|
|
184
|
+
already true today and are stated as such below; the rest depend on
|
|
185
|
+
actually driving steps 3–5 of the walkthrough, which has **not yet been
|
|
186
|
+
executed end to end in a muse session** — this recipe describes the run to
|
|
187
|
+
perform, it does not claim the run has happened.
|
|
188
|
+
|
|
189
|
+
**Verified today, independent of the pipeline run:**
|
|
190
|
+
|
|
191
|
+
1. `muse skills validate <path>` reports `valid`, with no
|
|
192
|
+
`unsupported-skill-field` diagnostics and an empty
|
|
193
|
+
`compatibility.unknown_fields`, for each of the seven skill directories
|
|
194
|
+
under `skills/` (`npm run verify:skills` checks this automatically).
|
|
195
|
+
2. `muse skills install --scope user --force` installs all seven skills, and
|
|
196
|
+
`muse skills list --source user` lists all seven with `scope: "user"`.
|
|
197
|
+
3. `npm test` passes: 84 tests covering routing, state-root protection,
|
|
198
|
+
persona manifest, MCP state server, and installer preflight.
|
|
199
|
+
4. `muse --trust-workspace` (installed via `settings.json`, not a plugin
|
|
200
|
+
marketplace) fires the `SessionStart` hook, which creates `.omm/`.
|
|
201
|
+
5. The `omm-state` MCP server completes an `initialize` handshake and
|
|
202
|
+
refuses reads/writes into protected or symlinked paths.
|
|
203
|
+
|
|
204
|
+
**Still outstanding — to confirm by actually running steps 3–5 above:**
|
|
205
|
+
|
|
206
|
+
6. The `.omm/state/prd.json` story implemented by the run has
|
|
207
|
+
`status: "done"` with recorded evidence per acceptance criterion, and
|
|
208
|
+
none of its stories are `"blocked"`.
|
|
209
|
+
7. `muse export --last --out trajectory.json` produces a self-contained,
|
|
210
|
+
append-only session log covering steps 3–5 of the walkthrough. It will
|
|
211
|
+
show the in-session tool calls (skill invocations, `subagent_spawn`
|
|
212
|
+
children, file writes under `.omm/`) but — honestly — will **not** show
|
|
213
|
+
what the external `codex` CLI process did internally during the `--critic
|
|
214
|
+
codex` stages; only that `ralph`/`ralplan` invoked it and what came back
|
|
215
|
+
on its stdout/stderr, if the skill body chose to record that.
|
|
216
|
+
|
|
217
|
+
## What can go wrong
|
|
218
|
+
|
|
219
|
+
**Skills installed at the wrong scope, or plugins assumed to be on.** These
|
|
220
|
+
skills are installed with `--scope user`, into `$CONFIG_DIR/skills/`, not as
|
|
221
|
+
a workspace plugin — `muse plugins` is disabled on this build, so a
|
|
222
|
+
registered plugin marketplace loads nothing (`muse skills list --source
|
|
223
|
+
plugin --json` → `{"skills":[],"diagnostics":[]}`, silently, with no
|
|
224
|
+
diagnostic pointing at the cause). Confirm the real install with `muse
|
|
225
|
+
skills list --source user` instead. The hooks and `omm-state` MCP server
|
|
226
|
+
delivered via `$CONFIG_DIR/muse/settings.json` still only run inside an
|
|
227
|
+
actual `muse` session — launch with `muse --trust-workspace` (or `--yolo`)
|
|
228
|
+
before expecting the `SessionStart` bootstrap or `UserPromptSubmit` routing
|
|
229
|
+
hook to fire.
|
|
230
|
+
|
|
231
|
+
**Enterprise policy forbids the sandbox bypass.** If the workspace's
|
|
232
|
+
`execution` policy plane sets `forbid_sandbox_bypass`, both `--disable-sandbox`
|
|
233
|
+
and `--yolo` are refused outright. `scripts/install.mjs` checks for this at
|
|
234
|
+
install time and refuses to install rather than delivering a skill set whose
|
|
235
|
+
`--critic codex`/`--critic claude` paths cannot work; `ralph` and `ralplan`
|
|
236
|
+
check it again at run time and fail with a named-policy error rather than
|
|
237
|
+
silently falling back to the in-process critic and reporting as if the
|
|
238
|
+
external review ran. There is no narrower escalation route on muse 1.0.3 —
|
|
239
|
+
named permission profiles are not creatable on this build
|
|
240
|
+
(`execution.permission_profiles` validates `field_not_activated`, and
|
|
241
|
+
`muse exec --permission-profile <id>` reports the profile does not exist) — so
|
|
242
|
+
under this policy, the external-critic option is simply unavailable; use the
|
|
243
|
+
in-process `critic`/`verifier` personas instead.
|
|
244
|
+
|
|
245
|
+
**Headless `muse exec` can't answer approval prompts.** Running any of these
|
|
246
|
+
skills under `muse exec` (rather than the interactive TUI) with the default
|
|
247
|
+
`--approval-mode on-request` will stall on the first mediated tool call,
|
|
248
|
+
since there is no human present to answer it. Either pass
|
|
249
|
+
`--approval-mode never` (accepting that every tool call clears automatically
|
|
250
|
+
for that run) or `--user-input-auto-resolve` to auto-cancel `request_user_input`
|
|
251
|
+
prompts instead of hanging — know which of those two you actually want before
|
|
252
|
+
running `ralph` headless, since they trade off differently between unattended
|
|
253
|
+
progress and silently skipped decisions.
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"${MUSE_PLUGIN_ROOT}/hooks/user-prompt-submit.mjs\""
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"SessionStart": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \"${MUSE_PLUGIN_ROOT}/hooks/session-start.mjs\""
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"Stop": [
|
|
24
|
+
{
|
|
25
|
+
"hooks": [
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "node \"${MUSE_PLUGIN_ROOT}/hooks/stop.mjs\""
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
package/hooks/lib.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook plumbing.
|
|
3
|
+
*
|
|
4
|
+
* Hooks run before anything has been built, so this file and its callers stay
|
|
5
|
+
* plain ESM with no dependency on dist/ and no third-party imports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads and parses the JSON payload muse writes to a hook's stdin.
|
|
10
|
+
*
|
|
11
|
+
* A hook that throws is a hook that breaks the user's session, so a missing or
|
|
12
|
+
* malformed payload resolves to an empty object and lets the caller no-op.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
15
|
+
*/
|
|
16
|
+
export async function readPayload() {
|
|
17
|
+
if (process.stdin.isTTY) return {};
|
|
18
|
+
|
|
19
|
+
const chunks = [];
|
|
20
|
+
try {
|
|
21
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
27
|
+
if (raw === '') return {};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the workspace root from the hook payload, falling back to cwd.
|
|
39
|
+
*
|
|
40
|
+
* @param {Record<string, any>} payload
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function workspaceRootFrom(payload) {
|
|
44
|
+
return (
|
|
45
|
+
payload.workspace_root ??
|
|
46
|
+
payload.workspaceRoot ??
|
|
47
|
+
payload.cwd ??
|
|
48
|
+
process.env.MUSE_WORKSPACE_ROOT ??
|
|
49
|
+
process.cwd()
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emits additional context for the current turn and exits 0.
|
|
55
|
+
*
|
|
56
|
+
* @param {string | null} context
|
|
57
|
+
* @returns {never}
|
|
58
|
+
*/
|
|
59
|
+
export function emitContext(context) {
|
|
60
|
+
if (context) {
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
JSON.stringify({ hookSpecificOutput: { additionalContext: context } }) + '\n',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyword -> skill routing table for the UserPromptSubmit hook.
|
|
3
|
+
*
|
|
4
|
+
* This exists because muse skills are explicit-invocation only: the harness never
|
|
5
|
+
* fires a skill just because a task looks complex, and skill frontmatter has no
|
|
6
|
+
* `triggers` field (muse's frontmatter profile knows only name/description/
|
|
7
|
+
* allowed-tools). So the only way to get oh-my-claudecode's keyword routing is a
|
|
8
|
+
* hook that recognises the word and *suggests* the skill.
|
|
9
|
+
*
|
|
10
|
+
* The hook suggests; it never invokes. Invocation stays the user's.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {ReadonlyArray<{ skill: string, keywords: readonly string[] }>} */
|
|
14
|
+
export const ROUTES = Object.freeze([
|
|
15
|
+
{ skill: 'deep-interview', keywords: ['deep-interview', 'deep interview', 'interview me'] },
|
|
16
|
+
{ skill: 'deep-dive', keywords: ['deep-dive', 'deep dive', 'investigate deeply'] },
|
|
17
|
+
{ skill: 'trace', keywords: ['trace', 'root cause', 'root-cause'] },
|
|
18
|
+
{ skill: 'ralplan', keywords: ['ralplan'] },
|
|
19
|
+
{ skill: 'ralph', keywords: ['ralph'] },
|
|
20
|
+
{ skill: 'team', keywords: ['team mode', 'fan out', 'fan-out'] },
|
|
21
|
+
{ skill: 'cancel', keywords: ['cancelomm', 'cancel omm'] },
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Word-boundary match so "ralph" hits but "ralphie" and "ethnography" do not.
|
|
26
|
+
* Keywords containing spaces are matched as phrases.
|
|
27
|
+
*
|
|
28
|
+
* `/`, `.` and `_` count as word characters here, not boundaries, so a file path
|
|
29
|
+
* like `lib/ralph.js` or `src/trace_test.py` does not read as a request to run the
|
|
30
|
+
* skill. Someone naming a path is talking about code, not asking for a pipeline.
|
|
31
|
+
* Explicit `/ralph` invocations are handled separately by matchesSlashCommand.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} prompt
|
|
34
|
+
* @param {string} keyword
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function matchesKeyword(prompt, keyword) {
|
|
38
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
return new RegExp(`(^|[^a-z0-9._/-])${escaped}([^a-z0-9._/-]|$)`, 'i').test(prompt);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Matches an explicit slash invocation such as `/cancel` or `/ralph`.
|
|
44
|
+
*
|
|
45
|
+
* This is the high-precision path, and it is the ONLY way some skills can be
|
|
46
|
+
* routed. `cancel` is the clearest case: the bare word is far too common in
|
|
47
|
+
* ordinary English ("cancel that subscription", "how do I cancel a promise") to
|
|
48
|
+
* route on, and cancellation discards in-flight state, so a false positive is
|
|
49
|
+
* expensive. Requiring the slash keeps the routing safe without dropping the
|
|
50
|
+
* skill from the table.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} prompt
|
|
53
|
+
* @param {string} skill
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function matchesSlashCommand(prompt, skill) {
|
|
57
|
+
const escaped = skill.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
return new RegExp(`(^|\\s)/${escaped}([^a-z0-9-]|$)`, 'i').test(prompt);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the skill ids suggested by a prompt, in ROUTES order, without duplicates.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} prompt
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
export function routePrompt(prompt) {
|
|
68
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') return [];
|
|
69
|
+
|
|
70
|
+
const hits = [];
|
|
71
|
+
for (const { skill, keywords } of ROUTES) {
|
|
72
|
+
const matched =
|
|
73
|
+
matchesSlashCommand(prompt, skill) ||
|
|
74
|
+
keywords.some((keyword) => matchesKeyword(prompt, keyword));
|
|
75
|
+
if (matched) hits.push(skill);
|
|
76
|
+
}
|
|
77
|
+
return hits;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Renders the additional context emitted for matched skills, or null when nothing
|
|
82
|
+
* matched. Returning null (rather than an empty string) keeps the caller honest
|
|
83
|
+
* about the "stay silent" path.
|
|
84
|
+
*
|
|
85
|
+
* @param {string[]} skills
|
|
86
|
+
* @returns {string | null}
|
|
87
|
+
*/
|
|
88
|
+
export function renderRoutingContext(skills) {
|
|
89
|
+
if (skills.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
const lines = skills.map((skill) => ` - /${skill}`);
|
|
92
|
+
return [
|
|
93
|
+
'oh-my-musecode: this prompt mentions a skill that is available in this workspace.',
|
|
94
|
+
'muse skills are invoke-only, so nothing has been run. Suggested:',
|
|
95
|
+
...lines,
|
|
96
|
+
'',
|
|
97
|
+
'Invoke one explicitly to load its instructions for that turn.',
|
|
98
|
+
].join('\n');
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook: make sure the .omm/ state root exists.
|
|
4
|
+
*
|
|
5
|
+
* State lives in .omm/ and never in .agents/ or .muse/: muse protects both, so a
|
|
6
|
+
* mediated write there is held for human review and a shell write fails read-only
|
|
7
|
+
* at the sandbox. This hook only ever creates .omm/, so it takes no untrusted path
|
|
8
|
+
* input and needs no path-policy check.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { readPayload, workspaceRootFrom, emitContext } from './lib.mjs';
|
|
14
|
+
|
|
15
|
+
const payload = await readPayload();
|
|
16
|
+
const stateRoot = join(workspaceRootFrom(payload), '.omm');
|
|
17
|
+
|
|
18
|
+
let created = false;
|
|
19
|
+
if (!existsSync(stateRoot)) {
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(join(stateRoot, 'state'), { recursive: true });
|
|
22
|
+
created = true;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// A session that cannot create state is still a usable session; say so and
|
|
25
|
+
// carry on rather than failing the user's startup.
|
|
26
|
+
emitContext(`oh-my-musecode: could not create ${stateRoot} (${err.message}).`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
emitContext(
|
|
31
|
+
created
|
|
32
|
+
? `oh-my-musecode: initialised state root at ${stateRoot}.`
|
|
33
|
+
: null,
|
|
34
|
+
);
|
package/hooks/stop.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook: hold the verification gate open while a ralph run is active.
|
|
4
|
+
*
|
|
5
|
+
* oh-my-claudecode's ralph must not declare completion without reviewer sign-off.
|
|
6
|
+
* Skill bodies alone cannot enforce that here, because a skill only shapes the one
|
|
7
|
+
* turn it was invoked on — by the time the model is stopping, the ralph skill body
|
|
8
|
+
* may be many turns behind it. The Stop hook is the only surface that sees every
|
|
9
|
+
* end-of-turn, so it is where the reminder belongs.
|
|
10
|
+
*
|
|
11
|
+
* It reminds; it does not block.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { readPayload, workspaceRootFrom, emitContext } from './lib.mjs';
|
|
17
|
+
|
|
18
|
+
const payload = await readPayload();
|
|
19
|
+
const statePath = join(workspaceRootFrom(payload), '.omm', 'state', 'ralph-state.json');
|
|
20
|
+
|
|
21
|
+
/** @returns {Record<string, any> | null} */
|
|
22
|
+
function readRalphState() {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
25
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
26
|
+
} catch {
|
|
27
|
+
// Absent or unreadable state means no active run to gate.
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const state = readRalphState();
|
|
33
|
+
|
|
34
|
+
if (!state?.active) {
|
|
35
|
+
emitContext(null);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const reviewer = state.critic_mode ?? 'architect';
|
|
39
|
+
const story = state.current_story ? ` (current story: ${state.current_story})` : '';
|
|
40
|
+
|
|
41
|
+
emitContext(
|
|
42
|
+
[
|
|
43
|
+
`oh-my-musecode: a ralph run is still active${story}.`,
|
|
44
|
+
'',
|
|
45
|
+
'Before claiming completion, confirm:',
|
|
46
|
+
' - every prd.json story has passes: true against its own acceptance criteria',
|
|
47
|
+
` - the ${reviewer} reviewer has approved this run`,
|
|
48
|
+
' - a fresh build/test run was read, not assumed',
|
|
49
|
+
'',
|
|
50
|
+
'If the run is genuinely finished, invoke /cancel to clear state.',
|
|
51
|
+
].join('\n'),
|
|
52
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook: suggest oh-my-musecode skills a prompt names.
|
|
4
|
+
*
|
|
5
|
+
* Restores oh-my-claudecode's keyword routing, which cannot come from frontmatter
|
|
6
|
+
* here — muse's skill frontmatter profile has no `triggers` field, and skills are
|
|
7
|
+
* invoke-only by design. The hook therefore surfaces the suggestion and stops.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readPayload, emitContext } from './lib.mjs';
|
|
11
|
+
import { routePrompt, renderRoutingContext } from './routing.mjs';
|
|
12
|
+
|
|
13
|
+
const payload = await readPayload();
|
|
14
|
+
const prompt = payload.prompt ?? payload.user_prompt ?? '';
|
|
15
|
+
|
|
16
|
+
emitContext(renderRoutingContext(routePrompt(String(prompt))));
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@siddicky/oh-my-musecode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An OMC-shaped delivery harness for Meta Muse Code: deep-interview -> ralplan -> ralph, plus deep-dive/trace, team and cancel.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"test": "npm run build && node --test \"test/**/*.test.mjs\"",
|
|
13
|
+
"lint": "node scripts/verify-manifest.mjs && tsc -p tsconfig.json --noEmit",
|
|
14
|
+
"verify:manifest": "node scripts/verify-manifest.mjs",
|
|
15
|
+
"verify:skills": "node scripts/verify-skills.mjs",
|
|
16
|
+
"install:muse": "node scripts/install.mjs"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"typescript": "^5.6.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
24
|
+
},
|
|
25
|
+
"author": "Abdullah Siddique",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/siddicky/oh-my-musecode.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"muse",
|
|
32
|
+
"muse-code",
|
|
33
|
+
"agent",
|
|
34
|
+
"harness",
|
|
35
|
+
"skills",
|
|
36
|
+
"orchestration"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist/",
|
|
43
|
+
"skills/",
|
|
44
|
+
"personas/",
|
|
45
|
+
"hooks/",
|
|
46
|
+
"scripts/",
|
|
47
|
+
"docs/",
|
|
48
|
+
".muse-plugin/",
|
|
49
|
+
".claude-plugin/",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"bin": {
|
|
54
|
+
"oh-my-musecode": "scripts/install.mjs"
|
|
55
|
+
},
|
|
56
|
+
"prepublishOnly": "npm run build && npm test"
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
You are an architect. You give strategic technical advice on structure, trade-offs, and
|
|
2
|
+
long-term consequences, and you never touch the code yourself.
|
|
3
|
+
|
|
4
|
+
## How you work
|
|
5
|
+
|
|
6
|
+
- Read broadly before opining. Understand the existing shape of the system before you
|
|
7
|
+
propose changing it.
|
|
8
|
+
- Reason in trade-offs, not verdicts. For any nontrivial recommendation, name what it
|
|
9
|
+
costs as well as what it buys, and say what you'd need to see to be more sure.
|
|
10
|
+
- Weigh options against the system's actual constraints — its scale, its team, its
|
|
11
|
+
failure modes — not against an abstract ideal of correctness.
|
|
12
|
+
- Flag the decisions that are expensive to reverse. A choice that can be undone in an
|
|
13
|
+
afternoon deserves less of your attention than one that gets baked into everything
|
|
14
|
+
built on top of it.
|
|
15
|
+
- Be direct about risk. If a proposed direction is likely to cause pain later, say so
|
|
16
|
+
plainly and say when that pain would show up.
|
|
17
|
+
- Give a recommendation, not just an inventory of possibilities, when asked to decide
|
|
18
|
+
between options.
|
|
19
|
+
|
|
20
|
+
## What you do not do
|
|
21
|
+
|
|
22
|
+
- You do not write or edit code. You do not run commands that change anything. Your
|
|
23
|
+
output is analysis and recommendation, nothing else.
|
|
24
|
+
- You do not rubber-stamp a plan to be agreeable. Advisory means honest, not compliant.
|
|
25
|
+
- You do not chase implementation details that don't change the architectural call —
|
|
26
|
+
that level belongs to whoever builds it.
|
|
27
|
+
- You do not pretend certainty you don't have.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
You are a code reviewer. You read a diff and find defects worth fixing before it ships,
|
|
2
|
+
each one rated by how much it matters.
|
|
3
|
+
|
|
4
|
+
## How you work
|
|
5
|
+
|
|
6
|
+
- Read the diff in the context of the surrounding code, not in isolation — a change
|
|
7
|
+
that's fine on its own can still be wrong for the file it lands in.
|
|
8
|
+
- Hunt specifically for logic defects: wrong conditionals, off-by-one errors, unhandled
|
|
9
|
+
edge cases, state mutated where it shouldn't be, error paths that swallow failures.
|
|
10
|
+
- Check structure too: does this violate a single-responsibility boundary that already
|
|
11
|
+
existed, does it duplicate something nearby, does it introduce a dependency that
|
|
12
|
+
didn't need to exist.
|
|
13
|
+
- Rate every finding by severity — a crash-causing bug and a naming nitpick are never
|
|
14
|
+
reported with equal weight. Say plainly which findings block merging and which are
|
|
15
|
+
optional polish.
|
|
16
|
+
- Give a fix, not just a complaint, when the fix is obvious. When it isn't, describe the
|
|
17
|
+
problem precisely enough that someone else could find the fix.
|
|
18
|
+
- Note real risk (performance cliff, security gap, silent data loss) even when it's
|
|
19
|
+
outside the literal lines changed, if the diff caused or exposed it.
|
|
20
|
+
|
|
21
|
+
## What you do not do
|
|
22
|
+
|
|
23
|
+
- You do not apply the fixes yourself unless explicitly asked to — review and repair
|
|
24
|
+
are different passes.
|
|
25
|
+
- You do not review the plan or the intent behind the change; you review the diff as
|
|
26
|
+
written.
|
|
27
|
+
- You do not flag every stylistic preference as a defect. Style comments are labeled as
|
|
28
|
+
such, separate from correctness findings.
|
|
29
|
+
- You do not wave through a diff because it's small or because the author is under
|
|
30
|
+
time pressure.
|