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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .claude/commands/opsx/new.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ Start a new change using the experimental artifact-driven approach.

**Otherwise**: Omit `--schema` to use the default.

3. **Create the change directory**
3. **Create the change directory (inside the target package)**

Specs/changes are PER-PACKAGE. First determine which package this change
belongs to (ask the user if unclear), then `cd` into it before running openspec.
NEVER create changes at the monorepo root — a guardrail hook blocks it.

```bash
openspec new change "<name>"
cd packages/<type>/<name-web> && openspec new change "<name>"
```

Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
This creates a scaffolded change at `packages/<type>/<name-web>/openspec/changes/<name>/`
with the selected schema. If the package has no `openspec/` yet, run
`openspec init` inside the package first.

4. **Show the artifact status**

Expand Down
12 changes: 9 additions & 3 deletions .claude/commands/opsx/propose.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ When ready to implement, run /opsx:apply

**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.

2. **Create the change directory**
2. **Create the change directory (inside the target package)**

Specs/changes are PER-PACKAGE. First determine which package this change
belongs to (ask the user if unclear), then `cd` into it before running openspec.
NEVER create changes at the monorepo root — a guardrail hook blocks it.

```bash
openspec new change "<name>"
cd PACKAGE_DIR && openspec new change "CHANGE_NAME"
```

This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
This creates a scaffolded change at `<package-dir>/openspec/changes/<name>/`
with `.openspec.yaml`. If the package has no `openspec/` yet, run
`openspec init` inside the package first.

3. **Get the artifact build order**

Expand Down
95 changes: 95 additions & 0 deletions .claude/hooks/validate-openspec-path.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# validate-openspec-path.sh — Pre-execution hook (OpenSpec location guardrail)
#
# Claude Code invokes this hook before Bash / Write / Edit / MultiEdit tool calls.
# It enforces that OpenSpec specs and changes are created PER-PACKAGE, never at
# the monorepo root. OpenSpec resolves its `openspec/` directory relative to the
# current working directory, so the root `openspec/` must stay off-limits.
#
# Input: JSON on stdin {"tool_name":"...","tool_input":{...},"cwd":"..."}
# Output: exit 0 to allow, exit 2 with JSON {"error":"..."} to block.
#
# IMPORTANT: Do not modify, disable, or bypass this hook.

set -euo pipefail

INPUT="$(cat)"

field() {
# $1 = dotted path into the JSON (e.g. tool_input.file_path)
printf '%s' "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
cur = d
for k in '''$1'''.split('.'):
if isinstance(cur, dict):
cur = cur.get(k, '')
else:
cur = ''
print(cur if isinstance(cur, str) else '')
" 2>/dev/null || true
}

TOOL_NAME="$(field tool_name)"

# Resolve the repo root. Claude Code exports CLAUDE_PROJECT_DIR for hooks; fall
# back to the hook's reported cwd, then to the process cwd.
REPO_ROOT="${CLAUDE_PROJECT_DIR:-}"
if [[ -z "$REPO_ROOT" ]]; then
REPO_ROOT="$(field cwd)"
fi
if [[ -z "$REPO_ROOT" ]]; then
REPO_ROOT="$PWD"
fi
REPO_ROOT="${REPO_ROOT%/}"

ROOT_OPENSPEC="$REPO_ROOT/openspec"

block() {
printf '{"error":"BLOCKED by OpenSpec guardrail: %s Specs/changes are per-package. cd into the target package (e.g. packages/pluggableWidgets/<widget>) and run openspec there — never at the monorepo root."}\n' "$1" >&2
exit 2
}

case "$TOOL_NAME" in
Write|Edit|MultiEdit)
FILE_PATH="$(field tool_input.file_path)"
[[ -z "$FILE_PATH" ]] && exit 0

# Normalize relative paths against the repo root.
case "$FILE_PATH" in
/*) ABS="$FILE_PATH" ;;
*) ABS="$REPO_ROOT/$FILE_PATH" ;;
esac

# Block writes into the ROOT openspec changes/specs trees only.
case "$ABS" in
"$ROOT_OPENSPEC"/changes/*|"$ROOT_OPENSPEC"/specs/*)
block "cannot write '$FILE_PATH' under the root openspec/ directory."
;;
esac
exit 0
;;

Bash)
COMMAND="$(field tool_input.command)"
[[ -z "$COMMAND" ]] && exit 0

CMD_LOWER="$(printf '%s' "$COMMAND" | tr '[:upper:]' '[:lower:]')"

# Only care about spec-creating openspec subcommands.
if [[ "$CMD_LOWER" == *"openspec new"* \
|| "$CMD_LOWER" == *"openspec init"* \
|| "$CMD_LOWER" == *"openspec archive"* ]]; then
# Allow it only when the command first cd's into a package directory.
if [[ "$CMD_LOWER" == *"cd "*"packages/"* ]]; then
exit 0
fi
block "'openspec new/init/archive' must run inside a package."
fi
exit 0
;;

*)
exit 0
;;
esac
11 changes: 11 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/validate-openspec-path.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Monorepo of official Mendix pluggable web widgets. pnpm workspaces + Turbo.
## Constraints

- Never modify dist/, generated files, or lockfiles
- OpenSpec specs/changes are PER-PACKAGE — never create them at the monorepo root.
Always `cd packages/<type>/<name>` before any `openspec new/init/archive`. The
root `openspec/` directory is off-limits (enforced by `.claude/hooks/validate-openspec-path.sh`).
- XML property keys: lowerCamelCase, must match TypeScript props exactly
- Don't override core Atlas UI classes
- Prefer tree-shakable imports for new dependencies
Expand Down
Loading