@pennyfarthing/core 7.6.1 → 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 +22 -9
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
- package/pennyfarthing-dist/guides/scale-levels.md +114 -0
- package/pennyfarthing-dist/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 +6 -1
- 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 +66 -53
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
- 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/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/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,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
|
|
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: [
|
|
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
|
|
236
|
-
name: sprint
|
|
237
|
-
description: Sprint status, backlog, and story management
|
|
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
|
|
261
|
+
invocation: /sprint
|
|
245
262
|
- context: Finding available stories
|
|
246
|
-
invocation: /sprint
|
|
263
|
+
invocation: /sprint backlog
|
|
247
264
|
anti_patterns:
|
|
248
|
-
- Don't manually edit sprint YAML - use
|
|
249
|
-
related_skills: [story
|
|
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]
|
|
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
|
|
272
|
-
name: story
|
|
273
|
-
description: Story creation, sizing, and
|
|
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
|
|
297
|
+
invocation: /story create
|
|
281
298
|
- context: Sizing stories
|
|
282
|
-
invocation: /story
|
|
299
|
+
invocation: /story size
|
|
283
300
|
anti_patterns:
|
|
284
301
|
- Don't create stories without acceptance criteria
|
|
285
|
-
related_skills: [sprint
|
|
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 `
|
|
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",
|