hstack 0.7.1 → 0.17.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 (131) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/README.md +90 -154
  3. package/VERSION +1 -1
  4. package/dist/commands/doctor.js +69 -9
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/init.js +7 -3
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.js +17 -4
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/lib/descriptions.js +160 -0
  11. package/dist/lib/descriptions.js.map +1 -0
  12. package/dist/lib/diff.js +1 -1
  13. package/dist/lib/git.js +16 -0
  14. package/dist/lib/git.js.map +1 -1
  15. package/dist/lib/wire.js +274 -142
  16. package/dist/lib/wire.js.map +1 -1
  17. package/dist/manifest.js +49 -22
  18. package/dist/manifest.js.map +1 -1
  19. package/package.json +4 -3
  20. package/template/.claude/agents/adversarial-reviewer.md +42 -98
  21. package/template/.claude/agents/app-architect.md +44 -133
  22. package/template/.claude/agents/data-architect.md +46 -127
  23. package/template/.claude/agents/test-strategist.md +39 -109
  24. package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -100
  25. package/template/.claude/skills/hstack-adversarial-review/SKILL.md +47 -174
  26. package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +134 -0
  27. package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -95
  28. package/template/.claude/skills/hstack-commit/SKILL.md +28 -97
  29. package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -82
  30. package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
  31. package/template/.claude/skills/hstack-story/SKILL.md +58 -0
  32. package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
  33. package/template/.claude/skills/hstack-wrap/SKILL.md +48 -0
  34. package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
  35. package/template/KERNEL.md +167 -0
  36. package/template/templates/adr.md +9 -18
  37. package/template/templates/app-architecture.md +38 -50
  38. package/template/templates/data-architecture.md +39 -93
  39. package/template/templates/infrastructure.md +54 -148
  40. package/template/templates/roadmap.md +35 -0
  41. package/template/templates/story.md +7 -13
  42. package/template/templates/tech-debt.md +2 -37
  43. package/template/templates/tech-stack.md +2 -12
  44. package/template/.claude/agents/data-specialist.md +0 -126
  45. package/template/.claude/agents/implementer.md +0 -154
  46. package/template/.claude/agents/kernel-fit-analyst.md +0 -190
  47. package/template/.claude/agents/planner.md +0 -110
  48. package/template/.claude/agents/product-discovery.md +0 -165
  49. package/template/.claude/agents/product-manager.md +0 -111
  50. package/template/.claude/agents/researcher.md +0 -123
  51. package/template/.claude/agents/security-reviewer.md +0 -123
  52. package/template/.claude/agents/spec-author.md +0 -123
  53. package/template/.claude/agents/stack-architect.md +0 -137
  54. package/template/.claude/agents/ui-ux-briefer.md +0 -105
  55. package/template/.claude/agents/verifier.md +0 -113
  56. package/template/.claude/skills/hstack-branch/SKILL.md +0 -103
  57. package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -158
  58. package/template/.claude/skills/hstack-change-new/SKILL.md +0 -111
  59. package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -119
  60. package/template/.claude/skills/hstack-configure/SKILL.md +0 -126
  61. package/template/.claude/skills/hstack-coord/SKILL.md +0 -131
  62. package/template/.claude/skills/hstack-data-review/SKILL.md +0 -132
  63. package/template/.claude/skills/hstack-finalize/SKILL.md +0 -159
  64. package/template/.claude/skills/hstack-flag/SKILL.md +0 -138
  65. package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -175
  66. package/template/.claude/skills/hstack-help/SKILL.md +0 -179
  67. package/template/.claude/skills/hstack-implement/SKILL.md +0 -185
  68. package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -164
  69. package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -197
  70. package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -159
  71. package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -105
  72. package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -101
  73. package/template/.claude/skills/hstack-research/SKILL.md +0 -145
  74. package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -140
  75. package/template/.claude/skills/hstack-security-review/SKILL.md +0 -133
  76. package/template/.claude/skills/hstack-ship/SKILL.md +0 -129
  77. package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -108
  78. package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -119
  79. package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -122
  80. package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -158
  81. package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -113
  82. package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -104
  83. package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -98
  84. package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -182
  85. package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -108
  86. package/template/.claude/skills/hstack-verify/SKILL.md +0 -150
  87. package/template/CLAUDE.md +0 -443
  88. package/template/scripts/coord/coord_scan.py +0 -681
  89. package/template/scripts/telemetry/__init__.py +0 -6
  90. package/template/scripts/telemetry/insights/__init__.py +0 -0
  91. package/template/scripts/telemetry/insights/contract_drift.py +0 -137
  92. package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
  93. package/template/scripts/telemetry/insights/overengineering.py +0 -115
  94. package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
  95. package/template/scripts/telemetry/insights/token_economics.py +0 -129
  96. package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
  97. package/template/scripts/telemetry/parsers/__init__.py +0 -0
  98. package/template/scripts/telemetry/parsers/bodies.py +0 -87
  99. package/template/scripts/telemetry/parsers/commits.py +0 -219
  100. package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
  101. package/template/scripts/telemetry/parsers/transcripts.py +0 -181
  102. package/template/scripts/telemetry/render.py +0 -395
  103. package/template/scripts/telemetry/report.py +0 -138
  104. package/template/scripts/telemetry/run_kernel_fit.py +0 -91
  105. package/template/templates/adversarial-review.md +0 -54
  106. package/template/templates/bootstrap.md +0 -105
  107. package/template/templates/change-spec.md +0 -82
  108. package/template/templates/ci-cd.md +0 -27
  109. package/template/templates/coord-message.md +0 -55
  110. package/template/templates/data-review.md +0 -54
  111. package/template/templates/discovery/brainstorm.md +0 -69
  112. package/template/templates/discovery/forcing-questions.md +0 -76
  113. package/template/templates/discovery/project-brief.md +0 -53
  114. package/template/templates/figma-handoff.md +0 -38
  115. package/template/templates/glossary.md +0 -20
  116. package/template/templates/hardening-checklist.md +0 -73
  117. package/template/templates/incident-runbook.md +0 -57
  118. package/template/templates/kernel-fit-finding.md +0 -63
  119. package/template/templates/kernel-fit-flag.md +0 -52
  120. package/template/templates/module-spec.md +0 -49
  121. package/template/templates/mvp-scope.md +0 -34
  122. package/template/templates/persona.md +0 -38
  123. package/template/templates/plan.md +0 -49
  124. package/template/templates/product-brief.md +0 -56
  125. package/template/templates/security-review.md +0 -63
  126. package/template/templates/telemetry-sidecar.md +0 -184
  127. package/template/templates/test-plan.md +0 -119
  128. package/template/templates/threat-model.md +0 -54
  129. package/template/templates/ui-brief.md +0 -49
  130. package/template/templates/verification.md +0 -63
  131. package/template/templates/vision.md +0 -34
@@ -1,181 +0,0 @@
1
- """Walk Claude Code transcript JSONL files for token economics and halt signals.
2
-
3
- Generalized from scripts/audit-token-usage.py. Same cost-score weighting; same
4
- classify-by-Skill heuristic. Adds: halt-sentinel detection, per-session
5
- start/end timestamps, cwd grouping for Conductor worktree dedup.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import re
12
- from collections import defaultdict
13
- from datetime import datetime
14
- from pathlib import Path
15
- from typing import Iterable
16
-
17
-
18
- PROJECTS_ROOT = Path.home() / ".claude" / "projects"
19
-
20
- HSTACK_SLASH = re.compile(r"/hstack:([a-z][a-z0-9\-]*)")
21
- HSTACK_CMD_TAG = re.compile(r"<command-name>/?hstack:?-?([a-z][a-z0-9\-]*)</command-name>")
22
- SUBAGENT_TYPE = re.compile(r'"subagent_type"\s*:\s*"([a-z][a-z0-9\-]*)"')
23
- HALT_SENTINEL = re.compile(r"HSTACK-HALT:\s*reason=([a-z-]+)", re.IGNORECASE)
24
-
25
-
26
- def message_text(msg) -> str:
27
- if msg is None:
28
- return ""
29
- if isinstance(msg, str):
30
- return msg
31
- content = msg.get("content") if isinstance(msg, dict) else None
32
- if content is None:
33
- return ""
34
- if isinstance(content, str):
35
- return content
36
- if isinstance(content, list):
37
- parts = []
38
- for blk in content:
39
- if isinstance(blk, dict):
40
- if blk.get("type") == "text" and isinstance(blk.get("text"), str):
41
- parts.append(blk["text"])
42
- else:
43
- parts.append(json.dumps(blk, default=str))
44
- else:
45
- parts.append(str(blk))
46
- return "\n".join(parts)
47
- return json.dumps(content, default=str)
48
-
49
-
50
- def discover_transcript_files(repo_paths: Iterable[Path]) -> list[Path]:
51
- """Discover transcript jsonl files for the given consuming-repo paths.
52
-
53
- Each repo_path corresponds to a Claude Code project dir (`-Users-...` form).
54
- We accept either real repo paths (we convert) or already-converted prefixes.
55
- """
56
- if not PROJECTS_ROOT.exists():
57
- return []
58
- prefixes = []
59
- for p in repo_paths:
60
- p_str = str(Path(p).resolve()).replace("/", "-")
61
- prefixes.append(p_str)
62
- # Also match Conductor worktrees of the repo's basename
63
- basename = Path(p).name
64
- prefixes.append(f"-Users-hugoganet-conductor-workspaces-{basename}-")
65
- files = []
66
- for d in PROJECTS_ROOT.iterdir():
67
- if not d.is_dir():
68
- continue
69
- name = d.name
70
- if not any(name.startswith(prefix) or name == prefix.rstrip("-") for prefix in prefixes):
71
- continue
72
- for f in d.glob("*.jsonl"):
73
- files.append(f)
74
- return files
75
-
76
-
77
- def load_session(path: Path) -> list[dict]:
78
- out = []
79
- try:
80
- with open(path, "r", errors="replace") as fh:
81
- for line in fh:
82
- line = line.strip()
83
- if not line:
84
- continue
85
- try:
86
- out.append(json.loads(line))
87
- except json.JSONDecodeError:
88
- continue
89
- except OSError:
90
- return []
91
- return out
92
-
93
-
94
- def classify_session(records: list[dict]) -> tuple[str | None, set[str], list[str]]:
95
- """Return (skill_name, set of subagent types invoked, halt reasons)."""
96
- skill = None
97
- subagents: set[str] = set()
98
- halts: list[str] = []
99
- for r in records:
100
- t = r.get("type")
101
- if t == "user" and skill is None:
102
- text = message_text(r.get("message"))
103
- m = HSTACK_CMD_TAG.search(text) or HSTACK_SLASH.search(text)
104
- if m:
105
- skill = m.group(1)
106
- elif t == "assistant":
107
- text = message_text(r.get("message"))
108
- for sa in SUBAGENT_TYPE.findall(text):
109
- subagents.add(sa)
110
- for h in HALT_SENTINEL.findall(text):
111
- halts.append(h.lower())
112
- return skill, subagents, halts
113
-
114
-
115
- def session_bounds(records: list[dict]) -> tuple[datetime | None, datetime | None]:
116
- first = last = None
117
- for r in records:
118
- ts = r.get("timestamp")
119
- if not ts:
120
- continue
121
- try:
122
- dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
123
- except (ValueError, AttributeError):
124
- continue
125
- if first is None or dt < first:
126
- first = dt
127
- if last is None or dt > last:
128
- last = dt
129
- return first, last
130
-
131
-
132
- def tally_usage(records: list[dict]) -> dict[str, int]:
133
- totals: dict[str, int] = defaultdict(int)
134
- for r in records:
135
- if r.get("type") != "assistant":
136
- continue
137
- usage = (r.get("message") or {}).get("usage") or {}
138
- for k in ("input_tokens", "cache_creation_input_tokens",
139
- "cache_read_input_tokens", "output_tokens"):
140
- totals[k] += usage.get(k, 0) or 0
141
- totals["turns"] += 1
142
- return dict(totals)
143
-
144
-
145
- def cost_score(totals: dict[str, int]) -> int:
146
- """Same weighting as scripts/audit-token-usage.py: input + 1.25*cache_creation
147
- + 0.10*cache_read + 5*output. Approximates billable $-weight for ranking."""
148
- return (
149
- totals.get("input_tokens", 0)
150
- + int(totals.get("cache_creation_input_tokens", 0) * 1.25)
151
- + int(totals.get("cache_read_input_tokens", 0) * 0.10)
152
- + totals.get("output_tokens", 0) * 5
153
- )
154
-
155
-
156
- def collect_session_rows(repo_paths: Iterable[Path], since: datetime | None = None) -> list[dict]:
157
- """Walk every transcript for the given repos and return one row per session."""
158
- out = []
159
- for f in discover_transcript_files(repo_paths):
160
- recs = load_session(f)
161
- if not recs:
162
- continue
163
- skill, subagents, halts = classify_session(recs)
164
- first, last = session_bounds(recs)
165
- if since is not None and last is not None and last < since:
166
- continue
167
- totals = tally_usage(recs)
168
- if totals.get("turns", 0) == 0:
169
- continue
170
- out.append({
171
- "file": f,
172
- "project_dir": f.parent.name,
173
- "skill": skill,
174
- "subagents": sorted(subagents),
175
- "halt_reasons": halts,
176
- "started_at": first,
177
- "ended_at": last,
178
- "totals": totals,
179
- "cost_score": cost_score(totals),
180
- })
181
- return out
@@ -1,395 +0,0 @@
1
- """Render the metrics dict to a markdown report."""
2
-
3
- from __future__ import annotations
4
-
5
- from datetime import date
6
- from typing import Any
7
-
8
-
9
- def render_report(metrics: dict, repo_name: str, window_days: int | None) -> str:
10
- lines: list[str] = []
11
- lines.append(f"# hstack-telemetry — {repo_name}")
12
- lines.append("")
13
- lines.append(f"_Generated {date.today().isoformat()}; window: "
14
- f"{'last ' + str(window_days) + ' days' if window_days else 'all history'}._")
15
- lines.append("")
16
- lines.append("Retrospective observability for the hstack workflow. All metrics are derived "
17
- "from on-disk artifacts (frontmatter + bodies), git history, and Claude Code "
18
- "transcripts. This report is read-only; the artifacts are the source of truth.")
19
- lines.append("")
20
-
21
- _render_token_economics(lines, metrics.get("token_economics", {}))
22
- _render_workflow_shape(lines, metrics.get("workflow_shape", {}))
23
- _render_quality_outcomes(lines, metrics.get("quality_outcomes", {}))
24
- _render_overengineering(lines, metrics.get("overengineering", {}))
25
- _render_contract_drift(lines, metrics.get("contract_drift", {}))
26
- _render_kernel_fit(lines, metrics.get("kernel_fit", {}))
27
-
28
- _render_watch_list(lines, metrics)
29
-
30
- return "\n".join(lines) + "\n"
31
-
32
-
33
- # ---------------- helpers ----------------
34
-
35
- def _h(lines: list[str], level: int, text: str) -> None:
36
- lines.append("#" * level + " " + text)
37
- lines.append("")
38
-
39
-
40
- def _p(lines: list[str], text: str) -> None:
41
- lines.append(text)
42
- lines.append("")
43
-
44
-
45
- def _table(lines: list[str], headers: list[str], rows: list[list[Any]]) -> None:
46
- if not rows:
47
- lines.append("_(no data)_")
48
- lines.append("")
49
- return
50
- lines.append("| " + " | ".join(headers) + " |")
51
- lines.append("|" + "|".join("---" for _ in headers) + "|")
52
- for r in rows:
53
- cells = ["" if v is None else str(v) for v in r]
54
- lines.append("| " + " | ".join(cells) + " |")
55
- lines.append("")
56
-
57
-
58
- # ---------------- buckets ----------------
59
-
60
- def _render_token_economics(lines: list[str], te: dict) -> None:
61
- _h(lines, 2, "Token economics")
62
-
63
- te1 = te.get("te_1_cost_per_change", {})
64
- _h(lines, 3, "TE-1 — cost-score per Skill (proxy for per-change cost)")
65
- _p(lines, te1.get("note", ""))
66
- _table(
67
- lines,
68
- ["skill", "sessions", "cost-score total", "cost-score / session"],
69
- [[r["skill"], r["sessions"], r["cost_score_total"], r["cost_score_mean"]]
70
- for r in te1.get("rows", [])],
71
- )
72
-
73
- te2 = te.get("te_2_cache_hit_per_subagent", {})
74
- _h(lines, 3, "TE-2 — cache-hit ratio (per Skill, v1 coarse grain)")
75
- _p(lines, "ratio = cache_read / (cache_read + cache_creation). Below 0.5 → context "
76
- "is being rebuilt rather than reused. Above 0.8 → cross-session reuse is healthy.")
77
- _table(
78
- lines,
79
- ["skill", "turns", "cache_read", "cache_creation", "ratio"],
80
- [[r["skill"], r["turns"], r["cache_read"], r["cache_creation"],
81
- (f"{r['ratio']:.2%}" if r["ratio"] is not None else "-")]
82
- for r in te2.get("rows", [])],
83
- )
84
-
85
- te3 = te.get("te_3_subagent_entry_tax", {})
86
- _h(lines, 3, "TE-3 — subagent entry-tax amortization")
87
- _p(lines, te3.get("note", ""))
88
- _table(
89
- lines,
90
- ["subagent", "appearances", "host cache_creation total", "/ invocation"],
91
- [[r["subagent"], r["appearances"], r["host_cache_creation_total"],
92
- r["host_cache_creation_per_invocation"]]
93
- for r in te3.get("rows", [])],
94
- )
95
-
96
-
97
- def _render_workflow_shape(lines: list[str], ws: dict) -> None:
98
- _h(lines, 2, "Workflow shape")
99
-
100
- ws1 = ws.get("ws_1_phase_duration", {})
101
- _h(lines, 3, "WS-1 — phase duration (between successive implement commits)")
102
- summary = ws1.get("summary")
103
- if summary:
104
- _table(
105
- lines,
106
- ["intervals counted", "median (h)", "p90 (h)", "max (h)"],
107
- [[summary["count"], summary["median_h"], summary["p90_h"], summary["max_h"]]],
108
- )
109
- else:
110
- _p(lines, "_(no implement commits in window)_")
111
- _table(
112
- lines,
113
- ["change", "phases", "min (h)", "mean (h)", "max (h)"],
114
- [[r["change"], r["phases"], r["min_h"], r["mean_h"], r["max_h"]]
115
- for r in ws1.get("per_change", [])[:10]],
116
- )
117
-
118
- ws2 = ws.get("ws_2_gate_findings_density", {})
119
- _h(lines, 3, "WS-2 — gate findings density")
120
- _table(
121
- lines,
122
- ["metric", "value"],
123
- [
124
- ["adversarial-review total findings (all changes)", ws2.get("adversarial_review_total_findings", 0)],
125
- ["changes with ≥1 adversarial finding", ws2.get("adversarial_review_changes_with_findings", 0)],
126
- ["security-reviews scored", ws2.get("security_review_changes_scored", 0)],
127
- ["security-review CONCERNS-or-FAIL items", ws2.get("security_review_concerns_or_fail_items", 0)],
128
- ["data-reviews scored", ws2.get("data_review_changes_scored", 0)],
129
- ["data-review partial/missing RLS items", ws2.get("data_review_partial_or_missing_items", 0)],
130
- ],
131
- )
132
-
133
- ws4 = ws.get("ws_4_scope_amendment_rate", {})
134
- _h(lines, 3, "WS-4 — scope-amendment rate (upper bound)")
135
- _p(lines, ws4.get("note", ""))
136
- _table(
137
- lines,
138
- ["metric", "value"],
139
- [
140
- ["total spec.md writes after ready-for-implementation", ws4.get("total_spec_writes_after_rfi", 0)],
141
- ["changes with ≥1 post-RFI spec write", ws4.get("changes_with_post_rfi_writes", 0)],
142
- ["rate (changes-with-amendment / total)", f"{ws4.get('rate', 0):.2%}"],
143
- ],
144
- )
145
-
146
- ws6 = ws.get("ws_6_halt_reasons", {})
147
- _h(lines, 3, "WS-6 — halt reasons (HSTACK-HALT sentinel)")
148
- _p(lines, ws6.get("note", ""))
149
- _table(
150
- lines,
151
- ["reason", "count"],
152
- [[r["reason"], r["count"]] for r in ws6.get("rows", [])],
153
- )
154
-
155
-
156
- def _render_quality_outcomes(lines: list[str], qo: dict) -> None:
157
- _h(lines, 2, "Quality outcomes")
158
-
159
- qo2 = qo.get("qo_2_severity_resolution_mix", {})
160
- _h(lines, 3, "QO-2 — severity × resolution-type")
161
- res_types = ["commit", "tech-debt", "justified-in-prose", "other"]
162
- _table(
163
- lines,
164
- ["severity"] + res_types,
165
- [[r["severity"]] + [r.get(rt, 0) for rt in res_types] for r in qo2.get("rows", [])],
166
- )
167
- smells = qo2.get("high_severity_in_prose_smells", [])
168
- if smells:
169
- _p(lines, "**High/critical findings resolved as `justified-in-prose` (smell):**")
170
- _table(
171
- lines,
172
- ["change", "finding", "category", "severity"],
173
- [[s["change"], s["finding_id"], s["category"], s["severity"]] for s in smells],
174
- )
175
- else:
176
- _p(lines, "_No high/critical findings resolved as `justified-in-prose` — healthy._")
177
-
178
- qo3 = qo.get("qo_3_test_immutability_audit", {})
179
- _h(lines, 3, "QO-3 — test-immutability audit")
180
- _p(lines, qo3.get("note", ""))
181
- _p(lines, f"Authorized test-change commits: **{qo3.get('authorized_count', 0)}**")
182
- cv = qo3.get("candidate_violations", [])
183
- if cv:
184
- _p(lines, "**Non-implement commits touching test files without canonical authorization (review manually):**")
185
- _table(
186
- lines,
187
- ["sha", "subject", "artifact_type", "test files"],
188
- [[c["sha"][:8], c["subject"][:80], c["artifact_type"] or "-",
189
- ", ".join(c["test_files"][:3]) + ("…" if len(c["test_files"]) > 3 else "")]
190
- for c in cv[:20]],
191
- )
192
- else:
193
- _p(lines, "_No candidate violations found — healthy._")
194
-
195
- qo4 = qo.get("qo_4_observed_vs_promised", {})
196
- _h(lines, 3, "QO-4 — verifier observed-vs-promised (test-plan-coverage)")
197
- summary = qo4.get("summary", {})
198
- if summary:
199
- for key, buckets in summary.items():
200
- _p(lines, f"**{key}:** " + ", ".join(f"{v}={n}" for v, n in sorted(buckets.items())))
201
- _table(
202
- lines,
203
- ["change", "edge-cases", "tenant-isolation", "performance-budgets"],
204
- [[r.get("change"), r.get("edge-cases", "-"),
205
- r.get("tenant-isolation", "-"), r.get("performance-budgets", "-")]
206
- for r in qo4.get("per_change", [])],
207
- )
208
-
209
-
210
- def _render_overengineering(lines: list[str], oe: dict) -> None:
211
- _h(lines, 2, "Overengineering detection")
212
-
213
- oe1 = oe.get("oe_1_artifact_to_diff_ratio", {})
214
- _h(lines, 3, "OE-1 — artifact tokens per diff line")
215
- _p(lines, "Ratio above ~50 tokens/line suggests heavy spec-vs-code; ratio below ~5 "
216
- "suggests an under-specified change. Honest signal, not a verdict.")
217
- _table(
218
- lines,
219
- ["change", "artifact tokens", "diff lines (implement commits)", "tokens / line"],
220
- [[r["change"], r["artifact_tokens"], r["diff_lines"], r["tokens_per_diff_line"] or "-"]
221
- for r in oe1.get("rows", [])],
222
- )
223
-
224
- oe3 = oe.get("oe_3_subagent_context_load_amortization", {})
225
- _h(lines, 3, "OE-3 — subagent invocations × host cost")
226
- _table(
227
- lines,
228
- ["subagent", "invocations", "host cost-score total", "/ invocation"],
229
- [[r["subagent"], r["invocations"], r["host_cost_score_total"], r["cost_per_invocation"]]
230
- for r in oe3.get("rows", [])],
231
- )
232
-
233
- oe5 = oe.get("oe_5_trivial_eligible_full_gauntlet", {})
234
- _h(lines, 3, "OE-5 — trivial-eligible changes that ran the full gauntlet")
235
- _p(lines, oe5.get("note", ""))
236
- _table(
237
- lines,
238
- ["change", "status", "surfaces", "files touched in implement"],
239
- [[r["change"], r["status"], r["surfaces"], r["files_touched_in_implement_commits"]]
240
- for r in oe5.get("rows", [])],
241
- )
242
-
243
-
244
- def _render_contract_drift(lines: list[str], cd: dict) -> None:
245
- _h(lines, 2, "Contract drift")
246
-
247
- ms = cd.get("module_spec_staleness", {})
248
- _h(lines, 3, "Module-spec staleness × recent activity")
249
- _table(
250
- lines,
251
- ["module", "spec status", "spec updated", "recent commits touching module", "drift flag"],
252
- [[r["module"], r["status"], r["updated"],
253
- r["recent_commits_touching_module"], "⚠️" if r["drift_flag"] else ""]
254
- for r in ms.get("rows", [])],
255
- )
256
-
257
- adr = cd.get("adr_supersession_lag", {})
258
- _h(lines, 3, "ADR supersession lag")
259
- _table(
260
- lines,
261
- ["ADR", "superseded by", "lag (days)"],
262
- [[r["adr"], r["superseded_by"], r["lag_days"]] for r in adr.get("rows", [])],
263
- )
264
-
265
- td = cd.get("tech_debt_half_life", {})
266
- _h(lines, 3, "Tech-debt half-life by exit path")
267
- summary = td.get("summary", {})
268
- _table(
269
- lines,
270
- ["exit path", "count", "median days", "max days"],
271
- [[k, v["count"], v["median_days"], v["max_days"]] for k, v in sorted(summary.items())],
272
- )
273
-
274
-
275
- def _render_kernel_fit(lines: list[str], kf: dict) -> None:
276
- _h(lines, 2, "Kernel-fit candidates")
277
- _p(lines, "Patterns suggesting the kernel itself (CLAUDE.md, templates, validators, Skill "
278
- "flows) may need revision. Each fired pattern is also written as a durable finding "
279
- "by `/hstack:kernel-fit-scan` at `hstack/kernel-fit/findings/KF-NNNN-*.md`. The "
280
- "table below is a rollup; the findings are the canonical artifact. See ADR-0004.")
281
-
282
- existing = kf.get("existing_open_findings_by_pattern", {})
283
- if existing:
284
- total_open = sum(len(v) for v in existing.values())
285
- _p(lines, f"**Currently open findings:** {total_open} "
286
- + "(" + ", ".join(f"{p}: {len(ids)}" for p, ids in sorted(existing.items())) + ")")
287
-
288
- patterns = [
289
- ("kf_p1_category_a_claim_spans_production_paths", "KF-P1 — category-a-claim-spans-production-paths"),
290
- ("kf_p2_halt_reason_cluster_uncovered_by_enum", "KF-P2 — halt-reason-cluster-uncovered-by-enum"),
291
- ("kf_p3_skill_precondition_violated_and_recoverable", "KF-P3 — skill-precondition-violated-and-recoverable"),
292
- ]
293
- for key, heading in patterns:
294
- block = kf.get(key, {})
295
- _h(lines, 3, heading)
296
- _p(lines, block.get("note", ""))
297
- fired = block.get("fired", False)
298
- rc = block.get("evidence_row_count", 0)
299
- if fired:
300
- _p(lines, f"**Fired** — {rc} evidence row(s).")
301
- else:
302
- _p(lines, f"_(not fired — {rc} evidence row(s); threshold not met)_")
303
-
304
- # Per-pattern row rendering.
305
- if key == "kf_p1_category_a_claim_spans_production_paths":
306
- rows = block.get("evidence_rows", [])
307
- _table(
308
- lines,
309
- ["change", "production paths", "enables", "downstream consumers", "classification"],
310
- [[r["change"], r["production_paths_count"], r["enables_count"],
311
- ", ".join(r["downstream_consumers"][:3]) + ("…" if len(r["downstream_consumers"]) > 3 else ""),
312
- r["classification_candidate"]]
313
- for r in rows[:10]],
314
- )
315
- elif key == "kf_p2_halt_reason_cluster_uncovered_by_enum":
316
- rows = block.get("evidence_rows", [])
317
- _table(
318
- lines,
319
- ["cluster", "size", "representative context (truncated)"],
320
- [[r["cluster_id"], r["size"], r["representative_context"][:120]]
321
- for r in rows[:10]],
322
- )
323
- elif key == "kf_p3_skill_precondition_violated_and_recoverable":
324
- rows = block.get("evidence_rows", [])
325
- _table(
326
- lines,
327
- ["change", "finding", "matched keywords", "commit subject (truncated)"],
328
- [[r["change"], r["finding_id"], ", ".join(r["matched_keywords"]),
329
- (r["commit_subject"] or "-")[:80]]
330
- for r in rows[:10]],
331
- )
332
-
333
-
334
- def watch_items(metrics: dict) -> list[str]:
335
- """Compute the watch-list lines from the metrics dict.
336
-
337
- Shared between the markdown renderer and the JSON emission so both
338
- surfaces flag the same anomalies.
339
- """
340
- items: list[str] = []
341
-
342
- # TE-2: any Skill cache-hit below 0.5
343
- te2 = metrics.get("token_economics", {}).get("te_2_cache_hit_per_subagent", {})
344
- for r in te2.get("rows", []):
345
- if r.get("ratio") is not None and r["ratio"] < 0.5 and r["turns"] > 5:
346
- items.append(f"Low cache-hit on `{r['skill']}` ({r['ratio']:.0%}) — context is being rebuilt.")
347
-
348
- # QO-2 smells
349
- smells = metrics.get("quality_outcomes", {}).get("qo_2_severity_resolution_mix", {}).get("high_severity_in_prose_smells", [])
350
- if smells:
351
- items.append(f"{len(smells)} high/critical adversarial finding(s) resolved as `justified-in-prose` — review.")
352
-
353
- # QO-3 violations
354
- cv = metrics.get("quality_outcomes", {}).get("qo_3_test_immutability_audit", {}).get("candidate_violations", [])
355
- if cv:
356
- items.append(f"{len(cv)} candidate test-immutability violations (non-implement commits touching tests without authorization).")
357
-
358
- # WS-4 amendment rate above 30%
359
- ws4 = metrics.get("workflow_shape", {}).get("ws_4_scope_amendment_rate", {})
360
- rate = ws4.get("rate", 0)
361
- if rate > 0.3:
362
- items.append(f"Scope-amendment upper-bound rate at {rate:.0%}. If real (not just status flips), planner/test-strategist may be missing scope up front.")
363
-
364
- # Module drift
365
- ms = metrics.get("contract_drift", {}).get("module_spec_staleness", {}).get("rows", [])
366
- for r in ms:
367
- if r["drift_flag"]:
368
- items.append(f"Module-spec drift: `{r['module']}` is `needs-refresh` with {r['recent_commits_touching_module']} recent commits.")
369
-
370
- # Kernel-fit fired patterns
371
- kf = metrics.get("kernel_fit", {})
372
- for key, label in (
373
- ("kf_p1_category_a_claim_spans_production_paths", "KF-P1"),
374
- ("kf_p2_halt_reason_cluster_uncovered_by_enum", "KF-P2"),
375
- ("kf_p3_skill_precondition_violated_and_recoverable", "KF-P3"),
376
- ):
377
- block = kf.get(key, {})
378
- if block.get("fired"):
379
- rc = block.get("evidence_row_count", 0)
380
- items.append(f"Kernel-fit {label} fired with {rc} evidence row(s) — "
381
- f"run `/hstack:kernel-fit-scan` to synthesize findings.")
382
-
383
- return items
384
-
385
-
386
- def _render_watch_list(lines: list[str], metrics: dict) -> None:
387
- _h(lines, 2, "Watch list")
388
- items = watch_items(metrics)
389
-
390
- if not items:
391
- _p(lines, "_Nothing flagged. Either everything is healthy, or the metrics need tuning._")
392
- return
393
- for item in items:
394
- lines.append(f"- {item}")
395
- lines.append("")