@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,502 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ check.py - Quality gate runner for pre-handoff verification
4
+
5
+ Story 21-1: /check command with dev-handoff integration
6
+
7
+ Usage: python check.py [OPTIONS]
8
+
9
+ Options:
10
+ --skip-check Skip all checks (emergency bypass)
11
+ --tests-only Run only tests, skip lint and typecheck
12
+ --filter PATTERN Filter tests by pattern (passed to test runner)
13
+ --repo REPO Run checks in specific repo subdirectory
14
+ --no-lint Skip lint check
15
+ --no-typecheck Skip type check
16
+ --fast Skip slow packages (cyclist/Electron) for rapid iteration
17
+
18
+ Runs lint, type check, and tests concurrently. Reports pass/fail status.
19
+ Returns exit code 0 on all passing, non-zero on any failure.
20
+ """
21
+
22
+ import argparse
23
+ import asyncio
24
+ import json
25
+ import os
26
+ import shutil
27
+ import sys
28
+ from dataclasses import dataclass, field
29
+ from enum import Enum
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+
34
+ class CheckStatus(Enum):
35
+ PASS = "pass"
36
+ FAIL = "fail"
37
+ SKIP = "skip"
38
+
39
+
40
+ @dataclass
41
+ class CheckResult:
42
+ name: str
43
+ status: CheckStatus
44
+ command: Optional[str] = None
45
+ message: Optional[str] = None
46
+
47
+
48
+ @dataclass
49
+ class CheckStats:
50
+ run: int = 0
51
+ passed: int = 0
52
+ failed: int = 0
53
+ skipped: int = 0
54
+
55
+ def record(self, status: CheckStatus) -> None:
56
+ if status == CheckStatus.PASS:
57
+ self.run += 1
58
+ self.passed += 1
59
+ elif status == CheckStatus.FAIL:
60
+ self.run += 1
61
+ self.failed += 1
62
+ elif status == CheckStatus.SKIP:
63
+ self.skipped += 1
64
+
65
+
66
+ @dataclass
67
+ class RepoConfig:
68
+ path: str = ""
69
+ lint_cmd: str = ""
70
+ test_cmd: str = ""
71
+ test_filter_flag: str = ""
72
+ language: str = ""
73
+
74
+
75
+ @dataclass
76
+ class CheckContext:
77
+ project_root: Path
78
+ working_dir: Path
79
+ target_repo: str = ""
80
+ test_filter: str = ""
81
+ no_lint: bool = False
82
+ no_typecheck: bool = False
83
+ fast_mode: bool = False
84
+ repo_config: RepoConfig = field(default_factory=RepoConfig)
85
+ stats: CheckStats = field(default_factory=CheckStats)
86
+
87
+
88
+ # ANSI colors
89
+ class Colors:
90
+ def __init__(self, enabled: bool = True):
91
+ if enabled and sys.stdout.isatty():
92
+ self.RED = "\033[0;31m"
93
+ self.GREEN = "\033[0;32m"
94
+ self.YELLOW = "\033[0;33m"
95
+ self.CYAN = "\033[0;36m"
96
+ self.NC = "\033[0m"
97
+ else:
98
+ self.RED = ""
99
+ self.GREEN = ""
100
+ self.YELLOW = ""
101
+ self.CYAN = ""
102
+ self.NC = ""
103
+
104
+
105
+ COLORS = Colors()
106
+
107
+
108
+ def print_pass(name: str, command: Optional[str] = None) -> None:
109
+ suffix = f" ({command})" if command else ""
110
+ print(f" {COLORS.GREEN}[PASS]{COLORS.NC} {name}{suffix}")
111
+
112
+
113
+ def print_fail(name: str, command: Optional[str] = None) -> None:
114
+ suffix = f" ({command})" if command else ""
115
+ print(f" {COLORS.RED}[FAIL]{COLORS.NC} {name}{suffix}")
116
+
117
+
118
+ def print_skip(name: str, reason: str = "") -> None:
119
+ suffix = f" - {reason}" if reason else ""
120
+ print(f" {COLORS.YELLOW}[SKIP]{COLORS.NC} {name}{suffix}")
121
+
122
+
123
+ def print_section(title: str) -> None:
124
+ print()
125
+ print(f"{COLORS.CYAN}{title}{COLORS.NC}")
126
+ print("=" * 40)
127
+
128
+
129
+ def find_project_root() -> Path:
130
+ """Find project root by looking for .claude directory."""
131
+ current = Path.cwd()
132
+ while current != current.parent:
133
+ if (current / ".claude").is_dir():
134
+ return current
135
+ current = current.parent
136
+ return Path.cwd()
137
+
138
+
139
+ def detect_project_type(working_dir: Path) -> str:
140
+ """Detect project type based on files present."""
141
+ if (working_dir / "package.json").exists():
142
+ return "node"
143
+
144
+ # Check for Go files
145
+ go_files = list(working_dir.glob("*.go"))
146
+ if go_files:
147
+ return "go"
148
+
149
+ if (working_dir / "go.mod").exists():
150
+ return "go"
151
+
152
+ return "unknown"
153
+
154
+
155
+ def has_just_recipe(recipe: str, working_dir: Path) -> bool:
156
+ """Check if justfile has a specific recipe."""
157
+ if not (working_dir / "justfile").exists():
158
+ return False
159
+ if not shutil.which("just"):
160
+ return False
161
+
162
+ try:
163
+ import subprocess
164
+ result = subprocess.run(
165
+ ["just", "--list"],
166
+ cwd=working_dir,
167
+ capture_output=True,
168
+ text=True,
169
+ timeout=5
170
+ )
171
+ return any(line.startswith(f"{recipe} ") or line.strip() == recipe
172
+ for line in result.stdout.splitlines())
173
+ except Exception:
174
+ return False
175
+
176
+
177
+ def has_npm_script(script: str, working_dir: Path) -> bool:
178
+ """Check if package.json has a specific script."""
179
+ package_json = working_dir / "package.json"
180
+ if not package_json.exists():
181
+ return False
182
+
183
+ try:
184
+ with open(package_json) as f:
185
+ data = json.load(f)
186
+ return script in data.get("scripts", {})
187
+ except Exception:
188
+ return False
189
+
190
+
191
+ async def run_command(cmd: list[str], cwd: Path) -> tuple[bool, str]:
192
+ """Run a command asynchronously and return (success, output)."""
193
+ try:
194
+ proc = await asyncio.create_subprocess_exec(
195
+ *cmd,
196
+ cwd=cwd,
197
+ stdout=asyncio.subprocess.PIPE,
198
+ stderr=asyncio.subprocess.PIPE
199
+ )
200
+ stdout, stderr = await proc.communicate()
201
+ success = proc.returncode == 0
202
+ output = stdout.decode() + stderr.decode()
203
+ return success, output
204
+ except Exception as e:
205
+ return False, str(e)
206
+
207
+
208
+ async def run_shell_command(cmd: str, cwd: Path) -> tuple[bool, str]:
209
+ """Run a shell command asynchronously."""
210
+ try:
211
+ proc = await asyncio.create_subprocess_shell(
212
+ cmd,
213
+ cwd=cwd,
214
+ stdout=asyncio.subprocess.PIPE,
215
+ stderr=asyncio.subprocess.PIPE
216
+ )
217
+ stdout, stderr = await proc.communicate()
218
+ success = proc.returncode == 0
219
+ output = stdout.decode() + stderr.decode()
220
+ return success, output
221
+ except Exception as e:
222
+ return False, str(e)
223
+
224
+
225
+ async def run_lint(ctx: CheckContext) -> CheckResult:
226
+ """Run lint check."""
227
+ if ctx.no_lint:
228
+ return CheckResult("Lint", CheckStatus.SKIP, message="skipped by --no-lint or --tests-only")
229
+
230
+ working_dir = ctx.working_dir
231
+
232
+ # Use repo config lint command if available
233
+ if ctx.repo_config.lint_cmd:
234
+ success, _ = await run_shell_command(ctx.repo_config.lint_cmd, working_dir)
235
+ return CheckResult(
236
+ "Lint",
237
+ CheckStatus.PASS if success else CheckStatus.FAIL,
238
+ command=ctx.repo_config.lint_cmd
239
+ )
240
+
241
+ # Prefer justfile recipes
242
+ if has_just_recipe("lint", working_dir):
243
+ success, _ = await run_command(["just", "lint"], working_dir)
244
+ return CheckResult("Lint", CheckStatus.PASS if success else CheckStatus.FAIL, command="just lint")
245
+
246
+ project_type = detect_project_type(working_dir)
247
+
248
+ if project_type == "node" and has_npm_script("lint", working_dir):
249
+ success, _ = await run_command(["npm", "run", "lint"], working_dir)
250
+ return CheckResult("Lint", CheckStatus.PASS if success else CheckStatus.FAIL, command="npm run lint")
251
+
252
+ if project_type == "go" and shutil.which("golangci-lint"):
253
+ success, _ = await run_command(["golangci-lint", "run"], working_dir)
254
+ return CheckResult("Lint", CheckStatus.PASS if success else CheckStatus.FAIL, command="golangci-lint")
255
+
256
+ eslint_path = working_dir / "node_modules" / ".bin" / "eslint"
257
+ if eslint_path.exists():
258
+ success, _ = await run_command([str(eslint_path), "."], working_dir)
259
+ return CheckResult("Lint", CheckStatus.PASS if success else CheckStatus.FAIL, command="eslint")
260
+
261
+ return CheckResult("Lint", CheckStatus.SKIP, message="no lint command configured")
262
+
263
+
264
+ async def run_typecheck(ctx: CheckContext) -> CheckResult:
265
+ """Run type check."""
266
+ if ctx.no_typecheck:
267
+ return CheckResult("Type Check", CheckStatus.SKIP, message="skipped by --no-typecheck or --tests-only")
268
+
269
+ working_dir = ctx.working_dir
270
+
271
+ # Prefer justfile recipes
272
+ if has_just_recipe("typecheck", working_dir):
273
+ success, _ = await run_command(["just", "typecheck"], working_dir)
274
+ return CheckResult("Type Check", CheckStatus.PASS if success else CheckStatus.FAIL, command="just typecheck")
275
+
276
+ has_tsconfig = (working_dir / "tsconfig.json").exists()
277
+
278
+ if has_tsconfig and has_npm_script("typecheck", working_dir):
279
+ success, _ = await run_command(["npm", "run", "typecheck"], working_dir)
280
+ return CheckResult("Type Check", CheckStatus.PASS if success else CheckStatus.FAIL, command="npm run typecheck")
281
+
282
+ if has_tsconfig and shutil.which("tsc"):
283
+ success, _ = await run_command(["tsc", "--noEmit"], working_dir)
284
+ return CheckResult("Type Check", CheckStatus.PASS if success else CheckStatus.FAIL, command="tsc --noEmit")
285
+
286
+ tsc_path = working_dir / "node_modules" / ".bin" / "tsc"
287
+ if has_tsconfig and tsc_path.exists():
288
+ success, _ = await run_command([str(tsc_path), "--noEmit"], working_dir)
289
+ return CheckResult("Type Check", CheckStatus.PASS if success else CheckStatus.FAIL, command="tsc --noEmit")
290
+
291
+ if has_tsconfig:
292
+ return CheckResult("Type Check", CheckStatus.SKIP, message="TypeScript found but no typecheck command")
293
+
294
+ return CheckResult("Type Check", CheckStatus.SKIP, message="not a TypeScript project")
295
+
296
+
297
+ async def run_tests(ctx: CheckContext) -> CheckResult:
298
+ """Run tests."""
299
+ working_dir = ctx.working_dir
300
+ test_filter = ctx.test_filter
301
+
302
+ if test_filter:
303
+ print(f" Filter: {test_filter}")
304
+
305
+ # Use repo config test command if available
306
+ if ctx.repo_config.test_cmd:
307
+ cmd = ctx.repo_config.test_cmd
308
+ if test_filter and ctx.repo_config.test_filter_flag:
309
+ cmd = f'{cmd} {ctx.repo_config.test_filter_flag} "{test_filter}"'
310
+ success, _ = await run_shell_command(cmd, working_dir)
311
+ label = ctx.repo_config.test_cmd
312
+ if test_filter:
313
+ label += f" {ctx.repo_config.test_filter_flag} {test_filter}"
314
+ return CheckResult("Tests", CheckStatus.PASS if success else CheckStatus.FAIL, command=label)
315
+
316
+ # Prefer justfile recipes
317
+ if has_just_recipe("test", working_dir):
318
+ cmd = ["just", "test"]
319
+ label = "just test"
320
+ if test_filter:
321
+ cmd.append(test_filter)
322
+ label += f" {test_filter}"
323
+ success, _ = await run_command(cmd, working_dir)
324
+ return CheckResult("Tests", CheckStatus.PASS if success else CheckStatus.FAIL, command=label)
325
+
326
+ project_type = detect_project_type(working_dir)
327
+
328
+ if project_type == "node" and has_npm_script("test", working_dir):
329
+ # Check for pnpm workspace (monorepo)
330
+ pnpm_workspace = (working_dir / "pnpm-workspace.yaml").exists()
331
+ has_pnpm = shutil.which("pnpm") is not None
332
+
333
+ if pnpm_workspace and has_pnpm:
334
+ if ctx.fast_mode:
335
+ cmd = "pnpm -r --filter '!@pennyfarthing/cyclist' test"
336
+ label = "pnpm test (fast mode - skipping cyclist)"
337
+ else:
338
+ cmd = "pnpm -r test"
339
+ label = "pnpm test"
340
+ else:
341
+ cmd = "npm test"
342
+ label = "npm test"
343
+
344
+ if test_filter:
345
+ cmd += f' -- -t "{test_filter}"'
346
+ label += f" -t {test_filter}"
347
+
348
+ success, _ = await run_shell_command(cmd, working_dir)
349
+ return CheckResult("Tests", CheckStatus.PASS if success else CheckStatus.FAIL, command=label)
350
+
351
+ if project_type == "go":
352
+ cmd = "go test ./..."
353
+ label = "go test ./..."
354
+ if test_filter:
355
+ cmd = f'go test -run "{test_filter}" ./...'
356
+ label = f"go test -run {test_filter} ./..."
357
+ success, _ = await run_shell_command(cmd, working_dir)
358
+ return CheckResult("Tests", CheckStatus.PASS if success else CheckStatus.FAIL, command=label)
359
+
360
+ jest_path = working_dir / "node_modules" / ".bin" / "jest"
361
+ if jest_path.exists():
362
+ cmd = [str(jest_path)]
363
+ label = "jest"
364
+ if test_filter:
365
+ cmd.extend(["-t", test_filter])
366
+ label += f" -t {test_filter}"
367
+ success, _ = await run_command(cmd, working_dir)
368
+ return CheckResult("Tests", CheckStatus.PASS if success else CheckStatus.FAIL, command=label)
369
+
370
+ return CheckResult("Tests", CheckStatus.SKIP, message="no test command configured")
371
+
372
+
373
+ def print_result(result: CheckResult, stats: CheckStats) -> None:
374
+ """Print a check result and update stats."""
375
+ stats.record(result.status)
376
+
377
+ if result.status == CheckStatus.PASS:
378
+ print_pass(result.name, result.command)
379
+ elif result.status == CheckStatus.FAIL:
380
+ print_fail(result.name, result.command)
381
+ else:
382
+ print_skip(result.name, result.message or "")
383
+
384
+
385
+ def print_summary(stats: CheckStats, project_type: str) -> int:
386
+ """Print summary and return exit code."""
387
+ print_section("Summary")
388
+ print()
389
+ print(f"Checks run: {stats.run}")
390
+ print(f"Checks passed: {COLORS.GREEN}{stats.passed}{COLORS.NC}")
391
+
392
+ if stats.failed > 0:
393
+ print(f"Checks failed: {COLORS.RED}{stats.failed}{COLORS.NC}")
394
+ else:
395
+ print(f"Checks failed: {stats.failed}")
396
+
397
+ if stats.skipped > 0:
398
+ print(f"Checks skipped: {COLORS.YELLOW}{stats.skipped}{COLORS.NC}")
399
+
400
+ print()
401
+
402
+ if stats.failed > 0:
403
+ print(f"{COLORS.RED}FAILED{COLORS.NC} - {stats.failed} check(s) failed")
404
+ print("Fix issues before handoff to Reviewer.")
405
+ return 1
406
+ elif stats.run == 0:
407
+ print(f"{COLORS.YELLOW}WARNING{COLORS.NC} - No checks ran (project type: {project_type})")
408
+ print("Consider adding lint/test scripts to package.json or justfile.")
409
+ return 0
410
+ else:
411
+ print(f"{COLORS.GREEN}PASSED{COLORS.NC} - All checks passed")
412
+ return 0
413
+
414
+
415
+ async def main() -> int:
416
+ parser = argparse.ArgumentParser(
417
+ description="Quality gate runner for pre-handoff verification"
418
+ )
419
+ parser.add_argument("--skip-check", action="store_true",
420
+ help="Skip all checks (emergency bypass)")
421
+ parser.add_argument("--tests-only", action="store_true",
422
+ help="Run only tests, skip lint and typecheck")
423
+ parser.add_argument("--filter", dest="test_filter", default="",
424
+ help="Filter tests by pattern")
425
+ parser.add_argument("--repo", dest="target_repo", default="",
426
+ help="Run checks in specific repo subdirectory")
427
+ parser.add_argument("--no-lint", action="store_true",
428
+ help="Skip lint check")
429
+ parser.add_argument("--no-typecheck", action="store_true",
430
+ help="Skip type check")
431
+ parser.add_argument("--fast", action="store_true",
432
+ help="Skip slow packages for rapid iteration")
433
+
434
+ args = parser.parse_args()
435
+
436
+ # Handle --skip-check
437
+ if args.skip_check:
438
+ print("Quality checks skipped by --skip-check flag")
439
+ print()
440
+ print("WARNING: Skipping checks is for emergencies only.")
441
+ print("Ensure checks pass before merging PR.")
442
+ return 0
443
+
444
+ # Handle --tests-only
445
+ no_lint = args.no_lint or args.tests_only
446
+ no_typecheck = args.no_typecheck or args.tests_only
447
+
448
+ # Find project root
449
+ project_root = find_project_root()
450
+
451
+ # Handle --repo
452
+ if args.target_repo:
453
+ working_dir = project_root / args.target_repo
454
+ if not working_dir.is_dir():
455
+ print(f"Error: Repo directory not found: {working_dir}")
456
+ return 1
457
+ else:
458
+ working_dir = project_root
459
+
460
+ # Create context
461
+ ctx = CheckContext(
462
+ project_root=project_root,
463
+ working_dir=working_dir,
464
+ target_repo=args.target_repo,
465
+ test_filter=args.test_filter,
466
+ no_lint=no_lint,
467
+ no_typecheck=no_typecheck,
468
+ fast_mode=args.fast
469
+ )
470
+
471
+ # Print header
472
+ print()
473
+ print("Quality Gate Check")
474
+ print("==================")
475
+ print(f"Project: {project_root}")
476
+ if args.target_repo:
477
+ print(f"Repo: {args.target_repo}")
478
+ print(f"Working dir: {working_dir}")
479
+ if args.fast:
480
+ print(f"{COLORS.YELLOW}Mode: FAST (skipping slow packages){COLORS.NC}")
481
+
482
+ project_type = detect_project_type(working_dir)
483
+
484
+ # Run lint and typecheck concurrently, then tests
485
+ # (Tests often depend on lint/typecheck passing in CI, but we run all for reporting)
486
+ print_section("Lint")
487
+ lint_result = await run_lint(ctx)
488
+ print_result(lint_result, ctx.stats)
489
+
490
+ print_section("Type Check")
491
+ typecheck_result = await run_typecheck(ctx)
492
+ print_result(typecheck_result, ctx.stats)
493
+
494
+ print_section("Tests")
495
+ test_result = await run_tests(ctx)
496
+ print_result(test_result, ctx.stats)
497
+
498
+ return print_summary(ctx.stats, project_type)
499
+
500
+
501
+ if __name__ == "__main__":
502
+ sys.exit(asyncio.run(main()))
@@ -141,7 +141,7 @@ skills:
141
141
  invocation: /jira assign ISSUE-123
142
142
  anti_patterns:
143
143
  - Don't bypass Jira for sprint tracking
144
- related_skills: [sprint-context, story-management]
144
+ related_skills: [sprint, story]
145
145
  keywords: [atlassian, issues, tickets, backlog]
146
146
 
147
147
  judge:
@@ -195,6 +195,23 @@ skills:
195
195
  related_skills: [changelog]
196
196
  keywords: [flowchart, sequence, er-diagram, gantt, class-diagram]
197
197
 
198
+ otel:
199
+ name: otel
200
+ description: Claude Code OTEL telemetry format documentation for span interception and enrichment
201
+ category: tools
202
+ tags: [telemetry, monitoring, otel]
203
+ version: "1.0.0"
204
+ prerequisites: []
205
+ examples:
206
+ - context: Working with OTEL spans
207
+ invocation: /otel
208
+ - context: Enriching tool telemetry in Cyclist
209
+ invocation: /otel enrichment
210
+ anti_patterns:
211
+ - Don't assume fields exist - verify against this documentation
212
+ related_skills: [cyclist]
213
+ keywords: [opentelemetry, spans, traces, enrichment, correlation]
214
+
198
215
  permissions:
199
216
  name: permissions
200
217
  description: Manage runtime permission grants - list, grant, and revoke tool access
@@ -212,7 +229,7 @@ skills:
212
229
  anti_patterns:
213
230
  - Don't manually edit settings.local.json permissions - use skill
214
231
  - Don't grant overly broad scope patterns
215
- related_skills: [sprint-context]
232
+ related_skills: []
216
233
  keywords: [grants, scopes, tools, runtime, security]
217
234
 
218
235
  persona-benchmark:
@@ -232,23 +249,23 @@ skills:
232
249
  related_skills: [judge, finalize-run, theme]
233
250
  keywords: [benchmark, comparison, personas, testing]
234
251
 
235
- sprint-context:
236
- name: sprint-context
237
- description: Sprint status, backlog, and story management context
252
+ sprint:
253
+ name: sprint
254
+ description: Sprint status, backlog, and story management for Pennyfarthing
238
255
  category: project-management
239
256
  tags: [sprint, status, backlog]
240
257
  version: "1.0.0"
241
258
  prerequisites: []
242
259
  examples:
243
260
  - context: Checking sprint status
244
- invocation: /sprint-context
261
+ invocation: /sprint
245
262
  - context: Finding available stories
246
- invocation: /sprint-context available
263
+ invocation: /sprint backlog
247
264
  anti_patterns:
248
- - Don't manually edit sprint YAML - use skills
249
- related_skills: [story-management, jira]
265
+ - Don't manually edit sprint YAML - use scripts
266
+ related_skills: [story, jira]
250
267
  keywords: [sprint, backlog, velocity, kanban]
251
- allowed_tools: [Read, Glob, Grep, Bash, Task] # Read-only sprint data access
268
+ allowed_tools: [Read, Glob, Grep, Bash, Task]
252
269
 
253
270
  systematic-debugging:
254
271
  name: systematic-debugging
@@ -268,21 +285,21 @@ skills:
268
285
  related_skills: [testing, dev-patterns, agentic-patterns]
269
286
  keywords: [debugging, bisect, reproduce, isolate, root-cause, regression]
270
287
 
271
- story-management:
272
- name: story-management
273
- description: Story creation, sizing, and sprint workflow patterns
288
+ story:
289
+ name: story
290
+ description: Story creation, sizing, and templates for Pennyfarthing workflow
274
291
  category: project-management
275
292
  tags: [stories, sizing, workflow]
276
293
  version: "1.0.0"
277
294
  prerequisites: []
278
295
  examples:
279
296
  - context: Creating new stories
280
- invocation: /story-management create
297
+ invocation: /story create
281
298
  - context: Sizing stories
282
- invocation: /story-management size
299
+ invocation: /story size
283
300
  anti_patterns:
284
301
  - Don't create stories without acceptance criteria
285
- related_skills: [sprint-context, jira]
302
+ related_skills: [sprint, jira]
286
303
  keywords: [user-stories, estimation, points, acceptance-criteria]
287
304
 
288
305
  testing:
@@ -339,6 +356,25 @@ skills:
339
356
  related_skills: [theme]
340
357
  keywords: [personas, custom, generation, wizard]
341
358
 
359
+ workflow:
360
+ name: workflow
361
+ description: Manage workflows - list, show, set, start, resume, and check status
362
+ category: project-management
363
+ tags: [workflow, phases, tdd]
364
+ version: "1.0.0"
365
+ prerequisites: []
366
+ examples:
367
+ - context: Listing available workflows
368
+ invocation: /workflow
369
+ - context: Showing current workflow
370
+ invocation: /workflow show
371
+ - context: Starting stepped workflow
372
+ invocation: /workflow start architecture
373
+ anti_patterns:
374
+ - Don't switch workflows mid-story unless requirements fundamentally changed
375
+ related_skills: [sprint, story]
376
+ keywords: [tdd, trivial, agent-docs, bdd, architecture, bikelane, stepped, phased]
377
+
342
378
  yq:
343
379
  name: yq
344
380
  description: YAML processor for reading, modifying, and querying YAML files
@@ -356,7 +356,7 @@ d="$PWD"; while [[ ! -d "$d/.claude" ]] && [[ "$d" != "/" ]]; do d="$(dirname "$
356
356
  <output>
357
357
  1. **Check story** via `check-story.sh` (if ID provided)
358
358
  2. **Write context** to `.session/context-story-{id}.md`
359
- 3. **Setup story** via `generic-sm-setup` subagent (claims Jira, creates branch)
359
+ 3. **Setup story** via `sm-setup` subagent (claims Jira, creates branch)
360
360
  4. **Handoff** to next agent based on workflow:
361
361
 
362
362
  | Workflow | Route |
@@ -53,6 +53,17 @@
53
53
  ]
54
54
  }
55
55
  ],
56
+ "Stop": [
57
+ {
58
+ "matcher": "",
59
+ "hooks": [
60
+ {
61
+ "type": "command",
62
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.pennyfarthing/scripts/hooks/question-reflector-check.sh"
63
+ }
64
+ ]
65
+ }
66
+ ],
56
67
  "PreToolUse": [
57
68
  {
58
69
  "matcher": "Edit|Write",