claude-dev-env 1.59.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CLAUDE.md +4 -0
  2. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  4. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  6. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  9. package/docs/CODE_RULES.md +2 -2
  10. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  11. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  12. package/hooks/blocking/code_rules_duplicate_body.py +152 -0
  13. package/hooks/blocking/code_rules_enforcer.py +38 -15
  14. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  15. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  16. package/hooks/blocking/config/__init__.py +5 -0
  17. package/hooks/blocking/config/verified_commit_constants.py +118 -0
  18. package/hooks/blocking/destructive_command_blocker.py +483 -61
  19. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  20. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  24. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  25. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  26. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +490 -0
  30. package/hooks/blocking/test_verified_commit_gate.py +495 -0
  31. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
  33. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  34. package/hooks/blocking/verification_verdict_store.py +686 -0
  35. package/hooks/blocking/verified_commit_gate.py +535 -0
  36. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  37. package/hooks/blocking/verifier_verdict_minter.py +221 -0
  38. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  39. package/hooks/hooks.json +43 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  42. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  43. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  44. package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
  45. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  46. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  47. package/hooks/validation/mypy_validator.py +59 -7
  48. package/hooks/validation/test_mypy_validator.py +94 -0
  49. package/package.json +1 -1
  50. package/rules/file-global-constants.md +7 -1
  51. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  52. package/rules/orphan-css-class.md +23 -0
  53. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  54. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  55. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  56. package/skills/autoconverge/SKILL.md +54 -17
  57. package/skills/autoconverge/reference/closing-report.md +59 -17
  58. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  59. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
  60. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
  62. package/skills/autoconverge/workflow/converge.mjs +520 -57
  63. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  64. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  65. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  66. package/skills/autoconverge/workflow/render_report.py +488 -397
  67. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  68. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  69. package/skills/autoconverge/workflow/test_render_report.py +518 -259
  70. package/skills/pr-converge/reference/per-tick.md +28 -8
  71. package/skills/rebase/SKILL.md +2 -4
  72. package/system-prompts/software-engineer.xml +2 -6
  73. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  74. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  75. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  76. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  77. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  78. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  79. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  80. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  81. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -6,100 +6,216 @@ LABEL_RESOLVE_HEAD = "resolve-head"
6
6
  LABEL_PREFIX_LENS = "lens:"
7
7
  LABEL_PREFIX_FIX = "fix:"
8
8
  LABEL_COPILOT_GATE = "copilot-gate"
9
+ LABEL_CONVERGENCE_SUMMARY = "convergence-summary"
9
10
 
10
11
  JOURNAL_SIBLING_SUBAGENTS = "subagents"
11
12
  JOURNAL_SIBLING_WORKFLOWS = "workflows"
12
13
 
13
- THEME_PATH_SEGMENT_COUNT = 2
14
- THEME_FALLBACK = "other"
14
+ DEFAULT_FINDING_CATEGORY = "bug"
15
+ DEFAULT_FINDING_SEVERITY = "P2"
15
16
 
16
- SEVERITY_CRITICAL_BUCKET = "Critical"
17
- SEVERITY_MINOR_BUCKET = "Minor"
18
- SEVERITY_CRITICAL_LEVELS = frozenset({"P0", "P1"})
19
- SEVERITY_BADGE_CLASS_BY_LEVEL = {
20
- "P0": "b-p0",
21
- "P1": "b-p1",
22
- "P2": "b-p2",
17
+ ISO_DATE_LENGTH = 10
18
+ SHORT_SHA_LENGTH = 8
19
+
20
+ WORKFLOW_NAME_AUTOCONVERGE = "autoconverge"
21
+ PROJECTS_DIR_NAME = "projects"
22
+ COMBINED_RUN_ID_PREFIX = "wf_combined-"
23
+ SUMMARY_DETAIL_MAX_CHARS = 400
24
+
25
+ ONEXC_PYTHON_MAJOR_VERSION = 3
26
+ ONEXC_PYTHON_MINOR_VERSION = 12
27
+
28
+ ARGS_FIELD_OWNER = "owner"
29
+ ARGS_FIELD_REPO = "repo"
30
+ ARGS_FIELD_PR_NUMBER = "prNumber"
31
+ JOURNAL_FIELD_ARGS = "args"
32
+ JOURNAL_FIELD_WORKFLOW_NAME = "workflowName"
33
+ JOURNAL_FIELD_TIMESTAMP = "timestamp"
34
+ JOURNAL_FIELD_RESULT = "result"
35
+ JOURNAL_FIELD_RUN_ID = "runId"
36
+ JOURNAL_FIELD_WORKFLOW_PROGRESS = "workflowProgress"
37
+ RESULT_FIELD_FINAL_SHA = "finalSha"
38
+ PROGRESS_FIELD_AGENT_ID = "agentId"
39
+ PROGRESS_FIELD_LABEL = "label"
40
+
41
+ SUMMARY_FIELD_PR_PROBLEM = "prProblem"
42
+ SUMMARY_FIELD_PR_FIX = "prFix"
43
+ SUMMARY_FIELD_PROBLEM_SCENES = "problemScenes"
44
+ SUMMARY_FIELD_FIX_SCENES = "fixScenes"
45
+ SUMMARY_FIELD_VERDICT_LINE = "verdictLine"
46
+ SUMMARY_FIELD_ISSUE_CLASSES = "issueClasses"
47
+
48
+ ISSUE_CLASS_FIELD_PLAINNAME = "plainName"
49
+ ISSUE_CLASS_FIELD_COUNT = "count"
50
+ ISSUE_CLASS_FIELD_SEVERITY = "severity"
51
+ ISSUE_CLASS_FIELD_CATEGORY = "category"
52
+ ISSUE_CLASS_FIELD_STATUS = "status"
53
+ ISSUE_CLASS_FIELD_CAUSE = "cause"
54
+ ISSUE_CLASS_FIELD_MEDIUM = "medium"
55
+ ISSUE_CLASS_FIELD_BEFORE_LINES = "beforeLines"
56
+ ISSUE_CLASS_FIELD_AFTER_LINES = "afterLines"
57
+
58
+ SCENE_FIELD_TRIGGER = "trigger"
59
+ SCENE_FIELD_CONDITION = "condition"
60
+ SCENE_FIELD_RESULT = "result"
61
+ SCENE_FIELD_CAPTION = "caption"
62
+
63
+ MEDIUM_TERMINAL = "terminal"
64
+ MEDIUM_CODE = "code"
65
+
66
+ CATEGORY_BUG = "bug"
67
+ CATEGORY_LABEL_BY_VALUE = {
68
+ "bug": "bug",
69
+ "code-standard": "code standard",
70
+ }
71
+ CATEGORY_SORT_ORDER = {
72
+ "bug": 0,
73
+ "code-standard": 1,
23
74
  }
24
75
 
25
- BAR_COLOR_SEVERITY_CRITICAL = "#dc2626"
26
- BAR_COLOR_SEVERITY_MINOR = "#eab308"
27
- BAR_COLOR_ROUND = "#2563eb"
28
- BAR_COLOR_TESTS = "#10b981"
29
- BAR_COLOR_THEME = "#8b5cf6"
76
+ STATUS_LABEL_BY_VALUE = {
77
+ "fixed": "fixed",
78
+ "deferred": "deferred",
79
+ }
80
+
81
+ SEVERITY_SORT_RANK = {
82
+ "P0": 0,
83
+ "P1": 1,
84
+ "P2": 2,
85
+ }
30
86
 
31
- TEST_DEFINITION_PATTERN = r"^\+\s*(async\s+)?def\s+(test|should)"
32
- TEST_PATH_GLOBS = ("*test*.py", "**/*test*.py")
87
+ TIMELINE_BEFORE_LABEL = "Before the fix"
88
+ TIMELINE_AFTER_LABEL = "After the fix"
89
+ TIMELINE_BEFORE_PILL = "BROKEN"
90
+ TIMELINE_AFTER_PILL = "WORKS"
91
+ TIMELINE_TERMINAL_BAR_LABEL = "terminal"
33
92
 
34
- BAR_FILL_MAX_PERCENT = 100
93
+ CAUSE_MUTED_STYLE = "color:#94a3b8;"
35
94
 
36
95
  GITHUB_PR_URL_TEMPLATE = "https://github.com/{owner}/{repo}/pull/{number}"
37
96
 
97
+ SUMMARY_PR_COORDINATES_TEMPLATE = "owner={owner} repo={repo} PR #{pr_number} ({url})"
98
+ SUMMARY_FINDING_LINE_TEMPLATE = (
99
+ "{number}. [{severity}/{category}] {file}:{line} - {title} :: {detail}"
100
+ )
101
+ SUMMARY_FIX_LINE_TEMPLATE = "{number}. {summary}"
102
+ SUMMARY_FINDINGS_EMPTY_TEXT = "none - every lens was clean on a stable HEAD"
103
+ SUMMARY_FIX_EMPTY_TEXT = "none"
104
+ SUMMARY_STANDARDS_NOTE_TEMPLATE = "\nDeferred code-standard note: {note}\n"
105
+ SUMMARY_COPILOT_NOTE_TEMPLATE = "\nCopilot gate note: {note}\n"
106
+
107
+ SUMMARY_PROMPT_TEMPLATE = """\
108
+ You write the plain-language convergence summary for {pr_coordinates}. The autoconverge run reached convergence in {round_count} round(s).
109
+
110
+ First read what THIS PR is for. Run exactly:
111
+ gh api repos/{owner}/{repo}/pulls/{pr_number} --jq '{{title: .title, body: .body}}'
112
+ Ground every sentence in that PR title and description plus the findings and fix summaries below; invent nothing not present in those sources.
113
+
114
+ Distinct findings caught across the run (already deduped):
115
+ {findings_block}
116
+
117
+ Per-round fix summaries:
118
+ {fix_block}
119
+ {standards_block}{copilot_block}
120
+ Write so a non-programmer understands every line. The reader has never seen the code and does not know its internals.
121
+ - prProblem and prFix are each ONE plain sentence describing THIS PR for a reader with zero prior context. prProblem: what was wrong or at risk before this PR. prFix: what this PR changes to solve it. They are the fallback shown when problemScenes/fixScenes are empty, so keep them self-contained.
122
+ - In prProblem, NAME the concrete project or component (from the repo name and PR description) and gloss in plain words what it is the first time you mention it. NEVER write a bare "the tool", "the app", or "the system" before naming it.
123
+ - Read the PR description for the motivation it states (often a "Hardens against X" / "Fixes Y" line) and write prProblem from that. Cover the whole PR, not only what the review caught.
124
+ - EVERY sentence must be concrete and checkable against the PR description. A reader must be able to picture exactly what happened. If you cannot state something concretely and truthfully from the PR title and description, leave it out - never fill space with a vague claim.
125
+ - Banned vague phrasings (a reader cannot picture them): "installs itself", "more reliable", "sets things up properly", "works better", "improves handling", "eliminates side effects", "hardens". Replace each with the concrete thing that happened.
126
+ - prProblem/prFix example (a DIFFERENT project, to show the style not the answer). WEAK: prProblem "This makes the tool's setup more reliable." STRONG: prProblem "PhotoSync, the app that backs up your phone photos, stopped backing them up after you switched accounts and never said so." prFix "It now re-checks your account on each backup, so a switch does not halt backups."
127
+ - problemScenes and fixScenes turn the problem and the fix into 1 to 3 short cause->effect scenes each. Each scene has very short fragments (chips): trigger = the starting action or state, condition = the middle event (may be the empty string), result = the outcome (bad in a problem scene, good in a fix scene), plus caption = one plain grounded sentence explaining the scene. A fix scene mirrors a problem scene: same trigger, good result. If the PR is one-dimensional, one scene each is fine.
128
+ - problemScenes/fixScenes example (a DIFFERENT project, to teach the style not the answer). Problem scene: trigger "export stops at batch 90", condition "you restart it", result "starts again at batch 1", caption "A halted export threw away the 90 batches it had already finished and began again." Mirroring fix scene: trigger "export stops at batch 90", condition "you restart it", result "continues at batch 91", caption "A restarted export now picks up at the next unfinished batch."
129
+ - GROUP near-duplicate findings into issue CLASSES: the same KIND of problem across different files or lines becomes ONE class with a count. Example: seven "Missing return type annotation on test function" findings become one class with count 7.
130
+ - TRANSLATE reviewer jargon into plain everyday English. Examples: "CodingGuidelineID 1000000 / Repository guideline (Types)" -> "a typing rule the project enforces"; "missing return type annotation / Add -> None" -> "a test did not declare what it returns"; "Banned identifier result" -> "a vague variable name the project bans"; a magic-value finding -> "a raw number or string that should be a named value".
131
+ - plainName: a short plain symptom a non-programmer recognizes - name what BROKE or what a person would notice, not the internal component. Carry NO tool token, rule id, file path, line number, severity code (P0/P1/P2), or bot name. Good: "The install command quietly did nothing". Weak: "Entry-point guard misfires under symlinked bin invocation".
132
+ - cause is the field that matters most. Sentence 1: the observable impact - what a person saw go wrong, or would have. Sentence 2: the cause in everyday terms. At most 2 sentences, no paragraphs.
133
+ - In cause describe the CONSEQUENCE, never the internal mechanism. Do not narrate control flow, comparisons, or internal state. Banned phrasings: "a check returned false", "two forms of the path", "treated the run as a non-run", "the values did not match", "a guard misfired". Say what that meant for the person instead.
134
+ - Prefer concrete everyday words: "the install command" and "on Mac and Linux" over "the launcher", "the entry-point guard", or "the symlink". Read each sentence as if aloud to a non-programmer; if they would not follow it, rewrite it.
135
+ - cause worked example (a DIFFERENT problem, to show the style, not the answer). WEAK, do not write like this: "A cached value's timestamp was compared across mismatched time zones, so the freshness check evaluated false and the entry was treated as stale and re-fetched on every request." STRONG, write like this: "Every page re-downloaded the same data from scratch instead of reusing the copy it already had, so pages loaded slower than needed. The app misjudged its saved copy as out of date."
136
+ - medium picks how the before/after panels are drawn: 'terminal' when the user-visible effect is command-line behavior; 'code' when the finding is best shown as a small before/after code snippet (e.g. a missing return type: before "def test_x():" / after "def test_x() -> None:"); 'text' otherwise.
137
+ - beforeLines and afterLines are the literal short lines to show in each panel - what a person sees. For a terminal: include the prompt + command + the (missing or present) output. For code: 1 to 4 lines before, 1 to 4 lines after. Keep every line short. Never fabricate exact output text that is not implied by the finding - if the exact text is unknown, show the shape (e.g. "(no output - nothing installed)"). Leave both arrays empty to fall back to the cause line alone.
138
+ - Lead with category 'bug' classes, then 'code-standard'. Create one class per distinct KIND of problem, however many that is; never merge different kinds or drop classes to hit a number.
139
+ - status is 'fixed' unless the fix summaries or the deferred code-standard note mark the class deferred, in which case status is 'deferred'.
140
+ - Use NO hedging words anywhere (likely, probably, should, appears, seems, may, might, could, possibly). State facts ("caught and fixed").
141
+ - When there are zero findings, return issueClasses: [] and a verdictLine stating the run converged with no issues caught.
142
+ - verdictLine is one plain factual sentence naming the round count and that all classes are fixed or deferred.
143
+
144
+ Return strictly a JSON object with keys prProblem, prFix, problemScenes, fixScenes, verdictLine, and issueClasses."""
145
+
38
146
  HTML_DOCTYPE = "<!DOCTYPE html>"
39
147
 
40
148
  HTML_HEAD_TEMPLATE = """\
41
149
  <head>
42
- <meta charset="utf-8">
43
- <title>PR #{pr_number} Convergence Insights</title>
44
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
45
- {style_block}
150
+ <meta charset="utf-8">
151
+ <title>PR #{pr_number} Convergence Summary</title>
152
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
153
+ {style_block}
46
154
  </head>"""
47
155
 
48
156
  HTML_STYLE_BLOCK = """\
49
157
  <style>
50
- * { box-sizing: border-box; margin: 0; padding: 0; }
51
- body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: #f8fafc; color: #334155; line-height: 1.65; padding: 48px 24px; }
52
- .container { max-width: 800px; margin: 0 auto; }
53
- h1 { font-size: 32px; font-weight: 700; color: #0f172a; margin-bottom: 8px; }
54
- h2 { font-size: 20px; font-weight: 600; color: #0f172a; margin-top: 48px; margin-bottom: 16px; }
55
- .subtitle { color: #64748b; font-size: 15px; margin-bottom: 32px; }
56
- .nav-toc { display: flex; flex-wrap: wrap; gap: 8px; margin: 24px 0 32px 0; padding: 16px; background: white; border-radius: 8px; border: 1px solid #e2e8f0; }
57
- .nav-toc a { font-size: 12px; color: #64748b; text-decoration: none; padding: 6px 12px; border-radius: 6px; background: #f1f5f9; transition: all 0.15s; }
58
- .nav-toc a:hover { background: #e2e8f0; color: #334155; }
59
- .stats-row { display: flex; gap: 24px; margin-bottom: 40px; padding: 20px 0; border-top: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0; flex-wrap: wrap; }
60
- .stat { text-align: center; }
61
- .stat-value { font-size: 24px; font-weight: 700; color: #0f172a; }
62
- .stat-label { font-size: 11px; color: #64748b; text-transform: uppercase; }
63
- .at-a-glance { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border: 1px solid #f59e0b; border-radius: 12px; padding: 20px 24px; margin-bottom: 32px; }
64
- .glance-title { font-size: 16px; font-weight: 700; color: #92400e; margin-bottom: 16px; }
65
- .glance-sections { display: flex; flex-direction: column; gap: 12px; }
66
- .glance-section { font-size: 14px; color: #78350f; line-height: 1.6; }
67
- .glance-section strong { color: #92400e; }
68
- .section-intro { font-size: 14px; color: #64748b; margin-bottom: 16px; }
69
- .charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin: 24px 0; }
70
- .chart-card { background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; }
71
- .chart-title { font-size: 12px; font-weight: 600; color: #64748b; text-transform: uppercase; margin-bottom: 12px; }
72
- .bar-row { display: flex; align-items: center; margin-bottom: 6px; }
73
- .bar-label { width: 116px; font-size: 11px; color: #475569; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
74
- .bar-track { flex: 1; height: 6px; background: #f1f5f9; border-radius: 3px; margin: 0 8px; }
75
- .bar-fill { height: 100%; border-radius: 3px; }
76
- .bar-value { width: 28px; font-size: 11px; font-weight: 500; color: #64748b; text-align: right; }
77
- .bugs { display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px; }
78
- .bug-card { border-radius: 8px; padding: 16px; }
79
- .bug-card.crit { background: #fef2f2; border: 1px solid #fca5a5; }
80
- .bug-card.minor { background: #fffbeb; border: 1px solid #fcd34d; }
81
- .bug-head { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
82
- .bug-num { font-size: 13px; font-weight: 700; color: #94a3b8; min-width: 28px; }
83
- .bug-title { flex: 1 1 300px; font-weight: 600; font-size: 15px; }
84
- .bug-card.crit .bug-title { color: #991b1b; }
85
- .bug-card.minor .bug-title { color: #92400e; }
86
- .badges { display: flex; gap: 6px; flex-wrap: wrap; }
87
- .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; padding: 3px 8px; border-radius: 20px; white-space: nowrap; }
88
- .b-p0 { background: #fee2e2; color: #991b1b; }
89
- .b-p1 { background: #fee2e2; color: #991b1b; }
90
- .b-p2 { background: #fef3c7; color: #92400e; }
91
- .b-fixed { background: #dcfce7; color: #166534; }
92
- .bug-impact { font-size: 13px; color: #7f1d1d; margin-top: 10px; line-height: 1.55; }
93
- .bug-card.minor .bug-impact { color: #78350f; }
94
- .bug-fix { background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 10px 12px; margin-top: 10px; font-size: 13px; color: #166534; line-height: 1.5; }
95
- .bug-fix b { color: #15803d; }
96
- .bug-meta { font-size: 11px; color: #94a3b8; margin-top: 10px; }
97
- .bug-meta code { background: #f1f5f9; padding: 1px 6px; border-radius: 4px; font-family: monospace; color: #475569; }
98
- .horizon-card { background: linear-gradient(135deg, #faf5ff 0%, #f5f3ff 100%); border: 1px solid #c4b5fd; border-radius: 8px; padding: 16px; }
99
- .horizon-title { font-weight: 600; font-size: 15px; color: #5b21b6; margin-bottom: 8px; }
100
- .horizon-possible { font-size: 14px; color: #334155; margin-bottom: 10px; line-height: 1.5; }
101
- .horizon-tip { font-size: 13px; color: #6b21a8; background: rgba(255,255,255,0.6); padding: 8px 12px; border-radius: 4px; }
102
- footer { margin-top: 40px; padding-top: 18px; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 12px; }
103
- footer code { background: #f1f5f9; padding: 1px 6px; border-radius: 4px; font-family: monospace; color: #475569; }
104
- @media (max-width: 640px) { .charts-row { grid-template-columns: 1fr; } .stats-row { justify-content: center; } }
158
+ * { box-sizing: border-box; margin: 0; padding: 0; }
159
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; color: #334155; line-height: 1.6; padding: 48px 20px; }
160
+ .container { max-width: 860px; margin: 0 auto; }
161
+ h1 { font-size: 30px; font-weight: 800; color: #0f172a; letter-spacing: -0.5px; }
162
+ .subtitle { color: #64748b; font-size: 14px; margin: 6px 0 26px; }
163
+ h2 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: #94a3b8; margin: 40px 0 16px; }
164
+
165
+ /* verdict */
166
+ .verdict { display: flex; align-items: center; gap: 14px; background: linear-gradient(135deg,#dcfce7,#bbf7d0); border:1px solid #22c55e; border-radius: 14px; padding: 18px 22px; }
167
+ .verdict .check { width: 34px; height: 34px; border-radius: 50%; background:#16a34a; color:#fff; display:flex; align-items:center; justify-content:center; font-size:19px; flex:0 0 auto; }
168
+ .verdict .vtext { font-size: 16px; font-weight: 700; color:#0f172a; }
169
+ .verdict .vsub { font-size: 13px; font-weight: 500; color:#15803d; }
170
+
171
+ /* problem / fix scenes */
172
+ .pf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
173
+ .pf { border-radius: 12px; padding: 18px 20px; border:1px solid #e2e8f0; background:#fff; }
174
+ .pf-tag { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing:.5px; padding: 3px 10px; border-radius: 20px; display:inline-block; margin-bottom: 14px; }
175
+ .pf.problem { border-left: 4px solid #ef4444; }
176
+ .pf.fix { border-left: 4px solid #22c55e; }
177
+ .pf.problem .pf-tag { background:#fee2e2; color:#b91c1c; }
178
+ .pf.fix .pf-tag { background:#dcfce7; color:#15803d; }
179
+ .scene { display:flex; align-items:center; gap:10px; font-family:'JetBrains Mono',monospace; font-size:12.5px; padding: 9px 0; flex-wrap: wrap; }
180
+ .chip { background:#f1f5f9; border:1px solid #e2e8f0; border-radius:6px; padding:4px 9px; color:#334155; white-space:nowrap; }
181
+ .arrow { color:#94a3b8; font-weight:700; }
182
+ .note { color:#94a3b8; font-size:11px; font-style:italic; }
183
+ .res-bad { color:#dc2626; font-weight:600; white-space:nowrap; }
184
+ .res-good { color:#16a34a; font-weight:600; white-space:nowrap; }
185
+ .scene-cap { font-size:12px; color:#64748b; margin: 2px 0 12px; }
186
+ .scene-cap:last-child { margin-bottom:0; }
187
+
188
+ /* bug class */
189
+ .bug-head { display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin:32px 0 12px; }
190
+ .bug-head:first-of-type { margin-top:8px; }
191
+ .bug-name { font-size:16px; font-weight:700; color:#0f172a; }
192
+ .bug-count { font-size:12px; font-weight:600; color:#64748b; background:#f1f5f9; border:1px solid #e2e8f0; border-radius:20px; padding:3px 11px; white-space:nowrap; flex:0 0 auto; }
193
+
194
+ /* terminals */
195
+ .term-grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; align-items:start; }
196
+ .term-wrap .tlabel { font-size:12px; font-weight:700; margin-bottom:7px; display:flex; align-items:center; gap:7px; }
197
+ .term-wrap.before .tlabel { color:#b91c1c; }
198
+ .term-wrap.after .tlabel { color:#15803d; }
199
+ .pill-x { background:#fee2e2; color:#b91c1c; border-radius:20px; font-size:10px; padding:2px 8px; font-weight:700; }
200
+ .pill-c { background:#dcfce7; color:#15803d; border-radius:20px; font-size:10px; padding:2px 8px; font-weight:700; }
201
+ .terminal { background:#0f172a; border-radius:10px; overflow:hidden; box-shadow:0 6px 18px rgba(15,23,42,.18); }
202
+ .term-bar { background:#1e293b; padding:9px 12px; display:flex; gap:7px; align-items:center; }
203
+ .term-bar i { width:11px; height:11px; border-radius:50%; display:inline-block; }
204
+ .term-bar .r{background:#ef4444;} .term-bar .y{background:#f59e0b;} .term-bar .g{background:#22c55e;}
205
+ .term-bar span { color:#64748b; font-size:11px; font-family:'JetBrains Mono',monospace; margin-left:6px; }
206
+ .term-body { padding:14px 16px; font-family:'JetBrains Mono',monospace; font-size:12.5px; line-height:1.7; color:#e2e8f0; min-height:104px; }
207
+ .term-body .pr { color:#38bdf8; }
208
+ .term-body .cmd { color:#e2e8f0; }
209
+ .term-body .dim { color:#64748b; }
210
+ .term-body .ok { color:#4ade80; }
211
+ .term-body .cursor { display:inline-block; width:8px; height:15px; background:#475569; vertical-align:-2px; }
212
+ .code-panel { background:#f8fafc; border:1px solid #e2e8f0; border-radius:10px; padding:14px 16px; font-family:'JetBrains Mono',monospace; font-size:12.5px; line-height:1.7; color:#334155; min-height:104px; overflow:auto; }
213
+ .text-panel { background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:14px 16px; font-size:13px; line-height:1.7; color:#475569; min-height:104px; }
214
+
215
+ .cause { background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:14px 18px; margin-top:16px; font-size:14px; color:#475569; }
216
+ .cause b { color:#0f172a; }
217
+
218
+ footer { margin-top:40px; padding-top:16px; border-top:1px solid #e2e8f0; color:#94a3b8; font-size:12px; }
219
+ footer code { background:#e2e8f0; padding:1px 6px; border-radius:4px; font-family:'JetBrains Mono',monospace; }
220
+ @media (max-width:680px){ .pf-grid,.term-grid{grid-template-columns:1fr;} }
105
221
  </style>"""
@@ -0,0 +1,76 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const workflowDirectory = dirname(fileURLToPath(import.meta.url));
8
+ const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
9
+
10
+ function functionBody(functionName) {
11
+ const functionStart = convergeSource.indexOf(`function ${functionName}(`);
12
+ assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
13
+ const nextFunctionStart = convergeSource.indexOf('\nfunction ', functionStart + 1);
14
+ const functionEnd = nextFunctionStart === -1 ? convergeSource.length : nextFunctionStart;
15
+ return convergeSource.slice(functionStart, functionEnd);
16
+ }
17
+
18
+ const productionModule = new Function(
19
+ `${functionBody('cleanAuditBlocker')}\n` + 'return { cleanAuditBlocker };',
20
+ )();
21
+
22
+ const { cleanAuditBlocker } = productionModule;
23
+
24
+ const CONVERGED_HEAD = 'abcdef0123456789abcdef0123456789abcdef01';
25
+
26
+ test('cleanAuditBlocker names the denial reason, the HEAD, and the unblock path', () => {
27
+ const message = cleanAuditBlocker(CONVERGED_HEAD, {
28
+ posted: false,
29
+ reviewUrl: '',
30
+ reason: 'denied by the auto mode classifier',
31
+ });
32
+ assert.match(message, /denied by the auto mode classifier/);
33
+ assert.match(message, /post_audit_thread\.py/);
34
+ assert.match(message, new RegExp(CONVERGED_HEAD));
35
+ assert.match(message, /can never pass without it/);
36
+ });
37
+
38
+ test('cleanAuditBlocker falls back to a no-result reason when the post agent died', () => {
39
+ const message = cleanAuditBlocker(CONVERGED_HEAD, null);
40
+ assert.match(message, /the post agent returned no result/);
41
+ });
42
+
43
+ test('postCleanAudit returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
44
+ const body = functionBody('postCleanAudit');
45
+ assert.match(body, /schema: CLEAN_AUDIT_SCHEMA/);
46
+ assert.doesNotMatch(body, /agent transcript \(unused\)/);
47
+ });
48
+
49
+ test('CLEAN_AUDIT_SCHEMA requires posted, reviewUrl, and reason', () => {
50
+ assert.match(
51
+ convergeSource,
52
+ /const CLEAN_AUDIT_SCHEMA = \{[\s\S]*?required: \['posted', 'reviewUrl', 'reason'\]/,
53
+ );
54
+ });
55
+
56
+ test('the standards-only call site breaks with a clean-audit blocker when the post does not land', () => {
57
+ const branch = convergeSource.slice(
58
+ convergeSource.indexOf('if (isStandardsOnlyRound(findings)) {'),
59
+ convergeSource.indexOf('if (findings.length > 0) {'),
60
+ );
61
+ assert.match(branch, /const auditResult = await postCleanAudit\(head\)/);
62
+ assert.match(branch, /if \(!auditResult\?\.posted\)/);
63
+ assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
64
+ assert.match(branch, /\bbreak\b/);
65
+ });
66
+
67
+ test('the all-clean call site breaks with a clean-audit blocker when the post does not land', () => {
68
+ const branch = convergeSource.slice(
69
+ convergeSource.indexOf('all lenses clean on'),
70
+ convergeSource.indexOf("if (phase === 'COPILOT') {"),
71
+ );
72
+ assert.match(branch, /const auditResult = await postCleanAudit\(head\)/);
73
+ assert.match(branch, /if \(!auditResult\?\.posted\)/);
74
+ assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
75
+ assert.match(branch, /\bbreak\b/);
76
+ });