@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.
- 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 +91 -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 +89 -47
- package/pennyfarthing-dist/agents/sm.md +171 -558
- 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 +34 -16
- package/pennyfarthing-dist/commands/benchmark.md +19 -1
- package/pennyfarthing-dist/commands/continue-session.md +1 -1
- package/pennyfarthing-dist/commands/solo.md +5 -0
- package/pennyfarthing-dist/commands/theme-maker.md +5 -5
- package/pennyfarthing-dist/commands/work.md +1 -1
- package/pennyfarthing-dist/guides/XML-TAGS.md +179 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +37 -2
- 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/personas/themes/gilligans-island.yaml +2 -2
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
- package/pennyfarthing-dist/scripts/core/check-context.sh +25 -8
- package/pennyfarthing-dist/scripts/core/prime.sh +57 -32
- 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/bell-mode-hook.sh +30 -11
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +393 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +20 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +402 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
- package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +545 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
- 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/deploy.sh +13 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
- 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/ground-truth-judge.py +24 -93
- package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
- package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
- 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"
|