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.
Files changed (99) hide show
  1. package/CHANGELOG.md +277 -0
  2. package/README.md +39 -13
  3. package/VERSION +1 -1
  4. package/dist/commands/doctor.js +51 -1
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/update.js +8 -2
  7. package/dist/commands/update.js.map +1 -1
  8. package/dist/lib/descriptions.js +167 -0
  9. package/dist/lib/descriptions.js.map +1 -0
  10. package/dist/lib/diff.js +1 -1
  11. package/dist/lib/git.js +16 -0
  12. package/dist/lib/git.js.map +1 -1
  13. package/dist/lib/wire.js +108 -4
  14. package/dist/lib/wire.js.map +1 -1
  15. package/dist/manifest.js +17 -2
  16. package/dist/manifest.js.map +1 -1
  17. package/package.json +3 -1
  18. package/template/.claude/agents/adversarial-reviewer.md +16 -64
  19. package/template/.claude/agents/app-architect.md +12 -49
  20. package/template/.claude/agents/data-architect.md +13 -51
  21. package/template/.claude/agents/data-specialist.md +5 -50
  22. package/template/.claude/agents/implementer.md +8 -65
  23. package/template/.claude/agents/kernel-fit-analyst.md +7 -68
  24. package/template/.claude/agents/planner.md +7 -42
  25. package/template/.claude/agents/product-discovery.md +12 -48
  26. package/template/.claude/agents/product-manager.md +8 -43
  27. package/template/.claude/agents/researcher.md +5 -41
  28. package/template/.claude/agents/security-reviewer.md +19 -54
  29. package/template/.claude/agents/spec-author.md +18 -52
  30. package/template/.claude/agents/stack-architect.md +14 -43
  31. package/template/.claude/agents/test-strategist.md +16 -57
  32. package/template/.claude/agents/ui-ux-briefer.md +6 -36
  33. package/template/.claude/agents/verifier.md +13 -45
  34. package/template/.claude/skills/hstack-adr-new/SKILL.md +6 -33
  35. package/template/.claude/skills/hstack-adversarial-review/SKILL.md +31 -52
  36. package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +157 -0
  37. package/template/.claude/skills/hstack-app-architecture/SKILL.md +2 -29
  38. package/template/.claude/skills/hstack-branch/SKILL.md +4 -31
  39. package/template/.claude/skills/hstack-brownfield-init/SKILL.md +10 -37
  40. package/template/.claude/skills/hstack-change-new/SKILL.md +4 -31
  41. package/template/.claude/skills/hstack-change-plan/SKILL.md +21 -32
  42. package/template/.claude/skills/hstack-commit/SKILL.md +7 -35
  43. package/template/.claude/skills/hstack-configure/SKILL.md +7 -34
  44. package/template/.claude/skills/hstack-coord/SKILL.md +3 -39
  45. package/template/.claude/skills/hstack-data-architecture/SKILL.md +4 -30
  46. package/template/.claude/skills/hstack-data-review/SKILL.md +3 -42
  47. package/template/.claude/skills/hstack-finalize/SKILL.md +30 -49
  48. package/template/.claude/skills/hstack-flag/SKILL.md +9 -48
  49. package/template/.claude/skills/hstack-greenfield-init/SKILL.md +9 -36
  50. package/template/.claude/skills/hstack-help/SKILL.md +11 -37
  51. package/template/.claude/skills/hstack-implement/SKILL.md +28 -58
  52. package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +7 -46
  53. package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +5 -60
  54. package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +42 -0
  55. package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +12 -50
  56. package/template/.claude/skills/hstack-module-spec/SKILL.md +5 -32
  57. package/template/.claude/skills/hstack-product-discovery/SKILL.md +5 -31
  58. package/template/.claude/skills/hstack-research/SKILL.md +3 -33
  59. package/template/.claude/skills/hstack-scaffold/SKILL.md +2 -29
  60. package/template/.claude/skills/hstack-security-review/SKILL.md +5 -43
  61. package/template/.claude/skills/hstack-ship/SKILL.md +43 -53
  62. package/template/.claude/skills/hstack-stack-decide/SKILL.md +3 -30
  63. package/template/.claude/skills/hstack-story-draft/SKILL.md +6 -33
  64. package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +4 -31
  65. package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +9 -44
  66. package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +10 -37
  67. package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +8 -35
  68. package/template/.claude/skills/hstack-telemetry/SKILL.md +5 -30
  69. package/template/.claude/skills/hstack-test-plan/SKILL.md +23 -46
  70. package/template/.claude/skills/hstack-ui-brief/SKILL.md +3 -30
  71. package/template/.claude/skills/hstack-verify/SKILL.md +26 -48
  72. package/template/KERNEL.md +410 -0
  73. package/template/scripts/compute-merge-readiness.mjs +780 -0
  74. package/template/scripts/coord/coord_scan.py +121 -22
  75. package/template/scripts/run-gates.sh +388 -0
  76. package/template/scripts/telemetry/insights/kernel_fit.py +1 -1
  77. package/template/scripts/telemetry/insights/token_economics.py +181 -8
  78. package/template/scripts/telemetry/parsers/sidecars.py +61 -0
  79. package/template/scripts/telemetry/parsers/transcripts.py +135 -22
  80. package/template/scripts/telemetry/render.py +68 -3
  81. package/template/scripts/telemetry/report.py +16 -4
  82. package/template/scripts/telemetry/run_kernel_fit.py +6 -2
  83. package/template/scripts/telemetry/session_id.py +139 -0
  84. package/template/scripts/validate-spec.mjs +3303 -0
  85. package/template/templates/adr.md +7 -0
  86. package/template/templates/adversarial-review.md +5 -5
  87. package/template/templates/ci-cd.md +14 -0
  88. package/template/templates/coord-message.md +3 -2
  89. package/template/templates/data-architecture.md +3 -6
  90. package/template/templates/kernel-fit-finding.md +2 -2
  91. package/template/templates/kernel-fit-flag.md +2 -2
  92. package/template/templates/plan.md +4 -0
  93. package/template/templates/product-brief.md +2 -2
  94. package/template/templates/roadmap.md +41 -0
  95. package/template/templates/security-review.md +1 -1
  96. package/template/templates/telemetry-sidecar.md +56 -13
  97. package/template/templates/test-plan.md +1 -1
  98. package/template/CLAUDE.md +0 -443
  99. 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
- if (repo / "specs").is_dir() and (repo / "CLAUDE.md").is_file():
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
- "schema_version": 1,
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
- if (repo / "specs").is_dir() and (repo / "CLAUDE.md").is_file():
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())