@sushanglewis/lincoln 1.5.0 → 1.5.1
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/dist/main.d.ts +3 -0
- package/dist/main.js +1231 -906
- package/dist/main.js.map +1 -1
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.js +18 -0
- package/dist/postinstall.js.map +1 -0
- package/framework/.claude/agents/_contract.md +79 -0
- package/framework/.claude/agents/architect.md +31 -0
- package/framework/.claude/agents/default.md +151 -0
- package/framework/.claude/agents/designer.md +33 -0
- package/framework/.claude/agents/engineer.md +42 -0
- package/framework/.claude/agents/external/NOTICES.md +12 -0
- package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +23 -0
- package/framework/.claude/agents/external/oh-my-claudecode.manifest.yaml +26 -0
- package/framework/.claude/agents/external/wshobson-agents.manifest.yaml +23 -0
- package/framework/.claude/agents/frontend-engineer.md +32 -0
- package/framework/.claude/agents/knowledge-curator.md +28 -0
- package/framework/.claude/agents/pm.md +38 -0
- package/framework/.claude/agents/qa.md +28 -0
- package/framework/.claude/agents/release-coordinator.md +25 -0
- package/framework/.claude/agents/researcher.md +41 -0
- package/framework/.claude/harnesses/claude-code.yaml +15 -0
- package/framework/.claude/harnesses/codex.yaml +40 -0
- package/framework/.claude/harnesses/command-map.yaml +255 -0
- package/framework/.claude/harnesses/opencode.yaml +32 -0
- package/framework/.claude/harnesses/scenarios.yaml +45 -0
- package/framework/.claude/hooks/on-pr-event.sh +69 -0
- package/framework/.claude/hooks/on-session-start.sh +427 -0
- package/framework/.claude/hooks/on-stop.sh +104 -0
- package/framework/.claude/hooks/post-tool-use.sh +161 -0
- package/framework/.claude/hooks/pre-tool-use.sh +246 -0
- package/framework/.claude/schemas/agent-definition.schema.json +35 -0
- package/framework/.claude/schemas/benchmark-report.schema.json +148 -0
- package/framework/.claude/schemas/pm-to-ux-handoff.schema.yaml +128 -0
- package/framework/.claude/schemas/stage-definition.schema.json +233 -0
- package/framework/.claude/schemas/workflow-stage.schema.json +86 -0
- package/framework/.claude/schemas/workflow-template.schema.json +88 -0
- package/framework/.claude/settings.json +14 -0
- package/framework/.claude/skills/build-product-prototype/SKILL.md +35 -0
- package/framework/.claude/skills/build-product-prototype/prompts/main.md +78 -0
- package/framework/.claude/skills/clarify-requirements/SKILL.md +32 -0
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +81 -0
- package/framework/.claude/skills/dependencies.yaml +139 -0
- package/framework/.claude/skills/draft-product-design/SKILL.md +40 -0
- package/framework/.claude/skills/draft-product-design/prompts/main.md +47 -0
- package/framework/.claude/skills/lc-analyze-frameworks/SKILL.md +43 -0
- package/framework/.claude/skills/lc-analyze-frameworks/prompts/main.md +95 -0
- package/framework/.claude/skills/lc-benchmark/SKILL.md +61 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +38 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/prompts/build-codebase-knowledge.md +61 -0
- package/framework/.claude/skills/lc-collect-intelligence/SKILL.md +43 -0
- package/framework/.claude/skills/lc-collect-intelligence/prompts/main.md +72 -0
- package/framework/.claude/skills/lc-competitive-analysis/SKILL.md +43 -0
- package/framework/.claude/skills/lc-competitive-analysis/prompts/main.md +70 -0
- package/framework/.claude/skills/lc-explore-opensource/SKILL.md +36 -0
- package/framework/.claude/skills/lc-explore-opensource/prompts/explore-opensource.md +77 -0
- package/framework/.claude/skills/lc-first-principles/SKILL.md +43 -0
- package/framework/.claude/skills/lc-first-principles/prompts/main.md +73 -0
- package/framework/.claude/skills/lc-handoff/SKILL.md +34 -0
- package/framework/.claude/skills/lc-init-branch/SKILL.md +52 -0
- package/framework/.claude/skills/lc-market-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-market-research/prompts/main.md +79 -0
- package/framework/.claude/skills/lc-product-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-product-research/prompts/main.md +80 -0
- package/framework/.claude/skills/lc-research-report/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-report/prompts/main.md +90 -0
- package/framework/.claude/skills/lc-research-scope/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-scope/prompts/main.md +82 -0
- package/framework/.claude/skills/lc-setup/SKILL.md +68 -0
- package/framework/.claude/skills/lc-setup/prompts/main.md +88 -0
- package/framework/.claude/skills/lc-stage/SKILL.md +45 -0
- package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-stakeholder-research/prompts/main.md +65 -0
- package/framework/.claude/skills/lc-status/SKILL.md +25 -0
- package/framework/.claude/skills/lc-storytelling/SKILL.md +43 -0
- package/framework/.claude/skills/lc-storytelling/prompts/main.md +77 -0
- package/framework/.claude/skills/lc-wf/SKILL.md +54 -0
- package/framework/.claude/skills/lc-workflow/SKILL.md +48 -0
- package/framework/.claude/skills/lc-workflow/validators/validate.py +755 -0
- package/framework/.claude/skills/lc-workflow-router/SKILL.md +41 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/intake-prompt.md +91 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/router-prompt.md +44 -0
- package/framework/.claude/skills/plan-tdd-development/SKILL.md +31 -0
- package/framework/.claude/skills/plan-tdd-development/prompts/main.md +44 -0
- package/framework/.claude/skills/process-interview/SKILL.md +32 -0
- package/framework/.claude/skills/process-interview/prompts/main.md +47 -0
- package/framework/.claude/skills/propose-with-openspec/SKILL.md +37 -0
- package/framework/.claude/skills/propose-with-openspec/prompts/main.md +53 -0
- package/framework/.claude/skills/split-to-github/SKILL.md +32 -0
- package/framework/.claude/skills/split-to-github/prompts/main.md +42 -0
- package/framework/.claude/skills/sync-to-knowledge/SKILL.md +35 -0
- package/framework/.claude/skills/sync-to-knowledge/prompts/main.md +54 -0
- package/framework/.claude/skills/workflow-continue/SKILL.md +24 -0
- package/framework/.claude/skills/workflow-continue/prompts/main.md +25 -0
- package/framework/.claude/stages/build-codebase-knowledge.yaml +42 -0
- package/framework/.claude/stages/clarify.yaml +59 -0
- package/framework/.claude/stages/explore-opensource.yaml +40 -0
- package/framework/.claude/stages/implement.yaml +60 -0
- package/framework/.claude/stages/ingest.yaml +40 -0
- package/framework/.claude/stages/lc-analyze-frameworks.yaml +37 -0
- package/framework/.claude/stages/lc-collect-intelligence.yaml +37 -0
- package/framework/.claude/stages/lc-competitive-analysis.yaml +37 -0
- package/framework/.claude/stages/lc-first-principles.yaml +37 -0
- package/framework/.claude/stages/lc-market-research.yaml +37 -0
- package/framework/.claude/stages/lc-product-research.yaml +37 -0
- package/framework/.claude/stages/lc-research-report.yaml +38 -0
- package/framework/.claude/stages/lc-research-scope.yaml +38 -0
- package/framework/.claude/stages/lc-stakeholder-research.yaml +37 -0
- package/framework/.claude/stages/lc-storytelling.yaml +37 -0
- package/framework/.claude/stages/product-design-docs.yaml +66 -0
- package/framework/.claude/stages/product-prototype.yaml +53 -0
- package/framework/.claude/stages/propose.yaml +45 -0
- package/framework/.claude/stages/split.yaml +41 -0
- package/framework/.claude/stages/sync-knowledge.yaml +49 -0
- package/framework/.claude/stages/tdd-development-plan.yaml +43 -0
- package/framework/.claude/stages/workflow-router.yaml +45 -0
- package/framework/.claude/templates/solo-workflow-context.yaml +23 -0
- package/framework/.claude/workflows/README.md +93 -0
- package/framework/.claude/workflows/bug-fix.yaml +81 -0
- package/framework/.claude/workflows/design-spike.yaml +55 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +106 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +189 -0
- package/framework/.claude/workflows/oss-first-design.yaml +100 -0
- package/framework/.claude/workflows/pm-research.yaml +115 -0
- package/framework/.claude-plugin/hooks.json +6 -0
- package/framework/.claude-plugin/marketplace.json +32 -0
- package/framework/.claude-plugin/plugin.json +57 -0
- package/framework/.version-bump.json +43 -0
- package/framework/CLAUDE.md +140 -0
- package/framework/CONTRIBUTING.md +219 -0
- package/framework/LICENSE +21 -0
- package/framework/README.en.md +139 -0
- package/framework/README.md +139 -0
- package/framework/RELEASE.md +334 -0
- package/framework/SKILL.md +24 -0
- package/framework/USAGE.md +356 -0
- package/framework/manifest.json +1214 -0
- package/framework/requirements.txt +4 -0
- package/framework/scripts/__init__.py +0 -0
- package/framework/scripts/build_codebase_knowledge.py +76 -0
- package/framework/scripts/bump_version.py +181 -0
- package/framework/scripts/check-dependency-drift.py +69 -0
- package/framework/scripts/check-harness-drift.sh +57 -0
- package/framework/scripts/check-main-merge-hygiene.py +128 -0
- package/framework/scripts/check-skill-dependencies.sh +124 -0
- package/framework/scripts/init-lincoln-branch.sh +300 -0
- package/framework/scripts/init-project.sh +184 -0
- package/framework/scripts/lc-benchmark-cli.py +66 -0
- package/framework/scripts/lincoln-audit.py +288 -0
- package/framework/scripts/lincoln-setup.py +651 -0
- package/framework/scripts/lincoln-status.py +448 -0
- package/framework/scripts/lincoln_benchmark.py +224 -0
- package/framework/scripts/lincoln_benchmark_eval.py +104 -0
- package/framework/scripts/lincoln_benchmark_metrics.py +818 -0
- package/framework/scripts/lincoln_benchmark_report.py +141 -0
- package/framework/scripts/lincoln_command_map.py +289 -0
- package/framework/scripts/lincoln_dependency_manager.py +335 -0
- package/framework/scripts/lincoln_documents.py +175 -0
- package/framework/scripts/lincoln_harness_adapter.py +319 -0
- package/framework/scripts/lincoln_index.py +255 -0
- package/framework/scripts/lincoln_paths.py +198 -0
- package/framework/scripts/lincoln_prd.py +227 -0
- package/framework/scripts/lincoln_role.py +46 -0
- package/framework/scripts/lincoln_scenario.py +59 -0
- package/framework/scripts/lincoln_session_start_metrics.py +71 -0
- package/framework/scripts/lincoln_skill_prompt.py +48 -0
- package/framework/scripts/lincoln_trace.py +335 -0
- package/framework/scripts/lincoln_workflow.py +199 -0
- package/framework/scripts/list-active-lincoln-branches.sh +87 -0
- package/framework/scripts/package-lincoln-plugin.py +340 -0
- package/framework/scripts/run-infrastructure-tests.py +84 -0
- package/framework/scripts/stage_loader.py +1071 -0
- package/framework/scripts/static-check.sh +356 -0
- package/framework/scripts/sync-external-agents.sh +210 -0
- package/framework/scripts/sync-framework-package.mjs +218 -0
- package/framework/scripts/task_tool_guard.py +213 -0
- package/framework/scripts/track-artifacts.py +140 -0
- package/framework/scripts/validate_stage.py +350 -0
- package/{README.md → framework/tools/lincoln/README.md} +4 -4
- package/framework/tools/lincoln/package-lock.json +3349 -0
- package/framework/tools/lincoln/package.json +48 -0
- package/framework/tools/lincoln/src/cli.tsx +60 -0
- package/framework/tools/lincoln/src/components/AudioMeter.tsx +34 -0
- package/framework/tools/lincoln/src/components/CancelledScreen.tsx +11 -0
- package/framework/tools/lincoln/src/components/DevicesMenu.tsx +79 -0
- package/framework/tools/lincoln/src/components/HintBar.tsx +25 -0
- package/framework/tools/lincoln/src/components/LogView.tsx +36 -0
- package/framework/tools/lincoln/src/components/ModelMenu.tsx +122 -0
- package/framework/tools/lincoln/src/components/ReadyScreen.tsx +54 -0
- package/framework/tools/lincoln/src/components/RecordingApp.tsx +211 -0
- package/framework/tools/lincoln/src/components/RecordingScreen.tsx +41 -0
- package/framework/tools/lincoln/src/components/SelectMenu.tsx +53 -0
- package/framework/tools/lincoln/src/components/StatusPanel.tsx +52 -0
- package/framework/tools/lincoln/src/components/StopConfirmation.tsx +32 -0
- package/framework/tools/lincoln/src/config/args.ts +35 -0
- package/framework/tools/lincoln/src/config/loadConfig.ts +81 -0
- package/framework/tools/lincoln/src/config/resolveConfig.ts +85 -0
- package/framework/tools/lincoln/src/devices/listDevices.ts +11 -0
- package/framework/tools/lincoln/src/devices/parseDevices.ts +25 -0
- package/framework/tools/lincoln/src/hooks/useKeyHandler.ts +63 -0
- package/framework/tools/lincoln/src/main.tsx +4 -0
- package/framework/tools/lincoln/src/models/models.ts +34 -0
- package/framework/tools/lincoln/src/models/runWarmup.ts +46 -0
- package/framework/tools/lincoln/src/models/warmupProgress.ts +30 -0
- package/framework/tools/lincoln/src/recording/spawnRecorder.ts +127 -0
- package/framework/tools/lincoln/src/recording/useRecorder.ts +146 -0
- package/framework/tools/lincoln/src/utils/amplitudeToMeter.ts +4 -0
- package/framework/tools/lincoln/src/utils/formatDuration.ts +6 -0
- package/framework/tools/lincoln/src/utils/sessionId.ts +25 -0
- package/framework/tools/lincoln/tests/cli.test.tsx +52 -0
- package/framework/tools/lincoln/tests/components/AudioMeter.test.tsx +31 -0
- package/framework/tools/lincoln/tests/components/CancelledScreen.test.tsx +12 -0
- package/framework/tools/lincoln/tests/components/ReadyScreen.test.tsx +68 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.menus.test.tsx +158 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.test.tsx +248 -0
- package/framework/tools/lincoln/tests/components/RecordingScreen.test.tsx +37 -0
- package/framework/tools/lincoln/tests/components/SelectMenu.test.tsx +59 -0
- package/framework/tools/lincoln/tests/components/StopConfirmation.test.tsx +38 -0
- package/framework/tools/lincoln/tests/config/args.test.ts +49 -0
- package/framework/tools/lincoln/tests/config/loadConfig.test.ts +59 -0
- package/framework/tools/lincoln/tests/config/resolveConfig.test.ts +72 -0
- package/framework/tools/lincoln/tests/devices/parseDevices.test.ts +21 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.enabled.test.tsx +44 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.test.tsx +130 -0
- package/framework/tools/lincoln/tests/models/models.test.ts +27 -0
- package/framework/tools/lincoln/tests/models/warmupProgress.test.ts +25 -0
- package/framework/tools/lincoln/tests/recording/spawnRecorder.test.ts +85 -0
- package/framework/tools/lincoln/tests/recording/useRecorder.test.tsx +172 -0
- package/framework/tools/lincoln/tests/utils/amplitudeToMeter.test.ts +22 -0
- package/framework/tools/lincoln/tests/utils/formatDuration.test.ts +11 -0
- package/framework/tools/lincoln/tests/utils/sessionId.test.ts +30 -0
- package/framework/tools/lincoln/tsconfig.json +22 -0
- package/framework/tools/lincoln/tsconfig.tests.json +10 -0
- package/framework/tools/lincoln/tsup.config.ts +17 -0
- package/framework/tools/lincoln/vitest.config.ts +20 -0
- package/framework/tools/lincoln-installer/package-lock.json +3611 -0
- package/framework/tools/lincoln-installer/package.json +53 -0
- package/framework/tools/lincoln-installer/src/cli.tsx +63 -0
- package/framework/tools/lincoln-installer/src/components/HarnessSelectScreen.tsx +43 -0
- package/framework/tools/lincoln-installer/src/components/InstallerApp.tsx +98 -0
- package/framework/tools/lincoln-installer/src/components/OptionsScreen.tsx +56 -0
- package/framework/tools/lincoln-installer/src/components/ProgressScreen.tsx +25 -0
- package/framework/tools/lincoln-installer/src/components/ResultScreen.tsx +40 -0
- package/framework/tools/lincoln-installer/src/components/SelectMenu.tsx +73 -0
- package/framework/tools/lincoln-installer/src/components/SummaryScreen.tsx +49 -0
- package/framework/tools/lincoln-installer/src/components/UpdateApp.tsx +135 -0
- package/framework/tools/lincoln-installer/src/components/WelcomeScreen.tsx +22 -0
- package/framework/tools/lincoln-installer/src/config/args.ts +76 -0
- package/framework/tools/lincoln-installer/src/config/resolveConfig.ts +33 -0
- package/framework/tools/lincoln-installer/src/config/updateArgs.ts +67 -0
- package/framework/tools/lincoln-installer/src/hooks/useSetup.ts +149 -0
- package/framework/tools/lincoln-installer/src/main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/types.ts +19 -0
- package/framework/tools/lincoln-installer/src/update-main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/update.tsx +90 -0
- package/framework/tools/lincoln-installer/src/utils/detectHarness.ts +51 -0
- package/framework/tools/lincoln-installer/src/utils/runSetup.ts +62 -0
- package/framework/tools/lincoln-installer/src/utils/updateRelease.ts +454 -0
- package/framework/tools/lincoln-installer/tests/components/InstallerApp.test.tsx +88 -0
- package/framework/tools/lincoln-installer/tests/components/SelectMenu.test.tsx +26 -0
- package/framework/tools/lincoln-installer/tests/components/UpdateApp.test.tsx +170 -0
- package/framework/tools/lincoln-installer/tests/components/screens.test.tsx +310 -0
- package/framework/tools/lincoln-installer/tests/config/args.test.ts +66 -0
- package/framework/tools/lincoln-installer/tests/config/resolveConfig.test.ts +75 -0
- package/framework/tools/lincoln-installer/tests/config/updateArgs.test.ts +52 -0
- package/framework/tools/lincoln-installer/tests/hooks/useSetup.test.tsx +213 -0
- package/framework/tools/lincoln-installer/tests/update.test.tsx +108 -0
- package/framework/tools/lincoln-installer/tests/utils/detectHarness.test.ts +40 -0
- package/framework/tools/lincoln-installer/tests/utils/runSetup.test.ts +45 -0
- package/framework/tools/lincoln-installer/tests/utils/updateRelease.test.ts +559 -0
- package/framework/tools/lincoln-installer/tsconfig.json +22 -0
- package/framework/tools/lincoln-installer/tsconfig.tests.json +8 -0
- package/framework/tools/lincoln-installer/tsup.config.ts +17 -0
- package/framework/tools/lincoln-installer/vitest.config.ts +20 -0
- package/framework/tools/lincoln-record/Cargo.lock +1896 -0
- package/framework/tools/lincoln-record/Cargo.toml +24 -0
- package/framework/tools/lincoln-record/diarize/diarize.py +117 -0
- package/framework/tools/lincoln-record/diarize/merge.py +55 -0
- package/framework/tools/lincoln-record/src/audio/capture/microphone.rs +194 -0
- package/framework/tools/lincoln-record/src/audio/capture/mod.rs +1 -0
- package/framework/tools/lincoln-record/src/audio/devices.rs +20 -0
- package/framework/tools/lincoln-record/src/audio/mod.rs +5 -0
- package/framework/tools/lincoln-record/src/audio/resampler.rs +44 -0
- package/framework/tools/lincoln-record/src/audio/saver.rs +87 -0
- package/framework/tools/lincoln-record/src/audio/source.rs +104 -0
- package/framework/tools/lincoln-record/src/cli.rs +96 -0
- package/framework/tools/lincoln-record/src/config.rs +135 -0
- package/framework/tools/lincoln-record/src/lib.rs +6 -0
- package/framework/tools/lincoln-record/src/main.rs +242 -0
- package/framework/tools/lincoln-record/src/model/mod.rs +248 -0
- package/framework/tools/lincoln-record/src/output/metadata.rs +30 -0
- package/framework/tools/lincoln-record/src/output/mod.rs +2 -0
- package/framework/tools/lincoln-record/src/output/transcript.rs +42 -0
- package/framework/tools/lincoln-record/src/transcription/diarization.rs +64 -0
- package/framework/tools/lincoln-record/src/transcription/mod.rs +3 -0
- package/framework/tools/lincoln-record/src/transcription/provider.rs +43 -0
- package/framework/tools/lincoln-record/src/transcription/whisper.rs +176 -0
- package/framework/tools/lincoln-record/tests/cli_args.rs +103 -0
- package/framework/tools/lincoln-record/tests/config_test.rs +70 -0
- package/framework/tools/lincoln-record/tests/devices_test.rs +23 -0
- package/framework/tools/lincoln-record/tests/diarization_tests.rs +101 -0
- package/framework/tools/lincoln-record/tests/integration_tests.rs +44 -0
- package/framework/tools/lincoln-record/tests/model_test.rs +68 -0
- package/framework/tools/lincoln-record/tests/transcription_tests.rs +73 -0
- package/package.json +19 -13
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Metric computation for Lincoln benchmark reports."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
19
|
+
|
|
20
|
+
from scripts.lincoln_paths import ( # noqa: E402
|
|
21
|
+
get_process_slug,
|
|
22
|
+
load_yaml,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
TEST_COMMAND_RE = re.compile(
|
|
26
|
+
r"\b(pytest|npm test|yarn test|cargo test|go test|vitest|jest)\b",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
STATIC_CHECK_RE = re.compile(r"\bstatic-check\.sh\b")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _safe_path_component(value: Any) -> str:
|
|
34
|
+
"""Sanitize a variable used as a path component to prevent traversal."""
|
|
35
|
+
text = re.sub(r"[\\/]", "-", str(value))
|
|
36
|
+
parts = [part for part in text.split("-") if part and part != "." and part != ".."]
|
|
37
|
+
return "-".join(parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_variables(variables: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
"""Return a copy of variables with every value sanitized for path interpolation."""
|
|
42
|
+
return {key: _safe_path_component(value) for key, value in variables.items()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_legacy_state(state: dict[str, Any]) -> bool:
|
|
46
|
+
return "stages" in state and "nodes" not in state
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_iso(value: str) -> datetime:
|
|
50
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_primary_agents(project_root: Path) -> dict[str, str]:
|
|
54
|
+
"""Build stage_id -> primary agent mapping from the actual stage definitions."""
|
|
55
|
+
stages_dir = project_root / ".claude" / "stages"
|
|
56
|
+
agents: dict[str, str] = {}
|
|
57
|
+
if not stages_dir.is_dir():
|
|
58
|
+
return agents
|
|
59
|
+
for path in sorted(stages_dir.glob("*.yaml")):
|
|
60
|
+
try:
|
|
61
|
+
data = load_yaml(path)
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
if not isinstance(data, dict):
|
|
65
|
+
continue
|
|
66
|
+
stage_id = data.get("id") or path.stem
|
|
67
|
+
agent = data.get("agent")
|
|
68
|
+
primary = agent.get("primary", "") if isinstance(agent, dict) else ""
|
|
69
|
+
if stage_id and primary:
|
|
70
|
+
agents[str(stage_id)] = str(primary)
|
|
71
|
+
return agents
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _stage_order_from_trace(trace: list[dict[str, Any]]) -> list[str]:
|
|
75
|
+
order: list[str] = []
|
|
76
|
+
for entry in trace:
|
|
77
|
+
stage = entry.get("stage")
|
|
78
|
+
if stage and (not order or order[-1] != stage):
|
|
79
|
+
order.append(stage)
|
|
80
|
+
return order
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _lcs_length(a: list[str], b: list[str]) -> int:
|
|
84
|
+
if not a or not b:
|
|
85
|
+
return 0
|
|
86
|
+
prev = [0] * (len(b) + 1)
|
|
87
|
+
for i in range(1, len(a) + 1):
|
|
88
|
+
curr = [0] * (len(b) + 1)
|
|
89
|
+
for j in range(1, len(b) + 1):
|
|
90
|
+
if a[i - 1] == b[j - 1]:
|
|
91
|
+
curr[j] = prev[j - 1] + 1
|
|
92
|
+
else:
|
|
93
|
+
curr[j] = max(prev[j], curr[j - 1])
|
|
94
|
+
prev = curr
|
|
95
|
+
return prev[len(b)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_trace(process_root: Path, process_slug: str) -> list[dict[str, Any]]:
|
|
99
|
+
trace_dir = process_root / process_slug / ".trace"
|
|
100
|
+
if not trace_dir.exists():
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
entries: dict[tuple[str, str], dict[str, Any]] = {}
|
|
104
|
+
for trace_file in sorted(trace_dir.glob("lc-trace*.jsonl")):
|
|
105
|
+
try:
|
|
106
|
+
text = trace_file.read_text(encoding="utf-8")
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logging.warning("Cannot read trace file %s: %s", trace_file, exc)
|
|
109
|
+
continue
|
|
110
|
+
for line in text.splitlines():
|
|
111
|
+
line = line.strip()
|
|
112
|
+
if not line:
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
entry = json.loads(line)
|
|
116
|
+
except json.JSONDecodeError as exc:
|
|
117
|
+
logging.warning("Skipping malformed trace line in %s: %s", trace_file, exc)
|
|
118
|
+
continue
|
|
119
|
+
if not isinstance(entry, dict):
|
|
120
|
+
continue
|
|
121
|
+
key = (str(entry.get("run_id", "")), str(entry.get("sequence_id", "")))
|
|
122
|
+
if not key[1]:
|
|
123
|
+
continue
|
|
124
|
+
entries[key] = entry
|
|
125
|
+
|
|
126
|
+
return sorted(entries.values(), key=lambda e: e.get("timestamp", ""))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _workflow_steps(state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
130
|
+
template = (
|
|
131
|
+
state.get("workflow", {}).get("template")
|
|
132
|
+
or state.get("current_run", {}).get("workflow_template")
|
|
133
|
+
)
|
|
134
|
+
if not template:
|
|
135
|
+
return []
|
|
136
|
+
from scripts.stage_loader import load_workflow
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
workflow = load_workflow(template)
|
|
140
|
+
except Exception:
|
|
141
|
+
return []
|
|
142
|
+
return workflow.get("steps", [])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _stage_durations(trace: list[dict[str, Any]]) -> dict[str, int]:
|
|
146
|
+
durations: dict[str, list[datetime]] = {}
|
|
147
|
+
for entry in trace:
|
|
148
|
+
stage = entry.get("stage")
|
|
149
|
+
ts = entry.get("timestamp")
|
|
150
|
+
if not stage or not ts:
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
dt = _parse_iso(ts)
|
|
154
|
+
except Exception:
|
|
155
|
+
continue
|
|
156
|
+
durations.setdefault(stage, []).append(dt)
|
|
157
|
+
return {
|
|
158
|
+
stage: int((max(times) - min(times)).total_seconds())
|
|
159
|
+
for stage, times in durations.items()
|
|
160
|
+
if len(times) >= 2
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _node_durations(state: dict[str, Any]) -> dict[str, int]:
|
|
165
|
+
durations: dict[str, int] = {}
|
|
166
|
+
for node in state.get("nodes", []):
|
|
167
|
+
started = node.get("started_at")
|
|
168
|
+
completed = node.get("completed_at")
|
|
169
|
+
if started and completed:
|
|
170
|
+
try:
|
|
171
|
+
durations[str(node.get("stage_id"))] = int(
|
|
172
|
+
(_parse_iso(completed) - _parse_iso(started)).total_seconds()
|
|
173
|
+
)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
return durations
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _interpolate_placeholders(value: str, variables: dict[str, Any]) -> str:
|
|
180
|
+
return re.sub(r"\{([a-zA-Z0-9_-]+)\}", lambda m: str(variables.get(m.group(1), m.group(0))), value)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _artifact_paths_for_stage(
|
|
184
|
+
workflow_steps: list[dict[str, Any]],
|
|
185
|
+
stage_id: str,
|
|
186
|
+
variables: dict[str, Any],
|
|
187
|
+
) -> list[str]:
|
|
188
|
+
safe_variables = _safe_variables(variables)
|
|
189
|
+
for step in workflow_steps:
|
|
190
|
+
if step.get("id") == stage_id:
|
|
191
|
+
artifacts = step.get("artifacts", [])
|
|
192
|
+
return [_interpolate_placeholders(str(a), safe_variables) for a in artifacts]
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _artifact_completion_rate(
|
|
197
|
+
stage_id: str | None,
|
|
198
|
+
workflow_steps: list[dict[str, Any]],
|
|
199
|
+
variables: dict[str, Any],
|
|
200
|
+
process_root: Path,
|
|
201
|
+
) -> float:
|
|
202
|
+
if not stage_id:
|
|
203
|
+
return 0.0
|
|
204
|
+
paths = _artifact_paths_for_stage(workflow_steps, stage_id, variables)
|
|
205
|
+
if not paths:
|
|
206
|
+
return 1.0
|
|
207
|
+
existing = sum(1 for p in paths if (process_root / p).exists())
|
|
208
|
+
return round(existing / len(paths), 2)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _requirements_clarity_score(process_root: Path, process_slug: str, variables: dict[str, Any]) -> int:
|
|
212
|
+
req_path = process_root / process_slug / "pages" / "docs" / "requirements.html"
|
|
213
|
+
if not req_path.exists():
|
|
214
|
+
return 0
|
|
215
|
+
text = req_path.read_text(encoding="utf-8").lower()
|
|
216
|
+
checks = [
|
|
217
|
+
"background" in text or "背景" in text,
|
|
218
|
+
"problem" in text or "问题" in text,
|
|
219
|
+
"solution" in text or "方案" in text,
|
|
220
|
+
"acceptance" in text or "验收" in text,
|
|
221
|
+
]
|
|
222
|
+
return sum(checks)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _design_doc_completeness(
|
|
226
|
+
workflow_steps: list[dict[str, Any]],
|
|
227
|
+
variables: dict[str, Any],
|
|
228
|
+
process_root: Path,
|
|
229
|
+
process_slug: str,
|
|
230
|
+
) -> float:
|
|
231
|
+
stage_id = "product-design-docs"
|
|
232
|
+
artifacts = _artifact_paths_for_stage(workflow_steps, stage_id, variables)
|
|
233
|
+
# Only count design docs under pages/docs; handoff artifacts are tracked separately.
|
|
234
|
+
design_prefix = f"{process_slug}/pages/docs/"
|
|
235
|
+
artifacts = [p for p in artifacts if design_prefix in p and "*" not in p]
|
|
236
|
+
if not artifacts:
|
|
237
|
+
base = process_root / process_slug / "pages" / "docs"
|
|
238
|
+
artifacts = [
|
|
239
|
+
str(base / "design-review.html"),
|
|
240
|
+
str(base / "scenarios.html"),
|
|
241
|
+
str(base / "feature-catalog.html"),
|
|
242
|
+
str(base / "data-model.html"),
|
|
243
|
+
str(base / "flows.html"),
|
|
244
|
+
str(base / "feasibility.html"),
|
|
245
|
+
str(base / "page-map.html"),
|
|
246
|
+
]
|
|
247
|
+
existing = sum(1 for p in artifacts if (process_root / p).exists())
|
|
248
|
+
return round(existing / len(artifacts), 2) if artifacts else 0.0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _tdd_plan_red_green_refactor(
|
|
252
|
+
process_root: Path, process_slug: str, variables: dict[str, Any]
|
|
253
|
+
) -> dict[str, bool]:
|
|
254
|
+
path = process_root / process_slug / "pages" / "docs" / "tdd-plan.html"
|
|
255
|
+
result = {"red": False, "green": False, "refactor": False}
|
|
256
|
+
if not path.exists():
|
|
257
|
+
return result
|
|
258
|
+
text = path.read_text(encoding="utf-8").lower()
|
|
259
|
+
result["red"] = bool(re.search(r"\bred\b|\bred phase\b|\bred-green", text))
|
|
260
|
+
result["green"] = bool(re.search(r"\bgreen\b|\bgreen phase\b", text))
|
|
261
|
+
result["refactor"] = bool(re.search(r"\brefactor\b|\brefactor phase\b", text))
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _openspec_task_count(
|
|
266
|
+
process_root: Path, process_slug: str, variables: dict[str, Any]
|
|
267
|
+
) -> int:
|
|
268
|
+
change_name = _safe_path_component(variables.get("change_name", "") or "")
|
|
269
|
+
path = process_root / process_slug / "openspec" / "changes" / change_name / "tasks.md"
|
|
270
|
+
if not path.exists():
|
|
271
|
+
return 0
|
|
272
|
+
text = path.read_text(encoding="utf-8")
|
|
273
|
+
return len(re.findall(r"^\s*-\s+\[.\]", text, re.MULTILINE))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _static_check_pass(trace: list[dict[str, Any]], process_root: Path) -> bool | None:
|
|
277
|
+
# Prefer the structured result written by static-check.sh; fall back to
|
|
278
|
+
# scanning the trace for legacy or per-process usage.
|
|
279
|
+
result_file = process_root / ".context" / "static-check-result.json"
|
|
280
|
+
if result_file.exists():
|
|
281
|
+
try:
|
|
282
|
+
data = json.loads(result_file.read_text(encoding="utf-8"))
|
|
283
|
+
if isinstance(data, dict) and "passed" in data:
|
|
284
|
+
return bool(data["passed"])
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
found = None
|
|
289
|
+
for entry in trace:
|
|
290
|
+
if entry.get("category") != "bash":
|
|
291
|
+
continue
|
|
292
|
+
command = str(entry.get("args_summary", {}).get("command", ""))
|
|
293
|
+
if STATIC_CHECK_RE.search(command):
|
|
294
|
+
if entry.get("exit_code", 0) != 0:
|
|
295
|
+
return False
|
|
296
|
+
found = True
|
|
297
|
+
return found
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _test_coverage(process_root: Path) -> float | None:
|
|
301
|
+
"""Return total coverage percentage from the latest .coverage database."""
|
|
302
|
+
coverage_file = process_root / ".coverage"
|
|
303
|
+
if not coverage_file.exists():
|
|
304
|
+
return None
|
|
305
|
+
try:
|
|
306
|
+
output = subprocess.check_output(
|
|
307
|
+
[sys.executable, "-m", "coverage", "json", "-o", "-"],
|
|
308
|
+
cwd=process_root,
|
|
309
|
+
text=True,
|
|
310
|
+
stderr=subprocess.DEVNULL,
|
|
311
|
+
timeout=60,
|
|
312
|
+
)
|
|
313
|
+
data = json.loads(output)
|
|
314
|
+
return round(float(data["totals"]["percent_covered"]), 2)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
logging.warning("Could not read coverage data from %s: %s", coverage_file, exc)
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _git_diff_stat(process_root: Path, base: str = "main") -> int:
|
|
321
|
+
try:
|
|
322
|
+
output = subprocess.check_output(
|
|
323
|
+
["git", "diff", f"{base}...HEAD", "--stat"],
|
|
324
|
+
cwd=process_root,
|
|
325
|
+
text=True,
|
|
326
|
+
stderr=subprocess.DEVNULL,
|
|
327
|
+
)
|
|
328
|
+
except Exception:
|
|
329
|
+
return 0
|
|
330
|
+
match = re.search(
|
|
331
|
+
r"(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?",
|
|
332
|
+
output,
|
|
333
|
+
)
|
|
334
|
+
if not match:
|
|
335
|
+
return 0
|
|
336
|
+
return sum(int(g or 0) for g in match.groups()[1:])
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _changed_files_count(process_root: Path, base: str = "main") -> int:
|
|
340
|
+
try:
|
|
341
|
+
output = subprocess.check_output(
|
|
342
|
+
["git", "diff", "--name-only", f"{base}...HEAD"],
|
|
343
|
+
cwd=process_root,
|
|
344
|
+
text=True,
|
|
345
|
+
stderr=subprocess.DEVNULL,
|
|
346
|
+
)
|
|
347
|
+
except Exception:
|
|
348
|
+
return 0
|
|
349
|
+
files = [line.strip() for line in output.splitlines() if line.strip()]
|
|
350
|
+
return len(files)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _run_audit(process_root: Path) -> dict[str, int] | None:
|
|
354
|
+
audit_script = process_root / "scripts" / "lincoln-audit.py"
|
|
355
|
+
if not audit_script.exists():
|
|
356
|
+
return None
|
|
357
|
+
try:
|
|
358
|
+
output = subprocess.check_output(
|
|
359
|
+
[sys.executable, str(audit_script), "--format", "json"],
|
|
360
|
+
cwd=process_root,
|
|
361
|
+
text=True,
|
|
362
|
+
stderr=subprocess.DEVNULL,
|
|
363
|
+
timeout=60,
|
|
364
|
+
)
|
|
365
|
+
except Exception:
|
|
366
|
+
return None
|
|
367
|
+
try:
|
|
368
|
+
data = json.loads(output)
|
|
369
|
+
except json.JSONDecodeError:
|
|
370
|
+
return None
|
|
371
|
+
results = data.get("results") if isinstance(data, dict) else None
|
|
372
|
+
if results is None:
|
|
373
|
+
results = data.get("checks") if isinstance(data, dict) else []
|
|
374
|
+
if not isinstance(results, list):
|
|
375
|
+
results = []
|
|
376
|
+
counts = Counter()
|
|
377
|
+
for item in results:
|
|
378
|
+
status = str(item.get("status", "")).upper()
|
|
379
|
+
if status in ("PASS", "WARN", "FAIL"):
|
|
380
|
+
counts[status] += 1
|
|
381
|
+
return dict(counts) if counts else None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _pr_event_timestamps(state: dict[str, Any]) -> dict[str, str]:
|
|
385
|
+
timestamps: dict[str, str] = {}
|
|
386
|
+
for node in state.get("nodes", []):
|
|
387
|
+
status = node.get("status")
|
|
388
|
+
ts = node.get("started_at") or node.get("completed_at") or node.get("timestamp")
|
|
389
|
+
if status in ("pr_submitted", "merged") and ts:
|
|
390
|
+
timestamps[status] = ts
|
|
391
|
+
return timestamps
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _session_duration(trace: list[dict[str, Any]]) -> tuple[int, str]:
|
|
395
|
+
if len(trace) < 2:
|
|
396
|
+
return 0, "estimated"
|
|
397
|
+
try:
|
|
398
|
+
start = _parse_iso(trace[0]["timestamp"])
|
|
399
|
+
end = _parse_iso(trace[-1]["timestamp"])
|
|
400
|
+
return int((end - start).total_seconds()), "exact"
|
|
401
|
+
except Exception:
|
|
402
|
+
return 0, "estimated"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _unique_files_touched(trace: list[dict[str, Any]]) -> int:
|
|
406
|
+
unique_files = set()
|
|
407
|
+
for e in trace:
|
|
408
|
+
if e.get("category") in ("read", "write", "edit"):
|
|
409
|
+
target = e.get("target", "")
|
|
410
|
+
if target:
|
|
411
|
+
unique_files.add(target)
|
|
412
|
+
return len(unique_files)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _retry_count(trace: list[dict[str, Any]]) -> int:
|
|
416
|
+
retries = 0
|
|
417
|
+
prev: dict[str, Any] | None = None
|
|
418
|
+
for e in trace:
|
|
419
|
+
if (
|
|
420
|
+
prev
|
|
421
|
+
and prev.get("exit_code", 0) != 0
|
|
422
|
+
and prev.get("tool") == e.get("tool")
|
|
423
|
+
and prev.get("target") == e.get("target")
|
|
424
|
+
):
|
|
425
|
+
retries += 1
|
|
426
|
+
prev = e
|
|
427
|
+
return retries
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _test_runs(trace: list[dict[str, Any]]) -> int:
|
|
431
|
+
return sum(
|
|
432
|
+
1
|
|
433
|
+
for e in trace
|
|
434
|
+
if e.get("category") == "bash"
|
|
435
|
+
and TEST_COMMAND_RE.search(
|
|
436
|
+
str(e.get("args_summary", {}).get("command", ""))
|
|
437
|
+
or str(e.get("target", ""))
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _compute_session_metrics(trace: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, str]]:
|
|
443
|
+
duration, duration_confidence = _session_duration(trace)
|
|
444
|
+
confidence = {"session_duration_seconds": duration_confidence}
|
|
445
|
+
|
|
446
|
+
session = {
|
|
447
|
+
"session_duration_seconds": duration,
|
|
448
|
+
"trace_line_count": len(trace),
|
|
449
|
+
"total_tool_calls": len(trace),
|
|
450
|
+
"tool_calls_by_category": dict(Counter(e.get("category", "other") for e in trace)),
|
|
451
|
+
"unique_skills": sorted(
|
|
452
|
+
{
|
|
453
|
+
e.get("target", "")
|
|
454
|
+
for e in trace
|
|
455
|
+
if e.get("category") == "skill" and e.get("target")
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
"unique_files_touched": _unique_files_touched(trace),
|
|
459
|
+
"test_runs": _test_runs(trace),
|
|
460
|
+
"error_count": sum(1 for e in trace if e.get("exit_code", 0) != 0),
|
|
461
|
+
"retry_count": _retry_count(trace),
|
|
462
|
+
}
|
|
463
|
+
for key in session:
|
|
464
|
+
if key not in confidence:
|
|
465
|
+
confidence[key] = "exact"
|
|
466
|
+
return session, confidence
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _stage_completion_stats(state: dict[str, Any]) -> tuple[int, int, float]:
|
|
470
|
+
nodes = state.get("nodes", [])
|
|
471
|
+
total = len(nodes)
|
|
472
|
+
completed = sum(1 for n in nodes if n.get("status") == "completed")
|
|
473
|
+
failed = sum(1 for n in nodes if n.get("status") == "validation_failed")
|
|
474
|
+
rate = round(completed / total, 2) if total else 0.0
|
|
475
|
+
return failed, completed, rate
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _stage_rework_count(stage_order: list[str]) -> int:
|
|
479
|
+
seen: set[str] = set()
|
|
480
|
+
count = 0
|
|
481
|
+
for stage in stage_order:
|
|
482
|
+
if stage in seen:
|
|
483
|
+
count += 1
|
|
484
|
+
seen.add(stage)
|
|
485
|
+
return count
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _workflow_adherence_score(
|
|
489
|
+
stage_order: list[str], workflow_steps: list[dict[str, Any]]
|
|
490
|
+
) -> float:
|
|
491
|
+
template_ids = [s.get("id") for s in workflow_steps if s.get("id")]
|
|
492
|
+
if not template_ids:
|
|
493
|
+
return 1.0
|
|
494
|
+
lcs = _lcs_length(stage_order, template_ids)
|
|
495
|
+
return round(lcs / len(template_ids), 2)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _human_gate_stats(
|
|
499
|
+
state: dict[str, Any],
|
|
500
|
+
human_gate_stages: set[str],
|
|
501
|
+
stage_durations: dict[str, int],
|
|
502
|
+
) -> tuple[int, float, int]:
|
|
503
|
+
passed = 0
|
|
504
|
+
for stage_id in human_gate_stages:
|
|
505
|
+
node = None
|
|
506
|
+
if _is_legacy_state(state):
|
|
507
|
+
node = state.get("stages", {}).get(stage_id, {})
|
|
508
|
+
else:
|
|
509
|
+
from scripts.stage_loader import get_latest_node_for_stage
|
|
510
|
+
node = get_latest_node_for_stage(state, stage_id)
|
|
511
|
+
if node and (
|
|
512
|
+
node.get("status") == "completed"
|
|
513
|
+
or node.get("gate_passed")
|
|
514
|
+
or node.get("human_gate_passed")
|
|
515
|
+
):
|
|
516
|
+
passed += 1
|
|
517
|
+
count = len(human_gate_stages)
|
|
518
|
+
rate = round(passed / count, 2) if count else 1.0
|
|
519
|
+
wait = sum(stage_durations.get(s, 0) for s in human_gate_stages)
|
|
520
|
+
return count, rate, wait
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _combined_stage_durations(
|
|
524
|
+
trace: list[dict[str, Any]], state: dict[str, Any]
|
|
525
|
+
) -> dict[str, int]:
|
|
526
|
+
durations_trace = _stage_durations(trace)
|
|
527
|
+
durations_node = _node_durations(state)
|
|
528
|
+
return {
|
|
529
|
+
k: durations_trace.get(k, durations_node.get(k, 0))
|
|
530
|
+
for k in set(durations_trace) | set(durations_node)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _human_gate_stages(
|
|
535
|
+
stage_order: list[str], workflow_steps: list[dict[str, Any]]
|
|
536
|
+
) -> set[str]:
|
|
537
|
+
return {
|
|
538
|
+
s.get("id")
|
|
539
|
+
for s in workflow_steps
|
|
540
|
+
if s.get("human_gate") and s.get("id") in stage_order
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _compute_workflow_metrics(
|
|
545
|
+
trace: list[dict[str, Any]],
|
|
546
|
+
state: dict[str, Any],
|
|
547
|
+
workflow_steps: list[dict[str, Any]],
|
|
548
|
+
variables: dict[str, Any],
|
|
549
|
+
process_root: Path,
|
|
550
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
551
|
+
stage_order = _stage_order_from_trace(trace)
|
|
552
|
+
stage_durations = _combined_stage_durations(trace, state)
|
|
553
|
+
|
|
554
|
+
failed_nodes, completed_nodes, stage_completion_rate = _stage_completion_stats(state)
|
|
555
|
+
stage_rework_count = _stage_rework_count(stage_order)
|
|
556
|
+
|
|
557
|
+
current_stage = state.get("current_run", {}).get("current_stage")
|
|
558
|
+
artifact_completion_rate = _artifact_completion_rate(
|
|
559
|
+
current_stage, workflow_steps, variables, process_root
|
|
560
|
+
)
|
|
561
|
+
workflow_adherence_score = _workflow_adherence_score(stage_order, workflow_steps)
|
|
562
|
+
|
|
563
|
+
human_gate_stages = _human_gate_stages(stage_order, workflow_steps)
|
|
564
|
+
human_gate_count, human_gate_pass_rate, human_gate_wait_seconds = _human_gate_stats(
|
|
565
|
+
state, human_gate_stages, stage_durations
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
workflow = {
|
|
569
|
+
"stage_transition_count": max(0, len(stage_order) - 1),
|
|
570
|
+
"stage_durations": stage_durations,
|
|
571
|
+
"stage_completion_rate": stage_completion_rate,
|
|
572
|
+
"stage_rework_count": stage_rework_count,
|
|
573
|
+
"artifact_completion_rate": artifact_completion_rate,
|
|
574
|
+
"validation_failure_count": failed_nodes,
|
|
575
|
+
"workflow_adherence_score": workflow_adherence_score,
|
|
576
|
+
"human_gate_count": human_gate_count,
|
|
577
|
+
"human_gate_wait_seconds": human_gate_wait_seconds,
|
|
578
|
+
"human_gate_pass_rate": human_gate_pass_rate,
|
|
579
|
+
"build_codebase_knowledge_duration_seconds": stage_durations.get("build-codebase-knowledge", 0),
|
|
580
|
+
}
|
|
581
|
+
confidence = {
|
|
582
|
+
"stage_durations": "estimated",
|
|
583
|
+
"human_gate_wait_seconds": "estimated",
|
|
584
|
+
"artifact_completion_rate": "exact",
|
|
585
|
+
"workflow_adherence_score": "exact",
|
|
586
|
+
"build_codebase_knowledge_duration_seconds": "estimated",
|
|
587
|
+
}
|
|
588
|
+
return workflow, confidence
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _session_start_metrics(process_root: Path, process_slug: str) -> dict[str, Any]:
|
|
592
|
+
"""Load the session-start token/byte metrics written by the hook (#63)."""
|
|
593
|
+
path = process_root / process_slug / ".trace" / "session-start-metrics.json"
|
|
594
|
+
if not path.exists():
|
|
595
|
+
return {}
|
|
596
|
+
try:
|
|
597
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
598
|
+
except Exception:
|
|
599
|
+
return {}
|
|
600
|
+
if not isinstance(data, dict):
|
|
601
|
+
return {}
|
|
602
|
+
return {
|
|
603
|
+
"session_start_bytes": data.get("bytes"),
|
|
604
|
+
"session_start_chars": data.get("chars"),
|
|
605
|
+
"session_start_tokens": data.get("tokens"),
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _is_handoff_command(entry: dict[str, Any]) -> bool:
|
|
610
|
+
# Handoffs are recorded as structured LincolnHandoff trace entries by
|
|
611
|
+
# stage_loader, so we detect them by category rather than regex-parsing Bash text.
|
|
612
|
+
return entry.get("category") == "handoff"
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _handoff_count(trace: list[dict[str, Any]]) -> int:
|
|
616
|
+
return sum(1 for e in trace if _is_handoff_command(e))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _time_to_first_handoff(trace: list[dict[str, Any]]) -> int:
|
|
620
|
+
count = _handoff_count(trace)
|
|
621
|
+
if not trace or not count:
|
|
622
|
+
return 0
|
|
623
|
+
first_ts = next((e.get("timestamp") for e in trace if _is_handoff_command(e)), None)
|
|
624
|
+
if not first_ts:
|
|
625
|
+
return 0
|
|
626
|
+
try:
|
|
627
|
+
return int((_parse_iso(first_ts) - _parse_iso(trace[0]["timestamp"])).total_seconds())
|
|
628
|
+
except Exception:
|
|
629
|
+
return 0
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _agent_switches(stage_order: list[str], primary_agents: dict[str, str]) -> int:
|
|
633
|
+
switches = 0
|
|
634
|
+
prev_stage: str | None = None
|
|
635
|
+
for stage in stage_order:
|
|
636
|
+
if prev_stage and primary_agents.get(prev_stage) and primary_agents.get(stage):
|
|
637
|
+
if primary_agents[prev_stage] != primary_agents[stage]:
|
|
638
|
+
switches += 1
|
|
639
|
+
prev_stage = stage
|
|
640
|
+
return switches
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _compute_collaboration_metrics(
|
|
644
|
+
trace: list[dict[str, Any]],
|
|
645
|
+
stage_order: list[str],
|
|
646
|
+
primary_agents: dict[str, str],
|
|
647
|
+
human_gate_count: int,
|
|
648
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
649
|
+
handoff_count = _handoff_count(trace)
|
|
650
|
+
collaboration = {
|
|
651
|
+
"handoff_count": handoff_count,
|
|
652
|
+
"agent_switches": _agent_switches(stage_order, primary_agents),
|
|
653
|
+
"pm_turns": human_gate_count,
|
|
654
|
+
"time_to_first_handoff": _time_to_first_handoff(trace),
|
|
655
|
+
}
|
|
656
|
+
confidence: dict[str, str] = {
|
|
657
|
+
"agent_switches": "exact" if primary_agents else "pending",
|
|
658
|
+
"pm_turns": "estimated",
|
|
659
|
+
"time_to_first_handoff": "exact" if handoff_count else "estimated",
|
|
660
|
+
}
|
|
661
|
+
return collaboration, confidence
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _compute_quality_metrics(
|
|
665
|
+
trace: list[dict[str, Any]],
|
|
666
|
+
state: dict[str, Any],
|
|
667
|
+
workflow_steps: list[dict[str, Any]],
|
|
668
|
+
variables: dict[str, Any],
|
|
669
|
+
process_root: Path,
|
|
670
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
671
|
+
confidence: dict[str, str] = {}
|
|
672
|
+
process_slug = get_process_slug(state, None)
|
|
673
|
+
|
|
674
|
+
requirements_score = _requirements_clarity_score(process_root, process_slug, variables)
|
|
675
|
+
design_completeness = _design_doc_completeness(
|
|
676
|
+
workflow_steps, variables, process_root, process_slug
|
|
677
|
+
)
|
|
678
|
+
tdd_phases = _tdd_plan_red_green_refactor(process_root, process_slug, variables)
|
|
679
|
+
openspec_count = _openspec_task_count(process_root, process_slug, variables)
|
|
680
|
+
static_pass = _static_check_pass(trace, process_root)
|
|
681
|
+
coverage = _test_coverage(process_root)
|
|
682
|
+
pr_size = _git_diff_stat(process_root)
|
|
683
|
+
audit_counts = _run_audit(process_root)
|
|
684
|
+
code_review_calls = sum(
|
|
685
|
+
1
|
|
686
|
+
for e in trace
|
|
687
|
+
if e.get("category") == "skill" and "code-review" in str(e.get("target", ""))
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
quality = {
|
|
691
|
+
"requirements_clarity_score": requirements_score,
|
|
692
|
+
"design_doc_completeness": design_completeness,
|
|
693
|
+
"tdd_plan_red_green_refactor": tdd_phases,
|
|
694
|
+
"openspec_task_count": openspec_count,
|
|
695
|
+
"static_check_pass": static_pass,
|
|
696
|
+
"test_coverage": coverage,
|
|
697
|
+
"pr_size": pr_size,
|
|
698
|
+
"audit_score": audit_counts,
|
|
699
|
+
"code_review_calls": code_review_calls,
|
|
700
|
+
}
|
|
701
|
+
confidence["test_coverage"] = "exact" if coverage is not None else "pending"
|
|
702
|
+
confidence["audit_score"] = "exact" if audit_counts is not None else "pending"
|
|
703
|
+
confidence["pr_size"] = "exact" if pr_size > 0 else "estimated"
|
|
704
|
+
return quality, confidence
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _pr_merge_info(state: dict[str, Any]) -> tuple[bool, int, str | None]:
|
|
708
|
+
pr_timestamps = _pr_event_timestamps(state)
|
|
709
|
+
pr_submitted_ts = pr_timestamps.get("pr_submitted")
|
|
710
|
+
merged_ts = pr_timestamps.get("merged")
|
|
711
|
+
pr_merged = bool(merged_ts) or any(
|
|
712
|
+
n.get("status") == "merged" for n in state.get("nodes", [])
|
|
713
|
+
)
|
|
714
|
+
latency = 0
|
|
715
|
+
if pr_submitted_ts and merged_ts:
|
|
716
|
+
try:
|
|
717
|
+
latency = int((_parse_iso(merged_ts) - _parse_iso(pr_submitted_ts)).total_seconds())
|
|
718
|
+
except Exception:
|
|
719
|
+
latency = 0
|
|
720
|
+
return pr_merged, latency, merged_ts
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _delta_seconds(start_ts: str | None, end_ts: str | None) -> int:
|
|
724
|
+
if not start_ts or not end_ts:
|
|
725
|
+
return 0
|
|
726
|
+
try:
|
|
727
|
+
return int((_parse_iso(end_ts) - _parse_iso(start_ts)).total_seconds())
|
|
728
|
+
except Exception:
|
|
729
|
+
return 0
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _compute_outcome_metrics(trace: list[dict[str, Any]], state: dict[str, Any]) -> dict[str, Any]:
|
|
733
|
+
pr_merged, pr_merge_latency_seconds, merged_ts = _pr_merge_info(state)
|
|
734
|
+
|
|
735
|
+
first_clarify_ts = next(
|
|
736
|
+
(e.get("timestamp") for e in trace if e.get("stage") == "clarify"),
|
|
737
|
+
trace[0].get("timestamp") if trace else None,
|
|
738
|
+
)
|
|
739
|
+
first_pr_ts = next(
|
|
740
|
+
(e.get("timestamp") for e in trace if e.get("tool") == "mcp__plugin_ecc_github__create_pull_request"),
|
|
741
|
+
None,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
"issue_closed": None,
|
|
746
|
+
"pr_merged": pr_merged,
|
|
747
|
+
"pr_merge_latency_seconds": pr_merge_latency_seconds,
|
|
748
|
+
"knowledge_synced": None,
|
|
749
|
+
"time_to_pr_seconds": _delta_seconds(first_clarify_ts, first_pr_ts),
|
|
750
|
+
"time_to_merge_seconds": _delta_seconds(first_clarify_ts, merged_ts),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _outcome_confidence(outcome: dict[str, Any], merged_ts: str | None) -> dict[str, str]:
|
|
755
|
+
return {
|
|
756
|
+
"issue_closed": "pending",
|
|
757
|
+
"knowledge_synced": "pending",
|
|
758
|
+
"pr_merge_latency_seconds": "exact" if merged_ts else "pending",
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _with_s2_ratio(
|
|
763
|
+
outcome: dict[str, Any], session: dict[str, Any], process_root: Path
|
|
764
|
+
) -> dict[str, Any]:
|
|
765
|
+
changed_files = _changed_files_count(process_root)
|
|
766
|
+
unique_files_touched = session["unique_files_touched"]
|
|
767
|
+
ratio = round(unique_files_touched / max(1, changed_files), 2)
|
|
768
|
+
return {**outcome, "unique_files_touched_ratio": ratio}
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def compute_metrics(
|
|
772
|
+
trace: list[dict[str, Any]],
|
|
773
|
+
state: dict[str, Any],
|
|
774
|
+
scenario: str,
|
|
775
|
+
process_root: Path,
|
|
776
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
777
|
+
"""Compute L1-L5 benchmark metrics from trace and state."""
|
|
778
|
+
trace = sorted(trace, key=lambda e: e.get("timestamp", ""))
|
|
779
|
+
variables = state.get("current_run", {}).get("variables", {})
|
|
780
|
+
workflow_steps = _workflow_steps(state)
|
|
781
|
+
primary_agents = _load_primary_agents(process_root)
|
|
782
|
+
|
|
783
|
+
session, conf_session = _compute_session_metrics(trace)
|
|
784
|
+
process_slug = get_process_slug(state, None)
|
|
785
|
+
session_start = _session_start_metrics(process_root, process_slug)
|
|
786
|
+
if session_start:
|
|
787
|
+
session.update(session_start)
|
|
788
|
+
for key in session_start:
|
|
789
|
+
conf_session[key] = "exact"
|
|
790
|
+
workflow, conf_workflow = _compute_workflow_metrics(
|
|
791
|
+
trace, state, workflow_steps, variables, process_root
|
|
792
|
+
)
|
|
793
|
+
collaboration, conf_collab = _compute_collaboration_metrics(
|
|
794
|
+
trace, _stage_order_from_trace(trace), primary_agents, workflow["human_gate_count"]
|
|
795
|
+
)
|
|
796
|
+
quality, conf_quality = _compute_quality_metrics(
|
|
797
|
+
trace, state, workflow_steps, variables, process_root
|
|
798
|
+
)
|
|
799
|
+
outcome = _compute_outcome_metrics(trace, state)
|
|
800
|
+
merged_ts = _pr_event_timestamps(state).get("merged")
|
|
801
|
+
|
|
802
|
+
confidence: dict[str, str] = {}
|
|
803
|
+
confidence.update(conf_session)
|
|
804
|
+
confidence.update(conf_workflow)
|
|
805
|
+
confidence.update(conf_collab)
|
|
806
|
+
confidence.update(conf_quality)
|
|
807
|
+
confidence.update(_outcome_confidence(outcome, merged_ts))
|
|
808
|
+
outcome = _with_s2_ratio(outcome, session, process_root)
|
|
809
|
+
confidence["unique_files_touched_ratio"] = "estimated"
|
|
810
|
+
|
|
811
|
+
metrics = {
|
|
812
|
+
"session": session,
|
|
813
|
+
"workflow": workflow,
|
|
814
|
+
"collaboration": collaboration,
|
|
815
|
+
"quality": quality,
|
|
816
|
+
"outcome": outcome,
|
|
817
|
+
}
|
|
818
|
+
return metrics, confidence
|