@sushanglewis/lincoln 1.5.0 → 1.5.2
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 +1286 -916
- 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,448 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Lincoln branch status reporter.
|
|
4
|
+
|
|
5
|
+
Reports the current Lincoln branch state including stage, status, waiting_for,
|
|
6
|
+
loaded context files, required skills, artifacts, and next action.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scripts/lincoln-status.py [--format json|table|markdown] [--branch <branch>]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
# Re-use stage_loader helpers
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
25
|
+
from scripts.stage_loader import ( # noqa: E402
|
|
26
|
+
PROJECT_ROOT,
|
|
27
|
+
STATE_PATH,
|
|
28
|
+
LEGACY_STATE_PATH,
|
|
29
|
+
STAGES_DIR,
|
|
30
|
+
compute_next_stage,
|
|
31
|
+
load_state,
|
|
32
|
+
load_stage_yaml,
|
|
33
|
+
load_workflow,
|
|
34
|
+
load_skill_routing,
|
|
35
|
+
get_stage_skills,
|
|
36
|
+
get_latest_node_for_stage,
|
|
37
|
+
get_variables,
|
|
38
|
+
interpolate_artifact,
|
|
39
|
+
_is_legacy_state,
|
|
40
|
+
now_iso,
|
|
41
|
+
resolve_state_path,
|
|
42
|
+
)
|
|
43
|
+
from scripts.lincoln_paths import get_process_slug
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_waiting_for(stage_status: str | None, stage_def: dict[str, Any]) -> str:
|
|
47
|
+
"""Determine who/what is being waited for based on stage status."""
|
|
48
|
+
if stage_status == "waiting_for_human":
|
|
49
|
+
return "pm"
|
|
50
|
+
if stage_status == "validation_failed":
|
|
51
|
+
return "agent-fix"
|
|
52
|
+
if stage_status == "in_progress":
|
|
53
|
+
return "agent"
|
|
54
|
+
if stage_status == "completed":
|
|
55
|
+
return "next-role"
|
|
56
|
+
if stage_status == "entry_validating":
|
|
57
|
+
return "agent"
|
|
58
|
+
if stage_status == "not_started":
|
|
59
|
+
# If stage has human_gate and hasn't started, waiting for human to kick it off
|
|
60
|
+
if stage_def.get("human_gate", False):
|
|
61
|
+
return "pm"
|
|
62
|
+
return "agent"
|
|
63
|
+
return "none"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_loaded_context(stage_id: str) -> list[str]:
|
|
67
|
+
"""Return the path to the stage YAML context file."""
|
|
68
|
+
stage_yaml = STAGES_DIR / f"{stage_id}.yaml"
|
|
69
|
+
if stage_yaml.exists():
|
|
70
|
+
return [str(stage_yaml.relative_to(PROJECT_ROOT))]
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_required_skills(stage_id: str) -> dict[str, list[str]]:
|
|
75
|
+
"""Read skill routing for required/optional skills for the stage."""
|
|
76
|
+
routing_data = load_skill_routing()
|
|
77
|
+
return get_stage_skills(routing_data, stage_id)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_required_artifacts(stage_id: str, state: dict[str, Any] | None = None, state_file: Path | None = None) -> list[str]:
|
|
81
|
+
"""Return artifact paths declared in the stage YAML, with variables resolved."""
|
|
82
|
+
try:
|
|
83
|
+
stage = load_stage_yaml(stage_id)
|
|
84
|
+
except Exception:
|
|
85
|
+
return []
|
|
86
|
+
artifacts = stage.get("artifacts", {})
|
|
87
|
+
paths = artifacts.get("required", [])
|
|
88
|
+
return [interpolate_artifact(str(art), state, state_file or resolve_state_path(None)) for art in paths]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_next_action(state: dict[str, Any], stage_id: str | None, workflow: dict[str, Any]) -> str:
|
|
92
|
+
"""Generate a human-readable one-liner describing the next action."""
|
|
93
|
+
if not stage_id:
|
|
94
|
+
run_status = state.get("current_run", {}).get("status", "unknown")
|
|
95
|
+
if run_status == "completed":
|
|
96
|
+
return "Workflow complete. No further action required."
|
|
97
|
+
return "No active stage. Run recovery to determine resume point."
|
|
98
|
+
|
|
99
|
+
if _is_legacy_state(state):
|
|
100
|
+
stage_state = state.get("stages", {}).get(stage_id, {})
|
|
101
|
+
stage_status = stage_state.get("status", "unknown")
|
|
102
|
+
else:
|
|
103
|
+
latest_node = get_latest_node_for_stage(state, stage_id)
|
|
104
|
+
stage_state = latest_node or {}
|
|
105
|
+
stage_status = stage_state.get("status", state.get("current_run", {}).get("status", "unknown"))
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
stage_yaml = load_stage_yaml(stage_id)
|
|
109
|
+
except Exception:
|
|
110
|
+
stage_yaml = {}
|
|
111
|
+
human_gate = stage_yaml.get("human_gate", False)
|
|
112
|
+
waiting_for = get_waiting_for(stage_status, stage_yaml)
|
|
113
|
+
|
|
114
|
+
if stage_status == "not_started":
|
|
115
|
+
if human_gate:
|
|
116
|
+
return f"Stage '{stage_id}' not started. Human confirmation required to begin."
|
|
117
|
+
return f"Stage '{stage_id}' ready to start. Run entry validation and begin work."
|
|
118
|
+
|
|
119
|
+
if stage_status == "entry_validating":
|
|
120
|
+
return f"Entry validation in progress for stage '{stage_id}'."
|
|
121
|
+
|
|
122
|
+
if stage_status == "in_progress":
|
|
123
|
+
if human_gate and not stage_state.get("gate_passed") and not stage_state.get("human_gate_passed"):
|
|
124
|
+
return f"Stage '{stage_id}' in progress. Complete work and await human gate approval."
|
|
125
|
+
return f"Stage '{stage_id}' in progress. Complete work and run exit validation."
|
|
126
|
+
|
|
127
|
+
if stage_status == "waiting_for_human":
|
|
128
|
+
return f"Stage '{stage_id}' paused for human review/approval."
|
|
129
|
+
|
|
130
|
+
if stage_status == "validation_failed":
|
|
131
|
+
retry = stage_state.get("retry_count", 0)
|
|
132
|
+
return f"Stage '{stage_id}' validation failed (retry {retry}). Fix issues and re-run validation."
|
|
133
|
+
|
|
134
|
+
if stage_status == "completed":
|
|
135
|
+
next_stage = compute_next_stage(workflow, stage_id)
|
|
136
|
+
if next_stage:
|
|
137
|
+
return f"Stage '{stage_id}' completed. Proceed to next stage: '{next_stage}'."
|
|
138
|
+
return f"Stage '{stage_id}' completed. This is the final stage."
|
|
139
|
+
|
|
140
|
+
return f"Stage '{stage_id}' status: {stage_status}. Waiting for: {waiting_for}."
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def compute_metrics(state: dict[str, Any]) -> dict[str, Any]:
|
|
144
|
+
"""Compute workflow metrics summary."""
|
|
145
|
+
if _is_legacy_state(state):
|
|
146
|
+
stages = state.get("stages", {})
|
|
147
|
+
total = len(stages)
|
|
148
|
+
completed = sum(1 for s in stages.values() if s.get("status") == "completed")
|
|
149
|
+
failed = sum(1 for s in stages.values() if s.get("status") == "validation_failed")
|
|
150
|
+
waiting_for_human = sum(
|
|
151
|
+
1 for sid, s in stages.items()
|
|
152
|
+
if s.get("status") in ("waiting_for_human", "not_started")
|
|
153
|
+
and _stage_has_human_gate(sid)
|
|
154
|
+
)
|
|
155
|
+
return {
|
|
156
|
+
"total_stages": total,
|
|
157
|
+
"completed": completed,
|
|
158
|
+
"failed": failed,
|
|
159
|
+
"waiting_for_human": waiting_for_human,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# New schema: metrics based on nodes array
|
|
163
|
+
nodes = state.get("nodes", [])
|
|
164
|
+
total_nodes = len(nodes)
|
|
165
|
+
completed = sum(1 for n in nodes if n.get("status") == "completed")
|
|
166
|
+
failed = sum(1 for n in nodes if n.get("status") == "validation_failed")
|
|
167
|
+
|
|
168
|
+
# Count unique stages that have been touched
|
|
169
|
+
touched_stages = set(n.get("stage_id") for n in nodes if n.get("stage_id"))
|
|
170
|
+
total_stages = len(touched_stages)
|
|
171
|
+
|
|
172
|
+
# Waiting for human: stages with human_gate where latest node is not completed/gate_passed
|
|
173
|
+
template_name = state.get("workflow", {}).get("template")
|
|
174
|
+
try:
|
|
175
|
+
workflow = load_workflow(template_name)
|
|
176
|
+
except Exception:
|
|
177
|
+
workflow = {"steps": []}
|
|
178
|
+
|
|
179
|
+
waiting_for_human = 0
|
|
180
|
+
for stage_id in touched_stages:
|
|
181
|
+
latest = get_latest_node_for_stage(state, stage_id)
|
|
182
|
+
if latest and latest.get("status") in ("not_started", "waiting_for_human"):
|
|
183
|
+
if _stage_has_human_gate(stage_id):
|
|
184
|
+
waiting_for_human += 1
|
|
185
|
+
elif latest and latest.get("status") == "in_progress":
|
|
186
|
+
if _stage_has_human_gate(stage_id) and not latest.get("gate_passed"):
|
|
187
|
+
waiting_for_human += 1
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"total_stages": total_stages,
|
|
191
|
+
"completed": completed,
|
|
192
|
+
"failed": failed,
|
|
193
|
+
"waiting_for_human": waiting_for_human,
|
|
194
|
+
"total_nodes": total_nodes,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _stage_has_human_gate(stage_id: str) -> bool:
|
|
199
|
+
"""Check if a stage has human_gate enabled by reading stage YAML."""
|
|
200
|
+
try:
|
|
201
|
+
stage = load_stage_yaml(stage_id)
|
|
202
|
+
return stage.get("human_gate", False)
|
|
203
|
+
except Exception:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def build_status_report(state: dict[str, Any], state_file: Path | None = None) -> dict[str, Any]:
|
|
208
|
+
"""Build the full status report dictionary."""
|
|
209
|
+
current_run = state.get("current_run", {})
|
|
210
|
+
stage_id = current_run.get("current_stage")
|
|
211
|
+
template_name = state.get("workflow", {}).get("template") or current_run.get("workflow_template")
|
|
212
|
+
|
|
213
|
+
workflow = load_workflow(template_name)
|
|
214
|
+
|
|
215
|
+
# Handle sentinel status values that are not real stage IDs
|
|
216
|
+
_stage_id = stage_id
|
|
217
|
+
if stage_id in ("not_started", "completed", "in_progress", "validation_failed", "waiting_for_human", "entry_validating"):
|
|
218
|
+
_stage_id = None
|
|
219
|
+
|
|
220
|
+
if _is_legacy_state(state):
|
|
221
|
+
stage_state = state.get("stages", {}).get(stage_id, {}) if stage_id else {}
|
|
222
|
+
stage_status = stage_state.get("status", "unknown")
|
|
223
|
+
entry_checks_passed = stage_state.get("entry_checks_passed")
|
|
224
|
+
exit_checks_passed = stage_state.get("exit_checks_passed")
|
|
225
|
+
human_gate_passed = stage_state.get("human_gate_passed")
|
|
226
|
+
retry_count = stage_state.get("retry_count", 0)
|
|
227
|
+
stage_yaml = {}
|
|
228
|
+
else:
|
|
229
|
+
latest_node = get_latest_node_for_stage(state, _stage_id) if _stage_id else None
|
|
230
|
+
stage_state = latest_node or {}
|
|
231
|
+
stage_status = stage_state.get("status", current_run.get("status", "unknown")) if _stage_id else current_run.get("status", "unknown")
|
|
232
|
+
entry_checks_passed = None
|
|
233
|
+
exit_checks_passed = None
|
|
234
|
+
human_gate_passed = stage_state.get("gate_passed")
|
|
235
|
+
retry_count = 0
|
|
236
|
+
try:
|
|
237
|
+
stage_yaml = load_stage_yaml(_stage_id) if _stage_id else {}
|
|
238
|
+
except Exception:
|
|
239
|
+
stage_yaml = {}
|
|
240
|
+
|
|
241
|
+
agent = stage_yaml.get("agent", {}) if stage_yaml else {}
|
|
242
|
+
skills = get_required_skills(_stage_id) if _stage_id else {"required": [], "optional": []}
|
|
243
|
+
artifacts = get_required_artifacts(_stage_id, state, state_file) if _stage_id else []
|
|
244
|
+
|
|
245
|
+
resolved_state_file = state_file or resolve_state_path(None)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"process_slug": get_process_slug(state, resolved_state_file),
|
|
249
|
+
"state_file": str(resolved_state_file.relative_to(PROJECT_ROOT) if resolved_state_file.is_relative_to(PROJECT_ROOT) else resolved_state_file),
|
|
250
|
+
"branch": current_run.get("branch", "unknown"),
|
|
251
|
+
"run_id": current_run.get("run_id", "unknown"),
|
|
252
|
+
"workflow_template": template_name or "unknown",
|
|
253
|
+
"current_stage": stage_id,
|
|
254
|
+
"previous_stage": current_run.get("previous_stage"),
|
|
255
|
+
"run_status": current_run.get("status", "unknown"),
|
|
256
|
+
"stage_status": stage_status,
|
|
257
|
+
"entry_checks_passed": entry_checks_passed,
|
|
258
|
+
"exit_checks_passed": exit_checks_passed,
|
|
259
|
+
"human_gate_passed": human_gate_passed,
|
|
260
|
+
"retry_count": retry_count,
|
|
261
|
+
"waiting_for": get_waiting_for(stage_status, stage_yaml) if _stage_id else "none",
|
|
262
|
+
"loaded_context": get_loaded_context(_stage_id) if _stage_id else [],
|
|
263
|
+
"required_skills": skills.get("required", []),
|
|
264
|
+
"optional_skills": skills.get("optional", []),
|
|
265
|
+
"required_artifacts": artifacts,
|
|
266
|
+
"primary_agent": agent.get("primary") if _stage_id else None,
|
|
267
|
+
"review_agents": agent.get("reviewers", []) if _stage_id else None,
|
|
268
|
+
"handoff_to": agent.get("handoff_to") if _stage_id else None,
|
|
269
|
+
"next_action": get_next_action(state, _stage_id, workflow),
|
|
270
|
+
"metrics": compute_metrics(state),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def format_json(report: dict[str, Any]) -> str:
|
|
275
|
+
return json.dumps(report, ensure_ascii=False, indent=2)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def format_table(report: dict[str, Any]) -> str:
|
|
279
|
+
lines = []
|
|
280
|
+
lines.append("=" * 70)
|
|
281
|
+
lines.append("LINCOLN BRANCH STATUS")
|
|
282
|
+
lines.append("=" * 70)
|
|
283
|
+
lines.append(f"{'Branch:':<25} {report['branch']}")
|
|
284
|
+
lines.append(f"{'Process Package:':<25} {report.get('process_slug', 'unknown')}")
|
|
285
|
+
lines.append(f"{'Run ID:':<25} {report['run_id']}")
|
|
286
|
+
lines.append(f"{'Workflow Template:':<25} {report['workflow_template']}")
|
|
287
|
+
lines.append(f"{'Current Stage:':<25} {report['current_stage'] or '(none)'}")
|
|
288
|
+
lines.append(f"{'Previous Stage:':<25} {report['previous_stage'] or '(none)'}")
|
|
289
|
+
lines.append(f"{'Run Status:':<25} {report['run_status']}")
|
|
290
|
+
lines.append(f"{'Stage Status:':<25} {report['stage_status']}")
|
|
291
|
+
lines.append(f"{'Entry Checks:':<25} {report['entry_checks_passed']}")
|
|
292
|
+
lines.append(f"{'Exit Checks:':<25} {report['exit_checks_passed']}")
|
|
293
|
+
lines.append(f"{'Human Gate:':<25} {report['human_gate_passed']}")
|
|
294
|
+
lines.append(f"{'Retry Count:':<25} {report['retry_count']}")
|
|
295
|
+
lines.append(f"{'Waiting For:':<25} {report['waiting_for']}")
|
|
296
|
+
lines.append(f"{'Primary Agent:':<25} {report.get('primary_agent') or '(none)'}")
|
|
297
|
+
lines.append(f"{'Review Agents:':<25} {', '.join(report.get('review_agents') or []) or '(none)'}")
|
|
298
|
+
lines.append(f"{'Handoff To:':<25} {report.get('handoff_to') or '(none)'}")
|
|
299
|
+
lines.append("")
|
|
300
|
+
lines.append("-" * 70)
|
|
301
|
+
lines.append("LOADED CONTEXT")
|
|
302
|
+
lines.append("-" * 70)
|
|
303
|
+
for ctx in report["loaded_context"]:
|
|
304
|
+
lines.append(f" - {ctx}")
|
|
305
|
+
if not report["loaded_context"]:
|
|
306
|
+
lines.append(" (none)")
|
|
307
|
+
lines.append("")
|
|
308
|
+
lines.append("-" * 70)
|
|
309
|
+
lines.append("REQUIRED SKILLS")
|
|
310
|
+
lines.append("-" * 70)
|
|
311
|
+
for skill in report["required_skills"]:
|
|
312
|
+
lines.append(f" [R] {skill}")
|
|
313
|
+
for skill in report.get("optional_skills", []):
|
|
314
|
+
lines.append(f" [O] {skill}")
|
|
315
|
+
if not report["required_skills"] and not report.get("optional_skills"):
|
|
316
|
+
lines.append(" (none configured)")
|
|
317
|
+
lines.append("")
|
|
318
|
+
lines.append("-" * 70)
|
|
319
|
+
lines.append("REQUIRED ARTIFACTS")
|
|
320
|
+
lines.append("-" * 70)
|
|
321
|
+
for art in report["required_artifacts"]:
|
|
322
|
+
lines.append(f" - {art}")
|
|
323
|
+
if not report["required_artifacts"]:
|
|
324
|
+
lines.append(" (none)")
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append("-" * 70)
|
|
327
|
+
lines.append("NEXT ACTION")
|
|
328
|
+
lines.append("-" * 70)
|
|
329
|
+
lines.append(f" {report['next_action']}")
|
|
330
|
+
lines.append("")
|
|
331
|
+
lines.append("-" * 70)
|
|
332
|
+
lines.append("METRICS")
|
|
333
|
+
lines.append("-" * 70)
|
|
334
|
+
m = report["metrics"]
|
|
335
|
+
lines.append(f" Total stages: {m['total_stages']}")
|
|
336
|
+
lines.append(f" Completed: {m['completed']}")
|
|
337
|
+
lines.append(f" Failed: {m['failed']}")
|
|
338
|
+
lines.append(f" Waiting for human: {m['waiting_for_human']}")
|
|
339
|
+
if "total_nodes" in m:
|
|
340
|
+
lines.append(f" Total nodes: {m['total_nodes']}")
|
|
341
|
+
lines.append("=" * 70)
|
|
342
|
+
return "\n".join(lines)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def format_markdown(report: dict[str, Any]) -> str:
|
|
346
|
+
lines = []
|
|
347
|
+
lines.append("# Lincoln Branch Status")
|
|
348
|
+
lines.append("")
|
|
349
|
+
lines.append("| Field | Value |")
|
|
350
|
+
lines.append("|-------|-------|")
|
|
351
|
+
lines.append(f"| Branch | `{report['branch']}` |")
|
|
352
|
+
lines.append(f"| Process Package | `{report.get('process_slug', 'unknown')}` |")
|
|
353
|
+
lines.append(f"| Run ID | `{report['run_id']}` |")
|
|
354
|
+
lines.append(f"| Workflow Template | `{report['workflow_template']}` |")
|
|
355
|
+
lines.append(f"| Current Stage | `{report['current_stage'] or 'none'}` |")
|
|
356
|
+
lines.append(f"| Previous Stage | `{report['previous_stage'] or 'none'}` |")
|
|
357
|
+
lines.append(f"| Run Status | `{report['run_status']}` |")
|
|
358
|
+
lines.append(f"| Stage Status | `{report['stage_status']}` |")
|
|
359
|
+
lines.append(f"| Entry Checks Passed | `{report['entry_checks_passed']}` |")
|
|
360
|
+
lines.append(f"| Exit Checks Passed | `{report['exit_checks_passed']}` |")
|
|
361
|
+
lines.append(f"| Human Gate Passed | `{report['human_gate_passed']}` |")
|
|
362
|
+
lines.append(f"| Retry Count | `{report['retry_count']}` |")
|
|
363
|
+
lines.append(f"| Waiting For | `{report['waiting_for']}` |")
|
|
364
|
+
lines.append(f"| Primary Agent | `{report.get('primary_agent') or 'none'}` |")
|
|
365
|
+
lines.append(f"| Review Agents | `{', '.join(report.get('review_agents') or []) or 'none'}` |")
|
|
366
|
+
lines.append(f"| Handoff To | `{report.get('handoff_to') or 'none'}` |")
|
|
367
|
+
lines.append("")
|
|
368
|
+
lines.append("## Loaded Context")
|
|
369
|
+
if report["loaded_context"]:
|
|
370
|
+
for ctx in report["loaded_context"]:
|
|
371
|
+
lines.append(f"- `{ctx}`")
|
|
372
|
+
else:
|
|
373
|
+
lines.append("_No context files found._")
|
|
374
|
+
lines.append("")
|
|
375
|
+
lines.append("## Required Skills")
|
|
376
|
+
if report["required_skills"]:
|
|
377
|
+
lines.append("**Required:**")
|
|
378
|
+
for skill in report["required_skills"]:
|
|
379
|
+
lines.append(f"- `{skill}`")
|
|
380
|
+
if report.get("optional_skills"):
|
|
381
|
+
lines.append("**Optional:**")
|
|
382
|
+
for skill in report["optional_skills"]:
|
|
383
|
+
lines.append(f"- `{skill}`")
|
|
384
|
+
if not report["required_skills"] and not report.get("optional_skills"):
|
|
385
|
+
lines.append("_No skills configured._")
|
|
386
|
+
lines.append("")
|
|
387
|
+
lines.append("## Required Artifacts")
|
|
388
|
+
if report["required_artifacts"]:
|
|
389
|
+
for art in report["required_artifacts"]:
|
|
390
|
+
lines.append(f"- `{art}`")
|
|
391
|
+
else:
|
|
392
|
+
lines.append("_No artifacts required._")
|
|
393
|
+
lines.append("")
|
|
394
|
+
lines.append("## Next Action")
|
|
395
|
+
lines.append(f"> {report['next_action']}")
|
|
396
|
+
lines.append("")
|
|
397
|
+
lines.append("## Metrics")
|
|
398
|
+
m = report["metrics"]
|
|
399
|
+
lines.append(f"- **Total stages:** {m['total_stages']}")
|
|
400
|
+
lines.append(f"- **Completed:** {m['completed']}")
|
|
401
|
+
lines.append(f"- **Failed:** {m['failed']}")
|
|
402
|
+
lines.append(f"- **Waiting for human:** {m['waiting_for_human']}")
|
|
403
|
+
if "total_nodes" in m:
|
|
404
|
+
lines.append(f"- **Total nodes:** {m['total_nodes']}")
|
|
405
|
+
return "\n".join(lines)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def main() -> int:
|
|
409
|
+
parser = argparse.ArgumentParser(description="Lincoln branch status reporter")
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"--format",
|
|
412
|
+
choices=["json", "table", "markdown"],
|
|
413
|
+
default="table",
|
|
414
|
+
help="Output format (default: table)",
|
|
415
|
+
)
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"--branch",
|
|
418
|
+
help="Branch name (default: read from workflow-stage.yaml)",
|
|
419
|
+
)
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"--state-file",
|
|
422
|
+
type=Path,
|
|
423
|
+
default=None,
|
|
424
|
+
help="Path to workflow-stage.yaml",
|
|
425
|
+
)
|
|
426
|
+
args = parser.parse_args()
|
|
427
|
+
|
|
428
|
+
state_file = resolve_state_path(args.state_file)
|
|
429
|
+
state = load_state(state_file)
|
|
430
|
+
|
|
431
|
+
# If --branch is specified but differs from state, note it but still report state
|
|
432
|
+
report = build_status_report(state, state_file)
|
|
433
|
+
report["state_file"] = str(state_file.relative_to(PROJECT_ROOT) if state_file.is_relative_to(PROJECT_ROOT) else state_file)
|
|
434
|
+
if args.branch and report["branch"] != args.branch:
|
|
435
|
+
report["_note"] = f"Requested branch '{args.branch}' but state shows '{report['branch']}'"
|
|
436
|
+
|
|
437
|
+
if args.format == "json":
|
|
438
|
+
print(format_json(report))
|
|
439
|
+
elif args.format == "table":
|
|
440
|
+
print(format_table(report))
|
|
441
|
+
elif args.format == "markdown":
|
|
442
|
+
print(format_markdown(report))
|
|
443
|
+
|
|
444
|
+
return 0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
sys.exit(main())
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lincoln benchmark report generator.
|
|
3
|
+
|
|
4
|
+
Computes L1-L5 metrics from session trace and workflow state, evaluates them
|
|
5
|
+
against scenario-specific thresholds, and writes a Markdown + JSON report pair
|
|
6
|
+
to the issue package's `benchmark/` directory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import uuid
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
25
|
+
|
|
26
|
+
from scripts.lincoln_benchmark_eval import evaluate_against_thresholds # noqa: E402
|
|
27
|
+
from scripts.lincoln_benchmark_metrics import compute_metrics, load_trace # noqa: E402
|
|
28
|
+
from scripts.lincoln_benchmark_report import build_markdown_report # noqa: E402
|
|
29
|
+
from scripts.lincoln_paths import get_process_slug # noqa: E402
|
|
30
|
+
|
|
31
|
+
REPORT_SCHEMA_VERSION = "1.0.0"
|
|
32
|
+
SESSION_STOP_DEDUP_SECONDS = 5
|
|
33
|
+
|
|
34
|
+
SCENARIO_MAP = {
|
|
35
|
+
"interview-to-knowledge": "S1",
|
|
36
|
+
"existing-project-iteration": "S2",
|
|
37
|
+
"bug-fix": "S3",
|
|
38
|
+
"design-spike": "S4",
|
|
39
|
+
"oss-first-design": "S5",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _now() -> str:
|
|
44
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect_scenario(state: dict[str, Any]) -> str:
|
|
48
|
+
template = (
|
|
49
|
+
state.get("workflow", {}).get("template")
|
|
50
|
+
or state.get("current_run", {}).get("workflow_template")
|
|
51
|
+
or ""
|
|
52
|
+
)
|
|
53
|
+
return SCENARIO_MAP.get(template, "S1")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _linear_id(state: dict[str, Any]) -> str:
|
|
57
|
+
"""Derive the Linear issue id from environment, branch, or state metadata."""
|
|
58
|
+
env_id = os.environ.get("LINCOLN_LINEAR_ID") or os.environ.get("LINEAR_ISSUE_ID")
|
|
59
|
+
if env_id:
|
|
60
|
+
match = re.search(r"\b(lew-\d+)\b", env_id, re.IGNORECASE)
|
|
61
|
+
return match.group(1).upper() if match else env_id
|
|
62
|
+
|
|
63
|
+
branch = state.get("current_run", {}).get("branch", "")
|
|
64
|
+
match = re.search(r"\b(lew-\d+)\b", branch, re.IGNORECASE)
|
|
65
|
+
if match:
|
|
66
|
+
return match.group(1).upper()
|
|
67
|
+
|
|
68
|
+
issue_number = state.get("current_run", {}).get("issue_number")
|
|
69
|
+
if issue_number and str(issue_number).strip():
|
|
70
|
+
return f"LEW-{issue_number}"
|
|
71
|
+
|
|
72
|
+
return "unknown"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _trace_files(process_dir: Path, project_root: Path) -> list[str]:
|
|
76
|
+
return sorted(
|
|
77
|
+
str(p.relative_to(project_root))
|
|
78
|
+
for p in (process_dir / ".trace").glob("lc-trace*.jsonl")
|
|
79
|
+
if p.exists()
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _build_metadata(
|
|
84
|
+
state: dict[str, Any], process_slug: str, trace_files: list[str]
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"process_slug": process_slug,
|
|
88
|
+
"run_id": state.get("current_run", {}).get("run_id", "unknown"),
|
|
89
|
+
"branch": state.get("current_run", {}).get("branch", "unknown"),
|
|
90
|
+
"issue_number": str(state.get("current_run", {}).get("issue_number", "")),
|
|
91
|
+
"linear_id": _linear_id(state),
|
|
92
|
+
"current_stage": state.get("current_run", {}).get("current_stage", "unknown"),
|
|
93
|
+
"trace_files": trace_files,
|
|
94
|
+
"workflow_template": state.get("workflow", {}).get("template", "unknown"),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _build_payload(
|
|
99
|
+
report_id: str,
|
|
100
|
+
generated_at: str,
|
|
101
|
+
scenario: str,
|
|
102
|
+
trigger: str,
|
|
103
|
+
metadata: dict[str, Any],
|
|
104
|
+
metrics: dict[str, Any],
|
|
105
|
+
confidence: dict[str, str],
|
|
106
|
+
evaluation: dict[str, Any],
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
return {
|
|
109
|
+
"report_id": report_id,
|
|
110
|
+
"schema_version": REPORT_SCHEMA_VERSION,
|
|
111
|
+
"scenario": scenario,
|
|
112
|
+
"trigger": trigger,
|
|
113
|
+
"generated_at": generated_at,
|
|
114
|
+
"metadata": metadata,
|
|
115
|
+
"metrics": metrics,
|
|
116
|
+
"confidence": confidence,
|
|
117
|
+
"evaluation": evaluation,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _atomic_write(path: Path, data: bytes) -> None:
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".tmp-lc-benchmark-")
|
|
124
|
+
try:
|
|
125
|
+
with os.fdopen(fd, "wb") as fh:
|
|
126
|
+
fh.write(data)
|
|
127
|
+
fh.flush()
|
|
128
|
+
os.replace(tmp, path)
|
|
129
|
+
except Exception:
|
|
130
|
+
try:
|
|
131
|
+
os.unlink(tmp)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _write_report_files(
|
|
138
|
+
benchmark_dir: Path,
|
|
139
|
+
trigger: str,
|
|
140
|
+
generated_at: str,
|
|
141
|
+
markdown: str,
|
|
142
|
+
payload: dict[str, Any],
|
|
143
|
+
) -> dict[str, Path]:
|
|
144
|
+
timestamp = generated_at.replace(":", "").replace("-", "").replace("T", "-")[:15]
|
|
145
|
+
md_path = benchmark_dir / f"lc-benchmark-{trigger}-{timestamp}.md"
|
|
146
|
+
json_path = benchmark_dir / f"lc-benchmark-{trigger}-{timestamp}.json"
|
|
147
|
+
_atomic_write(md_path, markdown.encode("utf-8"))
|
|
148
|
+
_atomic_write(json_path, json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8"))
|
|
149
|
+
return {"markdown": md_path, "json": json_path}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _recent_session_stop_exists(benchmark_dir: Path) -> bool:
|
|
153
|
+
latest = 0.0
|
|
154
|
+
for existing in benchmark_dir.glob("lc-benchmark-session_stop-*.json"):
|
|
155
|
+
try:
|
|
156
|
+
mtime = existing.stat().st_mtime
|
|
157
|
+
if mtime > latest:
|
|
158
|
+
latest = mtime
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
if not latest:
|
|
162
|
+
return False
|
|
163
|
+
return (datetime.now(timezone.utc).timestamp() - latest) < SESSION_STOP_DEDUP_SECONDS
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def write_benchmark_report(
|
|
167
|
+
state_file: Path,
|
|
168
|
+
trigger: str,
|
|
169
|
+
project_root: Path = _PROJECT_ROOT,
|
|
170
|
+
) -> dict[str, Path | None] | None:
|
|
171
|
+
from scripts.stage_loader import load_state
|
|
172
|
+
|
|
173
|
+
state_file = state_file.resolve()
|
|
174
|
+
# Derive the project root from the state file location; this prevents an
|
|
175
|
+
# attacker from using --project-root to redirect report writes or audit runs.
|
|
176
|
+
if len(state_file.parents) >= 2:
|
|
177
|
+
project_root = state_file.parents[1]
|
|
178
|
+
|
|
179
|
+
state = load_state(state_file)
|
|
180
|
+
process_slug = get_process_slug(state, state_file)
|
|
181
|
+
process_dir = project_root / process_slug
|
|
182
|
+
benchmark_dir = process_dir / "benchmark"
|
|
183
|
+
benchmark_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
if trigger == "session_stop" and _recent_session_stop_exists(benchmark_dir):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
scenario = detect_scenario(state)
|
|
189
|
+
trace = load_trace(project_root, process_slug)
|
|
190
|
+
metrics, confidence = compute_metrics(trace, state, scenario, project_root)
|
|
191
|
+
evaluation = evaluate_against_thresholds(metrics, scenario)
|
|
192
|
+
report_id = uuid.uuid4().hex
|
|
193
|
+
generated_at = _now()
|
|
194
|
+
trace_files = _trace_files(process_dir, project_root)
|
|
195
|
+
metadata = _build_metadata(state, process_slug, trace_files)
|
|
196
|
+
payload = _build_payload(
|
|
197
|
+
report_id, generated_at, scenario, trigger, metadata, metrics, confidence, evaluation
|
|
198
|
+
)
|
|
199
|
+
markdown = build_markdown_report(
|
|
200
|
+
report_id, scenario, trigger, metrics, confidence, evaluation, state, trace_files, generated_at
|
|
201
|
+
)
|
|
202
|
+
return _write_report_files(benchmark_dir, trigger, generated_at, markdown, payload)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main(argv: list[str] | None = None) -> int:
|
|
206
|
+
parser = argparse.ArgumentParser(description="Generate a Lincoln benchmark report")
|
|
207
|
+
parser.add_argument("--state-file", type=Path, required=True)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--trigger",
|
|
210
|
+
choices=["handoff", "pr_created", "pr_merged", "session_stop", "manual"],
|
|
211
|
+
default="manual",
|
|
212
|
+
)
|
|
213
|
+
args = parser.parse_args(argv)
|
|
214
|
+
|
|
215
|
+
result = write_benchmark_report(args.state_file, args.trigger)
|
|
216
|
+
if result is None:
|
|
217
|
+
print("Benchmark report skipped (recent session_stop report exists)")
|
|
218
|
+
return 0
|
|
219
|
+
print(f"Benchmark report written to:\n {result['markdown']}\n {result['json']}")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
sys.exit(main())
|