hstack 0.2.0 → 0.3.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.
- package/CHANGELOG.md +20 -0
- package/VERSION +1 -1
- package/dist/manifest.js +1 -0
- package/dist/manifest.js.map +1 -1
- package/package.json +1 -1
- package/template/.claude/agents/kernel-fit-analyst.md +137 -0
- package/template/.claude/skills/hstack-adr-new/SKILL.md +6 -1
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +164 -0
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +180 -0
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +159 -0
- package/template/.claude/skills/hstack-telemetry/SKILL.md +3 -2
- package/template/CLAUDE.md +19 -0
- package/template/scripts/telemetry/insights/kernel_fit.py +438 -0
- package/template/scripts/telemetry/render.py +73 -0
- package/template/scripts/telemetry/report.py +4 -1
- package/template/scripts/telemetry/run_kernel_fit.py +91 -0
- package/template/templates/adr.md +2 -1
- package/template/templates/kernel-fit-finding.md +62 -0
|
@@ -23,6 +23,7 @@ def render_report(metrics: dict, repo_name: str, window_days: int | None) -> str
|
|
|
23
23
|
_render_quality_outcomes(lines, metrics.get("quality_outcomes", {}))
|
|
24
24
|
_render_overengineering(lines, metrics.get("overengineering", {}))
|
|
25
25
|
_render_contract_drift(lines, metrics.get("contract_drift", {}))
|
|
26
|
+
_render_kernel_fit(lines, metrics.get("kernel_fit", {}))
|
|
26
27
|
|
|
27
28
|
_render_watch_list(lines, metrics)
|
|
28
29
|
|
|
@@ -271,6 +272,65 @@ def _render_contract_drift(lines: list[str], cd: dict) -> None:
|
|
|
271
272
|
)
|
|
272
273
|
|
|
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
|
+
|
|
274
334
|
def _render_watch_list(lines: list[str], metrics: dict) -> None:
|
|
275
335
|
_h(lines, 2, "Watch list")
|
|
276
336
|
items = []
|
|
@@ -303,6 +363,19 @@ def _render_watch_list(lines: list[str], metrics: dict) -> None:
|
|
|
303
363
|
if r["drift_flag"]:
|
|
304
364
|
items.append(f"Module-spec drift: `{r['module']}` is `needs-refresh` with {r['recent_commits_touching_module']} recent commits.")
|
|
305
365
|
|
|
366
|
+
# Kernel-fit fired patterns
|
|
367
|
+
kf = metrics.get("kernel_fit", {})
|
|
368
|
+
for key, label in (
|
|
369
|
+
("kf_p1_category_a_claim_spans_production_paths", "KF-P1"),
|
|
370
|
+
("kf_p2_halt_reason_cluster_uncovered_by_enum", "KF-P2"),
|
|
371
|
+
("kf_p3_skill_precondition_violated_and_recoverable", "KF-P3"),
|
|
372
|
+
):
|
|
373
|
+
block = kf.get(key, {})
|
|
374
|
+
if block.get("fired"):
|
|
375
|
+
rc = block.get("evidence_row_count", 0)
|
|
376
|
+
items.append(f"Kernel-fit {label} fired with {rc} evidence row(s) — "
|
|
377
|
+
f"run `/hstack:kernel-fit-scan` to synthesize findings.")
|
|
378
|
+
|
|
306
379
|
if not items:
|
|
307
380
|
_p(lines, "_Nothing flagged. Either everything is healthy, or the metrics need tuning._")
|
|
308
381
|
return
|
|
@@ -39,7 +39,7 @@ if str(_SCRIPTS) not in sys.path:
|
|
|
39
39
|
from telemetry.parsers import frontmatter, commits, transcripts # noqa: E402
|
|
40
40
|
from telemetry.insights import ( # noqa: E402
|
|
41
41
|
token_economics, workflow_shape, quality_outcomes,
|
|
42
|
-
overengineering, contract_drift,
|
|
42
|
+
overengineering, contract_drift, kernel_fit,
|
|
43
43
|
)
|
|
44
44
|
from telemetry import render # noqa: E402
|
|
45
45
|
|
|
@@ -85,12 +85,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
85
85
|
session_rows = transcripts.collect_session_rows([repo], since=since_dt)
|
|
86
86
|
print(f"telemetry: {len(session_rows)} sessions in window", file=sys.stderr)
|
|
87
87
|
|
|
88
|
+
findings_dir = hstack_root / "kernel-fit" / "findings"
|
|
88
89
|
metrics = {
|
|
89
90
|
"token_economics": token_economics.compute(session_rows, changes),
|
|
90
91
|
"workflow_shape": workflow_shape.compute(git_commits, changes, session_rows),
|
|
91
92
|
"quality_outcomes": quality_outcomes.compute(git_commits, changes),
|
|
92
93
|
"overengineering": overengineering.compute(git_commits, changes, session_rows, repo),
|
|
93
94
|
"contract_drift": contract_drift.compute(git_commits, changes, tech_debt, adrs, module_specs),
|
|
95
|
+
"kernel_fit": kernel_fit.compute(git_commits, changes, tech_debt, adrs, module_specs,
|
|
96
|
+
session_rows, findings_dir),
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
report_md = render.render_report(metrics, repo_name=repo.name, window_days=window_days)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the kernel-fit detection layer in isolation and dump JSON to stdout.
|
|
3
|
+
|
|
4
|
+
Thin wrapper around `telemetry.insights.kernel_fit.compute()`. Used by
|
|
5
|
+
`/hstack:kernel-fit-scan` to obtain the structured evidence blob the
|
|
6
|
+
`kernel-fit-analyst` subagent consumes, without producing the full
|
|
7
|
+
telemetry report.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python scripts/telemetry/run_kernel_fit.py [--repo <path>] [--window <days>]
|
|
11
|
+
|
|
12
|
+
Defaults match `report.py`: `--repo` is cwd; `--window` is 30 days.
|
|
13
|
+
|
|
14
|
+
The output is a single JSON object whose top-level keys mirror the
|
|
15
|
+
`compute()` return value (`existing_open_findings_by_pattern`, and one
|
|
16
|
+
key per pattern). The scan Skill reads stdout, parses, and passes the
|
|
17
|
+
blob to the subagent.
|
|
18
|
+
|
|
19
|
+
Read-only. No writes, no git side-effects.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from datetime import datetime, timedelta, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# Match report.py's import-path bootstrap so this script can be invoked
|
|
31
|
+
# from any directory.
|
|
32
|
+
_THIS = Path(__file__).resolve()
|
|
33
|
+
_SCRIPTS = _THIS.parent.parent
|
|
34
|
+
if str(_SCRIPTS) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(_SCRIPTS))
|
|
36
|
+
|
|
37
|
+
from telemetry.parsers import frontmatter, commits, transcripts # noqa: E402
|
|
38
|
+
from telemetry.insights import kernel_fit # noqa: E402
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main(argv: list[str] | None = None) -> int:
|
|
42
|
+
parser = argparse.ArgumentParser(description="Run kernel-fit detection and dump JSON.")
|
|
43
|
+
parser.add_argument("--repo", type=Path, default=Path.cwd(),
|
|
44
|
+
help="Consuming-repo root (default: cwd).")
|
|
45
|
+
parser.add_argument("--window", type=int, default=30,
|
|
46
|
+
help="Limit git/transcript history to last N days (default: 30; 0 = all).")
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
|
|
49
|
+
repo = args.repo.resolve()
|
|
50
|
+
hstack_root = repo / "hstack"
|
|
51
|
+
if not hstack_root.is_dir():
|
|
52
|
+
# Permit running against the template repo itself.
|
|
53
|
+
if (repo / "specs").is_dir() and (repo / "CLAUDE.md").is_file():
|
|
54
|
+
hstack_root = repo
|
|
55
|
+
else:
|
|
56
|
+
print(f"error: no hstack/ directory at {repo}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
window_days: int | None = args.window if args.window > 0 else None
|
|
60
|
+
since_dt: datetime | None = (
|
|
61
|
+
datetime.now(timezone.utc) - timedelta(days=window_days)
|
|
62
|
+
) if window_days else None
|
|
63
|
+
|
|
64
|
+
changes = frontmatter.load_change_artifacts(hstack_root)
|
|
65
|
+
tech_debt = frontmatter.load_tech_debt(hstack_root)
|
|
66
|
+
adrs = frontmatter.load_adrs(hstack_root)
|
|
67
|
+
module_specs = frontmatter.load_module_specs(hstack_root)
|
|
68
|
+
git_commits = commits.parse_commits(repo, since_days=window_days)
|
|
69
|
+
session_rows = transcripts.collect_session_rows([repo], since=since_dt)
|
|
70
|
+
|
|
71
|
+
findings_dir = hstack_root / "kernel-fit" / "findings"
|
|
72
|
+
|
|
73
|
+
result = kernel_fit.compute(
|
|
74
|
+
commits=git_commits,
|
|
75
|
+
changes=changes,
|
|
76
|
+
tech_debt=tech_debt,
|
|
77
|
+
adrs=adrs,
|
|
78
|
+
module_specs=module_specs,
|
|
79
|
+
session_rows=session_rows,
|
|
80
|
+
findings_dir=findings_dir,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Path objects are not JSON-serializable; strip them where they appear
|
|
84
|
+
# (existing-findings paths get re-derived by the analyst from the id).
|
|
85
|
+
json.dump(result, sys.stdout, default=str, indent=2)
|
|
86
|
+
sys.stdout.write("\n")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
sys.exit(main())
|
|
@@ -8,9 +8,10 @@ supersedes: null # ADR id when this ADR replaces another
|
|
|
8
8
|
superseded-by: null # ADR id when this ADR has been replaced; reciprocal with supersedes
|
|
9
9
|
related-change-specs: []
|
|
10
10
|
related-modules: []
|
|
11
|
+
promoted-from-kernel-fit: [] # KF-NNNN ids that motivated this ADR; reciprocal with kernel-fit-finding.promoted-to (KF-04)
|
|
11
12
|
created: <YYYY-MM-DD>
|
|
12
13
|
updated: <YYYY-MM-DD>
|
|
13
|
-
schema-version:
|
|
14
|
+
schema-version: 2
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
## Title
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: KF-<NNNN>-<slug>
|
|
3
|
+
type: kernel-fit-finding
|
|
4
|
+
status: open # open | acknowledged | dismissed | promoted | superseded | archived
|
|
5
|
+
owner: null # git-handle of the triager; null until first triage
|
|
6
|
+
pattern: <KF-P1 | KF-P2 | KF-P3 | …> # detector pattern that fired; enumerated in scripts/telemetry/insights/kernel_fit.py
|
|
7
|
+
confidence: medium # high | medium | low
|
|
8
|
+
detected-by: kernel-fit-analyst
|
|
9
|
+
detected-at: <ISO-8601 timestamp>
|
|
10
|
+
evidence-row-count: 0 # integer; must equal len(evidence-rows) per KF-01
|
|
11
|
+
evidence-rows: [] # YAML array of {change|adr|td, signal} dicts; one entry per row counted above
|
|
12
|
+
related-findings: [] # KF ids — prior or adjacent findings on the same kernel surface
|
|
13
|
+
promoted-to: null # `adr:<ADR-NNNN-slug>` | `tech-debt:<TD-NNNN-slug>` | null; reciprocal with the target artifact's `promoted-from-kernel-fit`; required when status: promoted
|
|
14
|
+
dismissed-reason: null # ≥50 chars of prose; required when status: dismissed (per KF-05)
|
|
15
|
+
superseded-by: null # KF id when status: superseded
|
|
16
|
+
created: <YYYY-MM-DD>
|
|
17
|
+
updated: <YYYY-MM-DD>
|
|
18
|
+
schema-version: 1
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Title
|
|
22
|
+
|
|
23
|
+
_Short noun phrase naming the kernel-fit gap. Example: "Category-A claim spans production paths — engineer likely meant Category B."_
|
|
24
|
+
|
|
25
|
+
## Pattern fired
|
|
26
|
+
|
|
27
|
+
_Name the detector pattern (KF-P1 / KF-P2 / KF-P3 / …) and one paragraph describing what the detector found. Quote the pattern's defining condition from `kernel_fit.py` if helpful._
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
_Per evidence row, a 2–3 sentence prose summary with at least one inline citation (change-id, ADR-id, TD-id, commit-sha, kernel section). KF-01 requires `len(evidence-rows)` in frontmatter to equal `evidence-row-count`; the prose here must cite each row at least once. No prose without a citation._
|
|
32
|
+
|
|
33
|
+
1. ...
|
|
34
|
+
2. ...
|
|
35
|
+
|
|
36
|
+
## Kernel surface implicated
|
|
37
|
+
|
|
38
|
+
_Single-sentence pointer to the kernel section, template, validator rule, or Skill flow that the finding suggests revising. Examples: "`template/CLAUDE.md § Frontmatter contract` — the `internal-tooling` field"; "`template/templates/change-spec.md` frontmatter — `surfaces` enum"; "`/hstack:adversarial-review` precondition check at SKILL.md line 61"._
|
|
39
|
+
|
|
40
|
+
## Proposed direction
|
|
41
|
+
|
|
42
|
+
_One paragraph. Name a direction the kernel revision could take — split a flag, add an enum case, add a Skill precondition, amend a section. This is NOT a full ADR — that work is done by `spec-author` if and when the engineer invokes `/hstack:kernel-fit-promote`. Keep this as a sketch, not a specification._
|
|
43
|
+
|
|
44
|
+
## Counter-explanations (challenge prompt — mandatory)
|
|
45
|
+
|
|
46
|
+
_Two reasons this finding might NOT warrant a kernel change. If you cannot produce two, the analyst auto-downgrades `confidence` to `low` per KF-03. The challenge defends against false-positives the same way `## Consequences` § "name two consequences that look bad" defends ADRs._
|
|
47
|
+
|
|
48
|
+
1. ...
|
|
49
|
+
2. ...
|
|
50
|
+
|
|
51
|
+
## Confidence rationale
|
|
52
|
+
|
|
53
|
+
_One paragraph defending the `confidence` enum value against the validator rules. `high` requires `evidence-row-count >= 3` AND ≥2 distinct change-specs cited (KF-02). `medium` is the conservative default. `low` carries no notification and is appropriate when evidence is thin or the challenge prompts substantially weaken the finding._
|
|
54
|
+
|
|
55
|
+
## Triage Log
|
|
56
|
+
|
|
57
|
+
_Populated by `/hstack:kernel-fit-triage` and `/hstack:kernel-fit-promote` as the finding's status transitions. Section is empty until the first transition out of `open`._
|
|
58
|
+
|
|
59
|
+
- `status: open → acknowledged` on `<YYYY-MM-DD>` by `<owner>`. Triggered by `/hstack:kernel-fit-triage <id> --action acknowledge`.
|
|
60
|
+
- `status: open → dismissed` on `<YYYY-MM-DD>` by `<owner>`. Reason: `<dismissed-reason>`.
|
|
61
|
+
- `status: acknowledged → promoted` on `<YYYY-MM-DD>` by `<owner>`. Promoted to: `<promoted-to>`. Triggered by `/hstack:kernel-fit-promote <id> --slug <adr-slug>`.
|
|
62
|
+
- `status: open → superseded` on `<YYYY-MM-DD>` by the next `kernel-fit-analyst` run. Superseded by: `<superseded-by>`.
|