@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.
- package/README.md +109 -201
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +205 -0
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.js +31 -0
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.js +31 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/pennyfarthing-dist/agents/architect.md +48 -53
- package/pennyfarthing-dist/agents/dev.md +74 -164
- package/pennyfarthing-dist/agents/devops.md +44 -39
- package/pennyfarthing-dist/agents/handoff.md +46 -23
- package/pennyfarthing-dist/agents/orchestrator.md +84 -255
- package/pennyfarthing-dist/agents/pm.md +40 -50
- package/pennyfarthing-dist/agents/reviewer-preflight.md +58 -26
- package/pennyfarthing-dist/agents/reviewer.md +107 -298
- package/pennyfarthing-dist/agents/sm-file-summary.md +51 -30
- package/pennyfarthing-dist/agents/sm-finish.md +59 -38
- package/pennyfarthing-dist/agents/sm-handoff.md +40 -33
- package/pennyfarthing-dist/agents/sm-setup.md +122 -45
- package/pennyfarthing-dist/agents/sm.md +204 -545
- package/pennyfarthing-dist/agents/tea.md +77 -146
- package/pennyfarthing-dist/agents/tech-writer.md +43 -24
- package/pennyfarthing-dist/agents/testing-runner.md +73 -30
- package/pennyfarthing-dist/agents/ux-designer.md +39 -25
- package/pennyfarthing-dist/agents/workflow-status-check.md +45 -17
- package/pennyfarthing-dist/commands/benchmark.md +19 -1
- package/pennyfarthing-dist/commands/continue-session.md +1 -1
- package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
- package/pennyfarthing-dist/commands/solo.md +36 -0
- package/pennyfarthing-dist/commands/theme-maker.md +5 -5
- package/pennyfarthing-dist/commands/work.md +1 -1
- package/pennyfarthing-dist/guides/agent-behavior.md +22 -9
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
- package/pennyfarthing-dist/guides/scale-levels.md +114 -0
- package/pennyfarthing-dist/guides/xml-tags.md +335 -0
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
- package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
- package/pennyfarthing-dist/scripts/core/check-context.sh +9 -1
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +3 -132
- package/pennyfarthing-dist/scripts/core/run.sh +9 -0
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +499 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
- package/pennyfarthing-dist/scripts/jira/README.md +10 -7
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
- package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
- package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
- package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
- package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
- package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
- package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
- package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
- package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
- package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
- package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
- package/pennyfarthing-dist/guides/XML-TAGS.md +0 -156
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -380
- package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
- package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
- package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
- package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
- /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md → agent-coordination.md} +0 -0
- /package/pennyfarthing-dist/guides/{HOOKS.md → hooks.md} +0 -0
- /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md → prompt-patterns.md} +0 -0
- /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
|
-
}
|