@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,545 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* question-reflector.test.mjs - Tests for question reflector enforcement hook
|
|
4
|
+
*
|
|
5
|
+
* Story: MSSCI-12393
|
|
6
|
+
* TDD Phase: RED
|
|
7
|
+
*
|
|
8
|
+
* Run with: node --test pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, beforeEach, mock } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
|
|
14
|
+
// The module under test
|
|
15
|
+
import {
|
|
16
|
+
detectQuestion,
|
|
17
|
+
hasReflectorMarker,
|
|
18
|
+
shouldSkipEnforcement,
|
|
19
|
+
extractLastAssistantMessage,
|
|
20
|
+
checkQuestionReflector,
|
|
21
|
+
checkAskUserQuestion,
|
|
22
|
+
} from '../question-reflector-check.mjs';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Test Fixtures
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
const MARKERS = {
|
|
29
|
+
yesno: '<!-- CYCLIST:QUESTION:yesno -->',
|
|
30
|
+
open: '<!-- CYCLIST:QUESTION:open -->',
|
|
31
|
+
choices: '<!-- CYCLIST:CHOICES:option1,option2,option3 -->',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CONFIG_MANUAL = {
|
|
35
|
+
workflow: { permission_mode: 'manual' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CONFIG_ACCEPT = {
|
|
39
|
+
workflow: { permission_mode: 'accept' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CONFIG_TURBO_LEGACY = {
|
|
43
|
+
workflow: { permission_mode: 'turbo' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CONFIG_RELAY_ON = {
|
|
47
|
+
workflow: { permission_mode: 'accept', relay_mode: true },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const CONFIG_RELAY_OFF = {
|
|
51
|
+
workflow: { permission_mode: 'accept', relay_mode: false },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Relay/Turbo Mode Bypass Tests
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
describe('shouldSkipEnforcement', () => {
|
|
59
|
+
it('should skip enforcement when permission_mode is turbo (legacy)', () => {
|
|
60
|
+
const result = shouldSkipEnforcement(CONFIG_TURBO_LEGACY);
|
|
61
|
+
assert.strictEqual(result, true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should skip enforcement when relay_mode is true', () => {
|
|
65
|
+
const result = shouldSkipEnforcement(CONFIG_RELAY_ON);
|
|
66
|
+
assert.strictEqual(result, true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should NOT skip enforcement when relay_mode is false', () => {
|
|
70
|
+
const result = shouldSkipEnforcement(CONFIG_RELAY_OFF);
|
|
71
|
+
assert.strictEqual(result, false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should NOT skip enforcement in manual mode without relay', () => {
|
|
75
|
+
const result = shouldSkipEnforcement(CONFIG_MANUAL);
|
|
76
|
+
assert.strictEqual(result, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should NOT skip enforcement in accept mode without relay', () => {
|
|
80
|
+
const result = shouldSkipEnforcement(CONFIG_ACCEPT);
|
|
81
|
+
assert.strictEqual(result, false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Question Detection Tests - Direct Questions
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
describe('detectQuestion - direct questions', () => {
|
|
90
|
+
it('should detect question mark at end of message', () => {
|
|
91
|
+
const msg = 'What would you like me to do?';
|
|
92
|
+
const result = detectQuestion(msg);
|
|
93
|
+
assert.strictEqual(result.detected, true);
|
|
94
|
+
assert.strictEqual(result.type, 'direct');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should detect question mark mid-message followed by new sentence', () => {
|
|
98
|
+
const msg = 'What do you need? I can help with several things.';
|
|
99
|
+
const result = detectQuestion(msg);
|
|
100
|
+
assert.strictEqual(result.detected, true);
|
|
101
|
+
assert.strictEqual(result.type, 'direct');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should detect question mark followed by newline and more content', () => {
|
|
105
|
+
const msg = 'What do you need?\n\nHere are your options:';
|
|
106
|
+
const result = detectQuestion(msg);
|
|
107
|
+
assert.strictEqual(result.detected, true);
|
|
108
|
+
assert.strictEqual(result.type, 'direct');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should detect multiple questions in message', () => {
|
|
112
|
+
const msg = 'Should I proceed? Or would you prefer a different approach?';
|
|
113
|
+
const result = detectQuestion(msg);
|
|
114
|
+
assert.strictEqual(result.detected, true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Question Detection Tests - Implicit Questions
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
describe('detectQuestion - implicit questions', () => {
|
|
123
|
+
it('should detect "would you like"', () => {
|
|
124
|
+
const msg = 'I can fix this. Would you like me to proceed.';
|
|
125
|
+
const result = detectQuestion(msg);
|
|
126
|
+
assert.strictEqual(result.detected, true);
|
|
127
|
+
assert.strictEqual(result.type, 'implicit');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should detect "should I"', () => {
|
|
131
|
+
const msg = 'The tests are passing. Should I commit the changes.';
|
|
132
|
+
const result = detectQuestion(msg);
|
|
133
|
+
assert.strictEqual(result.detected, true);
|
|
134
|
+
assert.strictEqual(result.type, 'implicit');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should detect "do you want"', () => {
|
|
138
|
+
const msg = 'Do you want me to run the full test suite.';
|
|
139
|
+
const result = detectQuestion(msg);
|
|
140
|
+
assert.strictEqual(result.detected, true);
|
|
141
|
+
assert.strictEqual(result.type, 'implicit');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should detect "let me know if"', () => {
|
|
145
|
+
const msg = 'I made the changes. Let me know if you need anything else.';
|
|
146
|
+
const result = detectQuestion(msg);
|
|
147
|
+
assert.strictEqual(result.detected, true);
|
|
148
|
+
assert.strictEqual(result.type, 'implicit');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should detect "what do you think"', () => {
|
|
152
|
+
const msg = 'Here is my proposed solution. What do you think.';
|
|
153
|
+
const result = detectQuestion(msg);
|
|
154
|
+
assert.strictEqual(result.detected, true);
|
|
155
|
+
assert.strictEqual(result.type, 'implicit');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should detect "your preference"', () => {
|
|
159
|
+
const msg = 'Both approaches work. Your preference on which to use.';
|
|
160
|
+
const result = detectQuestion(msg);
|
|
161
|
+
assert.strictEqual(result.detected, true);
|
|
162
|
+
assert.strictEqual(result.type, 'implicit');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should detect "ready to proceed"', () => {
|
|
166
|
+
const msg = 'Everything is set up. Ready to proceed with the deployment.';
|
|
167
|
+
const result = detectQuestion(msg);
|
|
168
|
+
assert.strictEqual(result.detected, true);
|
|
169
|
+
assert.strictEqual(result.type, 'implicit');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Question Detection Tests - Choice Offerings
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
describe('detectQuestion - choice offerings', () => {
|
|
178
|
+
it('should detect "option A" style choices', () => {
|
|
179
|
+
const msg = 'We have two paths: Option A uses Redis, Option B uses memory.';
|
|
180
|
+
const result = detectQuestion(msg);
|
|
181
|
+
assert.strictEqual(result.detected, true);
|
|
182
|
+
assert.strictEqual(result.type, 'choices');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should detect "we could either"', () => {
|
|
186
|
+
const msg = 'We could either refactor now or defer to next sprint.';
|
|
187
|
+
const result = detectQuestion(msg);
|
|
188
|
+
assert.strictEqual(result.detected, true);
|
|
189
|
+
assert.strictEqual(result.type, 'choices');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should detect "alternatively"', () => {
|
|
193
|
+
const msg = 'I can add it inline. Alternatively, we create a helper function.';
|
|
194
|
+
const result = detectQuestion(msg);
|
|
195
|
+
assert.strictEqual(result.detected, true);
|
|
196
|
+
assert.strictEqual(result.type, 'choices');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should detect "or would you prefer"', () => {
|
|
200
|
+
const msg = 'I can use async/await, or would you prefer callbacks.';
|
|
201
|
+
const result = detectQuestion(msg);
|
|
202
|
+
assert.strictEqual(result.detected, true);
|
|
203
|
+
assert.strictEqual(result.type, 'choices');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should detect "choose between"', () => {
|
|
207
|
+
const msg = 'You can choose between TypeScript or JavaScript.';
|
|
208
|
+
const result = detectQuestion(msg);
|
|
209
|
+
assert.strictEqual(result.detected, true);
|
|
210
|
+
assert.strictEqual(result.type, 'choices');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// Marker Detection Tests
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
describe('hasReflectorMarker', () => {
|
|
219
|
+
it('should detect QUESTION:yesno marker', () => {
|
|
220
|
+
const msg = `${MARKERS.yesno}\nShould I proceed?`;
|
|
221
|
+
const result = hasReflectorMarker(msg);
|
|
222
|
+
assert.strictEqual(result, true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should detect QUESTION:open marker', () => {
|
|
226
|
+
const msg = `${MARKERS.open}\nWhat approach would you prefer?`;
|
|
227
|
+
const result = hasReflectorMarker(msg);
|
|
228
|
+
assert.strictEqual(result, true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should detect CHOICES marker', () => {
|
|
232
|
+
const msg = `${MARKERS.choices}\nOption 1 or Option 2?`;
|
|
233
|
+
const result = hasReflectorMarker(msg);
|
|
234
|
+
assert.strictEqual(result, true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should detect marker with extra whitespace', () => {
|
|
238
|
+
const msg = '<!-- CYCLIST:QUESTION:yesno -->\nShould I proceed?';
|
|
239
|
+
const result = hasReflectorMarker(msg);
|
|
240
|
+
assert.strictEqual(result, true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return false when no marker present', () => {
|
|
244
|
+
const msg = 'Should I proceed?';
|
|
245
|
+
const result = hasReflectorMarker(msg);
|
|
246
|
+
assert.strictEqual(result, false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// False Positive Prevention Tests - Code Blocks
|
|
252
|
+
// =============================================================================
|
|
253
|
+
|
|
254
|
+
describe('detectQuestion - code block immunity', () => {
|
|
255
|
+
it('should NOT detect questions inside fenced code blocks', () => {
|
|
256
|
+
const msg = `Here is the code:
|
|
257
|
+
\`\`\`javascript
|
|
258
|
+
// What should this function return?
|
|
259
|
+
function test() { return true; }
|
|
260
|
+
\`\`\`
|
|
261
|
+
The implementation is complete.`;
|
|
262
|
+
const result = detectQuestion(msg);
|
|
263
|
+
assert.strictEqual(result.detected, false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should NOT detect questions inside inline code', () => {
|
|
267
|
+
const msg = 'The function `shouldIProceed()` returns a boolean.';
|
|
268
|
+
const result = detectQuestion(msg);
|
|
269
|
+
assert.strictEqual(result.detected, false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should detect questions OUTSIDE code blocks', () => {
|
|
273
|
+
const msg = `Here is the code:
|
|
274
|
+
\`\`\`javascript
|
|
275
|
+
function test() { return true; }
|
|
276
|
+
\`\`\`
|
|
277
|
+
What do you think?`;
|
|
278
|
+
const result = detectQuestion(msg);
|
|
279
|
+
assert.strictEqual(result.detected, true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle multiple code blocks correctly', () => {
|
|
283
|
+
const msg = `First block:
|
|
284
|
+
\`\`\`
|
|
285
|
+
code here?
|
|
286
|
+
\`\`\`
|
|
287
|
+
Second block:
|
|
288
|
+
\`\`\`
|
|
289
|
+
more code?
|
|
290
|
+
\`\`\`
|
|
291
|
+
Done.`;
|
|
292
|
+
const result = detectQuestion(msg);
|
|
293
|
+
assert.strictEqual(result.detected, false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// False Positive Prevention Tests - Rhetorical Questions
|
|
299
|
+
// =============================================================================
|
|
300
|
+
|
|
301
|
+
describe('detectQuestion - rhetorical question immunity', () => {
|
|
302
|
+
it('should NOT detect "the question was"', () => {
|
|
303
|
+
const msg = 'The question was whether to use async or sync. I chose async.';
|
|
304
|
+
const result = detectQuestion(msg);
|
|
305
|
+
assert.strictEqual(result.detected, false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should NOT detect "the question is"', () => {
|
|
309
|
+
const msg = 'The question is rhetorical. Here is the answer.';
|
|
310
|
+
const result = detectQuestion(msg);
|
|
311
|
+
assert.strictEqual(result.detected, false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should NOT detect "asked whether"', () => {
|
|
315
|
+
const msg = 'You asked whether this was possible. Yes, it is.';
|
|
316
|
+
const result = detectQuestion(msg);
|
|
317
|
+
assert.strictEqual(result.detected, false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should NOT detect "wondering if"', () => {
|
|
321
|
+
const msg = 'I was wondering if this approach would work. It does.';
|
|
322
|
+
const result = detectQuestion(msg);
|
|
323
|
+
assert.strictEqual(result.detected, false);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// No Question Tests
|
|
329
|
+
// =============================================================================
|
|
330
|
+
|
|
331
|
+
describe('detectQuestion - no question present', () => {
|
|
332
|
+
it('should NOT detect statements without questions', () => {
|
|
333
|
+
const msg = 'I have completed the implementation. All tests pass.';
|
|
334
|
+
const result = detectQuestion(msg);
|
|
335
|
+
assert.strictEqual(result.detected, false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should NOT detect exclamations', () => {
|
|
339
|
+
const msg = 'Done! The feature is ready.';
|
|
340
|
+
const result = detectQuestion(msg);
|
|
341
|
+
assert.strictEqual(result.detected, false);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should NOT detect periods only', () => {
|
|
345
|
+
const msg = 'Task complete. Moving on to the next item.';
|
|
346
|
+
const result = detectQuestion(msg);
|
|
347
|
+
assert.strictEqual(result.detected, false);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// Transcript Extraction Tests
|
|
353
|
+
// =============================================================================
|
|
354
|
+
|
|
355
|
+
describe('extractLastAssistantMessage', () => {
|
|
356
|
+
it('should extract last assistant message from JSONL', () => {
|
|
357
|
+
const transcript = [
|
|
358
|
+
{ role: 'user', content: 'Hello' },
|
|
359
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
360
|
+
{ role: 'user', content: 'Help me' },
|
|
361
|
+
{ role: 'assistant', content: 'What do you need?' },
|
|
362
|
+
];
|
|
363
|
+
const result = extractLastAssistantMessage(transcript);
|
|
364
|
+
assert.strictEqual(result, 'What do you need?');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should handle content as array of text blocks', () => {
|
|
368
|
+
const transcript = [
|
|
369
|
+
{
|
|
370
|
+
role: 'assistant',
|
|
371
|
+
content: [
|
|
372
|
+
{ type: 'text', text: 'First part. ' },
|
|
373
|
+
{ type: 'text', text: 'What do you think?' },
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
const result = extractLastAssistantMessage(transcript);
|
|
378
|
+
assert.strictEqual(result, 'First part. What do you think?');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should return empty string when no assistant message', () => {
|
|
382
|
+
const transcript = [{ role: 'user', content: 'Hello' }];
|
|
383
|
+
const result = extractLastAssistantMessage(transcript);
|
|
384
|
+
assert.strictEqual(result, '');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should skip tool_use content blocks', () => {
|
|
388
|
+
const transcript = [
|
|
389
|
+
{
|
|
390
|
+
role: 'assistant',
|
|
391
|
+
content: [
|
|
392
|
+
{ type: 'text', text: 'Let me check.' },
|
|
393
|
+
{ type: 'tool_use', id: '123', name: 'Read' },
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
const result = extractLastAssistantMessage(transcript);
|
|
398
|
+
assert.strictEqual(result, 'Let me check.');
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// =============================================================================
|
|
403
|
+
// Integration Tests - Full Hook Logic
|
|
404
|
+
// =============================================================================
|
|
405
|
+
|
|
406
|
+
describe('checkQuestionReflector - integration', () => {
|
|
407
|
+
it('should return ok:true when no question detected', () => {
|
|
408
|
+
const input = {
|
|
409
|
+
transcript_path: '/tmp/test.jsonl',
|
|
410
|
+
stop_hook_active: false,
|
|
411
|
+
};
|
|
412
|
+
const config = CONFIG_MANUAL;
|
|
413
|
+
const lastMessage = 'Task complete. All done.';
|
|
414
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
415
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should return ok:true when question has marker', () => {
|
|
419
|
+
const input = {
|
|
420
|
+
transcript_path: '/tmp/test.jsonl',
|
|
421
|
+
stop_hook_active: false,
|
|
422
|
+
};
|
|
423
|
+
const config = CONFIG_MANUAL;
|
|
424
|
+
const lastMessage = `${MARKERS.yesno}\nShould I proceed?`;
|
|
425
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
426
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should return ok:true when in turbo mode (legacy)', () => {
|
|
430
|
+
const input = {
|
|
431
|
+
transcript_path: '/tmp/test.jsonl',
|
|
432
|
+
stop_hook_active: false,
|
|
433
|
+
};
|
|
434
|
+
const config = CONFIG_TURBO_LEGACY;
|
|
435
|
+
const lastMessage = 'What do you need?'; // No marker, but turbo mode
|
|
436
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
437
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should return ok:true when relay_mode is true', () => {
|
|
441
|
+
const input = {
|
|
442
|
+
transcript_path: '/tmp/test.jsonl',
|
|
443
|
+
stop_hook_active: false,
|
|
444
|
+
};
|
|
445
|
+
const config = CONFIG_RELAY_ON;
|
|
446
|
+
const lastMessage = 'What do you need?'; // No marker, but relay on
|
|
447
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
448
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should return ok:true when stop_hook_active is true', () => {
|
|
452
|
+
const input = {
|
|
453
|
+
transcript_path: '/tmp/test.jsonl',
|
|
454
|
+
stop_hook_active: true, // Prevent infinite loops
|
|
455
|
+
};
|
|
456
|
+
const config = CONFIG_MANUAL;
|
|
457
|
+
const lastMessage = 'What do you need?';
|
|
458
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
459
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should block when question detected without marker', () => {
|
|
463
|
+
const input = {
|
|
464
|
+
transcript_path: '/tmp/test.jsonl',
|
|
465
|
+
stop_hook_active: false,
|
|
466
|
+
};
|
|
467
|
+
const config = CONFIG_MANUAL;
|
|
468
|
+
const lastMessage = 'What do you need?';
|
|
469
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
470
|
+
assert.strictEqual(result.decision, 'block');
|
|
471
|
+
assert.ok(result.reason.includes('CYCLIST:QUESTION'));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should include appropriate marker hint for direct questions', () => {
|
|
475
|
+
const input = {
|
|
476
|
+
transcript_path: '/tmp/test.jsonl',
|
|
477
|
+
stop_hook_active: false,
|
|
478
|
+
};
|
|
479
|
+
const config = CONFIG_MANUAL;
|
|
480
|
+
const lastMessage = 'Should I proceed?';
|
|
481
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
482
|
+
assert.strictEqual(result.decision, 'block');
|
|
483
|
+
assert.ok(result.reason.includes('CYCLIST:QUESTION:yesno'));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should include appropriate marker hint for choices', () => {
|
|
487
|
+
const input = {
|
|
488
|
+
transcript_path: '/tmp/test.jsonl',
|
|
489
|
+
stop_hook_active: false,
|
|
490
|
+
};
|
|
491
|
+
const config = CONFIG_MANUAL;
|
|
492
|
+
const lastMessage = 'We could either use Option A or Option B.';
|
|
493
|
+
const result = checkQuestionReflector(input, config, lastMessage);
|
|
494
|
+
assert.strictEqual(result.decision, 'block');
|
|
495
|
+
assert.ok(result.reason.includes('CYCLIST:CHOICES'));
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// AskUserQuestion PreToolUse Hook Tests
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
describe('checkAskUserQuestion - PreToolUse hook', () => {
|
|
504
|
+
it('should return ok:true when relay_mode is true', () => {
|
|
505
|
+
const input = {
|
|
506
|
+
tool_name: 'AskUserQuestion',
|
|
507
|
+
tool_input: { questions: [{ question: 'Which option?' }] },
|
|
508
|
+
};
|
|
509
|
+
const config = CONFIG_RELAY_ON;
|
|
510
|
+
const result = checkAskUserQuestion(input, config);
|
|
511
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should return ok:true when in turbo mode (legacy)', () => {
|
|
515
|
+
const input = {
|
|
516
|
+
tool_name: 'AskUserQuestion',
|
|
517
|
+
tool_input: { questions: [{ question: 'Which option?' }] },
|
|
518
|
+
};
|
|
519
|
+
const config = CONFIG_TURBO_LEGACY;
|
|
520
|
+
const result = checkAskUserQuestion(input, config);
|
|
521
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should block AskUserQuestion without prior marker in transcript', () => {
|
|
525
|
+
const input = {
|
|
526
|
+
tool_name: 'AskUserQuestion',
|
|
527
|
+
tool_input: { questions: [{ question: 'Which option?' }] },
|
|
528
|
+
};
|
|
529
|
+
const config = CONFIG_MANUAL;
|
|
530
|
+
const transcriptWithoutMarker = 'Here are some choices.';
|
|
531
|
+
const result = checkAskUserQuestion(input, config, transcriptWithoutMarker);
|
|
532
|
+
assert.strictEqual(result.decision, 'block');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should allow AskUserQuestion when marker present in recent output', () => {
|
|
536
|
+
const input = {
|
|
537
|
+
tool_name: 'AskUserQuestion',
|
|
538
|
+
tool_input: { questions: [{ question: 'Which option?' }] },
|
|
539
|
+
};
|
|
540
|
+
const config = CONFIG_MANUAL;
|
|
541
|
+
const transcriptWithMarker = `${MARKERS.choices}\nHere are some choices.`;
|
|
542
|
+
const result = checkAskUserQuestion(input, config, transcriptWithMarker);
|
|
543
|
+
assert.deepStrictEqual(result, { ok: true });
|
|
544
|
+
});
|
|
545
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Welcome Hook: Display a friendly welcome message on session start
|
|
3
|
+
#
|
|
4
|
+
# For CLI: Displays ASCII art of a penny-farthing bicycle
|
|
5
|
+
# For Cyclist: Sends WebSocket message to display logo and welcome
|
|
6
|
+
#
|
|
7
|
+
# Called by Claude Code SessionStart hook
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# Read and discard stdin (required by hook protocol)
|
|
12
|
+
cat > /dev/null
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
|
|
16
|
+
|
|
17
|
+
# Once-per-session guard: only show welcome on first invocation
|
|
18
|
+
# Use session ID from environment or generate a unique one
|
|
19
|
+
SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
|
|
20
|
+
WELCOME_LOCK="$PROJECT_ROOT/.session/.welcome-shown-$SESSION_ID"
|
|
21
|
+
|
|
22
|
+
# Ensure .session directory exists
|
|
23
|
+
mkdir -p "$PROJECT_ROOT/.session"
|
|
24
|
+
|
|
25
|
+
# Check if welcome was already shown for this session
|
|
26
|
+
if [[ -f "$WELCOME_LOCK" ]]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Mark welcome as shown for this session
|
|
31
|
+
touch "$WELCOME_LOCK"
|
|
32
|
+
|
|
33
|
+
# Check if running in Cyclist (port file exists)
|
|
34
|
+
PORT_FILE="$PROJECT_ROOT/.cyclist-port"
|
|
35
|
+
IN_CYCLIST=false
|
|
36
|
+
if [[ -f "$PORT_FILE" ]]; then
|
|
37
|
+
CYCLIST_PORT=$(cat "$PORT_FILE" 2>/dev/null)
|
|
38
|
+
if [[ "$CYCLIST_PORT" =~ ^[0-9]+$ ]]; then
|
|
39
|
+
IN_CYCLIST=true
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Get project name from package.json or directory name
|
|
44
|
+
PROJECT_NAME=""
|
|
45
|
+
if [[ -f "$PROJECT_ROOT/package.json" ]]; then
|
|
46
|
+
PROJECT_NAME=$(jq -r '.name // empty' "$PROJECT_ROOT/package.json" 2>/dev/null || echo "")
|
|
47
|
+
fi
|
|
48
|
+
if [[ -z "$PROJECT_NAME" ]]; then
|
|
49
|
+
PROJECT_NAME=$(basename "$PROJECT_ROOT")
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Get current theme from config
|
|
53
|
+
THEME=""
|
|
54
|
+
if [[ -f "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" ]]; then
|
|
55
|
+
THEME=$(grep -E '^theme:' "$PROJECT_ROOT/.pennyfarthing/config.local.yaml" 2>/dev/null | sed 's/theme:[[:space:]]*//' | tr -d '"' || echo "")
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ "$IN_CYCLIST" == "true" ]]; then
|
|
59
|
+
# Send welcome message via WebSocket API for Cyclist to display
|
|
60
|
+
# Cyclist will show the pennyfarthing logo image and welcome text
|
|
61
|
+
curl -s -X POST "http://localhost:$CYCLIST_PORT/api/welcome" \
|
|
62
|
+
-H "Content-Type: application/json" \
|
|
63
|
+
-d "{\"project\": \"$PROJECT_NAME\", \"theme\": \"$THEME\"}" \
|
|
64
|
+
>/dev/null 2>&1 || true
|
|
65
|
+
else
|
|
66
|
+
# CLI mode: Display ASCII art welcome
|
|
67
|
+
cat << 'EOF'
|
|
68
|
+
|
|
69
|
+
___
|
|
70
|
+
/ \
|
|
71
|
+
| | Welcome to
|
|
72
|
+
| | ╔═══════════════════════════════════╗
|
|
73
|
+
\___/ ║ ╔═╗╔═╗╔╗╔╔╗╔╦═╗╔═╗╔═╗╔═╗╦╔═╗ ║
|
|
74
|
+
║ ║ ╠═╝║╣ ║║║║║║ ╠╣ ╠═╣╠╦╝ ║ ╠═╣ ║
|
|
75
|
+
║ ║ ╩ ╚═╝╝╚╝╝╚╝╩ ╩ ╩╩╚═ ╩ ╩ ╩ ║
|
|
76
|
+
╔═╩═╗ ╚═══════════════════════════════════╝
|
|
77
|
+
/ \
|
|
78
|
+
│ O │ Agent-powered development with style
|
|
79
|
+
\ /
|
|
80
|
+
╚═══╝
|
|
81
|
+
|
|
82
|
+
EOF
|
|
83
|
+
|
|
84
|
+
# Add project-specific info
|
|
85
|
+
if [[ -n "$PROJECT_NAME" ]]; then
|
|
86
|
+
echo " Project: $PROJECT_NAME"
|
|
87
|
+
fi
|
|
88
|
+
if [[ -n "$THEME" ]]; then
|
|
89
|
+
echo " Theme: $THEME"
|
|
90
|
+
fi
|
|
91
|
+
echo ""
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
exit 0
|