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 +14 -0
- package/bin/install.js +24 -1
- package/hooks/proto-b-blocking.sh +51 -0
- package/hooks/proto-c-forced-question.sh +50 -0
- package/hooks/rrr-intent-gate.sh +86 -0
- package/hooks/statusline.sh +21 -6
- package/package.json +1 -1
- package/scripts/rrr-hud-dynamic.js +48 -19
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
|
|
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
|
package/hooks/statusline.sh
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
@@ -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 -
|
|
19
|
+
// RRR project paths - traverse up from cwd to find .planning, then check home
|
|
20
20
|
function getPlanningDir() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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);
|