@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.
Files changed (305) hide show
  1. package/dist/main.d.ts +3 -0
  2. package/dist/main.js +1286 -916
  3. package/dist/main.js.map +1 -1
  4. package/dist/postinstall.d.ts +3 -0
  5. package/dist/postinstall.js +18 -0
  6. package/dist/postinstall.js.map +1 -0
  7. package/framework/.claude/agents/_contract.md +79 -0
  8. package/framework/.claude/agents/architect.md +31 -0
  9. package/framework/.claude/agents/default.md +151 -0
  10. package/framework/.claude/agents/designer.md +33 -0
  11. package/framework/.claude/agents/engineer.md +42 -0
  12. package/framework/.claude/agents/external/NOTICES.md +12 -0
  13. package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +23 -0
  14. package/framework/.claude/agents/external/oh-my-claudecode.manifest.yaml +26 -0
  15. package/framework/.claude/agents/external/wshobson-agents.manifest.yaml +23 -0
  16. package/framework/.claude/agents/frontend-engineer.md +32 -0
  17. package/framework/.claude/agents/knowledge-curator.md +28 -0
  18. package/framework/.claude/agents/pm.md +38 -0
  19. package/framework/.claude/agents/qa.md +28 -0
  20. package/framework/.claude/agents/release-coordinator.md +25 -0
  21. package/framework/.claude/agents/researcher.md +41 -0
  22. package/framework/.claude/harnesses/claude-code.yaml +15 -0
  23. package/framework/.claude/harnesses/codex.yaml +40 -0
  24. package/framework/.claude/harnesses/command-map.yaml +255 -0
  25. package/framework/.claude/harnesses/opencode.yaml +32 -0
  26. package/framework/.claude/harnesses/scenarios.yaml +45 -0
  27. package/framework/.claude/hooks/on-pr-event.sh +69 -0
  28. package/framework/.claude/hooks/on-session-start.sh +427 -0
  29. package/framework/.claude/hooks/on-stop.sh +104 -0
  30. package/framework/.claude/hooks/post-tool-use.sh +161 -0
  31. package/framework/.claude/hooks/pre-tool-use.sh +246 -0
  32. package/framework/.claude/schemas/agent-definition.schema.json +35 -0
  33. package/framework/.claude/schemas/benchmark-report.schema.json +148 -0
  34. package/framework/.claude/schemas/pm-to-ux-handoff.schema.yaml +128 -0
  35. package/framework/.claude/schemas/stage-definition.schema.json +233 -0
  36. package/framework/.claude/schemas/workflow-stage.schema.json +86 -0
  37. package/framework/.claude/schemas/workflow-template.schema.json +88 -0
  38. package/framework/.claude/settings.json +14 -0
  39. package/framework/.claude/skills/build-product-prototype/SKILL.md +35 -0
  40. package/framework/.claude/skills/build-product-prototype/prompts/main.md +78 -0
  41. package/framework/.claude/skills/clarify-requirements/SKILL.md +32 -0
  42. package/framework/.claude/skills/clarify-requirements/prompts/main.md +81 -0
  43. package/framework/.claude/skills/dependencies.yaml +139 -0
  44. package/framework/.claude/skills/draft-product-design/SKILL.md +40 -0
  45. package/framework/.claude/skills/draft-product-design/prompts/main.md +47 -0
  46. package/framework/.claude/skills/lc-analyze-frameworks/SKILL.md +43 -0
  47. package/framework/.claude/skills/lc-analyze-frameworks/prompts/main.md +95 -0
  48. package/framework/.claude/skills/lc-benchmark/SKILL.md +61 -0
  49. package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +38 -0
  50. package/framework/.claude/skills/lc-build-codebase-knowledge/prompts/build-codebase-knowledge.md +61 -0
  51. package/framework/.claude/skills/lc-collect-intelligence/SKILL.md +43 -0
  52. package/framework/.claude/skills/lc-collect-intelligence/prompts/main.md +72 -0
  53. package/framework/.claude/skills/lc-competitive-analysis/SKILL.md +43 -0
  54. package/framework/.claude/skills/lc-competitive-analysis/prompts/main.md +70 -0
  55. package/framework/.claude/skills/lc-explore-opensource/SKILL.md +36 -0
  56. package/framework/.claude/skills/lc-explore-opensource/prompts/explore-opensource.md +77 -0
  57. package/framework/.claude/skills/lc-first-principles/SKILL.md +43 -0
  58. package/framework/.claude/skills/lc-first-principles/prompts/main.md +73 -0
  59. package/framework/.claude/skills/lc-handoff/SKILL.md +34 -0
  60. package/framework/.claude/skills/lc-init-branch/SKILL.md +52 -0
  61. package/framework/.claude/skills/lc-market-research/SKILL.md +43 -0
  62. package/framework/.claude/skills/lc-market-research/prompts/main.md +79 -0
  63. package/framework/.claude/skills/lc-product-research/SKILL.md +43 -0
  64. package/framework/.claude/skills/lc-product-research/prompts/main.md +80 -0
  65. package/framework/.claude/skills/lc-research-report/SKILL.md +43 -0
  66. package/framework/.claude/skills/lc-research-report/prompts/main.md +90 -0
  67. package/framework/.claude/skills/lc-research-scope/SKILL.md +43 -0
  68. package/framework/.claude/skills/lc-research-scope/prompts/main.md +82 -0
  69. package/framework/.claude/skills/lc-setup/SKILL.md +68 -0
  70. package/framework/.claude/skills/lc-setup/prompts/main.md +88 -0
  71. package/framework/.claude/skills/lc-stage/SKILL.md +45 -0
  72. package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +43 -0
  73. package/framework/.claude/skills/lc-stakeholder-research/prompts/main.md +65 -0
  74. package/framework/.claude/skills/lc-status/SKILL.md +25 -0
  75. package/framework/.claude/skills/lc-storytelling/SKILL.md +43 -0
  76. package/framework/.claude/skills/lc-storytelling/prompts/main.md +77 -0
  77. package/framework/.claude/skills/lc-wf/SKILL.md +54 -0
  78. package/framework/.claude/skills/lc-workflow/SKILL.md +48 -0
  79. package/framework/.claude/skills/lc-workflow/validators/validate.py +755 -0
  80. package/framework/.claude/skills/lc-workflow-router/SKILL.md +41 -0
  81. package/framework/.claude/skills/lc-workflow-router/prompts/intake-prompt.md +91 -0
  82. package/framework/.claude/skills/lc-workflow-router/prompts/router-prompt.md +44 -0
  83. package/framework/.claude/skills/plan-tdd-development/SKILL.md +31 -0
  84. package/framework/.claude/skills/plan-tdd-development/prompts/main.md +44 -0
  85. package/framework/.claude/skills/process-interview/SKILL.md +32 -0
  86. package/framework/.claude/skills/process-interview/prompts/main.md +47 -0
  87. package/framework/.claude/skills/propose-with-openspec/SKILL.md +37 -0
  88. package/framework/.claude/skills/propose-with-openspec/prompts/main.md +53 -0
  89. package/framework/.claude/skills/split-to-github/SKILL.md +32 -0
  90. package/framework/.claude/skills/split-to-github/prompts/main.md +42 -0
  91. package/framework/.claude/skills/sync-to-knowledge/SKILL.md +35 -0
  92. package/framework/.claude/skills/sync-to-knowledge/prompts/main.md +54 -0
  93. package/framework/.claude/skills/workflow-continue/SKILL.md +24 -0
  94. package/framework/.claude/skills/workflow-continue/prompts/main.md +25 -0
  95. package/framework/.claude/stages/build-codebase-knowledge.yaml +42 -0
  96. package/framework/.claude/stages/clarify.yaml +59 -0
  97. package/framework/.claude/stages/explore-opensource.yaml +40 -0
  98. package/framework/.claude/stages/implement.yaml +60 -0
  99. package/framework/.claude/stages/ingest.yaml +40 -0
  100. package/framework/.claude/stages/lc-analyze-frameworks.yaml +37 -0
  101. package/framework/.claude/stages/lc-collect-intelligence.yaml +37 -0
  102. package/framework/.claude/stages/lc-competitive-analysis.yaml +37 -0
  103. package/framework/.claude/stages/lc-first-principles.yaml +37 -0
  104. package/framework/.claude/stages/lc-market-research.yaml +37 -0
  105. package/framework/.claude/stages/lc-product-research.yaml +37 -0
  106. package/framework/.claude/stages/lc-research-report.yaml +38 -0
  107. package/framework/.claude/stages/lc-research-scope.yaml +38 -0
  108. package/framework/.claude/stages/lc-stakeholder-research.yaml +37 -0
  109. package/framework/.claude/stages/lc-storytelling.yaml +37 -0
  110. package/framework/.claude/stages/product-design-docs.yaml +66 -0
  111. package/framework/.claude/stages/product-prototype.yaml +53 -0
  112. package/framework/.claude/stages/propose.yaml +45 -0
  113. package/framework/.claude/stages/split.yaml +41 -0
  114. package/framework/.claude/stages/sync-knowledge.yaml +49 -0
  115. package/framework/.claude/stages/tdd-development-plan.yaml +43 -0
  116. package/framework/.claude/stages/workflow-router.yaml +45 -0
  117. package/framework/.claude/templates/solo-workflow-context.yaml +23 -0
  118. package/framework/.claude/workflows/README.md +93 -0
  119. package/framework/.claude/workflows/bug-fix.yaml +81 -0
  120. package/framework/.claude/workflows/design-spike.yaml +55 -0
  121. package/framework/.claude/workflows/existing-project-iteration.yaml +106 -0
  122. package/framework/.claude/workflows/interview-to-knowledge.yaml +189 -0
  123. package/framework/.claude/workflows/oss-first-design.yaml +100 -0
  124. package/framework/.claude/workflows/pm-research.yaml +115 -0
  125. package/framework/.claude-plugin/hooks.json +6 -0
  126. package/framework/.claude-plugin/marketplace.json +32 -0
  127. package/framework/.claude-plugin/plugin.json +57 -0
  128. package/framework/.version-bump.json +43 -0
  129. package/framework/CLAUDE.md +140 -0
  130. package/framework/CONTRIBUTING.md +219 -0
  131. package/framework/LICENSE +21 -0
  132. package/framework/README.en.md +139 -0
  133. package/framework/README.md +139 -0
  134. package/framework/RELEASE.md +334 -0
  135. package/framework/SKILL.md +24 -0
  136. package/framework/USAGE.md +356 -0
  137. package/framework/manifest.json +1214 -0
  138. package/framework/requirements.txt +4 -0
  139. package/framework/scripts/__init__.py +0 -0
  140. package/framework/scripts/build_codebase_knowledge.py +76 -0
  141. package/framework/scripts/bump_version.py +181 -0
  142. package/framework/scripts/check-dependency-drift.py +69 -0
  143. package/framework/scripts/check-harness-drift.sh +57 -0
  144. package/framework/scripts/check-main-merge-hygiene.py +128 -0
  145. package/framework/scripts/check-skill-dependencies.sh +124 -0
  146. package/framework/scripts/init-lincoln-branch.sh +300 -0
  147. package/framework/scripts/init-project.sh +184 -0
  148. package/framework/scripts/lc-benchmark-cli.py +66 -0
  149. package/framework/scripts/lincoln-audit.py +288 -0
  150. package/framework/scripts/lincoln-setup.py +651 -0
  151. package/framework/scripts/lincoln-status.py +448 -0
  152. package/framework/scripts/lincoln_benchmark.py +224 -0
  153. package/framework/scripts/lincoln_benchmark_eval.py +104 -0
  154. package/framework/scripts/lincoln_benchmark_metrics.py +818 -0
  155. package/framework/scripts/lincoln_benchmark_report.py +141 -0
  156. package/framework/scripts/lincoln_command_map.py +289 -0
  157. package/framework/scripts/lincoln_dependency_manager.py +335 -0
  158. package/framework/scripts/lincoln_documents.py +175 -0
  159. package/framework/scripts/lincoln_harness_adapter.py +319 -0
  160. package/framework/scripts/lincoln_index.py +255 -0
  161. package/framework/scripts/lincoln_paths.py +198 -0
  162. package/framework/scripts/lincoln_prd.py +227 -0
  163. package/framework/scripts/lincoln_role.py +46 -0
  164. package/framework/scripts/lincoln_scenario.py +59 -0
  165. package/framework/scripts/lincoln_session_start_metrics.py +71 -0
  166. package/framework/scripts/lincoln_skill_prompt.py +48 -0
  167. package/framework/scripts/lincoln_trace.py +335 -0
  168. package/framework/scripts/lincoln_workflow.py +199 -0
  169. package/framework/scripts/list-active-lincoln-branches.sh +87 -0
  170. package/framework/scripts/package-lincoln-plugin.py +340 -0
  171. package/framework/scripts/run-infrastructure-tests.py +84 -0
  172. package/framework/scripts/stage_loader.py +1071 -0
  173. package/framework/scripts/static-check.sh +356 -0
  174. package/framework/scripts/sync-external-agents.sh +210 -0
  175. package/framework/scripts/sync-framework-package.mjs +218 -0
  176. package/framework/scripts/task_tool_guard.py +213 -0
  177. package/framework/scripts/track-artifacts.py +140 -0
  178. package/framework/scripts/validate_stage.py +350 -0
  179. package/{README.md → framework/tools/lincoln/README.md} +4 -4
  180. package/framework/tools/lincoln/package-lock.json +3349 -0
  181. package/framework/tools/lincoln/package.json +48 -0
  182. package/framework/tools/lincoln/src/cli.tsx +60 -0
  183. package/framework/tools/lincoln/src/components/AudioMeter.tsx +34 -0
  184. package/framework/tools/lincoln/src/components/CancelledScreen.tsx +11 -0
  185. package/framework/tools/lincoln/src/components/DevicesMenu.tsx +79 -0
  186. package/framework/tools/lincoln/src/components/HintBar.tsx +25 -0
  187. package/framework/tools/lincoln/src/components/LogView.tsx +36 -0
  188. package/framework/tools/lincoln/src/components/ModelMenu.tsx +122 -0
  189. package/framework/tools/lincoln/src/components/ReadyScreen.tsx +54 -0
  190. package/framework/tools/lincoln/src/components/RecordingApp.tsx +211 -0
  191. package/framework/tools/lincoln/src/components/RecordingScreen.tsx +41 -0
  192. package/framework/tools/lincoln/src/components/SelectMenu.tsx +53 -0
  193. package/framework/tools/lincoln/src/components/StatusPanel.tsx +52 -0
  194. package/framework/tools/lincoln/src/components/StopConfirmation.tsx +32 -0
  195. package/framework/tools/lincoln/src/config/args.ts +35 -0
  196. package/framework/tools/lincoln/src/config/loadConfig.ts +81 -0
  197. package/framework/tools/lincoln/src/config/resolveConfig.ts +85 -0
  198. package/framework/tools/lincoln/src/devices/listDevices.ts +11 -0
  199. package/framework/tools/lincoln/src/devices/parseDevices.ts +25 -0
  200. package/framework/tools/lincoln/src/hooks/useKeyHandler.ts +63 -0
  201. package/framework/tools/lincoln/src/main.tsx +4 -0
  202. package/framework/tools/lincoln/src/models/models.ts +34 -0
  203. package/framework/tools/lincoln/src/models/runWarmup.ts +46 -0
  204. package/framework/tools/lincoln/src/models/warmupProgress.ts +30 -0
  205. package/framework/tools/lincoln/src/recording/spawnRecorder.ts +127 -0
  206. package/framework/tools/lincoln/src/recording/useRecorder.ts +146 -0
  207. package/framework/tools/lincoln/src/utils/amplitudeToMeter.ts +4 -0
  208. package/framework/tools/lincoln/src/utils/formatDuration.ts +6 -0
  209. package/framework/tools/lincoln/src/utils/sessionId.ts +25 -0
  210. package/framework/tools/lincoln/tests/cli.test.tsx +52 -0
  211. package/framework/tools/lincoln/tests/components/AudioMeter.test.tsx +31 -0
  212. package/framework/tools/lincoln/tests/components/CancelledScreen.test.tsx +12 -0
  213. package/framework/tools/lincoln/tests/components/ReadyScreen.test.tsx +68 -0
  214. package/framework/tools/lincoln/tests/components/RecordingApp.menus.test.tsx +158 -0
  215. package/framework/tools/lincoln/tests/components/RecordingApp.test.tsx +248 -0
  216. package/framework/tools/lincoln/tests/components/RecordingScreen.test.tsx +37 -0
  217. package/framework/tools/lincoln/tests/components/SelectMenu.test.tsx +59 -0
  218. package/framework/tools/lincoln/tests/components/StopConfirmation.test.tsx +38 -0
  219. package/framework/tools/lincoln/tests/config/args.test.ts +49 -0
  220. package/framework/tools/lincoln/tests/config/loadConfig.test.ts +59 -0
  221. package/framework/tools/lincoln/tests/config/resolveConfig.test.ts +72 -0
  222. package/framework/tools/lincoln/tests/devices/parseDevices.test.ts +21 -0
  223. package/framework/tools/lincoln/tests/hooks/useKeyHandler.enabled.test.tsx +44 -0
  224. package/framework/tools/lincoln/tests/hooks/useKeyHandler.test.tsx +130 -0
  225. package/framework/tools/lincoln/tests/models/models.test.ts +27 -0
  226. package/framework/tools/lincoln/tests/models/warmupProgress.test.ts +25 -0
  227. package/framework/tools/lincoln/tests/recording/spawnRecorder.test.ts +85 -0
  228. package/framework/tools/lincoln/tests/recording/useRecorder.test.tsx +172 -0
  229. package/framework/tools/lincoln/tests/utils/amplitudeToMeter.test.ts +22 -0
  230. package/framework/tools/lincoln/tests/utils/formatDuration.test.ts +11 -0
  231. package/framework/tools/lincoln/tests/utils/sessionId.test.ts +30 -0
  232. package/framework/tools/lincoln/tsconfig.json +22 -0
  233. package/framework/tools/lincoln/tsconfig.tests.json +10 -0
  234. package/framework/tools/lincoln/tsup.config.ts +17 -0
  235. package/framework/tools/lincoln/vitest.config.ts +20 -0
  236. package/framework/tools/lincoln-installer/package-lock.json +3611 -0
  237. package/framework/tools/lincoln-installer/package.json +53 -0
  238. package/framework/tools/lincoln-installer/src/cli.tsx +63 -0
  239. package/framework/tools/lincoln-installer/src/components/HarnessSelectScreen.tsx +43 -0
  240. package/framework/tools/lincoln-installer/src/components/InstallerApp.tsx +98 -0
  241. package/framework/tools/lincoln-installer/src/components/OptionsScreen.tsx +56 -0
  242. package/framework/tools/lincoln-installer/src/components/ProgressScreen.tsx +25 -0
  243. package/framework/tools/lincoln-installer/src/components/ResultScreen.tsx +40 -0
  244. package/framework/tools/lincoln-installer/src/components/SelectMenu.tsx +73 -0
  245. package/framework/tools/lincoln-installer/src/components/SummaryScreen.tsx +49 -0
  246. package/framework/tools/lincoln-installer/src/components/UpdateApp.tsx +135 -0
  247. package/framework/tools/lincoln-installer/src/components/WelcomeScreen.tsx +22 -0
  248. package/framework/tools/lincoln-installer/src/config/args.ts +76 -0
  249. package/framework/tools/lincoln-installer/src/config/resolveConfig.ts +33 -0
  250. package/framework/tools/lincoln-installer/src/config/updateArgs.ts +67 -0
  251. package/framework/tools/lincoln-installer/src/hooks/useSetup.ts +149 -0
  252. package/framework/tools/lincoln-installer/src/main.tsx +11 -0
  253. package/framework/tools/lincoln-installer/src/types.ts +19 -0
  254. package/framework/tools/lincoln-installer/src/update-main.tsx +11 -0
  255. package/framework/tools/lincoln-installer/src/update.tsx +90 -0
  256. package/framework/tools/lincoln-installer/src/utils/detectHarness.ts +51 -0
  257. package/framework/tools/lincoln-installer/src/utils/runSetup.ts +62 -0
  258. package/framework/tools/lincoln-installer/src/utils/updateRelease.ts +454 -0
  259. package/framework/tools/lincoln-installer/tests/components/InstallerApp.test.tsx +88 -0
  260. package/framework/tools/lincoln-installer/tests/components/SelectMenu.test.tsx +26 -0
  261. package/framework/tools/lincoln-installer/tests/components/UpdateApp.test.tsx +170 -0
  262. package/framework/tools/lincoln-installer/tests/components/screens.test.tsx +310 -0
  263. package/framework/tools/lincoln-installer/tests/config/args.test.ts +66 -0
  264. package/framework/tools/lincoln-installer/tests/config/resolveConfig.test.ts +75 -0
  265. package/framework/tools/lincoln-installer/tests/config/updateArgs.test.ts +52 -0
  266. package/framework/tools/lincoln-installer/tests/hooks/useSetup.test.tsx +213 -0
  267. package/framework/tools/lincoln-installer/tests/update.test.tsx +108 -0
  268. package/framework/tools/lincoln-installer/tests/utils/detectHarness.test.ts +40 -0
  269. package/framework/tools/lincoln-installer/tests/utils/runSetup.test.ts +45 -0
  270. package/framework/tools/lincoln-installer/tests/utils/updateRelease.test.ts +559 -0
  271. package/framework/tools/lincoln-installer/tsconfig.json +22 -0
  272. package/framework/tools/lincoln-installer/tsconfig.tests.json +8 -0
  273. package/framework/tools/lincoln-installer/tsup.config.ts +17 -0
  274. package/framework/tools/lincoln-installer/vitest.config.ts +20 -0
  275. package/framework/tools/lincoln-record/Cargo.lock +1896 -0
  276. package/framework/tools/lincoln-record/Cargo.toml +24 -0
  277. package/framework/tools/lincoln-record/diarize/diarize.py +117 -0
  278. package/framework/tools/lincoln-record/diarize/merge.py +55 -0
  279. package/framework/tools/lincoln-record/src/audio/capture/microphone.rs +194 -0
  280. package/framework/tools/lincoln-record/src/audio/capture/mod.rs +1 -0
  281. package/framework/tools/lincoln-record/src/audio/devices.rs +20 -0
  282. package/framework/tools/lincoln-record/src/audio/mod.rs +5 -0
  283. package/framework/tools/lincoln-record/src/audio/resampler.rs +44 -0
  284. package/framework/tools/lincoln-record/src/audio/saver.rs +87 -0
  285. package/framework/tools/lincoln-record/src/audio/source.rs +104 -0
  286. package/framework/tools/lincoln-record/src/cli.rs +96 -0
  287. package/framework/tools/lincoln-record/src/config.rs +135 -0
  288. package/framework/tools/lincoln-record/src/lib.rs +6 -0
  289. package/framework/tools/lincoln-record/src/main.rs +242 -0
  290. package/framework/tools/lincoln-record/src/model/mod.rs +248 -0
  291. package/framework/tools/lincoln-record/src/output/metadata.rs +30 -0
  292. package/framework/tools/lincoln-record/src/output/mod.rs +2 -0
  293. package/framework/tools/lincoln-record/src/output/transcript.rs +42 -0
  294. package/framework/tools/lincoln-record/src/transcription/diarization.rs +64 -0
  295. package/framework/tools/lincoln-record/src/transcription/mod.rs +3 -0
  296. package/framework/tools/lincoln-record/src/transcription/provider.rs +43 -0
  297. package/framework/tools/lincoln-record/src/transcription/whisper.rs +176 -0
  298. package/framework/tools/lincoln-record/tests/cli_args.rs +103 -0
  299. package/framework/tools/lincoln-record/tests/config_test.rs +70 -0
  300. package/framework/tools/lincoln-record/tests/devices_test.rs +23 -0
  301. package/framework/tools/lincoln-record/tests/diarization_tests.rs +101 -0
  302. package/framework/tools/lincoln-record/tests/integration_tests.rs +44 -0
  303. package/framework/tools/lincoln-record/tests/model_test.rs +68 -0
  304. package/framework/tools/lincoln-record/tests/transcription_tests.rs +73 -0
  305. 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())