@pennyfarthing/core 7.6.0 → 7.7.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 (70) 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 +91 -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 +89 -47
  22. package/pennyfarthing-dist/agents/sm.md +171 -558
  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 +34 -16
  28. package/pennyfarthing-dist/commands/benchmark.md +19 -1
  29. package/pennyfarthing-dist/commands/continue-session.md +1 -1
  30. package/pennyfarthing-dist/commands/solo.md +5 -0
  31. package/pennyfarthing-dist/commands/theme-maker.md +5 -5
  32. package/pennyfarthing-dist/commands/work.md +1 -1
  33. package/pennyfarthing-dist/guides/XML-TAGS.md +179 -0
  34. package/pennyfarthing-dist/guides/agent-behavior.md +37 -2
  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/personas/themes/gilligans-island.yaml +2 -2
  39. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
  40. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
  41. package/pennyfarthing-dist/scripts/core/check-context.sh +25 -8
  42. package/pennyfarthing-dist/scripts/core/prime.sh +57 -32
  43. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
  44. package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
  45. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
  46. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
  47. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +393 -0
  48. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +20 -0
  49. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +402 -0
  50. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
  51. package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +545 -0
  52. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
  53. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
  54. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
  55. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
  56. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
  57. package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
  58. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
  59. package/pennyfarthing-dist/scripts/misc/deploy.sh +13 -1
  60. package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
  61. package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
  62. package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
  63. package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
  64. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
  65. package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
  66. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
  67. package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
  68. package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
  69. package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
  70. package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * reflector-check.mjs - CYCLIST reflector marker enforcement hook
4
+ *
5
+ * Story: MSSCI-12393 (questions), extended for all markers
6
+ *
7
+ * EVERY turn end MUST have a CYCLIST reflector marker. This ensures:
8
+ * - Cyclist UI can render appropriate buttons/actions
9
+ * - User always has opportunity to intervene
10
+ * - Workflow handoffs are never silently dropped
11
+ *
12
+ * Valid markers (any one required):
13
+ * <!-- CYCLIST:HANDOFF:/agent --> - Workflow handoff to next agent
14
+ * <!-- CYCLIST:CONTEXT_CLEAR:/agent --> - Handoff with context clear (TirePump)
15
+ * <!-- CYCLIST:QUESTION:yesno --> - Yes/no question
16
+ * <!-- CYCLIST:QUESTION:open --> - Open-ended question
17
+ * <!-- CYCLIST:CHOICES:opt1,opt2,opt3 --> - Multiple choice
18
+ * <!-- CYCLIST:CONTINUE --> - Status update, user can continue or redirect
19
+ */
20
+
21
+ import { readFileSync } from 'fs';
22
+ import { join, dirname } from 'path';
23
+
24
+ // =============================================================================
25
+ // Constants
26
+ // =============================================================================
27
+
28
+ // Marker patterns - ALL valid CYCLIST markers
29
+ const QUESTION_MARKER_PATTERN = /<!--\s*CYCLIST:QUESTION:(yesno|open)\s*-->/i;
30
+ const CHOICES_MARKER_PATTERN = /<!--\s*CYCLIST:CHOICES:[^>]+\s*-->/i;
31
+ const HANDOFF_MARKER_PATTERN = /<!--\s*CYCLIST:HANDOFF:\/\w+\s*-->/i;
32
+ const CONTEXT_CLEAR_MARKER_PATTERN = /<!--\s*CYCLIST:CONTEXT_CLEAR:\/\w+\s*-->/i;
33
+ const CONTINUE_MARKER_PATTERN = /<!--\s*CYCLIST:CONTINUE\s*-->/i;
34
+
35
+ // Question patterns - direct (with ?)
36
+ // Match: end of line, followed by space+capital (new sentence), or followed by newline
37
+ const DIRECT_QUESTION_PATTERN = /\?(\s*$|\s+[A-Z]|\s*\n)/;
38
+
39
+ // Rhetorical patterns to exclude
40
+ const RHETORICAL_PATTERNS = /\b(the question (was|is)|asked whether|wondering if)\b/i;
41
+
42
+ // Implicit question patterns
43
+ const IMPLICIT_PATTERNS = [
44
+ /\bwould you like\b/i,
45
+ /\bshould I\b/i,
46
+ /\bdo you want\b/i,
47
+ /\blet me know if\b/i,
48
+ /\bwhat do you (think|prefer)\b/i,
49
+ /\byour (preference|thoughts)\b/i,
50
+ /\bcould you (clarify|confirm|specify)\b/i,
51
+ /\bwhich (option|approach)\b/i,
52
+ /\bready to proceed\b/i,
53
+ ];
54
+
55
+ // Choice offering patterns
56
+ const CHOICE_PATTERNS = [
57
+ /\boption [A-D]\b/i,
58
+ /\bchoice [0-9]\b/i,
59
+ /\bwe could (either|do)\b/i,
60
+ /\balternatively\b/i,
61
+ /\bor would you prefer\b/i,
62
+ /\bpick one\b/i,
63
+ /\bchoose between\b/i,
64
+ ];
65
+
66
+ // =============================================================================
67
+ // Helper Functions
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Strip fenced code blocks from text to avoid false positives
72
+ * @param {string} text - The text to process
73
+ * @returns {string} Text with code blocks removed
74
+ */
75
+ function stripCodeBlocks(text) {
76
+ // Remove fenced code blocks (```...```)
77
+ let result = text.replace(/```[\s\S]*?```/g, '');
78
+ // Remove inline code (`...`)
79
+ result = result.replace(/`[^`]+`/g, '');
80
+ return result;
81
+ }
82
+
83
+ // =============================================================================
84
+ // Exported Functions
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Check if enforcement should be skipped based on config
89
+ * @param {object} config - The config object with workflow settings
90
+ * @returns {boolean} True if enforcement should be skipped
91
+ */
92
+ export function shouldSkipEnforcement(config) {
93
+ // Skip enforcement in CLI mode - markers are only needed for Cyclist UI
94
+ // Cyclist sets CYCLIST=1 in the environment when spawning Claude
95
+ if (process.env.CYCLIST !== '1') {
96
+ return true;
97
+ }
98
+
99
+ // In Cyclist mode, never skip enforcement - markers must always be emitted.
100
+ // relay_mode only controls whether Cyclist auto-executes markers
101
+ // vs showing buttons to the user.
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 ANY valid CYCLIST reflector marker
143
+ * @param {string} message - The message to check
144
+ * @returns {boolean} True if any marker is present
145
+ */
146
+ export function hasReflectorMarker(message) {
147
+ return (
148
+ QUESTION_MARKER_PATTERN.test(message) ||
149
+ CHOICES_MARKER_PATTERN.test(message) ||
150
+ HANDOFF_MARKER_PATTERN.test(message) ||
151
+ CONTEXT_CLEAR_MARKER_PATTERN.test(message) ||
152
+ CONTINUE_MARKER_PATTERN.test(message)
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Extract the last assistant message from a transcript
158
+ * @param {Array} transcript - Array of message objects
159
+ * @returns {string} The last assistant message content
160
+ */
161
+ export function extractLastAssistantMessage(transcript) {
162
+ // Find the last assistant message (reverse order)
163
+ // Claude Code transcript format wraps messages: { message: { role, content }, type, ... }
164
+ for (let i = transcript.length - 1; i >= 0; i--) {
165
+ const entry = transcript[i];
166
+ // Support both wrapped format (Claude Code JSONL) and direct format (tests)
167
+ const msg = entry.message || entry;
168
+ if (msg.role === 'assistant') {
169
+ // Handle content as string or array
170
+ if (typeof msg.content === 'string') {
171
+ return msg.content;
172
+ }
173
+ if (Array.isArray(msg.content)) {
174
+ // Extract text from text blocks, skip tool_use blocks
175
+ return msg.content
176
+ .filter(block => block.type === 'text')
177
+ .map(block => block.text)
178
+ .join('');
179
+ }
180
+ return '';
181
+ }
182
+ }
183
+ return '';
184
+ }
185
+
186
+ /**
187
+ * Build the block reason message
188
+ * @param {string} questionType - The type of question detected (or empty for general)
189
+ * @returns {string} The reason message
190
+ */
191
+ function buildBlockReason(questionType) {
192
+ let reason = 'Every turn MUST end with a CYCLIST reflector marker. ';
193
+
194
+ if (questionType) {
195
+ // Specific question type detected
196
+ switch (questionType) {
197
+ case 'direct':
198
+ reason += 'You asked a question. Add <!-- CYCLIST:QUESTION:yesno --> or <!-- CYCLIST:QUESTION:open --> before your question.';
199
+ break;
200
+ case 'implicit':
201
+ reason += 'You asked an implicit question. Add <!-- CYCLIST:QUESTION:yesno --> before phrases like "would you like" or "should I".';
202
+ break;
203
+ case 'choices':
204
+ reason += 'You offered choices. Add <!-- CYCLIST:CHOICES:option1,option2,option3 --> listing the choices.';
205
+ break;
206
+ }
207
+ } else {
208
+ // No question detected, but still need a marker
209
+ reason += 'Valid markers:\n';
210
+ reason += ' <!-- CYCLIST:HANDOFF:/agent --> - workflow handoff\n';
211
+ reason += ' <!-- CYCLIST:QUESTION:yesno --> - yes/no question\n';
212
+ reason += ' <!-- CYCLIST:QUESTION:open --> - open question\n';
213
+ reason += ' <!-- CYCLIST:CHOICES:a,b,c --> - multiple choice\n';
214
+ reason += ' <!-- CYCLIST:CONTINUE --> - status update, user may continue or redirect';
215
+ }
216
+
217
+ return reason;
218
+ }
219
+
220
+ /**
221
+ * Main check for Stop hook - validates ALL turns have reflector markers
222
+ * @param {object} input - Hook input with transcript_path, stop_hook_active
223
+ * @param {object} config - Config with workflow settings
224
+ * @param {string} lastMessage - The last assistant message (pre-extracted for testing)
225
+ * @returns {{ ok: true } | { decision: 'block', reason: string }}
226
+ */
227
+ export function checkQuestionReflector(input, config, lastMessage) {
228
+ // Prevent infinite loops
229
+ if (input.stop_hook_active) {
230
+ return { ok: true };
231
+ }
232
+
233
+ // Skip enforcement in relay/turbo mode
234
+ if (shouldSkipEnforcement(config)) {
235
+ return { ok: true };
236
+ }
237
+
238
+ // If no message, allow (edge case - shouldn't happen)
239
+ if (!lastMessage) {
240
+ return { ok: true };
241
+ }
242
+
243
+ // If ANY marker present, allow
244
+ if (hasReflectorMarker(lastMessage)) {
245
+ return { ok: true };
246
+ }
247
+
248
+ // No marker found - block
249
+ // Check if it's a question to give more specific guidance
250
+ const detection = detectQuestion(lastMessage);
251
+ return {
252
+ decision: 'block',
253
+ reason: buildBlockReason(detection.detected ? detection.type : ''),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Check for AskUserQuestion PreToolUse hook
259
+ * @param {object} input - Hook input with tool_name, tool_input
260
+ * @param {object} config - Config with workflow settings
261
+ * @param {string} [recentOutput] - Recent assistant output to check for marker
262
+ * @returns {{ ok: true } | { decision: 'block', reason: string }}
263
+ */
264
+ export function checkAskUserQuestion(input, config, recentOutput = '') {
265
+ // Skip enforcement in relay/turbo mode
266
+ if (shouldSkipEnforcement(config)) {
267
+ return { ok: true };
268
+ }
269
+
270
+ // If marker present in recent output, allow
271
+ if (recentOutput && hasReflectorMarker(recentOutput)) {
272
+ return { ok: true };
273
+ }
274
+
275
+ // Block - AskUserQuestion requires a marker
276
+ return {
277
+ decision: 'block',
278
+ reason: 'AskUserQuestion tool requires a CYCLIST marker. Add <!-- CYCLIST:QUESTION:yesno -->, <!-- CYCLIST:QUESTION:open -->, or <!-- CYCLIST:CHOICES:... --> before using this tool.',
279
+ };
280
+ }
281
+
282
+ // =============================================================================
283
+ // CLI Entry Point (for bash wrapper)
284
+ // =============================================================================
285
+
286
+ /**
287
+ * Load config from .pennyfarthing/config.local.yaml
288
+ * @param {string} projectDir - The project directory
289
+ * @returns {object} The config object
290
+ */
291
+ function loadConfig(projectDir) {
292
+ try {
293
+ const configPath = join(projectDir, '.pennyfarthing', 'config.local.yaml');
294
+ const content = readFileSync(configPath, 'utf-8');
295
+ // Simple YAML parsing for the fields we need
296
+ const config = { workflow: {} };
297
+
298
+ // Extract permission_mode
299
+ const modeMatch = content.match(/permission_mode:\s*(\w+)/);
300
+ if (modeMatch) {
301
+ config.workflow.permission_mode = modeMatch[1];
302
+ }
303
+
304
+ // Extract relay_mode
305
+ const relayMatch = content.match(/relay_mode:\s*(true|false)/);
306
+ if (relayMatch) {
307
+ config.workflow.relay_mode = relayMatch[1] === 'true';
308
+ }
309
+
310
+ return config;
311
+ } catch {
312
+ // Default config if file doesn't exist
313
+ return { workflow: { permission_mode: 'manual' } };
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Read transcript and extract last assistant message
319
+ * @param {string} transcriptPath - Path to JSONL transcript
320
+ * @returns {string} The last assistant message
321
+ */
322
+ function readTranscript(transcriptPath) {
323
+ try {
324
+ const content = readFileSync(transcriptPath, 'utf-8');
325
+ const lines = content.trim().split('\n').filter(Boolean);
326
+
327
+ // Parse JSONL and build transcript array
328
+ const transcript = [];
329
+ for (const line of lines) {
330
+ try {
331
+ transcript.push(JSON.parse(line));
332
+ } catch {
333
+ // Skip malformed lines
334
+ }
335
+ }
336
+
337
+ return extractLastAssistantMessage(transcript);
338
+ } catch {
339
+ return '';
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Main CLI entry point
345
+ */
346
+ async function main() {
347
+ // Read input from stdin
348
+ let inputData = '';
349
+ for await (const chunk of process.stdin) {
350
+ inputData += chunk;
351
+ }
352
+
353
+ let input;
354
+ try {
355
+ input = JSON.parse(inputData);
356
+ } catch {
357
+ // Invalid input - allow to prevent breaking
358
+ console.log(JSON.stringify({ ok: true }));
359
+ process.exit(0);
360
+ }
361
+
362
+ // Determine project directory
363
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
364
+
365
+ // Load config
366
+ const config = loadConfig(projectDir);
367
+
368
+ // Determine hook type based on input
369
+ if (input.tool_name === 'AskUserQuestion') {
370
+ // PreToolUse hook for AskUserQuestion
371
+ // For PreToolUse, we'd need the recent output - for now, just check config
372
+ const result = checkAskUserQuestion(input, config, '');
373
+ console.log(JSON.stringify(result));
374
+ } else {
375
+ // Stop hook
376
+ const transcriptPath = input.transcript_path || '';
377
+ const lastMessage = transcriptPath ? readTranscript(transcriptPath) : '';
378
+ const result = checkQuestionReflector(input, config, lastMessage);
379
+ console.log(JSON.stringify(result));
380
+ }
381
+
382
+ process.exit(0);
383
+ }
384
+
385
+ // Run if called directly
386
+ if (import.meta.url === `file://${process.argv[1]}`) {
387
+ main().catch((err) => {
388
+ console.error(err);
389
+ // On error, allow to prevent breaking
390
+ console.log(JSON.stringify({ ok: true }));
391
+ process.exit(0);
392
+ });
393
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Question Reflector Enforcement Hook (Stop hook / PreToolUse hook)
4
+ #
5
+ # Thin wrapper that delegates to question_reflector_check.py for the actual logic.
6
+ # This allows the hook to be written in Python for easier testing and maintenance.
7
+ #
8
+ # Input (stdin): JSON with transcript_path, stop_hook_active, etc.
9
+ # Output (stdout): JSON decision to allow or block
10
+ #
11
+ # Story: MSSCI-12393
12
+ #
13
+
14
+ set -euo pipefail
15
+
16
+ # Get the directory where this script lives
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+
19
+ # Delegate to Python implementation
20
+ exec python3 "$SCRIPT_DIR/question_reflector_check.py"