@pennyfarthing/core 7.6.1 → 7.8.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 (109) hide show
  1. package/README.md +109 -201
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +205 -0
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.js +31 -0
  7. package/packages/core/dist/cli/commands/init.js.map +1 -1
  8. package/packages/core/dist/cli/commands/update.js +31 -0
  9. package/packages/core/dist/cli/commands/update.js.map +1 -1
  10. package/pennyfarthing-dist/agents/architect.md +48 -53
  11. package/pennyfarthing-dist/agents/dev.md +74 -164
  12. package/pennyfarthing-dist/agents/devops.md +44 -39
  13. package/pennyfarthing-dist/agents/handoff.md +46 -23
  14. package/pennyfarthing-dist/agents/orchestrator.md +84 -255
  15. package/pennyfarthing-dist/agents/pm.md +40 -50
  16. package/pennyfarthing-dist/agents/reviewer-preflight.md +58 -26
  17. package/pennyfarthing-dist/agents/reviewer.md +107 -298
  18. package/pennyfarthing-dist/agents/sm-file-summary.md +51 -30
  19. package/pennyfarthing-dist/agents/sm-finish.md +59 -38
  20. package/pennyfarthing-dist/agents/sm-handoff.md +40 -33
  21. package/pennyfarthing-dist/agents/sm-setup.md +122 -45
  22. package/pennyfarthing-dist/agents/sm.md +204 -545
  23. package/pennyfarthing-dist/agents/tea.md +77 -146
  24. package/pennyfarthing-dist/agents/tech-writer.md +43 -24
  25. package/pennyfarthing-dist/agents/testing-runner.md +73 -30
  26. package/pennyfarthing-dist/agents/ux-designer.md +39 -25
  27. package/pennyfarthing-dist/agents/workflow-status-check.md +45 -17
  28. package/pennyfarthing-dist/commands/benchmark.md +19 -1
  29. package/pennyfarthing-dist/commands/continue-session.md +1 -1
  30. package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
  31. package/pennyfarthing-dist/commands/solo.md +36 -0
  32. package/pennyfarthing-dist/commands/theme-maker.md +5 -5
  33. package/pennyfarthing-dist/commands/work.md +1 -1
  34. package/pennyfarthing-dist/guides/agent-behavior.md +22 -9
  35. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
  36. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
  37. package/pennyfarthing-dist/guides/scale-levels.md +114 -0
  38. package/pennyfarthing-dist/guides/xml-tags.md +335 -0
  39. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
  40. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
  41. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
  42. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
  43. package/pennyfarthing-dist/scripts/core/check-context.sh +9 -1
  44. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
  45. package/pennyfarthing-dist/scripts/core/prime.sh +3 -132
  46. package/pennyfarthing-dist/scripts/core/run.sh +9 -0
  47. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
  48. package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
  49. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  50. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
  51. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
  52. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
  53. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +499 -0
  54. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
  55. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
  56. package/pennyfarthing-dist/scripts/jira/README.md +10 -7
  57. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
  58. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
  59. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
  60. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
  61. package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
  62. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
  63. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
  64. package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
  65. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
  66. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
  67. package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
  68. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
  69. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
  70. package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
  71. package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
  72. package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
  73. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
  74. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
  75. package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
  76. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
  77. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
  78. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
  79. package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
  80. package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
  81. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
  82. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
  83. package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
  84. package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
  85. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
  86. package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
  87. package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
  88. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
  89. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
  90. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
  91. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
  92. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
  93. package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
  94. package/pennyfarthing-dist/guides/XML-TAGS.md +0 -156
  95. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -380
  96. package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
  97. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
  98. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
  99. package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
  100. package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
  101. package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
  102. package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
  103. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
  104. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
  105. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
  106. /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md → agent-coordination.md} +0 -0
  107. /package/pennyfarthing-dist/guides/{HOOKS.md → hooks.md} +0 -0
  108. /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md → prompt-patterns.md} +0 -0
  109. /package/pennyfarthing-dist/guides/{SESSION-ARTIFACTS.md → session-artifacts.md} +0 -0
@@ -0,0 +1,71 @@
1
+ # Step 5: Complete
2
+
3
+ Cleanup workflow finished. Final summary and next steps.
4
+
5
+ ## Summary
6
+
7
+ ```
8
+ ## Git Cleanup Complete ✅
9
+
10
+ ### Session Summary
11
+ - Groups committed: {n}
12
+ - Files organized: {count}
13
+ - Pushed to remote: {yes/no}
14
+
15
+ ### Commits
16
+ {list of commit hashes and messages}
17
+
18
+ ### Time Saved
19
+ Organizing {n} scattered changes into {m} proper commits
20
+ with conventional commit messages and branch workflow.
21
+ ```
22
+
23
+ ## Post-Cleanup Tasks
24
+
25
+ ### If Changes Remain
26
+
27
+ Intentionally skipped files can be:
28
+ - Committed in next cleanup session
29
+ - Added to .gitignore if generated
30
+ - Discarded with `git checkout -- {file}`
31
+
32
+ ### Branch Maintenance
33
+
34
+ Run periodically to clean up merged branches:
35
+
36
+ ```bash
37
+ # Delete branches merged into develop
38
+ git branch --merged develop | grep -v "develop\|main" | xargs -r git branch -d
39
+ ```
40
+
41
+ ### Stash Cleanup
42
+
43
+ If old stashes accumulated:
44
+
45
+ ```bash
46
+ # View stashes
47
+ git stash list
48
+
49
+ # Drop old cleanup stashes
50
+ git stash drop stash@{n}
51
+ ```
52
+
53
+ ## Quick Re-run
54
+
55
+ To run git-cleanup again:
56
+
57
+ ```
58
+ /git-cleanup
59
+ ```
60
+
61
+ Or for a quick status check:
62
+
63
+ ```bash
64
+ ./scripts/run.sh git/git-status-all.sh
65
+ ```
66
+
67
+ ---
68
+
69
+ **Cleanup complete.** Working directory is organized.
70
+
71
+ <!-- CYCLIST:CONTINUE -->
@@ -0,0 +1,59 @@
1
+ # Git Cleanup Workflow - Organize uncommitted changes into proper commits
2
+ # Stepped workflow for cleaning up scattered changes across repos
3
+ #
4
+ # Flow: Analyze → Categorize → [Approve] → Execute → Verify → [Push]
5
+ # Use for: end-of-session cleanup, organizing mixed changes, git hygiene
6
+ #
7
+ # Key features:
8
+ # - Multi-repo support (configured via repos.yaml)
9
+ # - Conventional commit enforcement
10
+ # - Branch workflow (never direct to develop)
11
+ # - Stash-based change isolation
12
+
13
+ workflow:
14
+ name: git-cleanup
15
+ description: Organize uncommitted changes into proper commits/branches by initiative
16
+ version: "1.0.0"
17
+ type: stepped
18
+
19
+ # Step configuration
20
+ steps:
21
+ path: ./git-cleanup/steps/
22
+ pattern: step-{nn}-*.md
23
+
24
+ # Variables available in step files
25
+ variables:
26
+ output_file: .session/git-cleanup-plan.md
27
+ repos_config: .claude/project/repos.yaml
28
+
29
+ # User approval gates
30
+ gates:
31
+ after_steps: [2, 4] # After categorize and after execute
32
+ gate_marker: "<!-- GATE -->"
33
+
34
+ # Collaboration menus
35
+ collaboration:
36
+ menus:
37
+ - key: A
38
+ name: Analyze More
39
+ description: Dig deeper into a specific repo or change set
40
+ - key: E
41
+ name: Edit Groupings
42
+ description: Modify the proposed change groupings
43
+ - key: T
44
+ name: Track in Jira
45
+ description: Promote a group to a tracked Jira story (standalone-style)
46
+ - key: C
47
+ name: Continue
48
+ description: Approve and proceed to next step
49
+ - key: S
50
+ name: Skip Group
51
+ description: Skip a group (leave changes uncommitted)
52
+
53
+ # Agent assignment - uses orchestrator for cross-repo coordination
54
+ agent: orchestrator
55
+
56
+ # Triggers
57
+ triggers:
58
+ commands: [git-cleanup, cleanup]
59
+ tags: [git, cleanup, maintenance]
@@ -1,156 +0,0 @@
1
- # XML Tag Taxonomy
2
-
3
- Pennyfarthing uses XML-style tags to structure agent definitions and skill documentation. These tags help LLMs identify and prioritize different types of content.
4
-
5
- ## Priority Tags
6
-
7
- Tags that affect LLM behavior and attention.
8
-
9
- ### `<critical>`
10
-
11
- **Purpose:** Non-negotiable rules that MUST be followed. LLMs should treat these as hard constraints.
12
-
13
- **Usage:** Gates, invariants, protocol requirements, things that break the system if ignored.
14
-
15
- ```markdown
16
- <critical>
17
- **Never edit sprint YAML directly.** Use scripts.
18
- </critical>
19
- ```
20
-
21
- **Examples:**
22
- - "Subagent output is NOT visible to Cyclist"
23
- - "NEVER mark acceptance criteria as complete" (for subagents)
24
- - "Write assessment BEFORE spawning handoff subagent"
25
-
26
- ### `<gate>`
27
-
28
- **Purpose:** Prerequisites that MUST be verified before proceeding. Checklist-style validation.
29
-
30
- **Usage:** Entry/exit conditions for workflows, handoff requirements, quality gates.
31
-
32
- ```markdown
33
- <gate>
34
- ## Handoff Checklist
35
- 1. Session file exists
36
- 2. Acceptance criteria defined
37
- 3. Feature branches created
38
- </gate>
39
- ```
40
-
41
- **Difference from `<critical>`:** Gates are procedural checkpoints; critical items are invariant rules.
42
-
43
- ### `<info>`
44
-
45
- **Purpose:** Contextual information that helps but doesn't constrain. Reference material.
46
-
47
- **Usage:** Background context, defaults, file locations, tips.
48
-
49
- ```markdown
50
- <info>
51
- **Workflow:** SM → TEA → Dev → Reviewer → SM
52
- **Skills:** `/sprint`, `/jira`, `/testing`
53
- </info>
54
- ```
55
-
56
- ## Identity Tags
57
-
58
- Tags that define agent personality and role.
59
-
60
- ### `<persona>`
61
-
62
- **Purpose:** Character personality from the active theme. Loaded at agent activation.
63
-
64
- **Usage:** Top of agent files, sets tone and style.
65
-
66
- ```markdown
67
- <persona>
68
- Auto-loaded by `agent-session.sh start` from theme config.
69
- **Fallback if not loaded:** Supportive, methodical, detail-oriented
70
- </persona>
71
- ```
72
-
73
- ### `<role>`
74
-
75
- **Purpose:** Agent's position in the workflow and primary responsibility.
76
-
77
- **Usage:** Brief statement of what the agent does and when it's invoked.
78
-
79
- ```markdown
80
- <role>
81
- Test specification, RED phase execution, handoff to Dev
82
- </role>
83
- ```
84
-
85
- ## Structure Tags
86
-
87
- Tags that organize agent content.
88
-
89
- ### `<helpers>`
90
-
91
- **Purpose:** Describes Haiku subagents and their invocation pattern.
92
-
93
- **Usage:** Lists subagents, their purposes, and how to spawn them.
94
-
95
- ### `<responsibilities>`
96
-
97
- **Purpose:** Bullet list of what this agent does vs delegates.
98
-
99
- ### `<skills>`
100
-
101
- **Purpose:** Slash commands this agent commonly uses.
102
-
103
- ### `<context>`
104
-
105
- **Purpose:** Guide files and sidecars to reference.
106
-
107
- ### `<reasoning-mode>`
108
-
109
- **Purpose:** Verbose/quiet toggle for showing thought process.
110
-
111
- ### `<on-activation>`
112
-
113
- **Purpose:** Startup checklist - what to do when agent is invoked.
114
-
115
- ### `<exit>`
116
-
117
- **Purpose:** How to leave agent mode and cleanup.
118
-
119
- ## Usage Guidelines
120
-
121
- 1. **`<critical>` sparingly** - If everything is critical, nothing is. Reserve for true invariants.
122
-
123
- 2. **`<gate>` for checkpoints** - Use when there's a clear pass/fail condition.
124
-
125
- 3. **`<info>` generously** - Helpful context improves agent performance.
126
-
127
- 4. **Order matters:**
128
- ```
129
- <persona> # Who am I?
130
- <role> # What do I do?
131
- <helpers> # Who helps me?
132
- <critical> # What must I never violate?
133
- <gate> # What must I check?
134
- <info> # What's helpful to know?
135
- ```
136
-
137
- 5. **Close your tags** - Always use `</tag>` even though markdown parsers are lenient.
138
-
139
- ## Tag Locations
140
-
141
- | Tag | Typical Location |
142
- |-----|------------------|
143
- | `<critical>` | Agent files, skill files, workflow instructions |
144
- | `<gate>` | Subagent files (handoff, finish, setup) |
145
- | `<info>` | Agent files, guide files |
146
- | `<persona>` | Agent files (top) |
147
- | `<role>` | Agent files (after persona) |
148
-
149
- ## Adding New Tags
150
-
151
- Before adding a new tag type:
152
-
153
- 1. Check if existing tags cover the use case
154
- 2. Document the tag's purpose and priority level
155
- 3. Update this file
156
- 4. Be consistent across all files using the tag
@@ -1,380 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * question-reflector-check.mjs - Question reflector enforcement hook
4
- *
5
- * Story: MSSCI-12393
6
- *
7
- * Validates that any question asked by the agent has an appropriate
8
- * CYCLIST reflector marker. Used by both Stop hook and PreToolUse hook.
9
- *
10
- * Question types detected:
11
- * - Direct questions (ends with ?)
12
- * - Implicit questions (would you like, should I, let me know if)
13
- * - Choice offerings (option A or B, we could do X or Y)
14
- *
15
- * Required markers:
16
- * <!-- CYCLIST:QUESTION:yesno -->
17
- * <!-- CYCLIST:QUESTION:open -->
18
- * <!-- CYCLIST:CHOICES:opt1,opt2,opt3 -->
19
- */
20
-
21
- import { readFileSync } from 'fs';
22
- import { join, dirname } from 'path';
23
-
24
- // =============================================================================
25
- // Constants
26
- // =============================================================================
27
-
28
- // Marker patterns
29
- const QUESTION_MARKER_PATTERN = /<!--\s*CYCLIST:QUESTION:(yesno|open)\s*-->/i;
30
- const CHOICES_MARKER_PATTERN = /<!--\s*CYCLIST:CHOICES:[^>]+\s*-->/i;
31
-
32
- // Question patterns - direct (with ?)
33
- // Match: end of line, followed by space+capital (new sentence), or followed by newline
34
- const DIRECT_QUESTION_PATTERN = /\?(\s*$|\s+[A-Z]|\s*\n)/;
35
-
36
- // Rhetorical patterns to exclude
37
- const RHETORICAL_PATTERNS = /\b(the question (was|is)|asked whether|wondering if)\b/i;
38
-
39
- // Implicit question patterns
40
- const IMPLICIT_PATTERNS = [
41
- /\bwould you like\b/i,
42
- /\bshould I\b/i,
43
- /\bdo you want\b/i,
44
- /\blet me know if\b/i,
45
- /\bwhat do you (think|prefer)\b/i,
46
- /\byour (preference|thoughts)\b/i,
47
- /\bcould you (clarify|confirm|specify)\b/i,
48
- /\bwhich (option|approach)\b/i,
49
- /\bready to proceed\b/i,
50
- ];
51
-
52
- // Choice offering patterns
53
- const CHOICE_PATTERNS = [
54
- /\boption [A-D]\b/i,
55
- /\bchoice [0-9]\b/i,
56
- /\bwe could (either|do)\b/i,
57
- /\balternatively\b/i,
58
- /\bor would you prefer\b/i,
59
- /\bpick one\b/i,
60
- /\bchoose between\b/i,
61
- ];
62
-
63
- // =============================================================================
64
- // Helper Functions
65
- // =============================================================================
66
-
67
- /**
68
- * Strip fenced code blocks from text to avoid false positives
69
- * @param {string} text - The text to process
70
- * @returns {string} Text with code blocks removed
71
- */
72
- function stripCodeBlocks(text) {
73
- // Remove fenced code blocks (```...```)
74
- let result = text.replace(/```[\s\S]*?```/g, '');
75
- // Remove inline code (`...`)
76
- result = result.replace(/`[^`]+`/g, '');
77
- return result;
78
- }
79
-
80
- // =============================================================================
81
- // Exported Functions
82
- // =============================================================================
83
-
84
- /**
85
- * Check if enforcement should be skipped based on config
86
- * @param {object} config - The config object with workflow settings
87
- * @returns {boolean} True if enforcement should be skipped
88
- */
89
- export function shouldSkipEnforcement(config) {
90
- const workflow = config?.workflow || {};
91
-
92
- // Legacy: turbo mode skips enforcement
93
- if (workflow.permission_mode === 'turbo') {
94
- return true;
95
- }
96
-
97
- // New: relay_mode skips enforcement (for auto-handoff flows)
98
- if (workflow.relay_mode === true) {
99
- return true;
100
- }
101
-
102
- return false;
103
- }
104
-
105
- /**
106
- * Detect if a message contains a question
107
- * @param {string} message - The message to check
108
- * @returns {{ detected: boolean, type: string }} Detection result
109
- */
110
- export function detectQuestion(message) {
111
- // Strip code blocks first
112
- const cleanMessage = stripCodeBlocks(message);
113
-
114
- // Check for rhetorical patterns - if found, not a real question
115
- if (RHETORICAL_PATTERNS.test(cleanMessage)) {
116
- return { detected: false, type: '' };
117
- }
118
-
119
- // Check for direct questions (with ?)
120
- if (DIRECT_QUESTION_PATTERN.test(cleanMessage)) {
121
- return { detected: true, type: 'direct' };
122
- }
123
-
124
- // Check for implicit questions
125
- for (const pattern of IMPLICIT_PATTERNS) {
126
- if (pattern.test(cleanMessage)) {
127
- return { detected: true, type: 'implicit' };
128
- }
129
- }
130
-
131
- // Check for choice offerings
132
- for (const pattern of CHOICE_PATTERNS) {
133
- if (pattern.test(cleanMessage)) {
134
- return { detected: true, type: 'choices' };
135
- }
136
- }
137
-
138
- return { detected: false, type: '' };
139
- }
140
-
141
- /**
142
- * Check if a message has a CYCLIST reflector marker
143
- * @param {string} message - The message to check
144
- * @returns {boolean} True if a marker is present
145
- */
146
- export function hasReflectorMarker(message) {
147
- return QUESTION_MARKER_PATTERN.test(message) || CHOICES_MARKER_PATTERN.test(message);
148
- }
149
-
150
- /**
151
- * Extract the last assistant message from a transcript
152
- * @param {Array} transcript - Array of message objects
153
- * @returns {string} The last assistant message content
154
- */
155
- export function extractLastAssistantMessage(transcript) {
156
- // Find the last assistant message (reverse order)
157
- for (let i = transcript.length - 1; i >= 0; i--) {
158
- const msg = transcript[i];
159
- if (msg.role === 'assistant') {
160
- // Handle content as string or array
161
- if (typeof msg.content === 'string') {
162
- return msg.content;
163
- }
164
- if (Array.isArray(msg.content)) {
165
- // Extract text from text blocks, skip tool_use blocks
166
- return msg.content
167
- .filter(block => block.type === 'text')
168
- .map(block => block.text)
169
- .join('');
170
- }
171
- return '';
172
- }
173
- }
174
- return '';
175
- }
176
-
177
- /**
178
- * Build the block reason message
179
- * @param {string} questionType - The type of question detected
180
- * @returns {string} The reason message
181
- */
182
- function buildBlockReason(questionType) {
183
- let reason = 'You asked a question but did not emit a CYCLIST reflector marker. ';
184
-
185
- switch (questionType) {
186
- case 'direct':
187
- reason += 'Add <!-- CYCLIST:QUESTION:yesno --> for yes/no questions or <!-- CYCLIST:QUESTION:open --> for open-ended questions before your question.';
188
- break;
189
- case 'implicit':
190
- reason += 'Add <!-- CYCLIST:QUESTION:yesno --> before phrases like "would you like" or "should I".';
191
- break;
192
- case 'choices':
193
- reason += 'Add <!-- CYCLIST:CHOICES:option1,option2,option3 --> listing the choices before presenting options.';
194
- break;
195
- default:
196
- reason += 'Add the appropriate marker before your question.';
197
- }
198
-
199
- reason += ' Re-state your question with the appropriate marker.';
200
- return reason;
201
- }
202
-
203
- /**
204
- * Main check for Stop hook - validates question reflector markers
205
- * @param {object} input - Hook input with transcript_path, stop_hook_active
206
- * @param {object} config - Config with workflow settings
207
- * @param {string} lastMessage - The last assistant message (pre-extracted for testing)
208
- * @returns {{ ok: true } | { decision: 'block', reason: string }}
209
- */
210
- export function checkQuestionReflector(input, config, lastMessage) {
211
- // Prevent infinite loops
212
- if (input.stop_hook_active) {
213
- return { ok: true };
214
- }
215
-
216
- // Skip enforcement in relay/turbo mode
217
- if (shouldSkipEnforcement(config)) {
218
- return { ok: true };
219
- }
220
-
221
- // If no message, allow
222
- if (!lastMessage) {
223
- return { ok: true };
224
- }
225
-
226
- // If marker present, allow
227
- if (hasReflectorMarker(lastMessage)) {
228
- return { ok: true };
229
- }
230
-
231
- // Check for questions
232
- const detection = detectQuestion(lastMessage);
233
- if (!detection.detected) {
234
- return { ok: true };
235
- }
236
-
237
- // Question detected without marker - block
238
- return {
239
- decision: 'block',
240
- reason: buildBlockReason(detection.type),
241
- };
242
- }
243
-
244
- /**
245
- * Check for AskUserQuestion PreToolUse hook
246
- * @param {object} input - Hook input with tool_name, tool_input
247
- * @param {object} config - Config with workflow settings
248
- * @param {string} [recentOutput] - Recent assistant output to check for marker
249
- * @returns {{ ok: true } | { decision: 'block', reason: string }}
250
- */
251
- export function checkAskUserQuestion(input, config, recentOutput = '') {
252
- // Skip enforcement in relay/turbo mode
253
- if (shouldSkipEnforcement(config)) {
254
- return { ok: true };
255
- }
256
-
257
- // If marker present in recent output, allow
258
- if (recentOutput && hasReflectorMarker(recentOutput)) {
259
- return { ok: true };
260
- }
261
-
262
- // Block - AskUserQuestion requires a marker
263
- return {
264
- decision: 'block',
265
- reason: 'AskUserQuestion tool requires a CYCLIST marker. Add <!-- CYCLIST:QUESTION:yesno -->, <!-- CYCLIST:QUESTION:open -->, or <!-- CYCLIST:CHOICES:... --> before using this tool.',
266
- };
267
- }
268
-
269
- // =============================================================================
270
- // CLI Entry Point (for bash wrapper)
271
- // =============================================================================
272
-
273
- /**
274
- * Load config from .pennyfarthing/config.local.yaml
275
- * @param {string} projectDir - The project directory
276
- * @returns {object} The config object
277
- */
278
- function loadConfig(projectDir) {
279
- try {
280
- const configPath = join(projectDir, '.pennyfarthing', 'config.local.yaml');
281
- const content = readFileSync(configPath, 'utf-8');
282
- // Simple YAML parsing for the fields we need
283
- const config = { workflow: {} };
284
-
285
- // Extract permission_mode
286
- const modeMatch = content.match(/permission_mode:\s*(\w+)/);
287
- if (modeMatch) {
288
- config.workflow.permission_mode = modeMatch[1];
289
- }
290
-
291
- // Extract relay_mode
292
- const relayMatch = content.match(/relay_mode:\s*(true|false)/);
293
- if (relayMatch) {
294
- config.workflow.relay_mode = relayMatch[1] === 'true';
295
- }
296
-
297
- return config;
298
- } catch {
299
- // Default config if file doesn't exist
300
- return { workflow: { permission_mode: 'manual' } };
301
- }
302
- }
303
-
304
- /**
305
- * Read transcript and extract last assistant message
306
- * @param {string} transcriptPath - Path to JSONL transcript
307
- * @returns {string} The last assistant message
308
- */
309
- function readTranscript(transcriptPath) {
310
- try {
311
- const content = readFileSync(transcriptPath, 'utf-8');
312
- const lines = content.trim().split('\n').filter(Boolean);
313
-
314
- // Parse JSONL and build transcript array
315
- const transcript = [];
316
- for (const line of lines) {
317
- try {
318
- transcript.push(JSON.parse(line));
319
- } catch {
320
- // Skip malformed lines
321
- }
322
- }
323
-
324
- return extractLastAssistantMessage(transcript);
325
- } catch {
326
- return '';
327
- }
328
- }
329
-
330
- /**
331
- * Main CLI entry point
332
- */
333
- async function main() {
334
- // Read input from stdin
335
- let inputData = '';
336
- for await (const chunk of process.stdin) {
337
- inputData += chunk;
338
- }
339
-
340
- let input;
341
- try {
342
- input = JSON.parse(inputData);
343
- } catch {
344
- // Invalid input - allow to prevent breaking
345
- console.log(JSON.stringify({ ok: true }));
346
- process.exit(0);
347
- }
348
-
349
- // Determine project directory
350
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
351
-
352
- // Load config
353
- const config = loadConfig(projectDir);
354
-
355
- // Determine hook type based on input
356
- if (input.tool_name === 'AskUserQuestion') {
357
- // PreToolUse hook for AskUserQuestion
358
- // For PreToolUse, we'd need the recent output - for now, just check config
359
- const result = checkAskUserQuestion(input, config, '');
360
- console.log(JSON.stringify(result));
361
- } else {
362
- // Stop hook
363
- const transcriptPath = input.transcript_path || '';
364
- const lastMessage = transcriptPath ? readTranscript(transcriptPath) : '';
365
- const result = checkQuestionReflector(input, config, lastMessage);
366
- console.log(JSON.stringify(result));
367
- }
368
-
369
- process.exit(0);
370
- }
371
-
372
- // Run if called directly
373
- if (import.meta.url === `file://${process.argv[1]}`) {
374
- main().catch((err) => {
375
- console.error(err);
376
- // On error, allow to prevent breaking
377
- console.log(JSON.stringify({ ok: true }));
378
- process.exit(0);
379
- });
380
- }