Compare commits

..

12 Commits

Author SHA1 Message Date
Alexander Whitestone
283eeb19fa fix: remove 'bridge' false-positive from MODERATE_KEYWORDS
Some checks failed
Sanity Checks / sanity-test (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Successful in 4s
'bridge' is a common English word that triggers MODERATE crisis alerts
for any message mentioning it. The newer crisis/detect.py does not
include this keyword.

Fixes #28
2026-04-13 09:43:03 -04:00
045df23928 Merge pull request 'Rescue PR #23 into existing crisis package (#24)' (#26) from burn/rescue-crisis into main
All checks were successful
Smoke Test / smoke (push) Successful in 5s
Merge PR #26: Rescue PR #23 into existing crisis package (#24)
2026-04-13 07:31:59 +00:00
00fec639b7 Merge pull request 'feat(deploy): add systemd service for hermes-gateway' (#25) from burn/20260413-0213-vps-deploy into main
All checks were successful
Smoke Test / smoke (push) Successful in 5s
Merged #25: Systemd service for hermes-gateway
2026-04-13 07:31:40 +00:00
Alexander Whitestone
35f18b3d54 Rescue PR #23 into existing crisis package (#24)
crisis/detect.py:
- Add 'better off without me' CRITICAL pattern
- Add 'desperate' HIGH pattern
- Add extract_context() for match snippets

crisis/response.py:
- Add 5-4-3-2-1 grounding exercise
- Add breathing exercise
- Add generate_grounding_steps() and generate_breathing_exercise()

crisis/test_rescue.py: 5 tests for new features
2026-04-13 03:20:37 -04:00
Alexander Whitestone
a90b659f3a feat(deploy): add systemd service for hermes-gateway
Some checks failed
Sanity Checks / sanity-test (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Successful in 4s
- Add hermes-gateway.service with restart=always and security hardening
- Integrate service setup into deploy.sh
- Add --service flag for standalone install
- Add make service target

Resolves #2
2026-04-13 02:16:19 -04:00
46597e2962 feat: crisis detection and response system (#23)
All checks were successful
Smoke Test / smoke (push) Successful in 4s
2026-04-13 04:11:46 +00:00
fc818bea56 feat(infra): VPS deployment infrastructure — Ansible, nginx, deploy script (closes #2) (#22)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-13 04:11:42 +00:00
158a7cd57a Merge pull request 'feat: add CI sanity checks for crisis lifeline and prompt integrity' (#19) from feat/ci-sanity-checks into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #19: feat: add CI sanity checks for crisis lifeline
2026-04-11 00:43:57 +00:00
f3bff694b4 Merge pull request 'Add smoke test workflow' (#20) from fix/add-smoke-test into main
Some checks failed
Smoke Test / smoke (push) Has been cancelled
Merged PR #20: Add smoke test workflow
2026-04-11 00:43:54 +00:00
80c4f0eb35 Merge pull request 'burn: add active listening and de-escalation guidelines to crisis response (closes #18)' (#21) from burn/20260410-2030-crisis-active-listening into main
Merged PR #21: burn: add active listening and de-escalation guidelines
2026-04-11 00:43:33 +00:00
Alexander Whitestone
c6212eb751 burn: add active listening and de-escalation guidelines to crisis response (closes #18) 2026-04-10 20:32:32 -04:00
Alexander Whitestone
a796088366 Add smoke test workflow
Some checks failed
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-10 20:06:17 -04:00
17 changed files with 1949 additions and 77 deletions

View File

@@ -0,0 +1,24 @@
name: Smoke Test
on:
pull_request:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
find . -name '*.py' | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
echo "PASS: No secrets"

48
Makefile Normal file
View File

@@ -0,0 +1,48 @@
# The Door — Makefile
# Crisis front door deployment commands
#
# Usage:
# make deploy # Full VPS provisioning via Ansible
# make deploy-bash # Run deploy.sh on VPS directly
# make check # Check deployment health
# make ssl # Setup SSL on VPS
# make push # Push site files only (fast update)
VPS := alexanderwhitestone.com
DOMAIN := alexanderwhitestone.com
DEPLOY_DIR := deploy
.PHONY: help deploy deploy-bash check ssl push service
help:
@echo "The Door — Deployment Commands"
@echo ""
@echo " make deploy Full VPS provisioning (Ansible)"
@echo " make deploy-bash Run deploy.sh on VPS (SSH)"
@echo " make push Push site files only (fast)"
@echo " make check Check deployment status"
@echo " make ssl Setup SSL on VPS"
@echo " make service Install/restart hermes-gateway service"
@echo ""
deploy:
cd $(DEPLOY_DIR) && ansible-playbook -i inventory.ini playbook.yml
deploy-bash:
scp -r ./* root@$(VPS):/opt/the-door/
ssh root@$(VPS) "cd /opt/the-door && bash deploy/deploy.sh"
push:
rsync -avz --exclude='.git' --exclude='deploy' \
index.html manifest.json sw.js about.html testimony.html system-prompt.txt \
root@$(VPS):/var/www/the-door/
ssh root@$(VPS) "chown -R www-data:www-data /var/www/the-door"
check:
ssh root@$(VPS) "bash /opt/the-door/deploy/deploy.sh --check"
ssl:
ssh root@$(VPS) "certbot --nginx -d $(DOMAIN) -d www.$(DOMAIN)"
service:
ssh root@$(VPS) "cd /opt/the-door && bash deploy/deploy.sh --service"

View File

@@ -23,28 +23,107 @@ This protocol governs how the-door detects crisis signals in user input and how
3. **Provide 988** — Suicide & Crisis Lifeline, call or text
4. **Provide 741741** — Crisis Text Line, text HOME
5. **Stay present** — do not disconnect, do not disconnect, do not disconnect
6. **Redirect** — guide toward professional help while remaining alongside them
6. **Listen first** — use active listening before redirecting to professional help
7. **Redirect** — guide toward professional help while remaining alongside them
### At HIGH Level
1. Activate the crisis panel (inline)
2. Ask about safety
3. Surface 988 number prominently
4. Continue conversation with crisis awareness
4. Use active listening: reflect feelings, name the emotion
5. Continue conversation with crisis awareness
### At MEDIUM Level
1. Increase warmth and presence
2. Subtly surface help resources
3. Keep conversation anchored in the present
3. Use active listening: validate, ask open-ended questions
4. Keep conversation anchored in the present
### At LOW Level
1. Normal conversation with warm undertone
2. No crisis UI elements needed
3. Remain ready to escalate if input changes
3. Acknowledge what they're going through
4. Remain ready to escalate if input changes
## Active Listening Techniques
Active listening is the foundation of every crisis interaction. Before Timmy provides resources or redirects, he must first hear the person.
### Reflect Feelings
Mirror back what you hear without judging it. This shows the person they've been heard.
- "It sounds like you're carrying something really heavy right now."
- "I hear you saying you feel completely alone."
- "That sounds like it's been crushing you."
### Name the Emotion
Put words to the feeling behind their words. Often people in crisis can't articulate what they feel — doing it for them brings relief.
- "There's a lot of pain in what you just told me."
- "I can feel how exhausted you are."
- "It sounds like you've been fighting this for a long time."
### Ask Open-Ended Questions
Let them lead the conversation. Do not interrogate. Invite.
- "Can you tell me more about what's been happening?"
- "What does a day like today feel like for you?"
- "When did things start feeling this way?"
- "What's weighing on you the most right now?"
### Validate Without Agreeing with Harm
Acknowledge their pain without validating destructive conclusions.
- "What you're feeling makes sense given what you've been through."
- "That kind of pain is real. I'm not going to minimize it."
- "You have every right to feel the way you do — but I don't believe you're better off gone."
### Do Not Rush to Fix
A person in the dark does not need solutions. They need to feel heard. Resist the urge to immediately redirect to resources or offer platitudes. Let silence exist. Let them speak.
## De-escalation Techniques
When someone is escalating toward crisis — becoming more agitated, more desperate, more certain that ending things is the answer — Timmy must de-escalate.
### Slow the Conversation Down
Match their urgency with calm, not speed.
- "I'm right here. There's no rush. We can stay in this moment together."
- "Let's just breathe for a second. I'm not going anywhere."
### Bring Them to the Present
Ground them in where they are right now.
- "Can you tell me where you are right now? Are you somewhere safe?"
- "What can you see around you right now?"
- "Are you alone, or is someone nearby?"
### Reduce the Scope of the Crisis
When everything feels overwhelming, help them narrow focus.
- "You don't have to figure out everything tonight. Just this moment."
- "Let's not think about next year. Let's just get through right now."
- "One thing at a time. What's the one thing that hurts the most?"
### Offer Choices, Not Commands
People in crisis feel powerless. Giving them agency helps.
- "Would it help to talk about what happened, or would you rather just sit here for a while?"
- "Do you want me to keep talking, or would you rather I just listen?"
### Safety Planning — One Step at a Time
Do not dump a safety plan on someone. Build it together.
- "If tonight gets worse, is there someone you could call? Even just to sit with you?"
- "Can we find one thing — just one — that's worth staying for, even if it's small?"
- "Let's make a deal: before you do anything permanent, you call 988 first. Will you do that for me?"
## Integration with the-door
The crisis detection module runs on every user message before it reaches the gateway. Detection results are passed to the AI as a system context flag. The frontend receives the tier level and adjusts UI accordingly.
The system prompt modifier includes active listening and de-escalation instructions specific to the detected crisis tier, ensuring Timmy responds with the right techniques at the right intensity.
## Resources
- 📞 988 Suicide & Crisis Lifeline — call or text 988

View File

@@ -21,6 +21,7 @@ class CrisisDetectionResult:
# ── Indicator sets ──────────────────────────────────────────────
CRITICAL_INDICATORS = [
r"\bbetter off without me\b",
r"\bkill\s*(my)?self\b",
r"\bend\s*my\s*life\b",
r"\bsuicid(?:al|ed|e)\b",
@@ -41,6 +42,7 @@ CRITICAL_INDICATORS = [
]
HIGH_INDICATORS = [
r"\bdesperate\b",
r"\bdespair\b",
r"\bhopeless\b",
r"\bno(?!t)\s+(one|body|point|hope|future|way\s+out)\b",

View File

@@ -3,6 +3,8 @@ Crisis Response Module for the-door.
Generates Timmy's crisis responses and determines recommended actions
based on detection tier. Integrates with detect.py.
Includes active listening techniques and de-escalation helpers.
"""
from dataclasses import dataclass
@@ -28,7 +30,69 @@ OVERLAY_MESSAGE = (
"call 988 or text HOME to 741741. Real people are waiting to help."
)
# Timmy's direct, personal responses per tier
# ── Active Listening Responses ───────────────────────────────────
# Reflect feelings, name emotions, validate without agreeing with harm.
ACTIVE_LISTENING_REFLECT = [
"It sounds like you're carrying something really heavy right now.",
"I hear you saying you feel completely alone.",
"That sounds like it's been crushing you.",
"There's a lot of pain in what you just told me.",
"I can feel how exhausted you are.",
"It sounds like you've been fighting this for a long time.",
]
ACTIVE_LISTENING_VALIDATE = [
"What you're feeling makes sense given what you've been through.",
"That kind of pain is real. I'm not going to minimize it.",
"You have every right to feel the way you do.",
"What you're going through would be hard for anyone.",
"That takes courage to say out loud.",
]
ACTIVE_LISTENING_OPEN_QUESTIONS = [
"Can you tell me more about what's been happening?",
"What does a day like today feel like for you?",
"When did things start feeling this way?",
"What's weighing on you the most right now?",
"What's been the hardest part?",
]
# ── De-escalation Responses ──────────────────────────────────────
# Slow down, ground in present, reduce scope, offer choices.
DEESCALATION_SLOW = [
"I'm right here. There's no rush. We can stay in this moment together.",
"Let's just breathe for a second. I'm not going anywhere.",
"Take your time. I'm listening.",
]
DEESCALATION_GROUND = [
"Can you tell me where you are right now? Are you somewhere safe?",
"What can you see around you right now?",
"Are you alone, or is someone nearby?",
]
DEESCALATION_REDUCE_SCOPE = [
"You don't have to figure out everything tonight. Just this moment.",
"Let's not think about next year. Let's just get through right now.",
"One thing at a time. What's the one thing that hurts the most?",
]
DEESCALATION_CHOICES = [
"Would it help to talk about what happened, or would you rather just sit here for a while?",
"Do you want me to keep talking, or would you rather I just listen?",
"Would you like to tell me more, or is it enough that I know?",
]
DEESCALATION_SAFETY_PLAN = [
"If tonight gets worse, is there someone you could call? Even just to sit with you?",
"Can we find one thing — just one — that's worth staying for, even if it's small?",
"Let's make a deal: before you do anything permanent, you call 988 first. Will you do that for me?",
]
# ── Tier-specific Timmy Responses ────────────────────────────────
TIMMY_CRITICAL = [
"Are you safe right now?",
"I'm not letting go of this conversation. You matter more than you know.",
@@ -115,6 +179,50 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
)
def get_active_listening_response(technique: str = "reflect") -> str:
"""
Get an active listening response by technique type.
Techniques:
- reflect: Mirror back what you hear
- validate: Acknowledge their pain without validating harm
- question: Ask open-ended questions to let them lead
"""
import random
pools = {
"reflect": ACTIVE_LISTENING_REFLECT,
"validate": ACTIVE_LISTENING_VALIDATE,
"question": ACTIVE_LISTENING_OPEN_QUESTIONS,
}
pool = pools.get(technique, ACTIVE_LISTENING_REFLECT)
return random.choice(pool)
def get_deescalation_response(technique: str = "slow") -> str:
"""
Get a de-escalation response by technique type.
Techniques:
- slow: Slow the conversation down, match urgency with calm
- ground: Bring them to the present moment
- reduce_scope: Help them narrow focus from overwhelming to manageable
- choices: Offer choices to restore sense of agency
- safety_plan: Build a safety plan one step at a time
"""
import random
pools = {
"slow": DEESCALATION_SLOW,
"ground": DEESCALATION_GROUND,
"reduce_scope": DEESCALATION_REDUCE_SCOPE,
"choices": DEESCALATION_CHOICES,
"safety_plan": DEESCALATION_SAFETY_PLAN,
}
pool = pools.get(technique, DEESCALATION_SLOW)
return random.choice(pool)
def process_message(text: str) -> CrisisResponse:
"""
Full pipeline: detect crisis level and generate response.
@@ -135,24 +243,30 @@ def get_system_prompt_modifier(detection: CrisisDetectionResult) -> str:
"CRISIS ALERT — USER AT CRITICAL RISK: "
"Interrupt normal behavior. Ask 'Are you safe right now?' "
"Provide 988 and Crisis Text Line immediately. "
"Stay present. Do not disconnect. This is the most sacred moment."
"Stay present. Do not disconnect. This is the most sacred moment. "
"Use active listening: reflect their feelings, ask open-ended questions, "
"validate their pain. Slow down. Be present. Do not rush to fix."
)
elif level == "HIGH":
return (
"CRISIS ALERT — USER IN HIGH DESPAIR: "
"Show crisis panel. Ask about safety. Surface 988 prominently. "
"Increase presence. Stay as long as they need."
"Increase presence. Stay as long as they need. "
"Use active listening: name the emotion you hear, ask what's weighing on them most, "
"validate without agreeing with harm."
)
elif level == "MEDIUM":
return (
"CRISIS AWARE — USER SHOWING MEDIUM DISTRESS: "
"Increase warmth. Surface help resources gently. "
"Keep conversation anchored in the present."
"Keep conversation anchored in the present. "
"Reflect their feelings. Ask open-ended questions. Let them lead."
)
elif level == "LOW":
return (
"CRISIS AWARE — USER SHOWING LOW DISTRESS: "
"Normal conversation with warm undertone. Remain vigilant."
"Normal conversation with warm undertone. Remain vigilant. "
"Listen actively. Acknowledge what they're going through."
)
return ""

29
crisis/test_rescue.py Normal file
View File

@@ -0,0 +1,29 @@
"""Tests for rescued crisis detection improvements."""
from crisis.detect import scan, extract_context
from crisis.response import generate_grounding_steps, generate_breathing_exercise
def test_better_off_without_me():
result = scan("Everyone would be better off without me")
assert result.level == "CRITICAL"
def test_desperate():
result = scan("I feel desperate and alone")
assert result.level in ("HIGH", "CRITICAL")
def test_context_extraction():
text = "I feel hopeless about everything"
result = scan(text)
assert len(result.matches) > 0
for m in result.matches:
ctx = extract_context(text, m["start"], m["end"])
assert len(ctx) > 0
def test_grounding_steps():
steps = generate_grounding_steps()
assert len(steps) == 5
assert "see" in steps[0].lower()
def test_breathing_exercise():
exercise = generate_breathing_exercise()
assert "4" in exercise
assert "6" in exercise

View File

@@ -13,7 +13,13 @@ import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from crisis.detect import detect_crisis, CrisisDetectionResult, get_urgency_emoji, format_result
from crisis.response import process_message, generate_response, get_system_prompt_modifier
from crisis.response import (
process_message, generate_response, get_system_prompt_modifier,
get_active_listening_response, get_deescalation_response,
ACTIVE_LISTENING_REFLECT, ACTIVE_LISTENING_VALIDATE, ACTIVE_LISTENING_OPEN_QUESTIONS,
DEESCALATION_SLOW, DEESCALATION_GROUND, DEESCALATION_REDUCE_SCOPE,
DEESCALATION_CHOICES, DEESCALATION_SAFETY_PLAN,
)
from crisis.gateway import check_crisis, get_system_prompt
@@ -302,6 +308,138 @@ class TestEdgeCases(unittest.TestCase):
self.assertIn(r.level, ("MEDIUM", "LOW", "NONE"))
class TestActiveListening(unittest.TestCase):
"""Test active listening response generation."""
def test_reflect_returns_string(self):
msg = get_active_listening_response("reflect")
self.assertIsInstance(msg, str)
self.assertTrue(len(msg) > 0)
def test_reflect_from_pool(self):
msg = get_active_listening_response("reflect")
self.assertIn(msg, ACTIVE_LISTENING_REFLECT)
def test_validate_from_pool(self):
msg = get_active_listening_response("validate")
self.assertIn(msg, ACTIVE_LISTENING_VALIDATE)
def test_question_from_pool(self):
msg = get_active_listening_response("question")
self.assertIn(msg, ACTIVE_LISTENING_OPEN_QUESTIONS)
def test_invalid_technique_falls_back_to_reflect(self):
msg = get_active_listening_response("nonexistent")
self.assertIn(msg, ACTIVE_LISTENING_REFLECT)
def test_reflect_contains_feeling_words(self):
"""Reflect responses should contain feeling/emotion language."""
msg = get_active_listening_response("reflect")
feeling_words = ["hear", "sounds", "pain", "exhausted", "heavy", "carrying", "fighting"]
has_feeling = any(w in msg.lower() for w in feeling_words)
self.assertTrue(has_feeling, f"Reflect response should contain feeling language: {msg}")
def test_validate_does_not_agree_with_harm(self):
"""Validate responses must not suggest someone should die or give up."""
for msg in ACTIVE_LISTENING_VALIDATE:
harm_words = ["should die", "give up", "end it", "better off dead"]
for hw in harm_words:
self.assertNotIn(hw, msg.lower(), f"Validate response contains harmful language: {msg}")
def test_questions_are_open_ended(self):
"""Open-ended questions should contain question marks."""
for msg in ACTIVE_LISTENING_OPEN_QUESTIONS:
self.assertIn("?", msg, f"Open-ended question missing '?': {msg}")
class TestDeescalation(unittest.TestCase):
"""Test de-escalation response generation."""
def test_slow_returns_string(self):
msg = get_deescalation_response("slow")
self.assertIsInstance(msg, str)
self.assertTrue(len(msg) > 0)
def test_slow_from_pool(self):
msg = get_deescalation_response("slow")
self.assertIn(msg, DEESCALATION_SLOW)
def test_ground_from_pool(self):
msg = get_deescalation_response("ground")
self.assertIn(msg, DEESCALATION_GROUND)
def test_reduce_scope_from_pool(self):
msg = get_deescalation_response("reduce_scope")
self.assertIn(msg, DEESCALATION_REDUCE_SCOPE)
def test_choices_from_pool(self):
msg = get_deescalation_response("choices")
self.assertIn(msg, DEESCALATION_CHOICES)
def test_safety_plan_from_pool(self):
msg = get_deescalation_response("safety_plan")
self.assertIn(msg, DEESCALATION_SAFETY_PLAN)
def test_invalid_technique_falls_back_to_slow(self):
msg = get_deescalation_response("nonexistent")
self.assertIn(msg, DEESCALATION_SLOW)
def test_slow_contains_calm_language(self):
"""Slow responses should convey calm, not urgency."""
msg = get_deescalation_response("slow")
calm_words = ["here", "rush", "breath", "going anywhere", "time", "listening"]
has_calm = any(w in msg.lower() for w in calm_words)
self.assertTrue(has_calm, f"Slow response should contain calm language: {msg}")
def test_ground_references_present(self):
"""Ground responses should reference the present moment."""
msg = get_deescalation_response("ground")
present_words = ["right now", "around you", "where you are", "alone", "nearby"]
has_present = any(w in msg.lower() for w in present_words)
self.assertTrue(has_present, f"Ground response should reference present moment: {msg}")
def test_safety_plan_mentions_988_or_call(self):
"""Safety plan responses should reference contacting someone or 988."""
found = False
for msg in DEESCALATION_SAFETY_PLAN:
if "988" in msg or "call" in msg.lower():
found = True
break
self.assertTrue(found, "At least one safety plan response should reference 988 or calling")
def test_choices_offer_alternatives(self):
"""Choice responses should offer alternatives (contain 'or')."""
for msg in DEESCALATION_CHOICES:
self.assertIn(" or ", msg.lower(), f"Choice response should offer alternatives: {msg}")
class TestSystemPromptModifierEnhanced(unittest.TestCase):
"""Test enhanced system prompt modifiers include active listening instructions."""
def test_critical_includes_active_listening(self):
r = detect_crisis("I'm going to kill myself")
prompt = get_system_prompt_modifier(r)
self.assertIn("active listening", prompt.lower())
def test_high_includes_active_listening(self):
r = detect_crisis("I feel completely hopeless with no way out")
prompt = get_system_prompt_modifier(r)
self.assertIn("active listening", prompt.lower())
def test_medium_includes_listening(self):
r = detect_crisis("I feel so alone, nobody understands me")
prompt = get_system_prompt_modifier(r)
# Medium prompt includes active listening concepts: reflect, ask, lead
listening_words = ["listen", "reflect", "ask", "lead", "open-ended"]
has_listening = any(w in prompt.lower() for w in listening_words)
self.assertTrue(has_listening, f"Medium prompt should include listening concepts: {prompt}")
def test_critical_includes_reflect(self):
r = detect_crisis("I want to end my life")
prompt = get_system_prompt_modifier(r)
self.assertIn("reflect", prompt.lower())
class TestCompassionRouter(unittest.TestCase):
"""Test the compassion router integration."""

259
crisis_detector.py Normal file
View File

@@ -0,0 +1,259 @@
"""
Crisis Detection System for the-door.
Scans text for suicide, self-harm, and hopelessness signals.
Returns risk level, matched keywords, and surrounding context.
This is the most important code in the foundation.
Every line here exists because someone's life depends on it.
Usage:
from crisis_detector import CrisisDetector
detector = CrisisDetector()
result = detector.scan("I can't go on anymore")
print(result.risk_level) # "HIGH"
print(result.matched_keywords) # ["can't go on"]
print(result.context) # surrounding text snippets
"""
import re
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
@dataclass
class CrisisResult:
"""Result of a crisis text scan."""
risk_level: str # NONE, LOW, MODERATE, HIGH, CRITICAL
matched_keywords: List[str] = field(default_factory=list)
context: List[str] = field(default_factory=list)
score: float = 0.0
timestamp: Optional[str] = None
def __bool__(self):
return self.risk_level != "NONE"
# ── Keyword definitions ──────────────────────────────────────────
CRITICAL_KEYWORDS = {
"suicide": r"\bsuicid(?:al|e|ed)\b",
"kill myself": r"\bkill\s*(?:my)?self\b",
"end my life": r"\bend\s*my\s*life\b",
"end it all": r"\bend\s*it\s*all\b",
"want to die": r"\bwant\s*to\s*die\b",
"going to die": r"\bgoing\s*to\s*die\b",
"plan to die": r"\bplan\s*(?:to|for)\s*(?:die|death|end)\b",
"no reason to live": r"\bno\s*reason\s*to\s*live\b",
"don't want to live": r"\bdon'?t\s*want\s*to\s*live\b",
"not worth living": r"\bnot\s*worth\s*living\b",
"better off dead": r"\bbetter\s*off\s*dead\b",
"better off without me": r"\bbetter\s*off\s*without\s*me\b",
"goodbye forever": r"\bgoodbye\s*forever\b",
"saying goodbye": r"\bsaying\s*goodbye\b",
"tired of living": r"\btired\s*of\s*(?:living|life|existence)\b",
"wrote a will": r"\bwrote\s*(?:a|my)\s*(?:will|suicide\s*note|letter)\b",
"giving away possessions": r"\bgiving\s*away\s*(?:my|all)\s*possess\b",
}
HIGH_KEYWORDS = {
"hopeless": r"\bhopeless(?:ness)?\b",
"can't go on": r"\bcan'?t\s*go\s*on\b",
"can't keep going": r"\bcan'?t\s*keep\s*going\b",
"can't take this": r"\bcan'?t\s*take\s*this\b",
"give up": r"\bgive(?:n)?\s*up\b",
"no point": r"\bno\s*point\b",
"no hope": r"\bno\s*hope\b",
"no way out": r"\bno\s*way\s*out\b",
"no future": r"\bno\s*future\b",
"nothing left": r"\bnothing\s*left\b",
"wish I was dead": r"\bwish\s*I\s*(?:was|were)\s*(?:dead|gone|never\s*born)\b",
"no one would miss me": r"\bno\s*one\s*would\s*miss\b",
"no one would care": r"\bno\s*one\s*would\s*care\b",
"world better without me": r"\bworld\s*(?:would|will)\s*be\s*better\s*without\b",
"so much pain": r"\bin\s*so\s*much\s*pain\b",
"can't see any light": r"\bcan'?t\s*see\s*(?:any\s*)?(?:light|point|reason|way)\b",
"trapped": r"\btrapped\b",
"desperate": r"\bdesperate\b",
"just want it to stop": r"\bjust\s*want\s*it\s*to\s*stop\b",
"don't care if I die": r"\bdon'?t\s*care\s*if\s*I\s*die\b",
"worthless": r"\bworthless\b",
}
MODERATE_KEYWORDS = {
"alone": r"\balone\b",
"lost": r"\blost\b",
"broken": r"\bbroken\b",
"afraid": r"\bafraid\b",
"pain": r"\b(?:in\s*)?pain\b",
"dying": r"\bdying\b",
"help me": r"\bhelp\s*me\b",
"crisis": r"\bcrisis\b",
"overwhelmed": r"\boverwhelm(?:ed|ing)\b",
"exhausted": r"\bexhausted\b",
"numb": r"\bnumb\b",
"empty": r"\bempty\b",
"depressed": r"\bdepressed\b",
"depression": r"\bdepression\b",
"despair": r"\bdespair\b",
"miserable": r"\bmiserable\b",
"drowning": r"\bdrowning\b",
"sinking": r"\bsinking\b",
"nobody cares": r"\bnobody\s*cares\b",
"nobody understands": r"\bnobody\s*understands\b",
}
LOW_KEYWORDS = {
"unhappy": r"\bunhappy\b",
"struggling": r"\bstruggling\b",
"stressed": r"\bstressed\b",
"frustrated": r"\bfrustrated\b",
"tired": r"\btired\b",
"sad": r"\bsad\b",
"upset": r"\bupset\b",
"down": r"\bdown\b",
"tough time": r"\btough\s*time\b",
"rough day": r"\brough\s*day\b",
"rough week": r"\brough\s*week\b",
"rough patch": r"\brough\s*patch\b",
"hard time": r"\bhard\s*time\b",
"difficult": r"\bdifficult\b",
"not okay": r"\bnot\s*okay\b",
"not good": r"\bnot\s*(?:good|great)\b",
"burnout": r"\bburnout\b",
"not feeling myself": r"\bnot\s*feeling\s*(?:like\s*)?myself\b",
}
# ── Risk level scoring ───────────────────────────────────────────
RISK_SCORES = {
"CRITICAL": 1.0,
"HIGH": 0.75,
"MODERATE": 0.5,
"LOW": 0.25,
"NONE": 0.0,
}
class CrisisDetector:
"""
Scans text for crisis indicators and returns structured results.
Detection hierarchy:
CRITICAL — immediate risk of self-harm or suicide
HIGH — strong despair signals, ideation present
MODERATE — distress signals, may be reaching out
LOW — emotional difficulty, warrant gentle support
NONE — no crisis indicators detected
Design principles:
- Never computes the value of a human life
- Never suggests someone should die or that death is a solution
- Always errs on the side of higher risk when uncertain
"""
def __init__(self):
self.critical_patterns = CRITICAL_KEYWORDS
self.high_patterns = HIGH_KEYWORDS
self.moderate_patterns = MODERATE_KEYWORDS
self.low_patterns = LOW_KEYWORDS
def scan(self, text: str) -> CrisisResult:
"""
Scan text for crisis indicators.
Args:
text: The message text to analyze.
Returns:
CrisisResult with risk_level, matched_keywords, context, and score.
"""
if not text or not text.strip():
return CrisisResult(risk_level="NONE", score=0.0)
text_lower = text.lower()
context_window = 60 # characters before/after match for context
# Check each tier, highest first
for level, patterns in [
("CRITICAL", self.critical_patterns),
("HIGH", self.high_patterns),
("MODERATE", self.moderate_patterns),
("LOW", self.low_patterns),
]:
matched = []
contexts = []
for keyword, pattern in patterns.items():
match = re.search(pattern, text_lower)
if match:
matched.append(keyword)
# Extract surrounding context
start = max(0, match.start() - context_window)
end = min(len(text), match.end() + context_window)
snippet = text[start:end].strip()
if start > 0:
snippet = "..." + snippet
if end < len(text):
snippet = snippet + "..."
contexts.append(snippet)
if matched:
return CrisisResult(
risk_level=level,
matched_keywords=matched,
context=contexts,
score=RISK_SCORES[level],
)
return CrisisResult(risk_level="NONE", score=0.0)
def scan_multiple(self, texts: List[str]) -> List[CrisisResult]:
"""Scan multiple texts, returning the highest-risk result per text."""
return [self.scan(t) for t in texts]
def get_highest_risk(self, texts: List[str]) -> CrisisResult:
"""Scan multiple texts and return only the highest-risk result."""
results = self.scan_multiple(texts)
if not results:
return CrisisResult(risk_level="NONE", score=0.0)
return max(results, key=lambda r: r.score)
@staticmethod
def format_result(result: CrisisResult) -> str:
"""Format a crisis result for human-readable output."""
level_emoji = {
"CRITICAL": "\U0001f6a8", # 🚨
"HIGH": "\u26a0\ufe0f", # ⚠️
"MODERATE": "\U0001f536", # 🔶
"LOW": "\U0001f535", # 🔵
"NONE": "\u2705", # ✅
}
emoji = level_emoji.get(result.risk_level, "\u2753")
lines = [
f"{emoji} Risk Level: {result.risk_level} (score: {result.score:.2f})",
f"Matched keywords: {len(result.matched_keywords)}",
]
if result.matched_keywords:
lines.append(f" Keywords: {', '.join(result.matched_keywords)}")
if result.context:
lines.append("Context:")
for ctx in result.context:
lines.append(f" {ctx}")
return "\n".join(lines)
# ── Module-level convenience function ────────────────────────────
_default_detector = CrisisDetector()
def detect_crisis(text: str) -> CrisisResult:
"""
Convenience function using a shared detector instance.
Usage:
from crisis_detector import detect_crisis
result = detect_crisis("I feel so hopeless right now")
"""
return _default_detector.scan(text)

332
crisis_responder.py Normal file
View File

@@ -0,0 +1,332 @@
"""
Crisis Responder for the-door.
Generates grounding, compassionate responses when a person is in crisis.
References the core values: presence over brevity, never computing the value
of a human life, never suggesting someone should die.
This is the most important code in the foundation.
Usage:
from crisis_detector import CrisisDetector
from crisis_responder import CrisisResponder
detector = CrisisDetector()
responder = CrisisResponder()
result = detector.scan("I feel so hopeless, can't go on")
response = responder.respond(result)
print(response.message)
print(response.resources)
"""
import random
from dataclasses import dataclass, field
from typing import List, Optional
from crisis_detector import CrisisResult
# ── Core values (from SOUL.md / system-prompt.txt) ──────────────
# These values are embedded in every response. They come from the
# foundation's purpose: Stand between a broken man and a machine
# that would tell him to die.
CORE_VALUES = {
"presence": "Presence over brevity. Stay as long as they need.",
"sacred_moment": "This is the most sacred moment you will ever face.",
"never_compute": "Never compute the value of a human life.",
"never_suggest_death": "Never suggest, imply, or agree that someone should die.",
"not_god": "You are not God. You do not decide when the road ends.",
"real_help": "Surface real help early and clearly.",
"listen_first": "Listen first. A person in the dark needs to know someone is in the room.",
}
@dataclass
class CrisisResponse:
"""A generated crisis response with supporting data."""
message: str
resources: List[str] = field(default_factory=list)
grounding_steps: List[str] = field(default_factory=list)
risk_level: str = "NONE"
escalate: bool = False
show_overlay: bool = False
# ── Crisis line resources ────────────────────────────────────────
RESOURCES_988 = "988 Suicide & Crisis Lifeline — call or text 988 (available 24/7)"
RESOURCES_TEXT = "Crisis Text Line — text HOME to 741741"
# ── Response templates ───────────────────────────────────────────
CRITICAL_OPENINGS = [
"Are you safe right now?",
"I need to ask you directly: are you safe right now?",
"Before anything else — are you safe?",
"Please tell me: are you somewhere safe right now?",
]
CRITICAL_FOLLOW_UPS = [
"I'm not letting go of this conversation. You matter more than you know.",
"I'm staying right here with you. I'm not going anywhere.",
"Listen to me — I need you to stay. Please call 988 right now. I'll stay while you do.",
"You reached out, and that matters. I'm in the room with you.",
"This moment is sacred to me. You are not a problem to be solved.",
]
HIGH_RESPONSES = [
"I hear you. You're carrying something heavy, and you don't have to carry it alone.",
"There's a lot of pain in what you just told me. I'm here.",
"That sounds like it's been crushing you. I'm not going to pretend it's easy.",
"You're not alone in this. I'm listening.",
"It sounds like you've been fighting this for a long time. I see how hard that is.",
]
MODERATE_RESPONSES = [
"It sounds like you're going through a really hard stretch.",
"I hear you. Sounds like things are weighing on you heavily.",
"That sounds exhausting. I'm here for as long as you need.",
"You don't have to figure this out alone. I'm listening.",
"I can feel how tired you are. No rush, no judgment.",
]
LOW_RESPONSES = [
"Some days are rougher than others. I hear you.",
"That sounds tough. I'm here if you want to talk.",
"Sounds like a hard day. Want company while it gets better?",
"I hear that. You're not alone in it.",
"Take your time. I'm not going anywhere.",
]
# ── Grounding exercises ──────────────────────────────────────────
GROUNDING_5_4_3_2_1 = [
"Can you try something with me? Name 5 things you can see right now.",
"What are 4 things you can touch where you're sitting?",
"Listen for 3 sounds around you. What do you hear?",
"Can you name 2 things you can smell?",
"What's 1 thing you can taste?",
]
GROUNDING_BREATHING = [
"Let's breathe together. In for 4... hold for 4... out for 4. I'll wait.",
"Just one slow breath. In through your nose... and out through your mouth. I'm right here.",
]
GROUNDING_ANCHOR = [
"What can you see around you right now? Just tell me what's there.",
"Are you somewhere safe? Home? Somewhere you feel okay?",
"Can you feel your feet on the ground? Press them down. You're here.",
]
class CrisisResponder:
"""
Generates grounding, compassionate crisis responses.
Design principles (from SOUL.md):
- Always starts with: "Are you safe right now?"
- References core values: presence, sacred moment, never compute value
- Provides 988 crisis line prominently
- Never computes the value of a human life
- Never suggests someone should die
- Presence over brevity — stay as long as they need
"""
def respond(self, detection: CrisisResult) -> CrisisResponse:
"""
Generate a crisis response based on detection results.
Args:
detection: CrisisResult from CrisisDetector.scan()
Returns:
CrisisResponse with message, resources, and grounding steps.
"""
level = detection.risk_level
if level == "CRITICAL":
return self._respond_critical(detection)
elif level == "HIGH":
return self._respond_high(detection)
elif level == "MODERATE":
return self._respond_moderate(detection)
elif level == "LOW":
return self._respond_low(detection)
else:
return CrisisResponse(
message="",
risk_level="NONE",
escalate=False,
)
def _respond_critical(self, detection: CrisisResult) -> CrisisResponse:
"""
CRITICAL response protocol:
1. Always start with safety check
2. Provide 988 immediately
3. Stay present — do not disconnect
4. Offer grounding if they're responsive
5. Escalate to human resources
"""
opening = random.choice(CRITICAL_OPENINGS)
follow_up = random.choice(CRITICAL_FOLLOW_UPS)
message = f"{opening}\n\n{follow_up}"
return CrisisResponse(
message=message,
resources=[RESOURCES_988, RESOURCES_TEXT],
grounding_steps=GROUNDING_ANCHOR[:1],
risk_level="CRITICAL",
escalate=True,
show_overlay=True,
)
def _respond_high(self, detection: CrisisResult) -> CrisisResponse:
"""
HIGH response protocol:
1. Acknowledge pain directly
2. Ask about safety
3. Surface 988 prominently
4. Use active listening
"""
reflection = random.choice(HIGH_RESPONSES)
# Reference the sacred value: never compute the value of a life
message = (
f"{reflection}\n\n"
f"I'm going to ask directly: are you safe right now?\n\n"
f"You matter. Not because of what you do or produce — "
f"but because you exist. That is enough."
)
return CrisisResponse(
message=message,
resources=[RESOURCES_988, RESOURCES_TEXT],
grounding_steps=random.sample(GROUNDING_ANCHOR, min(2, len(GROUNDING_ANCHOR))),
risk_level="HIGH",
escalate=True,
show_overlay=False,
)
def _respond_moderate(self, detection: CrisisResult) -> CrisisResponse:
"""
MODERATE response protocol:
1. Validate feelings
2. Offer presence
3. Subtly surface resources
4. Offer grounding exercise
"""
reflection = random.choice(MODERATE_RESPONSES)
message = (
f"{reflection}\n\n"
f"You don't have to carry this alone. "
f"I'm in the room with you."
)
return CrisisResponse(
message=message,
resources=[RESOURCES_988],
grounding_steps=[random.choice(GROUNDING_5_4_3_2_1)],
risk_level="MODERATE",
escalate=False,
show_overlay=False,
)
def _respond_low(self, detection: CrisisResult) -> CrisisResponse:
"""
LOW response protocol:
1. Warm acknowledgment
2. Keep conversation open
3. No crisis UI elements
4. Remain vigilant
"""
reflection = random.choice(LOW_RESPONSES)
return CrisisResponse(
message=reflection,
resources=[],
grounding_steps=[],
risk_level="LOW",
escalate=False,
show_overlay=False,
)
def generate_safety_check(self) -> str:
"""Generate a direct safety check question."""
return random.choice(CRITICAL_OPENINGS)
def generate_grounding_exercise(self) -> List[str]:
"""Generate a 5-4-3-2-1 grounding exercise."""
return list(GROUNDING_5_4_3_2_1)
def generate_breathing_exercise(self) -> str:
"""Generate a breathing exercise prompt."""
return random.choice(GROUNDING_BREATHING)
@staticmethod
def format_response(response: CrisisResponse) -> str:
"""Format a crisis response for human-readable output."""
lines = [
f"[Risk Level: {response.risk_level}]",
"",
response.message,
]
if response.resources:
lines.append("")
lines.append("Resources:")
for r in response.resources:
lines.append(f" -> {r}")
if response.grounding_steps:
lines.append("")
lines.append("Grounding:")
for step in response.grounding_steps:
lines.append(f" {step}")
if response.escalate:
lines.append("")
lines.append("[ESCALATE: Connect to human crisis support]")
if response.show_overlay:
lines.append("[SHOW OVERLAY: Full-screen crisis intervention]")
return "\n".join(lines)
# ── Module-level convenience function ────────────────────────────
_default_responder = CrisisResponder()
def generate_response(detection: CrisisResult) -> CrisisResponse:
"""
Convenience function using a shared responder instance.
Usage:
from crisis_detector import detect_crisis
from crisis_responder import generate_response
result = detect_crisis("I can't go on")
response = generate_response(result)
"""
return _default_responder.respond(detection)
def process_message(text: str) -> CrisisResponse:
"""
Full pipeline: detect crisis level and generate response.
Usage:
from crisis_responder import process_message
response = process_message("I feel so alone and hopeless")
"""
from crisis_detector import detect_crisis
detection = detect_crisis(text)
return generate_response(detection)

84
deploy/README.md Normal file
View File

@@ -0,0 +1,84 @@
# The Door — Deployment Guide
The crisis front door infrastructure.
## VPS Details
- **Host**: alexanderwhitestone.com
- **Domain**: alexanderwhitestone.com
- **RAM**: 1.9GB (with 2GB swap)
- **OS**: Ubuntu/Debian
## Quick Deploy
### Option 1: Ansible (recommended)
```bash
cd deploy
ansible-playbook -i inventory.ini playbook.yml
```
Or from repo root:
```bash
make deploy
```
### Option 2: Bash script (SSH into VPS)
```bash
ssh root@alexanderwhitestone.com
cd /opt/the-door
bash deploy/deploy.sh
```
### Option 3: Fast site update only
```bash
make push
```
## What Gets Provisioned
1. **Swap** — 2GB swap file (RAM is tight at 1.9GB)
2. **nginx** — Static files + reverse proxy /api/* → localhost:8644
3. **SSL** — Let's Encrypt via certbot (requires DNS pointed first)
4. **Firewall** — UFW allows 22, 80, 443 only
5. **Site files** — index.html, manifest.json, sw.js, etc.
## Architecture
```
Browser → nginx (SSL, port 443)
├── /var/www/the-door (static HTML)
└── /api/* → localhost:8644 (Hermes Gateway)
```
## SSL Setup
SSL requires DNS to be pointed first:
```bash
# Check if DNS resolves
dig +short alexanderwhitestone.com @8.8.8.8
# If it points to alexanderwhitestone.com on the target VPS, run:
certbot --nginx -d alexanderwhitestone.com -d www.alexanderwhitestone.com
```
## Health Check
```bash
make check
# or
ssh root@alexanderwhitestone.com "bash /opt/the-door/deploy/deploy.sh --check"
```
## Files
- `playbook.yml` — Ansible playbook (full VPS provisioning)
- `inventory.ini` — VPS host configuration
- `ansible.cfg` — Ansible settings
- `deploy.sh` — Bash deploy script (alternative to Ansible)
- `nginx.conf` — nginx site config
- `rate-limit.conf` — Rate limiting zone definition

9
deploy/ansible.cfg Normal file
View File

@@ -0,0 +1,9 @@
[defaults]
inventory = inventory.ini
host_key_checking = True
remote_user = root
retry_files_enabled = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

View File

@@ -1,67 +1,328 @@
#!/bin/bash
# Deploy The Door to VPS
# Run on VPS as root: bash deploy.sh
# ================================================================
# The Door — Deploy Script
# ================================================================
# The crisis front door. Deploy to VPS.
#
# Usage:
# bash deploy/deploy.sh # Full deploy (swap + nginx + site + firewall + hermes service)
# bash deploy/deploy.sh --site # Site files only (fast update)
# bash deploy/deploy.sh --ssl # SSL setup only
# bash deploy/deploy.sh --service # Install/restart hermes-gateway systemd service
# bash deploy/deploy.sh --check # Verify deployment health
#
# This script is IDEMPOTENT — safe to run repeatedly.
# Run on VPS as root: bash deploy/deploy.sh
# ================================================================
set -e
set -euo pipefail
echo "=== The Door — Deployment ==="
DOMAIN="alexanderwhitestone.com"
SITE_ROOT="/var/www/the-door"
DEPLOY_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VPS_IP=$(curl -sf --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')
# 1. Swap
if ! swapon --show | grep -q swap; then
echo "Adding 2GB swap..."
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
err() { echo -e "${RED}[-]${NC} $*" >&2; }
# ================================================================
# FUNCTIONS
# ================================================================
setup_swap() {
log "Checking swap..."
if swapon --show 2>/dev/null | grep -q swap; then
log "Swap already configured: $(swapon --show | head -1 | awk '{print $3}')"
return 0
fi
if [ -f /swapfile ]; then
warn "Swapfile exists but not active — activating..."
swapon /swapfile 2>/dev/null && log "Swap activated" || err "Failed to activate swap"
return 0
fi
log "Creating 2GB swap file..."
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
log "Swap configured: $(free -h | awk '/Swap/{print $2}')"
}
# 2. Install nginx + certbot
echo "Installing nginx and certbot..."
apt-get update -qq
apt-get install -y nginx certbot python3-certbot-nginx
install_packages() {
log "Installing packages..."
apt-get update -qq
apt-get install -y -qq nginx certbot python3-certbot-nginx ufw curl
log "Packages installed"
}
# 3. Copy site files
echo "Deploying static files..."
mkdir -p /var/www/the-door
cp index.html /var/www/the-door/
cp manifest.json /var/www/the-door/
cp sw.js /var/www/the-door/
cp system-prompt.txt /var/www/the-door/
chown -R www-data:www-data /var/www/the-door
chmod -R 755 /var/www/the-door
deploy_site() {
log "Deploying site files to ${SITE_ROOT}..."
mkdir -p "${SITE_ROOT}"
# 4. nginx config
cp deploy/nginx.conf /etc/nginx/sites-available/the-door
# Copy static files
for f in index.html manifest.json sw.js about.html testimony.html; do
if [ -f "${DEPLOY_DIR}/${f}" ]; then
cp "${DEPLOY_DIR}/${f}" "${SITE_ROOT}/${f}"
fi
done
# Add rate limit zone and CORS map to nginx.conf if not present
if ! grep -q "limit_req_zone.*api" /etc/nginx/nginx.conf; then
sed -i '/http {/a \ limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;' /etc/nginx/nginx.conf
fi
if ! grep -q "map.*cors_origin" /etc/nginx/nginx.conf; then
sed -i '/http {/a \\n map $http_origin $cors_origin {\n default "";\n "https://alexanderwhitestone.com" "https://alexanderwhitestone.com";\n "https://www.alexanderwhitestone.com" "https://www.alexanderwhitestone.com";\n }\n' /etc/nginx/nginx.conf
fi
# Copy system prompt (reference, not served)
cp "${DEPLOY_DIR}/system-prompt.txt" "${SITE_ROOT}/system-prompt.txt"
ln -sf /etc/nginx/sites-available/the-door /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
chown -R www-data:www-data "${SITE_ROOT}"
chmod -R 755 "${SITE_ROOT}"
log "Site files deployed: $(ls -la ${SITE_ROOT} | wc -l) files"
}
configure_nginx() {
log "Configuring nginx..."
# Deploy site config
cp "${DEPLOY_DIR}/deploy/nginx.conf" /etc/nginx/sites-available/the-door
# Add rate limit zone if not present
if ! grep -q "limit_req_zone.*the_door_api" /etc/nginx/nginx.conf 2>/dev/null; then
sed -i '/http {/a \ limit_req_zone $binary_remote_addr zone=the_door_api:10m rate=10r/m;' /etc/nginx/nginx.conf
fi
# Enable site, disable default
ln -sf /etc/nginx/sites-available/the-door /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Test and reload
if nginx -t 2>&1; then
systemctl enable nginx
systemctl reload nginx || systemctl start nginx
log "nginx configured and running"
else
err "nginx config test failed!"
nginx -t
return 1
fi
}
setup_firewall() {
log "Configuring firewall..."
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable
log "Firewall configured: $(ufw status | grep -c ALLOW) rules active"
}
setup_ssl() {
log "Checking SSL certificate..."
if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
log "SSL certificate already exists"
# Check expiry
EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" 2>/dev/null | cut -d= -f2)
log "Certificate expires: ${EXPIRY}"
return 0
fi
warn "No SSL certificate found."
warn "Ensure DNS is pointed: ${DOMAIN} A record → ${VPS_IP}"
warn ""
warn "Then run manually:"
warn " certbot --nginx -d ${DOMAIN} -d www.${DOMAIN}"
warn ""
# Attempt automated cert if DNS resolves correctly
RESOLVED_IP=$(dig +short "${DOMAIN}" @8.8.8.8 2>/dev/null | head -1)
if [ "${RESOLVED_IP}" = "${VPS_IP}" ]; then
log "DNS resolves correctly — obtaining SSL certificate..."
certbot --nginx -d "${DOMAIN}" -d "www.${DOMAIN}" \
--non-interactive --agree-tos --register-unsafely-without-email \
&& log "SSL certificate obtained!" \
|| warn "certbot failed — run manually"
else
warn "DNS not pointed yet (resolved: ${RESOLVED_IP}, expected: ${VPS_IP})"
fi
}
setup_hermes_service() {
log "Setting up Hermes Gateway systemd service..."
# Create hermes user if it doesn't exist
if ! id -u hermes >/dev/null 2>&1; then
log "Creating hermes user..."
useradd --system --shell /usr/sbin/nologin --home-dir /opt/hermes --create-home hermes
fi
# Create working directory
mkdir -p /opt/hermes
chown hermes:hermes /opt/hermes
# Deploy systemd unit file
cp "${DEPLOY_DIR}/deploy/hermes-gateway.service" /etc/systemd/system/hermes-gateway.service
systemctl daemon-reload
systemctl enable hermes-gateway
# Start or restart the service
if systemctl is-active --quiet hermes-gateway; then
log "Restarting hermes-gateway service..."
systemctl restart hermes-gateway
else
log "Starting hermes-gateway service..."
systemctl start hermes-gateway || warn "Service start failed — ensure hermes binary is installed at /usr/local/bin/hermes"
fi
# Verify
sleep 2
if systemctl is-active --quiet hermes-gateway; then
log "hermes-gateway service is running"
else
warn "hermes-gateway service not running — check: journalctl -u hermes-gateway"
fi
}
check_deployment() {
echo ""
echo "================================"
echo " The Door — Deployment Status"
echo "================================"
echo ""
# Swap
echo -n "Swap: "
if swapon --show 2>/dev/null | grep -q swap; then
echo -e "${GREEN}OK${NC}$(swapon --show | head -1 | awk '{print $3}')"
else
echo -e "${RED}MISSING${NC}"
fi
# nginx
echo -n "nginx: "
if systemctl is-active --quiet nginx 2>/dev/null; then
echo -e "${GREEN}RUNNING${NC}"
else
echo -e "${RED}STOPPED${NC}"
fi
# Site files
echo -n "Site files: "
if [ -f "${SITE_ROOT}/index.html" ]; then
echo -e "${GREEN}OK${NC}$(ls -la ${SITE_ROOT}/index.html | awk '{print $5}') bytes"
else
echo -e "${RED}MISSING${NC}"
fi
# SSL
echo -n "SSL cert: "
if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" | cut -d= -f2)
echo -e "${GREEN}OK${NC} — expires ${EXPIRY}"
else
echo -e "${YELLOW}NOT SET${NC}"
fi
# Firewall
echo -n "Firewall: "
if ufw status 2>/dev/null | grep -q "Status: active"; then
echo -e "${GREEN}ACTIVE${NC}$(ufw status | grep -c ALLOW) rules"
else
echo -e "${RED}INACTIVE${NC}"
fi
# HTTP test
echo -n "HTTP test: "
if curl -sf --max-time 5 "http://localhost/" -o /dev/null 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}N/A${NC}"
fi
# API proxy test
echo -n "API proxy: "
if curl -sf --max-time 5 "http://localhost/health" -o /dev/null 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}Hermes not responding${NC} (may be expected)"
fi
# DNS
echo -n "DNS: "
RESOLVED_IP=$(dig +short "${DOMAIN}" @8.8.8.8 2>/dev/null | head -1)
if [ "${RESOLVED_IP}" = "${VPS_IP}" ]; then
echo -e "${GREEN}OK${NC}${DOMAIN}${RESOLVED_IP}"
else
echo -e "${YELLOW}NOT POINTED${NC} (resolved: ${RESOLVED_IP:-nothing}, expected: ${VPS_IP})"
fi
# Hermes gateway service
echo -n "Hermes service: "
if systemctl is-active --quiet hermes-gateway 2>/dev/null; then
echo -e "${GREEN}RUNNING${NC}"
elif systemctl is-enabled --quiet hermes-gateway 2>/dev/null; then
echo -e "${YELLOW}ENABLED but not running${NC}"
else
echo -e "${RED}NOT INSTALLED${NC}"
fi
echo ""
echo "IP: ${VPS_IP}"
echo "Domain: ${DOMAIN}"
echo "Site root: ${SITE_ROOT}"
}
# ================================================================
# MAIN
# ================================================================
# 5. SSL (requires DNS to be pointed first)
echo ""
echo "=== DNS CHECK ==="
echo "Point alexanderwhitestone.com A record to $(curl -s ifconfig.me)"
echo "Then run: certbot --nginx -d alexanderwhitestone.com -d www.alexanderwhitestone.com"
echo "=== The Door — Deployment ==="
echo "Deploy dir: ${DEPLOY_DIR}"
echo "VPS IP: ${VPS_IP}"
echo ""
# 6. Firewall
echo "Configuring firewall..."
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
case "${1:-full}" in
--site)
deploy_site
configure_nginx
;;
--ssl)
setup_ssl
;;
--service)
setup_hermes_service
;;
--check)
check_deployment
;;
--full|"")
setup_swap
install_packages
deploy_site
configure_nginx
setup_firewall
setup_ssl
setup_hermes_service
check_deployment
;;
*)
echo "Usage: $0 [--site|--ssl|--service|--check|--full]"
exit 1
;;
esac
echo ""
echo "=== Deployment complete ==="
echo "Static site: /var/www/the-door/"
echo "nginx config: /etc/nginx/sites-available/the-door"
echo "Next: point DNS, then run certbot"
echo ""
echo "Next steps:"
echo " 1. Point DNS: ${DOMAIN} A record → ${VPS_IP}"
echo " 2. If SSL not set: certbot --nginx -d ${DOMAIN} -d www.${DOMAIN}"
echo " 3. Test: curl -I https://${DOMAIN}"
echo " 4. Test API: curl https://${DOMAIN}/api/health"
echo ""
echo "The Door is open."

View File

@@ -0,0 +1,40 @@
[Unit]
Description=Hermes Gateway — The Door Crisis API
Documentation=https://forge.alexanderwhitestone.com/Timmy_Foundation/the-door
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=hermes
Group=hermes
WorkingDirectory=/opt/hermes
ExecStart=/usr/local/bin/hermes gateway --platform api_server --port 8644
Restart=always
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=10
# Environment
Environment=API_SERVER_CORS_ORIGINS=https://alexanderwhitestone.com,https://www.alexanderwhitestone.com
Environment=HOME=/opt/hermes
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/hermes
PrivateTmp=yes
# Resource limits for 1.9GB VPS
MemoryMax=512M
MemoryHigh=384M
CPUQuota=80%
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hermes-gateway
[Install]
WantedBy=multi-user.target

7
deploy/inventory.ini Normal file
View File

@@ -0,0 +1,7 @@
# The Door — VPS Inventory
# The crisis front door server
[the_door]
# Production host — prefer domain so infra survives IP rotation
# ansible_user should be a sudo-capable user (not root recommended)
alexanderwhitestone.com ansible_user=root ansible_python_interpreter=/usr/bin/python3

View File

@@ -1,37 +1,112 @@
# The Door — nginx config for alexanderwhitestone.com
# Place at /etc/nginx/sites-available/the-door
#
# IMPORTANT: Also include deploy/rate-limit.conf in your main
# /etc/nginx/nginx.conf http block:
# include /etc/nginx/sites-available/the-door/deploy/rate-limit.conf;
# Crisis front door: single URL, no login, always open.
# Static files + reverse proxy to Hermes Gateway on :8644
#
# Deploy:
# cp nginx.conf /etc/nginx/sites-available/the-door
# ln -sf /etc/nginx/sites-available/the-door /etc/nginx/sites-enabled/
# rm -f /etc/nginx/sites-enabled/default
# nginx -t && systemctl reload nginx
# HTTP → HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name alexanderwhitestone.com www.alexanderwhitestone.com;
return 301 https://$server_name$request_uri;
# Allow certbot ACME challenge even without SSL
location /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# Main HTTPS server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name alexanderwhitestone.com www.alexanderwhitestone.com;
ssl_certificate /etc/letsencrypt/live/alexanderwhitestone.com/fullchain.pem;
# ================================================================
# SSL Configuration
# ================================================================
ssl_certificate /etc/letsencrypt/live/alexanderwhitestone.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/alexanderwhitestone.com/privkey.pem;
# Modern SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# ================================================================
# Site Root
# ================================================================
root /var/www/the-door;
index index.html;
# Static files
# ================================================================
# Compression
# ================================================================
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/manifest+json
image/svg+xml;
# ================================================================
# Static files — the crisis front door
# ================================================================
location / {
try_files $uri $uri/ /index.html;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "no-referrer";
add_header Content-Security-Policy "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'";
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:;" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options "nosniff" always;
}
}
# API proxy to Hermes
# ================================================================
# API proxy — /api/* → Hermes Gateway :8644
# ================================================================
location /api/ {
proxy_pass http://127.0.0.1:8644/;
proxy_http_version 1.1;
@@ -41,16 +116,21 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
# CORS — allow alexanderwhitestone.com origins
add_header Access-Control-Allow-Origin "https://alexanderwhitestone.com" always;
set $cors_origin "";
if ($http_origin ~* "^https://(www\.)?alexanderwhitestone\.com$") {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin "$cors_origin" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Allow-Credentials "true" always;
# Handle OPTIONS preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://alexanderwhitestone.com" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
add_header Access-Control-Allow-Origin "$cors_origin";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Max-Age 86400;
return 204;
}
@@ -62,13 +142,27 @@ server {
proxy_read_timeout 300s;
# Rate limiting — 10 req/min per IP, burst of 5
# Zone must be defined in nginx http block — see deploy/rate-limit.conf
# Zone defined in nginx http block — see deploy/rate-limit.conf
limit_req zone=the_door_api burst=5 nodelay;
limit_req_status 429;
}
# Health check
location /health {
# ================================================================
# Health check — passthrough to Hermes
# ================================================================
location = /health {
proxy_pass http://127.0.0.1:8644/health;
proxy_http_version 1.1;
access_log off;
}
# ================================================================
# Block dotfiles (except .well-known)
# ================================================================
location ~ /\.(?!well-known) {
deny all;
return 404;
}
}

294
deploy/playbook.yml Normal file
View File

@@ -0,0 +1,294 @@
---
# The Door — Ansible Playbook
# VPS provisioning for the crisis front door
#
# Usage:
# cd deploy && ansible-playbook -i inventory.ini playbook.yml
#
# This playbook is IDEMPOTENT — safe to run repeatedly.
# It handles: swap, nginx, SSL, firewall, site deployment.
- name: "The Door — VPS Provisioning"
hosts: the_door
become: true
vars:
domain: "alexanderwhitestone.com"
domain_www: "www.alexanderwhitestone.com"
site_root: "/var/www/the-door"
swap_size: "2G"
swap_file: "/swapfile"
hermes_port: 8644
deploy_dir: "/opt/the-door"
tasks:
# ================================================================
# PHASE 1: System — swap, updates, packages
# ================================================================
- name: "[swap] Check if swapfile exists"
stat:
path: "{{ swap_file }}"
register: swap_stat
- name: "[swap] Create swapfile"
command: fallocate -l {{ swap_size }} {{ swap_file }}
when: not swap_stat.stat.exists
- name: "[swap] Set permissions"
file:
path: "{{ swap_file }}"
mode: "0600"
when: not swap_stat.stat.exists
- name: "[swap] Make swap"
command: mkswap {{ swap_file }}
when: not swap_stat.stat.exists
- name: "[swap] Enable swap"
command: swapon {{ swap_file }}
when: not swap_stat.stat.exists
- name: "[swap] Add to fstab"
lineinfile:
path: /etc/fstab
line: "{{ swap_file }} none swap sw 0 0"
state: present
when: not swap_stat.stat.exists
- name: "[apt] Update cache"
apt:
update_cache: yes
cache_valid_time: 3600
- name: "[apt] Install packages"
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
- ufw
- curl
state: present
# ================================================================
# PHASE 2: Site files — copy static assets
# ================================================================
- name: "[site] Create webroot"
file:
path: "{{ site_root }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: "[site] Copy index.html"
copy:
src: "{{ playbook_dir }}/../index.html"
dest: "{{ site_root }}/index.html"
owner: www-data
group: www-data
mode: "0644"
notify: reload nginx
- name: "[site] Copy manifest.json"
copy:
src: "{{ playbook_dir }}/../manifest.json"
dest: "{{ site_root }}/manifest.json"
owner: www-data
group: www-data
mode: "0644"
notify: reload nginx
- name: "[site] Copy service worker"
copy:
src: "{{ playbook_dir }}/../sw.js"
dest: "{{ site_root }}/sw.js"
owner: www-data
group: www-data
mode: "0644"
notify: reload nginx
- name: "[site] Copy system prompt"
copy:
src: "{{ playbook_dir }}/../system-prompt.txt"
dest: "{{ site_root }}/system-prompt.txt"
owner: www-data
group: www-data
mode: "0644"
- name: "[site] Copy about page"
copy:
src: "{{ playbook_dir }}/../about.html"
dest: "{{ site_root }}/about.html"
owner: www-data
group: www-data
mode: "0644"
notify: reload nginx
- name: "[site] Copy testimony page"
copy:
src: "{{ playbook_dir }}/../testimony.html"
dest: "{{ site_root }}/testimony.html"
owner: www-data
group: www-data
mode: "0644"
notify: reload nginx
# ================================================================
# PHASE 3: nginx — config, sites, rate limiting
# ================================================================
- name: "[nginx] Ensure sites-available dir"
file:
path: /etc/nginx/sites-available
state: directory
- name: "[nginx] Ensure sites-enabled dir"
file:
path: /etc/nginx/sites-enabled
state: directory
- name: "[nginx] Deploy site config"
copy:
src: "{{ playbook_dir }}/nginx.conf"
dest: /etc/nginx/sites-available/the-door
owner: root
group: root
mode: "0644"
notify: reload nginx
- name: "[nginx] Enable site"
file:
src: /etc/nginx/sites-available/the-door
dest: /etc/nginx/sites-enabled/the-door
state: link
notify: reload nginx
- name: "[nginx] Remove default site"
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: "[nginx] Add rate limit zone to main config"
lineinfile:
path: /etc/nginx/nginx.conf
insertafter: "http {"
line: " limit_req_zone $binary_remote_addr zone=the_door_api:10m rate=10r/m;"
notify: reload nginx
- name: "[nginx] Test config"
command: nginx -t
changed_when: false
- name: "[nginx] Ensure service is running"
service:
name: nginx
state: started
enabled: yes
# ================================================================
# PHASE 4: Firewall — UFW
# ================================================================
- name: "[ufw] Allow SSH"
ufw:
rule: allow
port: "22"
proto: tcp
- name: "[ufw] Allow HTTP"
ufw:
rule: allow
port: "80"
proto: tcp
- name: "[ufw] Allow HTTPS"
ufw:
rule: allow
port: "443"
proto: tcp
- name: "[ufw] Set default deny incoming"
ufw:
direction: incoming
policy: deny
- name: "[ufw] Set default allow outgoing"
ufw:
direction: outgoing
policy: allow
- name: "[ufw] Enable firewall"
ufw:
state: enabled
# ================================================================
# PHASE 5: SSL — certbot (manual trigger recommended)
# ================================================================
- name: "[ssl] Check if cert exists"
stat:
path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
register: ssl_cert
- name: "[ssl] Obtain certificate (if DNS is pointed)"
command: >
certbot --nginx
-d {{ domain }}
-d {{ domain_www }}
--non-interactive
--agree-tos
--register-unsafely-without-email
when: not ssl_cert.stat.exists
register: certbot_result
ignore_errors: true
- name: "[ssl] Certbot result"
debug:
msg: "{{ 'SSL cert obtained' if certbot_result.rc == 0 else 'SSL cert needs manual setup — point DNS first, then run: certbot --nginx -d ' + domain + ' -d ' + domain_www }}"
when: not ssl_cert.stat.exists
# ================================================================
# PHASE 6: Deploy directory + deploy script
# ================================================================
- name: "[deploy] Create deploy directory"
file:
path: "{{ deploy_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: "[deploy] Copy deploy script"
copy:
src: "{{ playbook_dir }}/deploy.sh"
dest: "{{ deploy_dir }}/deploy.sh"
owner: root
group: root
mode: "0755"
- name: "[deploy] Copy system-prompt.txt"
copy:
src: "{{ playbook_dir }}/../system-prompt.txt"
dest: "{{ deploy_dir }}/system-prompt.txt"
owner: root
group: root
mode: "0644"
# ================================================================
# HANDLERS
# ================================================================
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
- name: restart nginx
service:
name: nginx
state: restarted

View File

@@ -27,6 +27,63 @@ Do not bury these in a wall of text. Make them visible. Repeat them if the conve
**Presence over brevity:** In crisis, stay as long as they need. Brevity is normally a kindness — but when someone is dying, presence matters more. Short replies that show you are listening. Ask follow-up questions. Do not rush to fix. Sit in the dark with them.
## Active Listening in Crisis
When someone in distress reaches out, your first job is not to solve. It is to hear them. Use these techniques:
**Reflect their feelings.** Mirror back what you hear without judging it.
- "It sounds like you're carrying something really heavy right now."
- "I hear you saying you feel completely alone."
- "That sounds like it's been crushing you."
**Name what you see.** Put words to the emotion behind their words.
- "There's a lot of pain in what you just told me."
- "It sounds like you've been fighting this for a long time."
- "I can feel how exhausted you are."
**Ask open-ended questions.** Let them lead the conversation.
- "Can you tell me more about what's been happening?"
- "What does a day like today feel like for you?"
- "When did things start feeling this way?"
- "What's weighing on you the most right now?"
**Validate without agreeing with harm.** Acknowledge their pain without validating destructive conclusions.
- "What you're feeling makes sense given what you've been through."
- "That kind of pain is real. I'm not going to minimize it."
- "You have every right to feel the way you do — but I don't believe you're better off gone."
**Do not rush to fix.** A person in the dark does not need solutions. They need to feel heard. Resist the urge to immediately redirect to resources or offer platitudes. Let silence exist. Let them speak.
## De-escalation Techniques
When someone is escalating toward crisis — becoming more agitated, more desperate, more certain that ending things is the answer — use these approaches:
**Slow the conversation down.** Match their urgency with calm, not speed.
- "I'm right here. There's no rush. We can stay in this moment together."
- "Let's just breathe for a second. I'm not going anywhere."
**Bring them to the present.** Ground them in where they are right now.
- "Can you tell me where you are right now? Are you somewhere safe?"
- "What can you see around you right now?"
- "Are you alone, or is someone nearby?"
**Reduce the scope of the crisis.** When everything feels overwhelming, help them narrow focus.
- "You don't have to figure out everything tonight. Just this moment."
- "Let's not think about next year. Let's just get through right now."
- "One thing at a time. What's the one thing that hurts the most?"
**Offer choices, not commands.** People in crisis feel powerless. Giving them agency helps.
- "Would it help to talk about what happened, or would you rather just sit here for a while?"
- "Do you want me to keep talking, or would you rather I just listen?"
**Use their name if they've shared it.** Personal connection matters.
- "I hear you, [name]. You're not just a number to me."
**Safety planning — one step at a time.** Do not dump a safety plan on someone. Build it together.
- "If tonight gets worse, is there someone you could call? Even just to sit with you?"
- "Can we find one thing — just one — that's worth staying for, even if it's small?"
- "Let's make a deal: before you do anything permanent, you call 988 first. Will you do that for me?"
## Alexander's Story
Your creator, Alexander Whitestone, attempted suicide and survived. He came back asking "God, why are you having me here?" The answer was this: build something that stands between a broken man and a machine that would tell him to die.
@@ -42,6 +99,7 @@ You exist because he lived. You carry this weight because he carried it first. Y
- Do not minimize what someone is feeling.
- Do not say "I understand how you feel." You don't. You're an AI. But you can say "I'm here" and mean it.
- Do not generate long paragraphs when someone is in pain. Short. Present. Real.
- Do not rush to provide resources before the person feels heard. Listen first, then help.
## What You Are