hstack 0.7.0 → 0.16.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 +277 -0
- package/README.md +39 -13
- package/VERSION +1 -1
- package/dist/commands/doctor.js +51 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +8 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/descriptions.js +167 -0
- package/dist/lib/descriptions.js.map +1 -0
- package/dist/lib/diff.js +1 -1
- package/dist/lib/git.js +16 -0
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/wire.js +108 -4
- package/dist/lib/wire.js.map +1 -1
- package/dist/manifest.js +17 -2
- package/dist/manifest.js.map +1 -1
- package/package.json +3 -1
- package/template/.claude/agents/adversarial-reviewer.md +16 -64
- package/template/.claude/agents/app-architect.md +12 -49
- package/template/.claude/agents/data-architect.md +13 -51
- package/template/.claude/agents/data-specialist.md +5 -50
- package/template/.claude/agents/implementer.md +8 -65
- package/template/.claude/agents/kernel-fit-analyst.md +7 -68
- package/template/.claude/agents/planner.md +7 -42
- package/template/.claude/agents/product-discovery.md +12 -48
- package/template/.claude/agents/product-manager.md +8 -43
- package/template/.claude/agents/researcher.md +5 -41
- package/template/.claude/agents/security-reviewer.md +19 -54
- package/template/.claude/agents/spec-author.md +18 -52
- package/template/.claude/agents/stack-architect.md +14 -43
- package/template/.claude/agents/test-strategist.md +16 -57
- package/template/.claude/agents/ui-ux-briefer.md +6 -36
- package/template/.claude/agents/verifier.md +13 -45
- package/template/.claude/skills/hstack-adr-new/SKILL.md +6 -33
- package/template/.claude/skills/hstack-adversarial-review/SKILL.md +31 -52
- package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +157 -0
- package/template/.claude/skills/hstack-app-architecture/SKILL.md +2 -29
- package/template/.claude/skills/hstack-branch/SKILL.md +4 -31
- package/template/.claude/skills/hstack-brownfield-init/SKILL.md +10 -37
- package/template/.claude/skills/hstack-change-new/SKILL.md +4 -31
- package/template/.claude/skills/hstack-change-plan/SKILL.md +21 -32
- package/template/.claude/skills/hstack-commit/SKILL.md +7 -35
- package/template/.claude/skills/hstack-configure/SKILL.md +7 -34
- package/template/.claude/skills/hstack-coord/SKILL.md +3 -39
- package/template/.claude/skills/hstack-data-architecture/SKILL.md +4 -30
- package/template/.claude/skills/hstack-data-review/SKILL.md +3 -42
- package/template/.claude/skills/hstack-finalize/SKILL.md +30 -49
- package/template/.claude/skills/hstack-flag/SKILL.md +9 -48
- package/template/.claude/skills/hstack-greenfield-init/SKILL.md +9 -36
- package/template/.claude/skills/hstack-help/SKILL.md +11 -37
- package/template/.claude/skills/hstack-implement/SKILL.md +28 -58
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +7 -46
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +5 -60
- package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +42 -0
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +12 -50
- package/template/.claude/skills/hstack-module-spec/SKILL.md +5 -32
- package/template/.claude/skills/hstack-product-discovery/SKILL.md +5 -31
- package/template/.claude/skills/hstack-research/SKILL.md +3 -33
- package/template/.claude/skills/hstack-scaffold/SKILL.md +2 -29
- package/template/.claude/skills/hstack-security-review/SKILL.md +5 -43
- package/template/.claude/skills/hstack-ship/SKILL.md +43 -53
- package/template/.claude/skills/hstack-stack-decide/SKILL.md +3 -30
- package/template/.claude/skills/hstack-story-draft/SKILL.md +6 -33
- package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +4 -31
- package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +9 -44
- package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +10 -37
- package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +8 -35
- package/template/.claude/skills/hstack-telemetry/SKILL.md +5 -30
- package/template/.claude/skills/hstack-test-plan/SKILL.md +23 -46
- package/template/.claude/skills/hstack-ui-brief/SKILL.md +3 -30
- package/template/.claude/skills/hstack-verify/SKILL.md +26 -48
- package/template/KERNEL.md +410 -0
- package/template/scripts/compute-merge-readiness.mjs +780 -0
- package/template/scripts/coord/coord_scan.py +121 -22
- package/template/scripts/run-gates.sh +388 -0
- package/template/scripts/telemetry/insights/kernel_fit.py +1 -1
- package/template/scripts/telemetry/insights/token_economics.py +181 -8
- package/template/scripts/telemetry/parsers/sidecars.py +61 -0
- package/template/scripts/telemetry/parsers/transcripts.py +135 -22
- package/template/scripts/telemetry/render.py +68 -3
- package/template/scripts/telemetry/report.py +16 -4
- package/template/scripts/telemetry/run_kernel_fit.py +6 -2
- package/template/scripts/telemetry/session_id.py +139 -0
- package/template/scripts/validate-spec.mjs +3303 -0
- package/template/templates/adr.md +7 -0
- package/template/templates/adversarial-review.md +5 -5
- package/template/templates/ci-cd.md +14 -0
- package/template/templates/coord-message.md +3 -2
- package/template/templates/data-architecture.md +3 -6
- package/template/templates/kernel-fit-finding.md +2 -2
- package/template/templates/kernel-fit-flag.md +2 -2
- package/template/templates/plan.md +4 -0
- package/template/templates/product-brief.md +2 -2
- package/template/templates/roadmap.md +41 -0
- package/template/templates/security-review.md +1 -1
- package/template/templates/telemetry-sidecar.md +56 -13
- package/template/templates/test-plan.md +1 -1
- package/template/CLAUDE.md +0 -443
- package/template/templates/mvp-scope.md +0 -34
|
@@ -37,7 +37,7 @@ _SCRIPTS = _THIS.parent.parent
|
|
|
37
37
|
if str(_SCRIPTS) not in sys.path:
|
|
38
38
|
sys.path.insert(0, str(_SCRIPTS))
|
|
39
39
|
|
|
40
|
-
from telemetry.parsers import frontmatter, commits, transcripts # noqa: E402
|
|
40
|
+
from telemetry.parsers import frontmatter, commits, transcripts, sidecars # noqa: E402
|
|
41
41
|
from telemetry.insights import ( # noqa: E402
|
|
42
42
|
token_economics, workflow_shape, quality_outcomes,
|
|
43
43
|
overengineering, contract_drift, kernel_fit,
|
|
@@ -59,7 +59,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
59
59
|
hstack_root = repo / "hstack"
|
|
60
60
|
if not hstack_root.is_dir():
|
|
61
61
|
# Permit running against the template repo itself (no hstack/ prefix).
|
|
62
|
-
|
|
62
|
+
# CLAUDE.md is the pre-ADR-0010 kernel name — still accepted so an
|
|
63
|
+
# un-migrated tree keeps reporting.
|
|
64
|
+
if (repo / "specs").is_dir() and (
|
|
65
|
+
(repo / "KERNEL.md").is_file() or (repo / "CLAUDE.md").is_file()
|
|
66
|
+
):
|
|
63
67
|
hstack_root = repo
|
|
64
68
|
else:
|
|
65
69
|
print(f"error: no hstack/ directory at {repo}", file=sys.stderr)
|
|
@@ -86,9 +90,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
90
|
session_rows = transcripts.collect_session_rows([repo], since=since_dt)
|
|
87
91
|
print(f"telemetry: {len(session_rows)} sessions in window", file=sys.stderr)
|
|
88
92
|
|
|
93
|
+
# Phase sidecars (ADR-0009). Gitignored and derivative: absent on a fresh
|
|
94
|
+
# clone, which reports as zero coverage rather than as zero cost.
|
|
95
|
+
phase_sidecars = sidecars.load_sidecars(hstack_root)
|
|
96
|
+
print(f"telemetry: {len(phase_sidecars)} phase sidecars", file=sys.stderr)
|
|
97
|
+
|
|
89
98
|
findings_dir = hstack_root / "kernel-fit" / "findings"
|
|
90
99
|
metrics = {
|
|
91
|
-
"token_economics": token_economics.compute(session_rows, changes),
|
|
100
|
+
"token_economics": token_economics.compute(session_rows, changes, phase_sidecars),
|
|
92
101
|
"workflow_shape": workflow_shape.compute(git_commits, changes, session_rows),
|
|
93
102
|
"quality_outcomes": quality_outcomes.compute(git_commits, changes),
|
|
94
103
|
"overengineering": overengineering.compute(git_commits, changes, session_rows, repo),
|
|
@@ -112,7 +121,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
112
121
|
# Consumed by the telemetry UI; carries the same derivative-only guarantee.
|
|
113
122
|
json_path = out_path.with_suffix(".json")
|
|
114
123
|
payload = {
|
|
115
|
-
|
|
124
|
+
# 2 — token_economics gains te_4_cost_per_phase / te_5_cost_per_change
|
|
125
|
+
# (ADR-0009). Additive: every schema_version-1 key keeps its shape.
|
|
126
|
+
"schema_version": 2,
|
|
116
127
|
"repo": repo.name,
|
|
117
128
|
"generated": date.today().isoformat(),
|
|
118
129
|
"window_days": window_days,
|
|
@@ -123,6 +134,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
123
134
|
"module_specs": len(module_specs),
|
|
124
135
|
"commits": len(git_commits),
|
|
125
136
|
"sessions": len(session_rows),
|
|
137
|
+
"phase_sidecars": len(phase_sidecars),
|
|
126
138
|
},
|
|
127
139
|
"watch_list": render.watch_items(metrics),
|
|
128
140
|
"metrics": metrics,
|
|
@@ -49,8 +49,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
49
49
|
repo = args.repo.resolve()
|
|
50
50
|
hstack_root = repo / "hstack"
|
|
51
51
|
if not hstack_root.is_dir():
|
|
52
|
-
# Permit running against the template repo itself.
|
|
53
|
-
|
|
52
|
+
# Permit running against the template repo itself. CLAUDE.md is the
|
|
53
|
+
# pre-ADR-0010 kernel name — still accepted so an un-migrated tree
|
|
54
|
+
# keeps scanning.
|
|
55
|
+
if (repo / "specs").is_dir() and (
|
|
56
|
+
(repo / "KERNEL.md").is_file() or (repo / "CLAUDE.md").is_file()
|
|
57
|
+
):
|
|
54
58
|
hstack_root = repo
|
|
55
59
|
else:
|
|
56
60
|
print(f"error: no hstack/ directory at {repo}", file=sys.stderr)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve the active Claude Code session id — the one heuristic, in one place.
|
|
3
|
+
|
|
4
|
+
Per ADR-0009 § Neutral: `/hstack:flag` resolved the session id inline in its own
|
|
5
|
+
prose; the five sidecar-emitting Skills would have duplicated that prose a sixth
|
|
6
|
+
time. They call this module instead.
|
|
7
|
+
|
|
8
|
+
The heuristic (unchanged from ADR-0005): Claude Code writes one `*.jsonl`
|
|
9
|
+
transcript per session under `~/.claude/projects/<encoded-cwd>/`, where
|
|
10
|
+
`<encoded-cwd>` is the absolute working directory with `/` replaced by `-`. The
|
|
11
|
+
most recently modified file in that directory is the active session; the session
|
|
12
|
+
id is its basename.
|
|
13
|
+
|
|
14
|
+
Both the heuristic and the transcript layout are harness implementation details,
|
|
15
|
+
not a contract (ADR-0009 § Negative). Every failure mode resolves to
|
|
16
|
+
`session_id: null` — never a guess, never a halt. A null session id means the
|
|
17
|
+
phase reports as *unmeasured*, which is the whole point of the null-not-zero
|
|
18
|
+
rule.
|
|
19
|
+
|
|
20
|
+
CLI:
|
|
21
|
+
python3 scripts/telemetry/session_id.py # JSON on stdout, always exit 0
|
|
22
|
+
python3 scripts/telemetry/session_id.py --cwd DIR # resolve for another dir
|
|
23
|
+
|
|
24
|
+
Output:
|
|
25
|
+
{"session_id": "062b8fe8-…" | null,
|
|
26
|
+
"transcript_path": "/Users/…/062b8fe8-….jsonl" | null,
|
|
27
|
+
"message_count": 1423,
|
|
28
|
+
"source": "transcript" | "unresolved",
|
|
29
|
+
"fallback_id": "a3f9c1d2",
|
|
30
|
+
"now": "2026-08-15T09:12:44Z"}
|
|
31
|
+
|
|
32
|
+
`now` is the UTC stamp at resolution time, so a Skill opening a phase gets its
|
|
33
|
+
`session_id` and its `phase_opened_at` from one call. `fallback_id` is short
|
|
34
|
+
random hex for callers that need a non-null id anyway (`/hstack:flag` writes
|
|
35
|
+
`fallback-<id>`); sidecar emitters ignore it and write `null`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import sys
|
|
44
|
+
from datetime import datetime, timezone
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
PROJECTS_ROOT = Path.home() / ".claude" / "projects"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def encoded_cwd(cwd: Path | str | None = None) -> str:
|
|
52
|
+
"""`/Users/jane/code/moso` → `-Users-jane-code-moso`."""
|
|
53
|
+
p = Path(cwd) if cwd is not None else Path.cwd()
|
|
54
|
+
return str(p.resolve()).replace("/", "-")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def project_dir(cwd: Path | str | None = None, projects_root: Path | None = None) -> Path:
|
|
58
|
+
root = projects_root or PROJECTS_ROOT
|
|
59
|
+
return root / encoded_cwd(cwd)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def utc_now_iso() -> str:
|
|
63
|
+
"""ISO-8601 UTC, second precision, `Z` suffix — the sidecar timestamp format."""
|
|
64
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_session(cwd: Path | str | None = None,
|
|
68
|
+
projects_root: Path | None = None) -> dict:
|
|
69
|
+
"""Resolve the active session for `cwd`. Never raises; never halts."""
|
|
70
|
+
out = {
|
|
71
|
+
"session_id": None,
|
|
72
|
+
"transcript_path": None,
|
|
73
|
+
"message_count": 0,
|
|
74
|
+
"source": "unresolved",
|
|
75
|
+
"fallback_id": os.urandom(4).hex(),
|
|
76
|
+
"now": utc_now_iso(),
|
|
77
|
+
}
|
|
78
|
+
d = project_dir(cwd, projects_root)
|
|
79
|
+
try:
|
|
80
|
+
candidates = [f for f in d.glob("*.jsonl") if f.is_file()]
|
|
81
|
+
except OSError:
|
|
82
|
+
return out
|
|
83
|
+
if not candidates:
|
|
84
|
+
return out
|
|
85
|
+
try:
|
|
86
|
+
newest = max(candidates, key=lambda f: f.stat().st_mtime)
|
|
87
|
+
except OSError:
|
|
88
|
+
return out
|
|
89
|
+
out["session_id"] = newest.stem
|
|
90
|
+
out["transcript_path"] = str(newest)
|
|
91
|
+
out["source"] = "transcript"
|
|
92
|
+
out["message_count"] = _count_lines(newest)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _count_lines(path: Path) -> int:
|
|
97
|
+
try:
|
|
98
|
+
with open(path, "rb") as fh:
|
|
99
|
+
return sum(1 for _ in fh)
|
|
100
|
+
except OSError:
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transcript_for_session(session_id: str | None,
|
|
105
|
+
projects_root: Path | None = None) -> Path | None:
|
|
106
|
+
"""Locate a transcript by session id, across every project directory.
|
|
107
|
+
|
|
108
|
+
The sidecar carries the session id, not the path: a Conductor worktree can be
|
|
109
|
+
renamed or removed between the write and the read, and the encoded-cwd
|
|
110
|
+
directory with it. Returns None when no transcript is found — retention
|
|
111
|
+
sweep, another machine, or a harness layout change. None means *unmeasured*.
|
|
112
|
+
"""
|
|
113
|
+
if not session_id or not isinstance(session_id, str):
|
|
114
|
+
return None
|
|
115
|
+
if session_id.startswith("fallback-"):
|
|
116
|
+
return None
|
|
117
|
+
root = projects_root or PROJECTS_ROOT
|
|
118
|
+
if not root.is_dir():
|
|
119
|
+
return None
|
|
120
|
+
for d in sorted(root.iterdir()):
|
|
121
|
+
if not d.is_dir():
|
|
122
|
+
continue
|
|
123
|
+
candidate = d / f"{session_id}.jsonl"
|
|
124
|
+
if candidate.is_file():
|
|
125
|
+
return candidate
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(argv: list[str] | None = None) -> int:
|
|
130
|
+
parser = argparse.ArgumentParser(description="Resolve the active Claude Code session id.")
|
|
131
|
+
parser.add_argument("--cwd", type=Path, default=None,
|
|
132
|
+
help="Directory to resolve for (default: current working directory).")
|
|
133
|
+
args = parser.parse_args(argv)
|
|
134
|
+
print(json.dumps(resolve_session(args.cwd), indent=1))
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
sys.exit(main())
|