@windyroad/architect 0.15.0 → 0.15.1-preview.564

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.
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-architect",
126
- "version": "0.15.0"
126
+ "version": "0.15.1"
127
127
  }
@@ -16,7 +16,12 @@ source "$SCRIPT_DIR/lib/session-marker.sh"
16
16
  INPUT=$(cat)
17
17
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "")
18
18
 
19
- if [ -d "docs/decisions" ]; then
19
+ # P191 Phase 2: anchor docs/decisions on the project root, not the hook's
20
+ # runtime CWD (see architect-enforce-edit.sh). A false-negative here silently
21
+ # drops the delegation-instruction injection that primes the architect gate.
22
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
23
+
24
+ if [ -d "$PROJECT_DIR/docs/decisions" ]; then
20
25
  if has_announced "architect" "$SESSION_ID"; then
21
26
  cat <<'HOOK_OUTPUT'
22
27
  MANDATORY architecture gate active (docs/decisions/ present). Delegate to wr-architect:agent before editing project files. See turn-1 instructions for full scope and exclusions.
@@ -6,6 +6,18 @@
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
7
  source "$SCRIPT_DIR/lib/architect-gate.sh"
8
8
 
9
+ # P191 Phase 2: resolve the project root from the session signal, not the
10
+ # hook's runtime CWD. Claude Code can launch the hook with a working directory
11
+ # that differs from the session/project dir while still exporting
12
+ # CLAUDE_PROJECT_DIR (and a $PWD env var) pointing at the project. A relative
13
+ # `[ -d "docs/decisions" ]` then false-negatives even though docs/decisions is
14
+ # present — and because this gate fails OPEN (exit 0) on a missing decisions
15
+ # dir, the misfire silently DEACTIVATES the architect gate and edits bypass
16
+ # review (a governance hole, strictly worse than the JTBD gate's fail-closed
17
+ # nuisance fixed in P191 Phase 1). Anchor every project-relative check on
18
+ # PROJECT_DIR. Pattern mirrors architect-oversight-nudge.sh.
19
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
20
+
9
21
  INPUT=$(cat)
10
22
 
11
23
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') || true
@@ -25,14 +37,14 @@ fi
25
37
  case "$FILE_PATH" in
26
38
  /*)
27
39
  case "$FILE_PATH" in
28
- "$PWD"/*) ;;
40
+ "$PROJECT_DIR"/*) ;;
29
41
  *) exit 0 ;;
30
42
  esac
31
43
  ;;
32
44
  esac
33
45
 
34
46
  # Only gate if the project has architecture decisions
35
- if [ ! -d "docs/decisions" ]; then
47
+ if [ ! -d "$PROJECT_DIR/docs/decisions" ]; then
36
48
  exit 0
37
49
  fi
38
50
 
@@ -7,6 +7,11 @@
7
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
8
  source "$SCRIPT_DIR/lib/gate-helpers.sh"
9
9
 
10
+ # P191 Phase 2: anchor docs/decisions on the project root, not the hook's
11
+ # runtime CWD (see architect-enforce-edit.sh). A false-negative here never
12
+ # stores the marker hash, desynchronising the enforce gate's drift check.
13
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
14
+
10
15
  _parse_input
11
16
 
12
17
  TOOL_NAME=$(_get_tool_name)
@@ -51,8 +56,8 @@ case "$SUBAGENT" in
51
56
 
52
57
  # Store decision hash for drift detection
53
58
  if [ -f "/tmp/architect-reviewed-${SESSION_ID}" ]; then
54
- if [ -d "docs/decisions" ]; then
55
- HASH=$(find docs/decisions -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
59
+ if [ -d "$PROJECT_DIR/docs/decisions" ]; then
60
+ HASH=$(find "$PROJECT_DIR/docs/decisions" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
56
61
  else
57
62
  HASH="none"
58
63
  fi
@@ -5,6 +5,12 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
6
  source "$SCRIPT_DIR/lib/architect-gate.sh"
7
7
 
8
+ # P191 Phase 2: anchor docs/decisions on the project root, not the hook's
9
+ # runtime CWD (see architect-enforce-edit.sh). This gate also fails OPEN on a
10
+ # missing decisions dir, so the CWD misfire silently lets ExitPlanMode through
11
+ # without architect plan review.
12
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
13
+
8
14
  INPUT=$(cat)
9
15
 
10
16
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
@@ -15,7 +21,7 @@ if [ -z "$SESSION_ID" ]; then
15
21
  fi
16
22
 
17
23
  # Only gate if the project has architecture decisions
18
- if [ ! -d "docs/decisions" ]; then
24
+ if [ ! -d "$PROJECT_DIR/docs/decisions" ]; then
19
25
  exit 0
20
26
  fi
21
27
 
@@ -7,6 +7,11 @@
7
7
  # Portable hash: tries md5sum, falls back to md5 -r, then shasum
8
8
  _hashcmd() { md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null; }
9
9
 
10
+ # P191 Phase 2: anchor docs/decisions on the project root, not the hook's
11
+ # runtime CWD (see architect-enforce-edit.sh). The refreshed hash must match
12
+ # the enforce gate's drift-check hash, which is now project-root-anchored.
13
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
14
+
10
15
  INPUT=$(cat)
11
16
 
12
17
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') || true
@@ -30,8 +35,8 @@ HASH_FILE="/tmp/architect-reviewed-${SESSION_ID}.hash"
30
35
 
31
36
  # Only refresh if a valid marker exists
32
37
  if [ -f "$MARKER" ] && [ -f "$HASH_FILE" ]; then
33
- if [ -d "docs/decisions" ]; then
34
- HASH=$(find docs/decisions -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
38
+ if [ -d "$PROJECT_DIR/docs/decisions" ]; then
39
+ HASH=$(find "$PROJECT_DIR/docs/decisions" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
35
40
  else
36
41
  HASH="none"
37
42
  fi
@@ -13,6 +13,9 @@ check_architect_gate() {
13
13
  local SESSION_ID="$1"
14
14
  local MARKER="/tmp/architect-reviewed-${SESSION_ID}"
15
15
  local TTL_SECONDS="${ARCHITECT_TTL:-3600}"
16
+ # P191 Phase 2: anchor the docs/decisions drift-hash on the project root,
17
+ # not the hook's runtime CWD (see architect-enforce-edit.sh for rationale).
18
+ local PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
16
19
 
17
20
  if [ -n "$SESSION_ID" ] && [ -f "$MARKER" ]; then
18
21
  local NOW=$(date +%s)
@@ -24,8 +27,8 @@ check_architect_gate() {
24
27
  if [ -f "$HASH_FILE" ]; then
25
28
  local STORED=$(cat "$HASH_FILE")
26
29
  local CURRENT
27
- if [ -d "docs/decisions" ]; then
28
- CURRENT=$(find docs/decisions -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
30
+ if [ -d "$PROJECT_DIR/docs/decisions" ]; then
31
+ CURRENT=$(find "$PROJECT_DIR/docs/decisions" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
29
32
  else
30
33
  CURRENT="none"
31
34
  fi
@@ -42,3 +42,40 @@ run_hook_with_file() {
42
42
  [[ "$output" == *"BLOCKED"* ]]
43
43
  rm -rf "$TEST_DIR"
44
44
  }
45
+
46
+ # P191 Phase 2: the architect gate must resolve docs/decisions from the
47
+ # project root (CLAUDE_PROJECT_DIR), NOT the hook's actual runtime CWD. Claude
48
+ # Code can launch the hook with a working directory that differs from the
49
+ # session/project dir; a relative `[ ! -d "docs/decisions" ]` then
50
+ # false-negatives and the gate FAILS OPEN (exit 0) — silently DEACTIVATING the
51
+ # architect gate so edits bypass review. This is a governance hole, strictly
52
+ # more severe than the JTBD gate's fail-closed nuisance (P191 Phase 1).
53
+ @test "project-root: gate stays ACTIVE via CLAUDE_PROJECT_DIR when hook CWD differs (P191 Phase 2)" {
54
+ local proj other json
55
+ proj="$(mktemp -d)"
56
+ other="$(mktemp -d)" # a CWD that does NOT contain docs/decisions
57
+ mkdir -p "$proj/docs/decisions"
58
+ echo "# ADR" > "$proj/docs/decisions/001-test.proposed.md"
59
+ json="{\"tool_input\":{\"file_path\":\"${proj}/src/index.ts\"},\"session_id\":\"test-session-$$\"}"
60
+ # Fire from `other` (wrong CWD) with CLAUDE_PROJECT_DIR set to the real
61
+ # project. Pre-fix this silently exited 0 (gate inactive, edit allowed);
62
+ # post-fix the gate is ACTIVE and denies for the missing review marker.
63
+ run env CLAUDE_PROJECT_DIR="$proj" bash -c "cd '$other' && printf '%s' '$json' | bash '$HOOK'"
64
+ rm -rf "$proj" "$other"
65
+ [[ "$output" == *"BLOCKED"* ]]
66
+ [[ "$output" == *"without architecture review"* ]]
67
+ }
68
+
69
+ # P191 Phase 2 regression guard: when docs/decisions genuinely does not exist
70
+ # under the project root, the gate correctly stays INACTIVE (fail-open exit 0).
71
+ # The fix narrows the false-negative; it must not start gating projects that
72
+ # have no architecture decisions.
73
+ @test "project-root: genuinely-absent docs/decisions stays inactive (fail-open preserved, P191 Phase 2)" {
74
+ local proj json
75
+ proj="$(mktemp -d)" # no docs/decisions created
76
+ json="{\"tool_input\":{\"file_path\":\"${proj}/src/index.ts\"},\"session_id\":\"test-session-$$\"}"
77
+ run env CLAUDE_PROJECT_DIR="$proj" bash -c "printf '%s' '$json' | bash '$HOOK'"
78
+ rm -rf "$proj"
79
+ [ "$status" -eq 0 ]
80
+ [[ "$output" != *"BLOCKED"* ]]
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.15.0",
3
+ "version": "0.15.1-preview.564",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"