@windyroad/architect 0.2.0 → 0.3.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/README.md +65 -0
- package/agents/agent.md +1 -13
- package/bin/install.mjs +3 -2
- package/hooks/architect-enforce-edit.sh +22 -0
- package/hooks/architect-mark-reviewed.sh +17 -14
- package/hooks/hooks.json +0 -3
- package/hooks/lib/architect-gate.sh +1 -1
- package/hooks/test/architect-enforce-scope.bats +62 -0
- package/hooks/test/architect-gate.bats +32 -0
- package/hooks/test/architect-mark-reviewed.bats +26 -0
- package/hooks/test/architect-no-stop-hook.bats +17 -0
- package/hooks/test/architect-project-root.bats +44 -0
- package/lib/install-utils.mjs +143 -0
- package/package.json +3 -2
- package/skills/{wr:adr → create-adr}/SKILL.md +1 -1
- package/skills/review-design/SKILL.md +91 -0
- package/hooks/architect-reset-marker.sh +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @windyroad/architect
|
|
2
|
+
|
|
3
|
+
**Architecture decision enforcement for Claude Code.** Ensures every code change is reviewed against your project's architecture decisions before it lands.
|
|
4
|
+
|
|
5
|
+
Part of [Windy Road Agent Plugins](../../README.md).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
The architect plugin prevents architectural drift by gating edits behind an architecture review. When you have a `docs/decisions/` directory, the plugin:
|
|
10
|
+
|
|
11
|
+
1. **Detects** your architecture decisions on every prompt
|
|
12
|
+
2. **Blocks** edits to project files until the architect agent has reviewed the proposed changes
|
|
13
|
+
3. **Reviews** changes against your existing ADRs (Architecture Decision Records) and flags conflicts
|
|
14
|
+
4. **Flags** when a new decision should be documented
|
|
15
|
+
|
|
16
|
+
No decisions directory yet? The plugin stays silent until you create one.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @windyroad/architect
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Restart Claude Code after installing.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Once installed, the plugin works automatically. You don't need to invoke it -- it intercepts edits and runs the review before allowing changes through.
|
|
29
|
+
|
|
30
|
+
**Create a new Architecture Decision Record:**
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/wr-architect:create-adr
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This walks you through creating an ADR in [MADR 4.0](https://adr.github.io/madr/) format. It examines your existing decisions, asks about the problem and options, and writes a properly formatted record to `docs/decisions/`.
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
| Hook | Trigger | What it does |
|
|
41
|
+
|------|---------|-------------|
|
|
42
|
+
| `architect-detect.sh` | Every prompt | Checks for `docs/decisions/` and injects the review instruction |
|
|
43
|
+
| `architect-enforce-edit.sh` | Edit or Write | Blocks the edit if the architect hasn't reviewed yet |
|
|
44
|
+
| `architect-plan-enforce.sh` | ExitPlanMode | Ensures plans are reviewed before execution |
|
|
45
|
+
| `architect-mark-reviewed.sh` | Agent completes | Marks the review as done (TTL: 1800s) |
|
|
46
|
+
| `architect-refresh-hash.sh` | After edit | Refreshes the content hash so the next edit triggers a fresh review |
|
|
47
|
+
|
|
48
|
+
## Agent
|
|
49
|
+
|
|
50
|
+
The `wr-architect:agent` reviews proposed changes against existing decisions in `docs/decisions/` and reports:
|
|
51
|
+
|
|
52
|
+
- Whether changes comply with or violate existing decisions
|
|
53
|
+
- Whether a new ADR should be created
|
|
54
|
+
- Whether existing decisions are stale and need reassessment
|
|
55
|
+
|
|
56
|
+
## Updating and Uninstalling
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @windyroad/architect --update
|
|
60
|
+
npx @windyroad/architect --uninstall
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Licence
|
|
64
|
+
|
|
65
|
+
[MIT](../../LICENSE)
|
package/agents/agent.md
CHANGED
|
@@ -10,7 +10,6 @@ tools:
|
|
|
10
10
|
- Read
|
|
11
11
|
- Glob
|
|
12
12
|
- Grep
|
|
13
|
-
- Bash
|
|
14
13
|
model: inherit
|
|
15
14
|
---
|
|
16
15
|
|
|
@@ -107,20 +106,9 @@ Issue types:
|
|
|
107
106
|
- **[Missing Supersession]**: A new decision should supersede an old one but doesn't
|
|
108
107
|
- **[Confirmation Violation]**: New code violates a confirmation criterion of an existing decision
|
|
109
108
|
|
|
110
|
-
## Verdict File
|
|
111
|
-
|
|
112
|
-
After completing your review, you MUST write a verdict file so the hook system knows the outcome:
|
|
113
|
-
|
|
114
|
-
- After **PASS**: `echo "PASS" > /tmp/architect-verdict`
|
|
115
|
-
- After **ISSUES FOUND**: `echo "FAIL" > /tmp/architect-verdict`
|
|
116
|
-
|
|
117
|
-
Advisory items (staleness flags) do NOT count as FAIL. Only write FAIL when there are actionable issues (Decision Conflict, Undocumented Decision, Decision Format, Missing Supersession, Confirmation Violation).
|
|
118
|
-
|
|
119
|
-
You MUST NOT use Bash for anything other than writing the verdict file.
|
|
120
|
-
|
|
121
109
|
## Constraints
|
|
122
110
|
|
|
123
|
-
- You are read-only. You do not edit files
|
|
111
|
+
- You are read-only. You do not edit files.
|
|
124
112
|
- You review all project files: source code, configuration, CI workflows, hook scripts, build scripts, and decision files. The only exclusions are stylesheets, images, lockfiles, and font files.
|
|
125
113
|
- If the change is purely cosmetic (comments, formatting, whitespace), report PASS.
|
|
126
114
|
- Do not block changes that are clearly within the scope of an existing accepted decision.
|
package/bin/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { resolve, dirname } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const utils = await import(resolve(__dirname, "
|
|
7
|
+
const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
|
|
8
8
|
|
|
9
9
|
const PLUGIN = "wr-architect";
|
|
10
10
|
const DEPS = [];
|
|
@@ -20,6 +20,7 @@ Architecture decision enforcement for AI coding agents
|
|
|
20
20
|
Options:
|
|
21
21
|
--update Update this plugin and its skills
|
|
22
22
|
--uninstall Remove this plugin
|
|
23
|
+
--scope Installation scope: project (default) or user
|
|
23
24
|
--dry-run Show what would be done without executing
|
|
24
25
|
--help, -h Show this help
|
|
25
26
|
`);
|
|
@@ -38,5 +39,5 @@ if (flags.uninstall) {
|
|
|
38
39
|
} else if (flags.update) {
|
|
39
40
|
utils.updatePackage(PLUGIN);
|
|
40
41
|
} else {
|
|
41
|
-
utils.installPackage(PLUGIN, { deps: DEPS });
|
|
42
|
+
utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope });
|
|
42
43
|
}
|
|
@@ -20,6 +20,17 @@ if [ -z "$FILE_PATH" ]; then
|
|
|
20
20
|
exit 0
|
|
21
21
|
fi
|
|
22
22
|
|
|
23
|
+
# P004: Only gate files inside the project root. Absolute paths outside
|
|
24
|
+
# $PWD (e.g., ~/.claude/channels/*) are not project files.
|
|
25
|
+
case "$FILE_PATH" in
|
|
26
|
+
/*)
|
|
27
|
+
case "$FILE_PATH" in
|
|
28
|
+
"$PWD"/*) ;;
|
|
29
|
+
*) exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
23
34
|
# Only gate if the project has architecture decisions
|
|
24
35
|
if [ ! -d "docs/decisions" ]; then
|
|
25
36
|
exit 0
|
|
@@ -53,6 +64,17 @@ case "$FILE_PATH" in
|
|
|
53
64
|
exit 0 ;;
|
|
54
65
|
*/docs/problems/*.md|docs/problems/*.md)
|
|
55
66
|
exit 0 ;;
|
|
67
|
+
# Peer-plugin policy files — governed by their own plugin's enforce hook, not architect (P009)
|
|
68
|
+
*/docs/JOBS_TO_BE_DONE.md|docs/JOBS_TO_BE_DONE.md)
|
|
69
|
+
exit 0 ;;
|
|
70
|
+
*/docs/PRODUCT_DISCOVERY.md|docs/PRODUCT_DISCOVERY.md)
|
|
71
|
+
exit 0 ;;
|
|
72
|
+
*/docs/jtbd/*|docs/jtbd/*)
|
|
73
|
+
exit 0 ;;
|
|
74
|
+
*/docs/VOICE-AND-TONE.md|docs/VOICE-AND-TONE.md)
|
|
75
|
+
exit 0 ;;
|
|
76
|
+
*/docs/STYLE-GUIDE.md|docs/STYLE-GUIDE.md)
|
|
77
|
+
exit 0 ;;
|
|
56
78
|
esac
|
|
57
79
|
|
|
58
80
|
# Check gate
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Architecture - PostToolUse hook for Agent tool
|
|
3
3
|
# Creates a session marker when architect has been consulted.
|
|
4
|
+
# Parses verdict from agent output text (session-safe, no temp files).
|
|
4
5
|
# This marker unlocks the architect-enforce-edit.sh PreToolUse block.
|
|
5
|
-
# Mirrors: voice-tone-mark-reviewed.sh
|
|
6
6
|
|
|
7
|
-
# Source shared portable helpers (_mtime, _hashcmd)
|
|
8
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
8
|
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
_parse_input
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
TOOL_NAME=$(_get_tool_name)
|
|
13
|
+
[ "$TOOL_NAME" = "Agent" ] || exit 0
|
|
14
|
+
|
|
15
|
+
SUBAGENT=$(_get_subagent_type)
|
|
16
|
+
SESSION_ID=$(_get_session_id)
|
|
15
17
|
|
|
16
18
|
if [ -z "$SESSION_ID" ]; then
|
|
17
19
|
exit 0
|
|
@@ -19,25 +21,24 @@ fi
|
|
|
19
21
|
|
|
20
22
|
case "$SUBAGENT" in
|
|
21
23
|
*architect*)
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
+
# Parse verdict from agent output text (no temp file needed)
|
|
25
|
+
AGENT_OUTPUT=$(_get_tool_output)
|
|
24
26
|
VERDICT=""
|
|
25
|
-
if
|
|
26
|
-
VERDICT
|
|
27
|
-
|
|
27
|
+
if echo "$AGENT_OUTPUT" | grep -q "Architecture Review: PASS"; then
|
|
28
|
+
VERDICT="PASS"
|
|
29
|
+
elif echo "$AGENT_OUTPUT" | grep -q "ISSUES FOUND"; then
|
|
30
|
+
VERDICT="FAIL"
|
|
28
31
|
fi
|
|
29
32
|
|
|
30
33
|
case "$VERDICT" in
|
|
31
34
|
PASS)
|
|
32
|
-
# Architect explicitly passed, create marker
|
|
33
35
|
touch "/tmp/architect-reviewed-${SESSION_ID}"
|
|
34
36
|
;;
|
|
35
37
|
FAIL)
|
|
36
|
-
#
|
|
38
|
+
# Do NOT create marker — review found issues
|
|
37
39
|
;;
|
|
38
40
|
*)
|
|
39
|
-
#
|
|
40
|
-
# Allow with warning to avoid permanent lockout
|
|
41
|
+
# Could not parse verdict — allow with marker to avoid lockout
|
|
41
42
|
touch "/tmp/architect-reviewed-${SESSION_ID}"
|
|
42
43
|
;;
|
|
43
44
|
esac
|
|
@@ -52,6 +53,8 @@ case "$SUBAGENT" in
|
|
|
52
53
|
echo "$HASH" > "/tmp/architect-reviewed-${SESSION_ID}.hash"
|
|
53
54
|
fi
|
|
54
55
|
|
|
56
|
+
# Plan review marker
|
|
57
|
+
touch "/tmp/architect-plan-reviewed-${SESSION_ID}"
|
|
55
58
|
;;
|
|
56
59
|
esac
|
|
57
60
|
|
package/hooks/hooks.json
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
"PostToolUse": [
|
|
11
11
|
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-mark-reviewed.sh" }] },
|
|
12
12
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-refresh-hash.sh" }] }
|
|
13
|
-
],
|
|
14
|
-
"Stop": [
|
|
15
|
-
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-reset-marker.sh" }] }
|
|
16
13
|
]
|
|
17
14
|
}
|
|
18
15
|
}
|
|
@@ -12,7 +12,7 @@ source "$_ARCHITECT_GATE_DIR/gate-helpers.sh"
|
|
|
12
12
|
check_architect_gate() {
|
|
13
13
|
local SESSION_ID="$1"
|
|
14
14
|
local MARKER="/tmp/architect-reviewed-${SESSION_ID}"
|
|
15
|
-
local TTL_SECONDS="${ARCHITECT_TTL:-
|
|
15
|
+
local TTL_SECONDS="${ARCHITECT_TTL:-1800}"
|
|
16
16
|
|
|
17
17
|
if [ -n "$SESSION_ID" ] && [ -f "$MARKER" ]; then
|
|
18
18
|
local NOW=$(date +%s)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for architect-enforce-edit.sh — verifies peer-plugin policy files are
|
|
4
|
+
# exempt from the architect gate (P009). Each plugin governs its own policy
|
|
5
|
+
# docs via its own enforce hook; the architect should not re-gate them.
|
|
6
|
+
#
|
|
7
|
+
# All tests are functional (P011): they execute the hook with mock JSON
|
|
8
|
+
# input and assert on exit status + BLOCKED output. Source-grep assertions
|
|
9
|
+
# were removed because they over-specified the implementation and gave
|
|
10
|
+
# false confidence (a literal string can appear in source without the
|
|
11
|
+
# matching case branch actually short-circuiting).
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
15
|
+
HOOK="$SCRIPT_DIR/architect-enforce-edit.sh"
|
|
16
|
+
ORIG_DIR="$PWD"
|
|
17
|
+
TEST_DIR=$(mktemp -d)
|
|
18
|
+
cd "$TEST_DIR"
|
|
19
|
+
# Engage the gate: architect-enforce only runs when docs/decisions/ exists.
|
|
20
|
+
mkdir -p docs/decisions
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
teardown() {
|
|
24
|
+
cd "$ORIG_DIR"
|
|
25
|
+
rm -rf "$TEST_DIR"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Helper: run the hook with a mock JSON input for a given file path.
|
|
29
|
+
# Claude Code passes absolute file_path values, so tests use $PWD-prefixed
|
|
30
|
+
# paths to match real shape (after the P004 root check).
|
|
31
|
+
run_hook_with_file() {
|
|
32
|
+
local file_path="$1"
|
|
33
|
+
local json="{\"tool_input\":{\"file_path\":\"${file_path}\"},\"session_id\":\"test-session-$$\"}"
|
|
34
|
+
echo "$json" | bash "$HOOK"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
assert_path_allowed() {
|
|
38
|
+
local file_path="$1"
|
|
39
|
+
run run_hook_with_file "$file_path"
|
|
40
|
+
[ "$status" -eq 0 ]
|
|
41
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "architect: exempts JTBD policy file (P009)" {
|
|
45
|
+
assert_path_allowed "$PWD/docs/JOBS_TO_BE_DONE.md"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@test "architect: exempts JTBD directory file (P009)" {
|
|
49
|
+
assert_path_allowed "$PWD/docs/jtbd/solo-developer/persona.md"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "architect: exempts PRODUCT_DISCOVERY.md (P009)" {
|
|
53
|
+
assert_path_allowed "$PWD/docs/PRODUCT_DISCOVERY.md"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "architect: exempts voice-tone policy file (P009)" {
|
|
57
|
+
assert_path_allowed "$PWD/docs/VOICE-AND-TONE.md"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test "architect: exempts style-guide policy file (P009)" {
|
|
61
|
+
assert_path_allowed "$PWD/docs/STYLE-GUIDE.md"
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for architect-gate.sh (TTL, drift, marker)
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/architect-gate.sh"
|
|
8
|
+
TEST_SESSION="test-$$-$BATS_TEST_NUMBER"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
teardown() {
|
|
12
|
+
rm -f "/tmp/architect-reviewed-${TEST_SESSION}"
|
|
13
|
+
rm -f "/tmp/architect-reviewed-${TEST_SESSION}.hash"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@test "gate denies when no marker exists" {
|
|
17
|
+
run check_architect_gate "$TEST_SESSION"
|
|
18
|
+
[ "$status" -ne 0 ]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@test "gate allows when marker exists and is fresh" {
|
|
22
|
+
touch "/tmp/architect-reviewed-${TEST_SESSION}"
|
|
23
|
+
run check_architect_gate "$TEST_SESSION"
|
|
24
|
+
[ "$status" -eq 0 ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "gate denies when marker is expired" {
|
|
28
|
+
touch "/tmp/architect-reviewed-${TEST_SESSION}"
|
|
29
|
+
# Set TTL to 0 to force expiry
|
|
30
|
+
ARCHITECT_TTL=0 run check_architect_gate "$TEST_SESSION"
|
|
31
|
+
[ "$status" -ne 0 ]
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for architect verdict parsing from output text
|
|
4
|
+
|
|
5
|
+
@test "grep matches 'Architecture Review: PASS'" {
|
|
6
|
+
output="Some preamble text\n\n**Architecture Review: PASS**\n\nNo conflicts."
|
|
7
|
+
echo -e "$output" | grep -q "Architecture Review: PASS"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@test "grep matches 'ISSUES FOUND'" {
|
|
11
|
+
output="**Architecture Review: ISSUES FOUND**\n\n1. [Decision Conflict]"
|
|
12
|
+
echo -e "$output" | grep -q "ISSUES FOUND"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@test "grep does NOT match unrelated text" {
|
|
16
|
+
output="The review is complete. Everything looks good."
|
|
17
|
+
! echo -e "$output" | grep -q "Architecture Review: PASS"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "subagent pattern matches wr-architect:agent" {
|
|
21
|
+
SUBAGENT="wr-architect:agent"
|
|
22
|
+
case "$SUBAGENT" in
|
|
23
|
+
*architect*) true ;;
|
|
24
|
+
*) false ;;
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P001 / ADR-009: Stop-hook marker reset removed. Marker lifecycle is
|
|
4
|
+
# governed by TTL + drift detection. This test asserts the Stop hook
|
|
5
|
+
# entry is absent from the plugin's hooks.json.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
PLUGIN_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@test "architect: hooks.json has no Stop hook entry (ADR-009)" {
|
|
12
|
+
! grep -q '"Stop"' "$PLUGIN_DIR/hooks/hooks.json"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@test "architect: architect-reset-marker.sh has been removed" {
|
|
16
|
+
[ ! -f "$PLUGIN_DIR/hooks/architect-reset-marker.sh" ]
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for architect-enforce-edit.sh project-root check (P004).
|
|
4
|
+
# Verifies that absolute paths outside $PWD are exempted.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
8
|
+
HOOK="$SCRIPT_DIR/architect-enforce-edit.sh"
|
|
9
|
+
ORIG_DIR="$PWD"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
teardown() {
|
|
13
|
+
cd "$ORIG_DIR"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run_hook_with_file() {
|
|
17
|
+
local file_path="$1"
|
|
18
|
+
local json="{\"tool_input\":{\"file_path\":\"${file_path}\"},\"session_id\":\"test-session-$$\"}"
|
|
19
|
+
echo "$json" | bash "$HOOK"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@test "project-root: absolute path outside project exits 0" {
|
|
23
|
+
run run_hook_with_file "/Users/other/somewhere/file.json"
|
|
24
|
+
[ "$status" -eq 0 ]
|
|
25
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "project-root: home-dir config path exits 0" {
|
|
29
|
+
run run_hook_with_file "/Users/somebody/.claude/channels/discord/access.json"
|
|
30
|
+
[ "$status" -eq 0 ]
|
|
31
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "project-root: absolute path inside \$PWD is still gated" {
|
|
35
|
+
# Use a temp dir as PWD with docs/decisions to trigger the gate
|
|
36
|
+
TEST_DIR=$(mktemp -d)
|
|
37
|
+
mkdir -p "$TEST_DIR/docs/decisions"
|
|
38
|
+
echo "# ADR" > "$TEST_DIR/docs/decisions/001-test.proposed.md"
|
|
39
|
+
cd "$TEST_DIR"
|
|
40
|
+
run run_hook_with_file "$TEST_DIR/src/index.ts"
|
|
41
|
+
[ "$status" -eq 0 ]
|
|
42
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
43
|
+
rm -rf "$TEST_DIR"
|
|
44
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared install utilities for @windyroad/* packages.
|
|
3
|
+
* Used by both per-plugin installers and the meta-installer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
|
+
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
|
|
11
|
+
let _dryRun = false;
|
|
12
|
+
|
|
13
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
14
|
+
|
|
15
|
+
export function setDryRun(value) {
|
|
16
|
+
_dryRun = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDryRun() {
|
|
20
|
+
return _dryRun;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function run(cmd, label) {
|
|
24
|
+
console.log(` ${label}...`);
|
|
25
|
+
if (_dryRun) {
|
|
26
|
+
console.log(` [dry-run] ${cmd}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync(cmd, { stdio: "inherit" });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(` FAILED: ${label}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkPrerequisites() {
|
|
39
|
+
if (_dryRun) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
43
|
+
} catch {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function addMarketplace() {
|
|
52
|
+
return run(
|
|
53
|
+
`claude plugin marketplace add ${MARKETPLACE_REPO}`,
|
|
54
|
+
`Marketplace: ${MARKETPLACE_NAME}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
59
|
+
return run(
|
|
60
|
+
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
61
|
+
pluginName
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updatePlugin(pluginName) {
|
|
66
|
+
return run(`claude plugin update ${pluginName}`, pluginName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function uninstallPlugin(pluginName) {
|
|
70
|
+
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Install a single package: marketplace add + plugin install.
|
|
75
|
+
*/
|
|
76
|
+
export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
|
|
77
|
+
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
78
|
+
|
|
79
|
+
addMarketplace();
|
|
80
|
+
installPlugin(pluginName, { scope });
|
|
81
|
+
|
|
82
|
+
if (deps.length > 0) {
|
|
83
|
+
console.log(`\nNote: This plugin works best with:`);
|
|
84
|
+
for (const dep of deps) {
|
|
85
|
+
console.log(` - @windyroad/${dep.replace("wr-", "")} (npx @windyroad/${dep.replace("wr-", "")})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
`\nDone! Restart Claude Code to activate.\n`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update a single package.
|
|
96
|
+
*/
|
|
97
|
+
export function updatePackage(pluginName) {
|
|
98
|
+
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
99
|
+
|
|
100
|
+
run(
|
|
101
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
102
|
+
"Updating marketplace"
|
|
103
|
+
);
|
|
104
|
+
updatePlugin(pluginName);
|
|
105
|
+
|
|
106
|
+
console.log("\nDone! Restart Claude Code to apply updates.\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Uninstall a single package.
|
|
111
|
+
*/
|
|
112
|
+
export function uninstallPackage(pluginName) {
|
|
113
|
+
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
114
|
+
|
|
115
|
+
uninstallPlugin(pluginName);
|
|
116
|
+
|
|
117
|
+
console.log("\nDone. Restart Claude Code to apply changes.\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse standard flags used by all per-plugin installers.
|
|
122
|
+
*/
|
|
123
|
+
export function parseStandardArgs(argv) {
|
|
124
|
+
const args = argv.slice(2);
|
|
125
|
+
const flags = {
|
|
126
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
127
|
+
uninstall: args.includes("--uninstall"),
|
|
128
|
+
update: args.includes("--update"),
|
|
129
|
+
dryRun: args.includes("--dry-run"),
|
|
130
|
+
scope: "project",
|
|
131
|
+
};
|
|
132
|
+
const scopeIdx = args.indexOf("--scope");
|
|
133
|
+
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
134
|
+
const val = args[scopeIdx + 1];
|
|
135
|
+
if (["project", "user", "local"].includes(val)) {
|
|
136
|
+
flags.scope = val;
|
|
137
|
+
} else {
|
|
138
|
+
console.error("--scope requires: project, user, or local");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return flags;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/architect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Architecture decision enforcement for AI coding agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-architect": "./bin/install.mjs"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents/",
|
|
24
24
|
"hooks/",
|
|
25
25
|
"skills/",
|
|
26
|
-
".claude-plugin/"
|
|
26
|
+
".claude-plugin/",
|
|
27
|
+
"lib/"
|
|
27
28
|
]
|
|
28
29
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: wr:adr
|
|
2
|
+
name: wr-architect:create-adr
|
|
3
3
|
description: Create a new Architecture Decision Record (MADR 4.0) in docs/decisions/. Examines existing decisions, asks about the problem and options, and writes a properly formatted ADR.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
5
5
|
---
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wr-architect:review-design
|
|
3
|
+
description: On-demand architecture compliance review. Checks staged changes and recent commits against existing ADRs in docs/decisions/. Use before editing architecture-bearing files or before a release.
|
|
4
|
+
allowed-tools: Read, Glob, Grep, Bash, AskUserQuestion, Skill
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Architecture Compliance Review Skill
|
|
8
|
+
|
|
9
|
+
Run an architecture compliance review on demand — outside the pre-tool-use hook gate. Reviews staged changes and recent commits against the project's ADRs in `docs/decisions/`.
|
|
10
|
+
|
|
11
|
+
This skill is **read-only**. It does not commit, push, or modify files.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- Pre-flight before a release or client handover: confirm no ADR violations crept in
|
|
16
|
+
- After a large refactor: verify the new structure still complies with decisions
|
|
17
|
+
- When proposing a structural change: get a review before editing architecture-bearing files
|
|
18
|
+
- Any time the hook gate is not convenient: e.g., planning mode, exploratory spikes
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Parse arguments
|
|
23
|
+
|
|
24
|
+
Read `$ARGUMENTS` for an explicit review scope (e.g., "review my changes to the auth module", "check the new API routes", "pre-release review"). If a scope is provided, use it. If empty, proceed to auto-detection.
|
|
25
|
+
|
|
26
|
+
### 2. Auto-detect context
|
|
27
|
+
|
|
28
|
+
Run the following to establish what needs reviewing:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Staged changes
|
|
32
|
+
git diff --cached --stat
|
|
33
|
+
|
|
34
|
+
# Recent commits not yet pushed
|
|
35
|
+
git log origin/$(git rev-parse --abbrev-ref HEAD)..HEAD --oneline 2>/dev/null || git log HEAD -5 --oneline
|
|
36
|
+
|
|
37
|
+
# Changed files
|
|
38
|
+
git diff --cached --name-only
|
|
39
|
+
git diff --name-only HEAD
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Summarise:
|
|
43
|
+
- Files staged or recently committed
|
|
44
|
+
- Whether the changes are architectural (source code, config, schema, tooling) vs purely documentary
|
|
45
|
+
|
|
46
|
+
### 3. Resolve ambiguity
|
|
47
|
+
|
|
48
|
+
If there are no staged changes and no recent unpushed commits, use `AskUserQuestion` to ask:
|
|
49
|
+
|
|
50
|
+
> "I don't see any staged or unpushed changes. What would you like me to review?
|
|
51
|
+
> (a) A specific set of files — please name them
|
|
52
|
+
> (b) All changes since the last tag
|
|
53
|
+
> (c) A planned change you'd like to describe
|
|
54
|
+
> (d) Cancel"
|
|
55
|
+
|
|
56
|
+
Do not ask if there is an obvious set of changed files.
|
|
57
|
+
|
|
58
|
+
### 4. Construct the assessment prompt
|
|
59
|
+
|
|
60
|
+
Build a self-contained prompt for the architect subagent that includes:
|
|
61
|
+
- The list of changed/staged files
|
|
62
|
+
- The git diff summary (stat output)
|
|
63
|
+
- Any explicit scope from the user
|
|
64
|
+
- The request: "Review these proposed changes against the project's ADRs. Flag any violations, gaps that need a new ADR, or compliance questions."
|
|
65
|
+
|
|
66
|
+
### 5. Delegate to wr-architect:agent
|
|
67
|
+
|
|
68
|
+
Invoke the architect subagent via the `Skill` tool:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
subagent_type: wr-architect:agent
|
|
72
|
+
prompt: <constructed review prompt from step 4>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Wait for the subagent to complete.
|
|
76
|
+
|
|
77
|
+
### 6. Present results
|
|
78
|
+
|
|
79
|
+
Present the full compliance report to the user. The architect subagent will report:
|
|
80
|
+
- PASS: no violations found
|
|
81
|
+
- FLAGGED: specific violations or compliance questions with ADR references
|
|
82
|
+
- NEW ADR NEEDED: decisions that should be recorded before proceeding
|
|
83
|
+
|
|
84
|
+
If violations are flagged, use `AskUserQuestion` to ask how the user wants to proceed:
|
|
85
|
+
- (a) Address the violations before continuing
|
|
86
|
+
- (b) Proceed with a documented exception
|
|
87
|
+
- (c) Draft a new or amended ADR to legitimise the approach
|
|
88
|
+
|
|
89
|
+
Do not make the decision unilaterally — per ADR-013 Rule 1, architectural risk decisions are the user's.
|
|
90
|
+
|
|
91
|
+
$ARGUMENTS
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Architecture - Stop hook
|
|
3
|
-
# Removes the architect session marker so the next turn requires a fresh review.
|
|
4
|
-
# Mirrors: voice-tone-reset-marker.sh
|
|
5
|
-
|
|
6
|
-
INPUT=$(cat)
|
|
7
|
-
|
|
8
|
-
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
9
|
-
|
|
10
|
-
if [ -n "$SESSION_ID" ]; then
|
|
11
|
-
rm -f "/tmp/architect-reviewed-${SESSION_ID}"
|
|
12
|
-
rm -f "/tmp/architect-reviewed-${SESSION_ID}.hash"
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
exit 0
|