projecta-rrr 1.18.1 → 1.18.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to RRR will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.18.3] - 2026-01-30
8
+
9
+ ### Fixed
10
+
11
+ - **Launchd PATH for grepai** - Added `~/.local/bin` to scheduled indexing PATH so grepai (installed via pipx) is discoverable
12
+
13
+ ## [1.18.2] - 2026-01-30
14
+
15
+ ### Fixed
16
+
17
+ - **Intent gate character threshold** - Lowered minimum message length from 15 to 8 characters to catch "work on a bug" (13 chars) and similar short work requests
18
+ - **Intent gate not installed** - Added `rrr-intent-gate.sh` hook configuration to install.js (UserPromptSubmit)
19
+ - **Statusline shows milestone when idle** - When between milestones, show last completed milestone version instead of empty
20
+
7
21
  ## [1.18.1] - 2026-01-30
8
22
 
9
23
  ### Fixed
package/bin/install.js CHANGED
@@ -1443,6 +1443,27 @@ function install(isGlobal) {
1443
1443
  });
1444
1444
  console.log(` ${green}✓${reset} Configured intent classification hook (UserPromptSubmit)`);
1445
1445
  }
1446
+
1447
+ // Configure UserPromptSubmit hook for intent gate (blocking work requests without plan)
1448
+ const intentGateCommand = isGlobal
1449
+ ? '$HOME/.claude/hooks/rrr-intent-gate.sh'
1450
+ : `${localDirName}/hooks/rrr-intent-gate.sh`;
1451
+
1452
+ const hasIntentGateHook = settings.hooks.UserPromptSubmit.some(entry =>
1453
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('rrr-intent-gate'))
1454
+ );
1455
+
1456
+ if (!hasIntentGateHook && hookFileExists(claudeDir, 'rrr-intent-gate.sh')) {
1457
+ settings.hooks.UserPromptSubmit.push({
1458
+ hooks: [
1459
+ {
1460
+ type: 'command',
1461
+ command: intentGateCommand
1462
+ }
1463
+ ]
1464
+ });
1465
+ console.log(` ${green}✓${reset} Configured intent gate hook (UserPromptSubmit)`);
1466
+ }
1446
1467
  }
1447
1468
 
1448
1469
  // Install Pushpa Mode and MCP setup scripts to the project directory
@@ -1671,7 +1692,9 @@ fi
1671
1692
  <key>EnvironmentVariables</key>
1672
1693
  <dict>
1673
1694
  <key>PATH</key>
1674
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
1695
+ <string>${os.homedir()}/.local/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
1696
+ <key>HOME</key>
1697
+ <string>${os.homedir()}</string>
1675
1698
  </dict>
1676
1699
  </dict>
1677
1700
  </plist>`;
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PROTOTYPE B: Blocking hook
4
+ # Exits non-zero when drifting, blocking Claude until user classifies intent
5
+ #
6
+
7
+ set +e
8
+
9
+ INPUT=$(cat)
10
+ USER_MESSAGE=""
11
+ if command -v jq &>/dev/null; then
12
+ USER_MESSAGE=$(echo "$INPUT" | jq -r '.user_message // .prompt // .message // empty' 2>/dev/null)
13
+ fi
14
+
15
+ # Skip short messages, commands, confirmations
16
+ [[ -z "$USER_MESSAGE" || ${#USER_MESSAGE} -lt 20 ]] && exit 0
17
+ [[ "$USER_MESSAGE" == /* ]] && exit 0
18
+
19
+ msg_lower=$(echo "$USER_MESSAGE" | tr '[:upper:]' '[:lower:]')
20
+ echo "$msg_lower" | grep -qE "^(ok|yes|no|sure|thanks|got it|continue|proceed|[0-9]+)$" && exit 0
21
+
22
+ # Check state
23
+ HUD_FILE="${HOME}/.claude/rrr/hud-state.json"
24
+ [[ ! -f "$HUD_FILE" ]] && exit 0
25
+
26
+ status=$(jq -r '.status // ""' "$HUD_FILE" 2>/dev/null)
27
+ plan=$(jq -r '.plan // ""' "$HUD_FILE" 2>/dev/null)
28
+ phase=$(jq -r '.phase // ""' "$HUD_FILE" 2>/dev/null)
29
+
30
+ # If idle/no-project and substantial request, BLOCK
31
+ if [[ "$status" == "idle" || "$status" == "no-project" || -z "$plan" || "$plan" == "null" ]]; then
32
+ # Check for work patterns
33
+ if echo "$msg_lower" | grep -qiE "(fix|add|implement|create|build|update|change|modify|bug|feature|issue)"; then
34
+ echo ""
35
+ echo "=============================================="
36
+ echo "BLOCKED: Work request detected without active plan"
37
+ echo "=============================================="
38
+ echo ""
39
+ echo "Before I can help, please classify this work:"
40
+ echo ""
41
+ echo " 1. /rrr:plan-phase - Track as new phase"
42
+ echo " 2. /rrr:add-todo - Capture for later"
43
+ echo " 3. Type 'adhoc' - Continue untracked"
44
+ echo ""
45
+ echo "Current state: Phase=${phase:-none}, Plan=${plan:-none}"
46
+ echo "=============================================="
47
+ exit 1 # NON-ZERO = BLOCK
48
+ fi
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PROTOTYPE C: Forced Question Injection
4
+ # Injects a mandatory question that Claude must answer
5
+ # Uses special marker that CLAUDE.md instructs Claude to ALWAYS respond to
6
+ #
7
+
8
+ set +e
9
+
10
+ INPUT=$(cat)
11
+ USER_MESSAGE=""
12
+ if command -v jq &>/dev/null; then
13
+ USER_MESSAGE=$(echo "$INPUT" | jq -r '.user_message // .prompt // .message // empty' 2>/dev/null)
14
+ fi
15
+
16
+ # Skip short messages, commands, confirmations
17
+ [[ -z "$USER_MESSAGE" || ${#USER_MESSAGE} -lt 20 ]] && exit 0
18
+ [[ "$USER_MESSAGE" == /* ]] && exit 0
19
+
20
+ msg_lower=$(echo "$USER_MESSAGE" | tr '[:upper:]' '[:lower:]')
21
+ echo "$msg_lower" | grep -qE "^(ok|yes|no|sure|thanks|got it|continue|proceed|adhoc|[0-9]+)$" && exit 0
22
+
23
+ # Check state
24
+ HUD_FILE="${HOME}/.claude/rrr/hud-state.json"
25
+ [[ ! -f "$HUD_FILE" ]] && exit 0
26
+
27
+ status=$(jq -r '.status // ""' "$HUD_FILE" 2>/dev/null)
28
+ plan=$(jq -r '.plan // ""' "$HUD_FILE" 2>/dev/null)
29
+
30
+ # If idle/no-project and substantial request, INJECT QUESTION
31
+ if [[ "$status" == "idle" || "$status" == "no-project" || -z "$plan" || "$plan" == "null" ]]; then
32
+ # Check for work patterns
33
+ if echo "$msg_lower" | grep -qiE "(fix|add|implement|create|build|update|change|modify|bug|feature|issue)"; then
34
+ echo ""
35
+ echo "<MANDATORY-INTENT-GATE>"
36
+ echo "Work request detected without active plan."
37
+ echo ""
38
+ echo "CLAUDE: You MUST use AskUserQuestion to ask:"
39
+ echo " Question: 'Where should this work be tracked?'"
40
+ echo " Options:"
41
+ echo " - 'New phase' (run /rrr:plan-phase after)"
42
+ echo " - 'Add to backlog' (run /rrr:add-todo after)"
43
+ echo " - 'Quick adhoc fix' (proceed without tracking)"
44
+ echo ""
45
+ echo "DO NOT proceed with any implementation until user answers."
46
+ echo "</MANDATORY-INTENT-GATE>"
47
+ fi
48
+ fi
49
+
50
+ exit 0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # rrr-intent-gate.sh - HYBRID Intent Gate (Blocking + Friendly)
4
+ #
5
+ # BLOCKING: Exits non-zero when work request detected without active plan
6
+ # FRIENDLY: Clear message with options, not harsh error
7
+ #
8
+ # Bypass: User types "adhoc", "continue", or runs /rrr:plan-phase first
9
+ #
10
+
11
+ set +e
12
+
13
+ INPUT=$(cat)
14
+ USER_MESSAGE=""
15
+ if command -v jq &>/dev/null; then
16
+ USER_MESSAGE=$(echo "$INPUT" | jq -r '.user_message // .prompt // .message // empty' 2>/dev/null)
17
+ fi
18
+
19
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20
+ # SKIP CONDITIONS - Let these through without gate
21
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
+
23
+ # Skip empty or very short messages (lowered from 15 to 8 to catch "fix a bug")
24
+ [[ -z "$USER_MESSAGE" || ${#USER_MESSAGE} -lt 8 ]] && exit 0
25
+
26
+ # Skip slash commands (already intentional)
27
+ [[ "$USER_MESSAGE" == /* ]] && exit 0
28
+
29
+ msg_lower=$(echo "$USER_MESSAGE" | tr '[:upper:]' '[:lower:]')
30
+
31
+ # Skip explicit bypass words
32
+ echo "$msg_lower" | grep -qE "^(adhoc|ad-hoc|ad hoc|untracked|skip tracking)$" && exit 0
33
+
34
+ # Skip confirmations and short responses
35
+ echo "$msg_lower" | grep -qE "^(ok|okay|yes|no|y|n|sure|thanks|thank you|got it|understood|continue|proceed|go ahead|do it|[0-9]+)$" && exit 0
36
+
37
+ # Skip questions (user is asking, not requesting work)
38
+ echo "$msg_lower" | grep -qE "^(what|why|how|where|when|which|can you explain|explain|show me|tell me)" && exit 0
39
+ echo "$msg_lower" | grep -qE "\?$" && exit 0
40
+
41
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42
+ # CHECK STATE - Only gate if no active plan
43
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44
+
45
+ HUD_FILE="${HOME}/.claude/rrr/hud-state.json"
46
+ [[ ! -f "$HUD_FILE" ]] && exit 0
47
+
48
+ status=$(jq -r '.status // ""' "$HUD_FILE" 2>/dev/null)
49
+ plan=$(jq -r '.plan // ""' "$HUD_FILE" 2>/dev/null)
50
+ phase=$(jq -r '.phase // ""' "$HUD_FILE" 2>/dev/null)
51
+ milestone=$(jq -r '.milestone // ""' "$HUD_FILE" 2>/dev/null)
52
+
53
+ # If we have an active plan, let through
54
+ [[ -n "$plan" && "$plan" != "null" ]] && exit 0
55
+
56
+ # If status is active (working on something), let through
57
+ [[ "$status" == "active" ]] && exit 0
58
+
59
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
+ # DETECT WORK REQUESTS - Only gate substantial work
61
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62
+
63
+ work_patterns="(fix|bug|issue|add|implement|create|build|update|change|modify|refactor|feature|integrate|setup|set up|migrate|develop|design|write|make)"
64
+
65
+ if echo "$msg_lower" | grep -qiE "$work_patterns"; then
66
+ # This looks like a work request without an active plan - GATE IT
67
+ echo ""
68
+ echo "<MANDATORY-INTENT-GATE>"
69
+ echo ""
70
+ echo "I detected a work request but you have no active plan."
71
+ echo ""
72
+ echo " Current: ${milestone:-no milestone} / ${phase:-no phase} / ${plan:-no plan}"
73
+ echo " Status: ${status:-unknown}"
74
+ echo ""
75
+ echo "Before I proceed, where should this work go?"
76
+ echo ""
77
+ echo " 1) /rrr:plan-phase - Create a tracked phase for this work"
78
+ echo " 2) /rrr:add-todo - Save to backlog for later"
79
+ echo " 3) Type 'adhoc' - Do it now without tracking"
80
+ echo ""
81
+ echo "</MANDATORY-INTENT-GATE>"
82
+ exit 1 # BLOCK until user classifies
83
+ fi
84
+
85
+ # Not a work request - let through
86
+ exit 0
@@ -120,6 +120,16 @@ if [[ -f "$HUD_FILE" ]]; then
120
120
  location="${location} (${plans_completed}/${plans_in_phase})"
121
121
  fi
122
122
  fi
123
+ elif [[ "$status" == "idle" ]]; then
124
+ # Between milestones - show ready state with milestone count if available
125
+ if [[ -n "$milestone" && "$milestone" != "null" ]]; then
126
+ location="${milestone}:done"
127
+ else
128
+ location="RRR:ready"
129
+ fi
130
+ elif [[ "$status" == "no-project" ]]; then
131
+ # Not in an RRR project - show minimal indicator
132
+ location=""
123
133
  fi
124
134
 
125
135
  # ────────────────────────────────────────────────────────
@@ -134,14 +144,19 @@ if [[ -f "$HUD_FILE" ]]; then
134
144
  drift_reason="off-topic"
135
145
  fi
136
146
 
137
- # Condition 2: No active plan (ad-hoc coding)
138
- if [[ (-z "$plan" || "$plan" == "null") && (-z "$phase" || "$phase" == "null") ]]; then
139
- is_drifting=true
140
- drift_reason="no plan"
147
+ # Condition 2: No active plan (ad-hoc coding) - only if we HAVE a milestone
148
+ # If milestone is null/empty, we're either not in RRR project or between milestones (valid states)
149
+ # If status is "idle" or "no-project", drift doesn't apply
150
+ if [[ "$status" != "idle" && "$status" != "no-project" && -n "$milestone" && "$milestone" != "null" ]]; then
151
+ if [[ (-z "$plan" || "$plan" == "null") && (-z "$phase" || "$phase" == "null") ]]; then
152
+ is_drifting=true
153
+ drift_reason="no plan"
154
+ fi
141
155
  fi
142
156
 
143
- # Condition 3: Code changed outside plan scope
144
- if [[ "$drift_detected" == "true" && "$drift_files" -gt 0 ]]; then
157
+ # Condition 3: Code changed outside plan scope (only if actively working on a milestone)
158
+ # When idle/between milestones or not in a project, changed files aren't "drift"
159
+ if [[ "$status" != "idle" && "$status" != "no-project" && "$drift_detected" == "true" && "$drift_files" -gt 0 ]]; then
145
160
  is_drifting=true
146
161
  drift_reason="${drift_files} files"
147
162
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projecta-rrr",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by Projecta.ai",
5
5
  "bin": {
6
6
  "projecta-rrr": "bin/install.js"
@@ -16,18 +16,31 @@ const { execSync } = require('child_process');
16
16
  const CLAUDE_DIR = path.join(process.env.HOME, '.claude');
17
17
  const STATE_FILE = path.join(CLAUDE_DIR, 'rrr', 'hud-state.json');
18
18
 
19
- // RRR project paths - look in cwd first, then home
19
+ // RRR project paths - traverse up from cwd to find .planning, then check home
20
20
  function getPlanningDir() {
21
- const cwdPlanning = path.join(process.cwd(), '.planning');
22
- const homePlanning = path.join(process.env.HOME, '.planning');
23
-
24
- if (fs.existsSync(cwdPlanning)) {
25
- return cwdPlanning;
21
+ // First, traverse upward from cwd to find .planning (up to 5 levels)
22
+ let dir = process.cwd();
23
+ const root = path.parse(dir).root;
24
+ let depth = 0;
25
+ const maxDepth = 5;
26
+
27
+ while (depth < maxDepth && dir !== root) {
28
+ const planningDir = path.join(dir, '.planning');
29
+ if (fs.existsSync(planningDir)) {
30
+ return planningDir;
31
+ }
32
+ dir = path.dirname(dir);
33
+ depth++;
26
34
  }
35
+
36
+ // Fallback to home directory
37
+ const homePlanning = path.join(process.env.HOME, '.planning');
27
38
  if (fs.existsSync(homePlanning)) {
28
39
  return homePlanning;
29
40
  }
30
- return cwdPlanning; // Return cwd even if not exists
41
+
42
+ // Return cwd even if not exists (for error handling downstream)
43
+ return path.join(process.cwd(), '.planning');
31
44
  }
32
45
 
33
46
  function getActiveMilestone() {
@@ -380,18 +393,21 @@ function syncState() {
380
393
  state.progress_percent = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
381
394
  }
382
395
 
383
- // Update status based on project state
384
- const stalePlans = getStalePlans();
385
- if (stalePlans.length > 0) {
386
- state.status = 'drifting';
387
- state.drift.detected = true;
388
- state.drift.files_changed = stalePlans.length;
389
- state.drift.details = stalePlans.map(p => `${p.planId} (${p.age}h old)`);
390
- } else {
391
- state.status = 'active';
392
- state.drift.detected = false;
393
- state.drift.files_changed = 0;
394
- state.drift.details = [];
396
+ // Update status based on project state (only if we have an active milestone)
397
+ // If between milestones, keep the 'idle' status set above
398
+ if (milestone) {
399
+ const stalePlans = getStalePlans();
400
+ if (stalePlans.length > 0) {
401
+ state.status = 'drifting';
402
+ state.drift.detected = true;
403
+ state.drift.files_changed = stalePlans.length;
404
+ state.drift.details = stalePlans.map(p => `${p.planId} (${p.age}h old)`);
405
+ } else {
406
+ state.status = 'active';
407
+ state.drift.detected = false;
408
+ state.drift.files_changed = 0;
409
+ state.drift.details = [];
410
+ }
395
411
  }
396
412
 
397
413
  // Update scope
@@ -417,6 +433,19 @@ function syncState() {
417
433
  if (!state.session_start) {
418
434
  state.session_start = new Date().toISOString();
419
435
  }
436
+ } else {
437
+ // Not in an RRR project - clear project-specific state
438
+ state.milestone = null;
439
+ state.phase = null;
440
+ state.plan = null;
441
+ state.tasks_completed = 0;
442
+ state.tasks_total = 0;
443
+ state.progress_percent = 0;
444
+ state.status = 'no-project';
445
+ state.drift.detected = false;
446
+ state.drift.files_changed = 0;
447
+ state.drift.details = [];
448
+ state.scope = { files: 0, phases: 0, plans: 0, milestones: 0, last_scan: null, cache_hit_rate: 0 };
420
449
  }
421
450
 
422
451
  writeState(state);