@pennyfarthing/core 8.1.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +3 -3
  2. package/package.json +3 -3
  3. package/pennyfarthing-dist/agents/README.md +1 -1
  4. package/pennyfarthing-dist/agents/dev.md +1 -1
  5. package/pennyfarthing-dist/agents/handoff.md +1 -1
  6. package/pennyfarthing-dist/agents/reviewer-preflight.md +1 -1
  7. package/pennyfarthing-dist/agents/sm-setup.md +3 -3
  8. package/pennyfarthing-dist/agents/sm.md +1 -1
  9. package/pennyfarthing-dist/agents/tea.md +1 -1
  10. package/pennyfarthing-dist/agents/testing-runner.md +3 -3
  11. package/pennyfarthing-dist/commands/architect.md +2 -0
  12. package/pennyfarthing-dist/commands/continue-session.md +2 -2
  13. package/pennyfarthing-dist/commands/dev.md +2 -0
  14. package/pennyfarthing-dist/commands/devops.md +2 -0
  15. package/pennyfarthing-dist/commands/health-check.md +2 -0
  16. package/pennyfarthing-dist/commands/new-work.md +23 -0
  17. package/pennyfarthing-dist/commands/orchestrator.md +2 -0
  18. package/pennyfarthing-dist/commands/parallel-work.md +4 -2
  19. package/pennyfarthing-dist/commands/pm.md +2 -0
  20. package/pennyfarthing-dist/commands/reviewer.md +2 -0
  21. package/pennyfarthing-dist/commands/sm.md +2 -0
  22. package/pennyfarthing-dist/commands/tea.md +2 -0
  23. package/pennyfarthing-dist/commands/tech-writer.md +2 -0
  24. package/pennyfarthing-dist/commands/ux-designer.md +2 -0
  25. package/pennyfarthing-dist/commands/work.md +2 -0
  26. package/pennyfarthing-dist/guides/agent-behavior.md +29 -264
  27. package/pennyfarthing-dist/scripts/core/agent-session.sh +7 -0
  28. package/pennyfarthing-dist/scripts/core/check-context.sh +140 -226
  29. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
  30. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +4 -1
  31. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -7
  32. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +4 -11
  33. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +3 -8
  34. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +3 -3
  35. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -7
  36. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +2 -8
  37. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +2 -8
  38. package/pennyfarthing-dist/scripts/lib/find-root.sh +17 -45
  39. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -7
  40. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +2 -8
  41. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +2 -8
  42. package/pennyfarthing-dist/scripts/sprint/check-story.sh +2 -8
  43. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +2 -8
  44. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +2 -8
  45. package/pennyfarthing-dist/scripts/sprint/list-future.sh +2 -8
  46. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +2 -8
  47. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +2 -8
  48. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +2 -8
  49. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +2 -1
  50. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +4 -9
  51. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +2 -8
  52. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +2 -8
  53. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -7
  54. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +2 -8
  55. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +2 -8
  56. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +2 -8
  57. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +2 -8
  58. package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +1 -1
  59. package/pennyfarthing-dist/skills/jira/SKILL.md +48 -24
  60. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +7 -0
  61. package/pennyfarthing-dist/skills/sprint/skill.md +30 -30
  62. package/pennyfarthing-dist/workflows/patch.yaml +68 -0
  63. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  64. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  65. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  67. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  68. package/pennyfarthing_scripts/context.py +414 -0
  69. package/pennyfarthing_scripts/patch_mode.py +449 -0
  70. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  71. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  72. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  73. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  74. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  75. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  76. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/prime/cli.py +201 -0
  79. package/pennyfarthing_scripts/prime/models.py +9 -0
  80. package/pennyfarthing_scripts/prime/persona.py +41 -0
  81. package/pennyfarthing_scripts/prime/tiers.py +201 -0
  82. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  83. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  84. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  85. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  86. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  87. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  88. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  89. package/pennyfarthing_scripts/tests/test_patch_mode.py +830 -0
  90. package/pennyfarthing_scripts/tests/test_tiers.py +1090 -0
  91. package/pennyfarthing_scripts/tests/test_token_counting.py +559 -0
  92. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  94. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  96. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  97. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  98. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  103. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  104. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  105. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  106. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  107. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  108. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  110. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  111. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  112. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  113. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  114. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  124. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  126. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  135. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  136. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  137. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  138. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  139. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  140. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  141. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  142. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  143. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  144. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  145. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  146. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  147. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  148. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -2,15 +2,55 @@
2
2
  # check-context.sh - Check current Claude Code context usage
3
3
  # Returns: percentage and recommendation for handoff
4
4
  #
5
+ # This is a thin wrapper around pennyfarthing_scripts/context.py
6
+ #
5
7
  # Usage:
6
8
  # ./check-context.sh # Output env vars (most recent transcript)
7
9
  # ./check-context.sh --human # Human-readable output
8
10
  # ./check-context.sh --session <id> # Check specific session transcript
9
- # SESSION_ID=<id> ./check-context.sh # Alternative: session via env var
10
11
  # eval $(./check-context.sh) # Load vars into shell
11
12
 
13
+ set -euo pipefail
14
+
15
+ # Find project root (where pennyfarthing_scripts lives)
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
17
+
18
+ # Resolve symlinks to find actual location
19
+ REAL_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")"
20
+ REAL_DIR="$(cd "$(dirname "$REAL_SCRIPT")" && pwd -P)"
21
+
22
+ # Determine if we're in pennyfarthing-dist (3 levels up to package root)
23
+ # or in node_modules (need to find project root differently)
24
+ if [[ "$REAL_DIR" == *"/pennyfarthing-dist/scripts/core" ]]; then
25
+ # In pennyfarthing source or node_modules/@pennyfarthing/core
26
+ PACKAGE_ROOT="${REAL_DIR%/pennyfarthing-dist/scripts/core}"
27
+ elif [[ "$REAL_DIR" == *"/node_modules/"* ]]; then
28
+ # In node_modules, walk up to find project root
29
+ PACKAGE_ROOT="${REAL_DIR}"
30
+ while [[ "$PACKAGE_ROOT" != "/" ]] && [[ ! -d "$PACKAGE_ROOT/pennyfarthing_scripts" ]]; do
31
+ PACKAGE_ROOT="$(dirname "$PACKAGE_ROOT")"
32
+ done
33
+ fi
34
+
35
+ # Try to find pennyfarthing_scripts
36
+ PYTHON_MODULE=""
37
+ if [[ -f "$PACKAGE_ROOT/pennyfarthing_scripts/context.py" ]]; then
38
+ PYTHON_MODULE="$PACKAGE_ROOT/pennyfarthing_scripts/context.py"
39
+ elif [[ -f "${PROJECT_ROOT:-}/pennyfarthing_scripts/context.py" ]]; then
40
+ PYTHON_MODULE="${PROJECT_ROOT}/pennyfarthing_scripts/context.py"
41
+ fi
42
+
43
+ # If Python module exists, use it
44
+ if [[ -n "$PYTHON_MODULE" ]]; then
45
+ exec python3 "$PYTHON_MODULE" "$@"
46
+ fi
47
+
48
+ # Fallback: inline implementation for environments without the Python module
49
+ # This ensures backwards compatibility
50
+
12
51
  # Parse command line arguments
13
52
  HUMAN_MODE=false
53
+ EXPLICIT_SESSION=""
14
54
  while [[ $# -gt 0 ]]; do
15
55
  case "$1" in
16
56
  --human)
@@ -18,7 +58,7 @@ while [[ $# -gt 0 ]]; do
18
58
  shift
19
59
  ;;
20
60
  --session)
21
- SESSION_ID="$2"
61
+ EXPLICIT_SESSION="$2"
22
62
  shift 2
23
63
  ;;
24
64
  *)
@@ -28,253 +68,127 @@ while [[ $# -gt 0 ]]; do
28
68
  done
29
69
 
30
70
  # Derive Claude project path from current directory
31
- # Claude Code stores transcripts at ~/.claude/projects/<path-with-dashes>
32
- # The path format is: -Users-name-Projects-project (leading dash, all slashes become dashes)
33
71
  PROJECT_DIR="${PROJECT_ROOT:-$(pwd)}"
34
72
  CLAUDE_PROJECT_PATH="$HOME/.claude/projects/$(echo "$PROJECT_DIR" | tr '/' '-')"
35
73
 
36
- # Default thresholds (can be overridden by settings.local.json or config.local.yaml)
37
- DEFAULT_IMMINENT_THRESHOLD=65
38
- DEFAULT_WARNING_THRESHOLD=60
39
- DEFAULT_CRITICAL_THRESHOLD=85
40
- DEFAULT_MAX_TOKENS=200000
41
- DEFAULT_TIREPUMP_THRESHOLD=60
42
-
43
- # Load thresholds and permission_mode from .pennyfarthing/config.local.yaml (preferred) or settings.local.json (fallback)
44
- PENNYFARTHING_CONFIG="${CLAUDE_PROJECT_DIR:-$PROJECT_DIR}/.pennyfarthing/config.local.yaml"
45
- SETTINGS_FILE="${CLAUDE_PROJECT_DIR:-$PROJECT_DIR}/.claude/settings.local.json"
46
- CONFIG=$(python3 -c "
47
- import json
48
- import sys
49
-
50
- imminent_threshold = $DEFAULT_IMMINENT_THRESHOLD
51
- warning_threshold = $DEFAULT_WARNING_THRESHOLD
52
- critical_threshold = $DEFAULT_CRITICAL_THRESHOLD
53
- max_tokens = $DEFAULT_MAX_TOKENS
54
- tirepump_threshold = $DEFAULT_TIREPUMP_THRESHOLD
55
- permission_mode = 'manual' # Default to manual
56
- relay_mode = False # MSSCI-12395: Independent auto-handoff toggle
57
-
58
- # First try .pennyfarthing/config.local.yaml (preferred location)
74
+ # Default config
75
+ WARNING_THRESHOLD=60
76
+ CRITICAL_THRESHOLD=85
77
+ MAX_TOKENS=200000
78
+ TIREPUMP_THRESHOLD=60
79
+ PERMISSION_MODE="manual"
80
+ RELAY_MODE="false"
81
+
82
+ # Load config from .pennyfarthing/config.local.yaml if available
83
+ CONFIG_FILE="${PROJECT_DIR}/.pennyfarthing/config.local.yaml"
84
+ if [[ -f "$CONFIG_FILE" ]] && command -v python3 &>/dev/null; then
85
+ eval "$(python3 -c "
86
+ import yaml
59
87
  try:
60
- import yaml
61
- with open('$PENNYFARTHING_CONFIG', 'r') as f:
62
- config = yaml.safe_load(f)
63
- if config:
64
- if 'context_budget' in config:
65
- cb = config['context_budget']
66
- imminent_threshold = cb.get('imminent_threshold', imminent_threshold)
67
- warning_threshold = cb.get('warning_threshold', warning_threshold)
68
- critical_threshold = cb.get('critical_threshold', critical_threshold)
69
- max_tokens = cb.get('max_tokens', max_tokens)
70
- tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
71
- # Read permission_mode and relay_mode from workflow section
72
- if 'workflow' in config:
73
- if 'permission_mode' in config['workflow']:
74
- permission_mode = config['workflow']['permission_mode']
75
- # MSSCI-12395: relay_mode for auto-handoff (independent of permission_mode)
76
- if 'relay_mode' in config['workflow']:
77
- relay_mode = config['workflow']['relay_mode'] == True
78
- except:
79
- # Fallback to settings.local.json (legacy location)
80
- try:
81
- with open('$SETTINGS_FILE', 'r') as f:
82
- settings = json.load(f)
83
- if 'context_budget' in settings:
84
- cb = settings['context_budget']
85
- imminent_threshold = cb.get('imminent_threshold', imminent_threshold)
86
- warning_threshold = cb.get('warning_threshold', warning_threshold)
87
- critical_threshold = cb.get('critical_threshold', critical_threshold)
88
- max_tokens = cb.get('max_tokens', max_tokens)
89
- tirepump_threshold = cb.get('tirepump_threshold', tirepump_threshold)
90
- if 'workflow' in settings:
91
- if 'permission_mode' in settings['workflow']:
92
- permission_mode = settings['workflow']['permission_mode']
93
- # MSSCI-12395: relay_mode for auto-handoff (independent of permission_mode)
94
- if 'relay_mode' in settings['workflow']:
95
- relay_mode = settings['workflow']['relay_mode'] == True
96
- except:
97
- pass
98
-
99
- print(f'IMMINENT_THRESHOLD={imminent_threshold}')
100
- print(f'WARNING_THRESHOLD={warning_threshold}')
101
- print(f'CRITICAL_THRESHOLD={critical_threshold}')
102
- print(f'MAX_TOKENS={max_tokens}')
103
- print(f'TIREPUMP_THRESHOLD={tirepump_threshold}')
104
- print(f'PERMISSION_MODE={permission_mode}')
105
- print(f'RELAY_MODE={str(relay_mode).lower()}')
106
- " 2>/dev/null)
88
+ with open('$CONFIG_FILE') as f:
89
+ c = yaml.safe_load(f) or {}
90
+ cb = c.get('context_budget', {})
91
+ wf = c.get('workflow', {})
92
+ print(f'WARNING_THRESHOLD={cb.get(\"warning_threshold\", 60)}')
93
+ print(f'CRITICAL_THRESHOLD={cb.get(\"critical_threshold\", 85)}')
94
+ print(f'MAX_TOKENS={cb.get(\"max_tokens\", 200000)}')
95
+ print(f'TIREPUMP_THRESHOLD={cb.get(\"tirepump_threshold\", 60)}')
96
+ print(f'PERMISSION_MODE={wf.get(\"permission_mode\", \"manual\")}')
97
+ print(f'RELAY_MODE={str(wf.get(\"relay_mode\", False)).lower()}')
98
+ except: pass
99
+ " 2>/dev/null)" || true
100
+ fi
107
101
 
108
- # Apply config or use defaults
109
- eval "$CONFIG" 2>/dev/null || {
110
- IMMINENT_THRESHOLD=$DEFAULT_IMMINENT_THRESHOLD
111
- WARNING_THRESHOLD=$DEFAULT_WARNING_THRESHOLD
112
- CRITICAL_THRESHOLD=$DEFAULT_CRITICAL_THRESHOLD
113
- MAX_TOKENS=$DEFAULT_MAX_TOKENS
114
- TIREPUMP_THRESHOLD=$DEFAULT_TIREPUMP_THRESHOLD
102
+ # Find transcript with stale SESSION_ID handling
103
+ find_most_recent() {
104
+ ls -t "$CLAUDE_PROJECT_PATH"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1
115
105
  }
116
106
 
117
- # Find transcript - either specific session or most recent
118
- if [ -n "$SESSION_ID" ]; then
119
- # Session-specific: look for transcript with matching session ID
120
- TRANSCRIPT="$CLAUDE_PROJECT_PATH/${SESSION_ID}.jsonl"
121
- if [ ! -f "$TRANSCRIPT" ]; then
122
- if [ "$HUMAN_MODE" = "true" ]; then
123
- echo "⚠️ Context: unknown (session transcript not found: $SESSION_ID)"
107
+ TRANSCRIPT=""
108
+ if [[ -n "$EXPLICIT_SESSION" ]]; then
109
+ TRANSCRIPT="$CLAUDE_PROJECT_PATH/${EXPLICIT_SESSION}.jsonl"
110
+ [[ ! -f "$TRANSCRIPT" ]] && { echo "CONTEXT_ERROR=session_not_found"; exit 1; }
111
+ elif [[ -n "${SESSION_ID:-}" ]]; then
112
+ CANDIDATE="$CLAUDE_PROJECT_PATH/${SESSION_ID}.jsonl"
113
+ if [[ -f "$CANDIDATE" ]]; then
114
+ NOW=$(date +%s)
115
+ MOD_TIME=$(stat -f %m "$CANDIDATE" 2>/dev/null || stat -c %Y "$CANDIDATE" 2>/dev/null || echo 0)
116
+ AGE=$((NOW - MOD_TIME))
117
+ if [[ "$AGE" -lt 60 ]]; then
118
+ TRANSCRIPT="$CANDIDATE"
124
119
  else
125
- echo "CONTEXT_ERROR=session_not_found"
126
- echo "CONTEXT_SESSION=$SESSION_ID"
120
+ TRANSCRIPT=$(find_most_recent)
127
121
  fi
128
- exit 1
122
+ else
123
+ TRANSCRIPT=$(find_most_recent)
129
124
  fi
130
125
  else
131
- # Default: find most recent transcript (current session)
132
- TRANSCRIPT=$(ls -t "$CLAUDE_PROJECT_PATH"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1)
133
- if [ -z "$TRANSCRIPT" ]; then
134
- if [ "$HUMAN_MODE" = "true" ]; then
135
- echo "⚠️ Context: unknown (no transcript found)"
136
- else
137
- echo "CONTEXT_ERROR=no_transcript"
138
- fi
139
- exit 1
140
- fi
126
+ TRANSCRIPT=$(find_most_recent)
141
127
  fi
142
128
 
143
- # Parse transcript for baseline (first turn) and current (last turn) usage data
144
- # Baseline = system prompt overhead, cached per session
145
- # Usable = current - baseline (what the user's conversation has consumed)
146
- RESULT=$(python3 -c "
147
- import sys
148
- import json
149
-
150
- warning_threshold = $WARNING_THRESHOLD
151
- max_tokens = $MAX_TOKENS
152
- permission_mode = '$PERMISSION_MODE'
153
- relay_mode = '$RELAY_MODE' == 'true' # MSSCI-12395: Independent auto-handoff toggle
154
- tirepump_threshold = $TIREPUMP_THRESHOLD # Threshold for TirePump auto-handoff (configurable)
129
+ [[ -z "$TRANSCRIPT" ]] && { echo "CONTEXT_ERROR=no_transcript"; exit 1; }
155
130
 
156
- with open('$TRANSCRIPT', 'r') as f:
157
- lines = f.readlines()
158
-
159
- # Find first and last lines with usage data
160
- first_total = None
161
- last_total = None
162
-
163
- for line in lines:
164
- try:
165
- data = json.loads(line.strip())
166
- if 'message' in data and 'usage' in data['message']:
167
- usage = data['message']['usage']
168
- input_t = usage.get('input_tokens', 0)
169
- cache_read = usage.get('cache_read_input_tokens', 0)
170
- cache_create = usage.get('cache_creation_input_tokens', 0)
171
- total = cache_read + cache_create + input_t
172
-
173
- if first_total is None:
174
- first_total = total
175
- last_total = total
176
- except:
177
- continue
178
-
179
- if last_total is not None:
180
- # Baseline is first turn's total (system prompt + tools + CLAUDE.md etc)
181
- baseline = first_total if first_total is not None else 0
182
-
183
- # Usable tokens = what user's conversation has consumed
184
- usable_tokens = last_total - baseline
185
-
186
- # Available capacity = max minus baseline overhead
187
- available_capacity = max_tokens - baseline
188
-
189
- # Usable percent = conversation usage as % of available capacity
190
- usable_pct = (usable_tokens / available_capacity * 100) if available_capacity > 0 else 0
191
-
192
- # Total percent (for backwards compatibility)
193
- total_pct = (last_total / max_tokens) * 100
131
+ # Parse transcript
132
+ RESULT=$(python3 -c "
133
+ import json, os
134
+ from pathlib import Path
135
+
136
+ with open('$TRANSCRIPT') as f:
137
+ first_total = last_total = None
138
+ for line in f:
139
+ try:
140
+ data = json.loads(line.strip())
141
+ if 'message' in data and 'usage' in data['message']:
142
+ u = data['message']['usage']
143
+ total = u.get('input_tokens',0) + u.get('cache_read_input_tokens',0) + u.get('cache_creation_input_tokens',0)
144
+ if first_total is None: first_total = total
145
+ last_total = total
146
+ except: continue
147
+
148
+ if last_total:
149
+ baseline = first_total or 0
150
+ usable = last_total - baseline
151
+ available = $MAX_TOKENS - baseline
152
+ usable_pct = int((usable / available * 100) if available > 0 else 0)
153
+ total_pct = int((last_total / $MAX_TOKENS) * 100)
154
+ status = 'HIGH' if usable_pct > $WARNING_THRESHOLD else 'OK'
155
+ relay = '$RELAY_MODE' == 'true'
156
+ tirepump = (relay or '$PERMISSION_MODE' == 'turbo') and usable_pct > $TIREPUMP_THRESHOLD
157
+ is_cyclist = os.environ.get('CYCLIST') == '1' or Path('$PROJECT_DIR/packages/cyclist/.cyclist-port').exists()
194
158
 
195
- # Output all values
196
159
  print(f'CONTEXT_TOKENS={last_total}')
197
- print(f'CONTEXT_PERCENT={total_pct:.0f}')
160
+ print(f'CONTEXT_PERCENT={total_pct}')
198
161
  print(f'CONTEXT_BASELINE={baseline}')
199
- print(f'CONTEXT_USABLE_TOKENS={usable_tokens}')
200
- print(f'CONTEXT_USABLE_PERCENT={usable_pct:.0f}')
201
- print(f'CONTEXT_AVAILABLE={available_capacity}')
202
- print(f'PERMISSION_MODE={permission_mode}')
203
-
204
- # Use usable percent for status decisions (more accurate for user)
205
- if usable_pct > warning_threshold:
206
- print('CONTEXT_STATUS=HIGH')
207
- else:
208
- print('CONTEXT_STATUS=OK')
209
-
210
- # RELAY_MODE: Output for handoff-marker.sh to use
211
- print(f'RELAY_MODE={str(relay_mode).lower()}')
212
-
213
- # HANDOFF_MODE: 'auto' if relay_mode enabled, 'ask' otherwise
214
- # MSSCI-12395: relay_mode controls autohandoff independent of context level
215
- if relay_mode:
216
- print('HANDOFF_MODE=auto')
217
- else:
218
- print('HANDOFF_MODE=ask')
219
-
220
- # TirePump: Use CONTEXT_CLEAR (clear + load next agent) when:
221
- # 1. relay_mode is true (auto-handoff enabled) - MSSCI-12395
222
- # 2. context > 60% (tirepump_threshold)
223
- # This enables continuous autonomous runs without manual intervention
224
- # Legacy: also support permission_mode == 'turbo' for backwards compatibility
225
- use_tirepump = (relay_mode or permission_mode == 'turbo') and usable_pct > tirepump_threshold
226
- print(f'USE_TIREPUMP={str(use_tirepump).lower()}')
227
-
228
- # Cyclist detection: Multiple methods for robustness
229
- # 1. CYCLIST env var set to '1' (Electron mode - Cyclist spawns Claude)
230
- # 2. .cyclist-port file exists (Web mode - Claude connects to running Cyclist)
231
- import os
232
- from pathlib import Path
233
- is_cyclist = os.environ.get('CYCLIST', '') == '1'
234
- if not is_cyclist:
235
- # Check for .cyclist-port file in packages/cyclist directory
236
- # This indicates Cyclist is running in web mode
237
- project_dir = os.environ.get('CYCLIST_PROJECT_DIR', os.getcwd())
238
- port_file = Path(project_dir) / 'packages' / 'cyclist' / '.cyclist-port'
239
- if not port_file.exists():
240
- # Also check cwd in case we're already in cyclist dir
241
- port_file = Path(os.getcwd()) / '.cyclist-port'
242
- is_cyclist = port_file.exists()
162
+ print(f'CONTEXT_USABLE_TOKENS={usable}')
163
+ print(f'CONTEXT_USABLE_PERCENT={usable_pct}')
164
+ print(f'CONTEXT_AVAILABLE={available}')
165
+ print(f'CONTEXT_STATUS={status}')
166
+ print(f'PERMISSION_MODE=$PERMISSION_MODE')
167
+ print(f'RELAY_MODE=$RELAY_MODE')
168
+ print(f'HANDOFF_MODE={\"auto\" if relay else \"ask\"}')
169
+ print(f'USE_TIREPUMP={str(tirepump).lower()}')
243
170
  print(f'IS_CYCLIST={str(is_cyclist).lower()}')
171
+ if usable_pct >= $CRITICAL_THRESHOLD:
172
+ print('CONTEXT_WARNING=Critical')
173
+ print(\"CONTEXT_RECOMMENDATION='checkpoint and handoff recommended'\")
174
+ elif usable_pct >= $WARNING_THRESHOLD:
175
+ print('CONTEXT_WARNING=High')
176
+ print(\"CONTEXT_RECOMMENDATION='consider handoff soon'\")
244
177
  " 2>/dev/null)
245
178
 
246
- if [ "$HUMAN_MODE" = "true" ]; then
179
+ if [[ "$HUMAN_MODE" == "true" ]]; then
247
180
  eval "$RESULT"
248
- if [ "$USE_TIREPUMP" = "true" ]; then
249
- echo "🔄 Context: ${CONTEXT_USABLE_PERCENT}% used (${CONTEXT_USABLE_TOKENS} of ${CONTEXT_AVAILABLE} available) - TIREPUMP (clear + next agent)"
250
- elif [ "$CONTEXT_STATUS" = "HIGH" ]; then
251
- echo "⚠️ Context: ${CONTEXT_USABLE_PERCENT}% used (${CONTEXT_USABLE_TOKENS} of ${CONTEXT_AVAILABLE} available) - AUTO-HANDOFF"
181
+ if [[ "${USE_TIREPUMP:-}" == "true" ]]; then
182
+ echo "🔄 Context: ${CONTEXT_USABLE_PERCENT}% used (${CONTEXT_USABLE_TOKENS} of ${CONTEXT_AVAILABLE} available) - TIREPUMP"
183
+ elif [[ "${CONTEXT_STATUS:-}" == "HIGH" ]]; then
184
+ echo "⚠️ Context: ${CONTEXT_USABLE_PERCENT}% used (${CONTEXT_USABLE_TOKENS} of ${CONTEXT_AVAILABLE} available) - HIGH"
252
185
  else
253
- echo "✅ Context: ${CONTEXT_USABLE_PERCENT}% used (${CONTEXT_USABLE_TOKENS} of ${CONTEXT_AVAILABLE} available)"
254
- fi
255
- echo " Overhead: ${CONTEXT_BASELINE} tokens (system prompt + tools)"
256
- echo " Mode: ${PERMISSION_MODE}"
257
-
258
- # Output warning messages at configurable thresholds (use usable percent)
259
- if [ -n "$CONTEXT_USABLE_PERCENT" ]; then
260
- if [ "$CONTEXT_USABLE_PERCENT" -ge "$CRITICAL_THRESHOLD" ] 2>/dev/null; then
261
- echo "CONTEXT_WARNING: Critical (${CONTEXT_USABLE_PERCENT}%) - checkpoint and handoff recommended"
262
- elif [ "$CONTEXT_USABLE_PERCENT" -ge "$WARNING_THRESHOLD" ] 2>/dev/null; then
263
- echo "CONTEXT_WARNING: High (${CONTEXT_USABLE_PERCENT}%) - consider handoff soon"
264
- fi
186
+ echo "✅ Context: ${CONTEXT_USABLE_PERCENT:-?}% used (${CONTEXT_USABLE_TOKENS:-?} of ${CONTEXT_AVAILABLE:-?} available)"
265
187
  fi
188
+ echo " Overhead: ${CONTEXT_BASELINE:-?} tokens (system prompt + tools)"
189
+ echo " Mode: ${PERMISSION_MODE:-manual}"
190
+ [[ "${CONTEXT_WARNING:-}" == "Critical" ]] && echo "CONTEXT_WARNING: Critical - checkpoint and handoff recommended"
191
+ [[ "${CONTEXT_WARNING:-}" == "High" ]] && echo "CONTEXT_WARNING: High - consider handoff soon"
266
192
  else
267
193
  echo "$RESULT"
268
-
269
- # Also output warnings in non-human mode for scripting (use usable percent)
270
- eval "$RESULT" 2>/dev/null || true
271
- if [ -n "$CONTEXT_USABLE_PERCENT" ]; then
272
- if [ "$CONTEXT_USABLE_PERCENT" -ge "$CRITICAL_THRESHOLD" ] 2>/dev/null; then
273
- echo "CONTEXT_WARNING=Critical"
274
- echo "CONTEXT_RECOMMENDATION='checkpoint and handoff recommended'"
275
- elif [ "$CONTEXT_USABLE_PERCENT" -ge "$WARNING_THRESHOLD" ] 2>/dev/null; then
276
- echo "CONTEXT_WARNING=High"
277
- echo "CONTEXT_RECOMMENDATION='consider handoff soon'"
278
- fi
279
- fi
280
194
  fi
@@ -62,12 +62,23 @@ AGENT_COMMAND:
62
62
  ---
63
63
  EOF
64
64
  elif [[ "$IS_CYCLIST" != "true" ]]; then
65
- # Not in Cyclist - no marker
65
+ # Not in Cyclist - no marker, include context info
66
+ # Get context percentage for warning
67
+ CONTEXT_PCT="${CONTEXT_USABLE_PERCENT:-unknown}"
68
+ if [[ "$CONTEXT_PCT" != "unknown" ]] && [[ "$CONTEXT_PCT" -ge 60 ]] 2>/dev/null; then
69
+ CONTEXT_WARNING=" (context: ${CONTEXT_PCT}% - consider /clear before continuing)"
70
+ elif [[ "$CONTEXT_PCT" != "unknown" ]]; then
71
+ CONTEXT_WARNING=" (context: ${CONTEXT_PCT}%)"
72
+ else
73
+ CONTEXT_WARNING=""
74
+ fi
66
75
  cat <<EOF
67
76
  ---
68
77
  AGENT_COMMAND:
69
78
  marker: ""
70
- fallback: "Run \`/${NEXT_AGENT}\` to continue"
79
+ fallback: "Run \`/${NEXT_AGENT}\` to continue${CONTEXT_WARNING}"
80
+ relay_mode: ${RELAY_MODE}
81
+ context_percent: ${CONTEXT_PCT}
71
82
  ---
72
83
  EOF
73
84
  elif [[ "$RELAY_MODE" != "true" ]]; then
@@ -8,6 +8,9 @@
8
8
 
9
9
  set -e
10
10
 
11
+ # Self-locate: derive paths from this script's position
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
13
+
11
14
  # Load environment
12
15
  if [ -f "$PROJECT_ROOT/.env" ]; then
13
16
  set -a; source "$PROJECT_ROOT/.env"; set +a
@@ -20,7 +23,7 @@ fi
20
23
 
21
24
  # Source repo utilities (handles repos.yaml or legacy env vars)
22
25
  REPO_UTILS_LAZY=1 # Don't auto-load, we'll do it after validation
23
- source "$PROJECT_ROOT/scripts/repo-utils.sh"
26
+ source "$SCRIPT_DIR/../misc/repo-utils.sh"
24
27
 
25
28
  WORKTREE_ROOT="${WORKTREE_ROOT:-$PROJECT_ROOT/worktrees}"
26
29
  WORKTREE_PORT_OFFSET="${WORKTREE_PORT_OFFSET:-100}"
@@ -22,13 +22,7 @@ while [[ $# -gt 0 ]]; do
22
22
  done
23
23
 
24
24
  # Find project root
25
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
26
- d="$PWD"
27
- while [[ ! -d "$d/.pennyfarthing" ]] && [[ "$d" != "/" ]]; do
28
- d="$(dirname "$d")"
29
- done
30
- PROJECT_ROOT="$d"
31
- fi
25
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/find-root.sh"
32
26
 
33
27
  ARCHIVE_DIR="$PROJECT_ROOT/sprint/archive"
34
28
  SESSION_DIR="$PROJECT_ROOT/.session/archive"
@@ -14,18 +14,11 @@
14
14
 
15
15
  set -uo pipefail
16
16
 
17
- # Self-locate and set up PROJECT_ROOT
18
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
19
- source "$SCRIPT_DIR/../lib/find-root.sh"
20
-
21
- # Initialize paths
22
- PROJECT_ROOT="$(find_project_root 2>/dev/null || echo "")"
23
- if [[ -z "$PROJECT_ROOT" ]]; then
24
- # Not in a pennyfarthing project, silently exit
25
- exit 0
26
- fi
17
+ # Self-locate (resolve symlink first for .git/hooks/ symlinks)
18
+ REAL_SCRIPT="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
19
+ source "$(dirname "$REAL_SCRIPT")/../lib/find-root.sh"
27
20
 
28
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
21
+ # PROJECT_ROOT is now set by find-root.sh
29
22
  SESSION_DIR="$PROJECT_ROOT/.session"
30
23
  SPRINT_FILE="$PROJECT_ROOT/sprint/current-sprint.yaml"
31
24
 
@@ -12,14 +12,9 @@
12
12
 
13
13
  set -uo pipefail
14
14
 
15
- # Find project root
16
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
- # Handle both direct execution and symlink from .git/hooks
18
- if [[ "$SCRIPT_DIR" == *".git/hooks"* ]]; then
19
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
20
- else
21
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
22
- fi
15
+ # Find project root (resolve symlink first for .git/hooks/ symlinks)
16
+ REAL_SCRIPT="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
17
+ source "$(dirname "$REAL_SCRIPT")/../lib/find-root.sh"
23
18
 
24
19
  # =============================================================================
25
20
  # Check 1: Branch Protection
@@ -9,9 +9,9 @@
9
9
 
10
10
  set -uo pipefail
11
11
 
12
- # Self-locate and set up PROJECT_ROOT
13
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
14
- if ! source "$SCRIPT_DIR/../lib/find-root.sh" 2>/dev/null; then
12
+ # Self-locate (resolve symlink first for .git/hooks/ symlinks)
13
+ REAL_SCRIPT="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
14
+ if ! source "$(dirname "$REAL_SCRIPT")/../lib/find-root.sh" 2>/dev/null; then
15
15
  exit 0
16
16
  fi
17
17
 
@@ -19,13 +19,7 @@ if [[ -z "$EPIC_ID" ]]; then
19
19
  fi
20
20
 
21
21
  # Find project root
22
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
23
- d="$PWD"
24
- while [[ ! -d "$d/.pennyfarthing" ]] && [[ "$d" != "/" ]]; do
25
- d="$(dirname "$d")"
26
- done
27
- PROJECT_ROOT="$d"
28
- fi
22
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/find-root.sh"
29
23
 
30
24
  SPRINT_FILE="$PROJECT_ROOT/sprint/current-sprint.yaml"
31
25
  SCRIPTS_DIR="$PROJECT_ROOT/.pennyfarthing/scripts"
@@ -14,14 +14,8 @@ if [[ -z "$EPIC_JIRA_KEY" || -z "$STORY_ID" ]]; then
14
14
  exit 1
15
15
  fi
16
16
 
17
- # PROJECT_ROOT should be set by find-root.sh, but find it if not
18
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
19
- d="$PWD"
20
- while [[ ! -d "$d/.pennyfarthing" ]] && [[ "$d" != "/" ]]; do
21
- d="$(dirname "$d")"
22
- done
23
- PROJECT_ROOT="$d"
24
- fi
17
+ # Find project root
18
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/find-root.sh"
25
19
 
26
20
  SPRINT_FILE="$PROJECT_ROOT/sprint/current-sprint.yaml"
27
21
  JIRA_PROJECT="${JIRA_PROJECT_KEY:-MSSCI}"
@@ -13,14 +13,8 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
- # PROJECT_ROOT should be set by find-root.sh, but find it if not
17
- if [[ -z "${PROJECT_ROOT:-}" ]]; then
18
- d="$PWD"
19
- while [[ ! -d "$d/.pennyfarthing" ]] && [[ "$d" != "/" ]]; do
20
- d="$(dirname "$d")"
21
- done
22
- PROJECT_ROOT="$d"
23
- fi
16
+ # Find project root
17
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/find-root.sh"
24
18
 
25
19
  SPRINT_FILE="$PROJECT_ROOT/sprint/current-sprint.yaml"
26
20
  FIX_MODE="${1:-}"