hstack 0.16.0 → 0.18.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 (134) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +76 -163
  3. package/VERSION +1 -1
  4. package/dist/commands/doctor.js +18 -8
  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 +9 -2
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/lib/descriptions.js +10 -17
  11. package/dist/lib/descriptions.js.map +1 -1
  12. package/dist/lib/wire.js +166 -138
  13. package/dist/lib/wire.js.map +1 -1
  14. package/dist/manifest.js +41 -29
  15. package/dist/manifest.js.map +1 -1
  16. package/package.json +3 -4
  17. package/template/.claude/agents/adversarial-reviewer.md +44 -50
  18. package/template/.claude/agents/app-architect.md +44 -96
  19. package/template/.claude/agents/data-architect.md +46 -89
  20. package/template/.claude/agents/test-strategist.md +39 -68
  21. package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
  22. package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
  23. package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +89 -103
  24. package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
  25. package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
  26. package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
  27. package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
  28. package/template/.claude/skills/hstack-story/SKILL.md +58 -0
  29. package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
  30. package/template/.claude/skills/hstack-wrap/SKILL.md +50 -0
  31. package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
  32. package/template/KERNEL.md +75 -318
  33. package/template/templates/adr.md +2 -18
  34. package/template/templates/app-architecture.md +38 -50
  35. package/template/templates/code-standards.md +107 -0
  36. package/template/templates/data-architecture.md +38 -89
  37. package/template/templates/eslint-clean-code.mjs +322 -0
  38. package/template/templates/infrastructure.md +54 -148
  39. package/template/templates/roadmap.md +6 -12
  40. package/template/templates/story.md +7 -13
  41. package/template/templates/tech-debt.md +2 -37
  42. package/template/templates/tech-stack.md +2 -12
  43. package/template/.claude/agents/data-specialist.md +0 -81
  44. package/template/.claude/agents/implementer.md +0 -97
  45. package/template/.claude/agents/kernel-fit-analyst.md +0 -129
  46. package/template/.claude/agents/planner.md +0 -75
  47. package/template/.claude/agents/product-discovery.md +0 -129
  48. package/template/.claude/agents/product-manager.md +0 -76
  49. package/template/.claude/agents/researcher.md +0 -87
  50. package/template/.claude/agents/security-reviewer.md +0 -88
  51. package/template/.claude/agents/spec-author.md +0 -89
  52. package/template/.claude/agents/stack-architect.md +0 -108
  53. package/template/.claude/agents/ui-ux-briefer.md +0 -75
  54. package/template/.claude/agents/verifier.md +0 -81
  55. package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
  56. package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
  57. package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
  58. package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
  59. package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
  60. package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
  61. package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
  62. package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
  63. package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
  64. package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
  65. package/template/.claude/skills/hstack-help/SKILL.md +0 -153
  66. package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
  67. package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
  68. package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
  69. package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
  70. package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
  71. package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
  72. package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
  73. package/template/.claude/skills/hstack-research/SKILL.md +0 -115
  74. package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
  75. package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
  76. package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
  77. package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
  78. package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
  79. package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
  80. package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
  81. package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
  82. package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
  83. package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
  84. package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
  85. package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
  86. package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
  87. package/template/scripts/compute-merge-readiness.mjs +0 -780
  88. package/template/scripts/coord/coord_scan.py +0 -681
  89. package/template/scripts/run-gates.sh +0 -388
  90. package/template/scripts/telemetry/__init__.py +0 -6
  91. package/template/scripts/telemetry/insights/__init__.py +0 -0
  92. package/template/scripts/telemetry/insights/contract_drift.py +0 -137
  93. package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
  94. package/template/scripts/telemetry/insights/overengineering.py +0 -115
  95. package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
  96. package/template/scripts/telemetry/insights/token_economics.py +0 -302
  97. package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
  98. package/template/scripts/telemetry/parsers/__init__.py +0 -0
  99. package/template/scripts/telemetry/parsers/bodies.py +0 -87
  100. package/template/scripts/telemetry/parsers/commits.py +0 -219
  101. package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
  102. package/template/scripts/telemetry/parsers/sidecars.py +0 -61
  103. package/template/scripts/telemetry/parsers/transcripts.py +0 -294
  104. package/template/scripts/telemetry/render.py +0 -460
  105. package/template/scripts/telemetry/report.py +0 -150
  106. package/template/scripts/telemetry/run_kernel_fit.py +0 -95
  107. package/template/scripts/telemetry/session_id.py +0 -139
  108. package/template/scripts/validate-spec.mjs +0 -3303
  109. package/template/templates/adversarial-review.md +0 -54
  110. package/template/templates/bootstrap.md +0 -105
  111. package/template/templates/change-spec.md +0 -82
  112. package/template/templates/ci-cd.md +0 -41
  113. package/template/templates/coord-message.md +0 -56
  114. package/template/templates/data-review.md +0 -54
  115. package/template/templates/discovery/brainstorm.md +0 -69
  116. package/template/templates/discovery/forcing-questions.md +0 -76
  117. package/template/templates/discovery/project-brief.md +0 -53
  118. package/template/templates/figma-handoff.md +0 -38
  119. package/template/templates/glossary.md +0 -20
  120. package/template/templates/hardening-checklist.md +0 -73
  121. package/template/templates/incident-runbook.md +0 -57
  122. package/template/templates/kernel-fit-finding.md +0 -63
  123. package/template/templates/kernel-fit-flag.md +0 -52
  124. package/template/templates/module-spec.md +0 -49
  125. package/template/templates/persona.md +0 -38
  126. package/template/templates/plan.md +0 -53
  127. package/template/templates/product-brief.md +0 -56
  128. package/template/templates/security-review.md +0 -63
  129. package/template/templates/telemetry-sidecar.md +0 -227
  130. package/template/templates/test-plan.md +0 -119
  131. package/template/templates/threat-model.md +0 -54
  132. package/template/templates/ui-brief.md +0 -49
  133. package/template/templates/verification.md +0 -63
  134. package/template/templates/vision.md +0 -34
@@ -1,294 +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. Adds:
4
- halt-sentinel detection, per-session start/end timestamps, cwd grouping for
5
- Conductor worktree dedup, and (ADR-0009) per-phase usage summation bounded by a
6
- sidecar's phase window.
7
-
8
- Session→Skill attribution reads *structured invocation markers only* — the
9
- `<command-name>` tag the harness writes for a slash command, and `Skill`
10
- tool_use blocks. Free text is never matched: a prompt that merely mentions
11
- `/hstack:coord` used to capture the whole session, which credited `coord` with
12
- roughly half of all measured cache-read tokens (ADR-0009 § Context, defect 2).
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import json
18
- import re
19
- from collections import defaultdict
20
- from datetime import datetime, timezone
21
- from pathlib import Path
22
- from typing import Iterable
23
-
24
- from telemetry.session_id import transcript_for_session
25
-
26
-
27
- PROJECTS_ROOT = Path.home() / ".claude" / "projects"
28
-
29
- # Structured markers only. `<command-name>` is emitted by the harness around a
30
- # slash-command invocation; both the `/hstack:foo` and `/hstack-foo` spellings
31
- # appear in real transcripts.
32
- HSTACK_CMD_TAG = re.compile(r"<command-name>/?hstack[:\-]([a-z][a-z0-9\-]*)</command-name>")
33
- # `Skill` tool_use input, optionally namespaced by a plugin prefix.
34
- HSTACK_SKILL_NAME = re.compile(r"^(?:[A-Za-z0-9_.\-]+:)?hstack[:\-]([a-z][a-z0-9\-]*)$")
35
- SUBAGENT_TYPE = re.compile(r'"subagent_type"\s*:\s*"([a-z][a-z0-9\-]*)"')
36
- HALT_SENTINEL = re.compile(r"HSTACK-HALT:\s*reason=([a-z-]+)", re.IGNORECASE)
37
-
38
- USAGE_KEYS = ("input_tokens", "cache_creation_input_tokens",
39
- "cache_read_input_tokens", "output_tokens")
40
-
41
-
42
- def message_text(msg) -> str:
43
- if msg is None:
44
- return ""
45
- if isinstance(msg, str):
46
- return msg
47
- content = msg.get("content") if isinstance(msg, dict) else None
48
- if content is None:
49
- return ""
50
- if isinstance(content, str):
51
- return content
52
- if isinstance(content, list):
53
- parts = []
54
- for blk in content:
55
- if isinstance(blk, dict):
56
- if blk.get("type") == "text" and isinstance(blk.get("text"), str):
57
- parts.append(blk["text"])
58
- else:
59
- parts.append(json.dumps(blk, default=str))
60
- else:
61
- parts.append(str(blk))
62
- return "\n".join(parts)
63
- return json.dumps(content, default=str)
64
-
65
-
66
- def discover_transcript_files(repo_paths: Iterable[Path]) -> list[Path]:
67
- """Discover transcript jsonl files for the given consuming-repo paths.
68
-
69
- Each repo_path corresponds to a Claude Code project dir (`-Users-...` form).
70
- We accept either real repo paths (we convert) or already-converted prefixes.
71
- """
72
- if not PROJECTS_ROOT.exists():
73
- return []
74
- prefixes = []
75
- for p in repo_paths:
76
- p_str = str(Path(p).resolve()).replace("/", "-")
77
- prefixes.append(p_str)
78
- # Also match Conductor worktrees of the repo's basename
79
- basename = Path(p).name
80
- prefixes.append(f"-Users-hugoganet-conductor-workspaces-{basename}-")
81
- files = []
82
- for d in PROJECTS_ROOT.iterdir():
83
- if not d.is_dir():
84
- continue
85
- name = d.name
86
- if not any(name.startswith(prefix) or name == prefix.rstrip("-") for prefix in prefixes):
87
- continue
88
- for f in d.glob("*.jsonl"):
89
- files.append(f)
90
- return files
91
-
92
-
93
- def load_session(path: Path) -> list[dict]:
94
- out = []
95
- try:
96
- with open(path, "r", errors="replace") as fh:
97
- for line in fh:
98
- line = line.strip()
99
- if not line:
100
- continue
101
- try:
102
- out.append(json.loads(line))
103
- except json.JSONDecodeError:
104
- continue
105
- except OSError:
106
- return []
107
- return out
108
-
109
-
110
- def skill_tool_uses(msg) -> list[str]:
111
- """hstack Skill names invoked via a `Skill` tool_use block in this message.
112
-
113
- Structural walk, not a regex over flattened text: the marker is the block's
114
- `type`/`name`/`input.skill` triple, which prose cannot forge.
115
- """
116
- if not isinstance(msg, dict):
117
- return []
118
- content = msg.get("content")
119
- if not isinstance(content, list):
120
- return []
121
- out = []
122
- for blk in content:
123
- if not isinstance(blk, dict):
124
- continue
125
- if blk.get("type") != "tool_use" or blk.get("name") != "Skill":
126
- continue
127
- inp = blk.get("input")
128
- name = inp.get("skill") if isinstance(inp, dict) else None
129
- if not isinstance(name, str):
130
- continue
131
- m = HSTACK_SKILL_NAME.match(name.strip())
132
- if m:
133
- out.append(m.group(1))
134
- return out
135
-
136
-
137
- def classify_session(records: list[dict]) -> tuple[str | None, set[str], list[str]]:
138
- """Return (skill_name, set of subagent types invoked, halt reasons).
139
-
140
- `skill_name` is the first hstack Skill invoked through a structured marker —
141
- a `<command-name>` tag or a `Skill` tool_use block. A session carrying
142
- neither returns `None` and is reported as *unattributed* (ADR-0009 piece 3);
143
- it is never captured by whatever hstack command its prompt happened to name.
144
- """
145
- skill = None
146
- subagents: set[str] = set()
147
- halts: list[str] = []
148
- for r in records:
149
- t = r.get("type")
150
- if t == "user":
151
- if skill is None:
152
- m = HSTACK_CMD_TAG.search(message_text(r.get("message")))
153
- if m:
154
- skill = m.group(1)
155
- elif t == "assistant":
156
- msg = r.get("message")
157
- if skill is None:
158
- invoked = skill_tool_uses(msg)
159
- if invoked:
160
- skill = invoked[0]
161
- text = message_text(msg)
162
- for sa in SUBAGENT_TYPE.findall(text):
163
- subagents.add(sa)
164
- for h in HALT_SENTINEL.findall(text):
165
- halts.append(h.lower())
166
- return skill, subagents, halts
167
-
168
-
169
- def parse_ts(value) -> datetime | None:
170
- """Parse an ISO-8601 stamp to an aware UTC datetime. None on anything else."""
171
- if not isinstance(value, str) or not value:
172
- return None
173
- try:
174
- dt = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
175
- except ValueError:
176
- return None
177
- if dt.tzinfo is None:
178
- dt = dt.replace(tzinfo=timezone.utc)
179
- return dt.astimezone(timezone.utc)
180
-
181
-
182
- def session_bounds(records: list[dict]) -> tuple[datetime | None, datetime | None]:
183
- first = last = None
184
- for r in records:
185
- dt = parse_ts(r.get("timestamp"))
186
- if dt is None:
187
- continue
188
- if first is None or dt < first:
189
- first = dt
190
- if last is None or dt > last:
191
- last = dt
192
- return first, last
193
-
194
-
195
- def tally_usage(records: list[dict], start: datetime | None = None,
196
- end: datetime | None = None) -> dict[str, int]:
197
- """Sum assistant-turn usage. With `start`/`end`, only turns whose timestamp
198
- falls inside the closed interval are counted."""
199
- totals: dict[str, int] = defaultdict(int)
200
- for k in USAGE_KEYS:
201
- totals[k] = 0
202
- totals["turns"] = 0
203
- for r in records:
204
- if r.get("type") != "assistant":
205
- continue
206
- if start is not None or end is not None:
207
- ts = parse_ts(r.get("timestamp"))
208
- if ts is None:
209
- continue
210
- if start is not None and ts < start:
211
- continue
212
- if end is not None and ts > end:
213
- continue
214
- usage = (r.get("message") or {}).get("usage") or {}
215
- for k in USAGE_KEYS:
216
- totals[k] += usage.get(k, 0) or 0
217
- totals["turns"] += 1
218
- return dict(totals)
219
-
220
-
221
- def phase_usage(sidecar: dict, projects_root: Path | None = None) -> dict | None:
222
- """Token usage inside one phase window, per ADR-0009 piece 2.
223
-
224
- Reads the transcript named by `sidecar["session_id"]` and sums assistant-turn
225
- usage over records whose `timestamp` falls in
226
- `[phase_opened_at, phase_closed_at]`.
227
-
228
- Returns `None` — *unmeasured* — whenever the window cannot be honoured: a
229
- schema_version-1 sidecar with no window, a null session id, a transcript
230
- swept by `cleanupPeriodDays` or living on another machine, an unparseable or
231
- inverted window. Never zero: zero is a measurement, and a phase whose
232
- transcript is gone was not measured. Read-only; nothing is written.
233
- """
234
- if not isinstance(sidecar, dict):
235
- return None
236
- opened = parse_ts(sidecar.get("phase_opened_at"))
237
- closed = parse_ts(sidecar.get("phase_closed_at"))
238
- if opened is None or closed is None or closed < opened:
239
- return None
240
- path = transcript_for_session(sidecar.get("session_id"), projects_root=projects_root)
241
- if path is None:
242
- return None
243
- records = load_session(path)
244
- if not records:
245
- return None
246
- totals = tally_usage(records, start=opened, end=closed)
247
- total_tokens = sum(totals.get(k, 0) for k in USAGE_KEYS)
248
- return {
249
- **totals,
250
- "total_tokens": total_tokens,
251
- "cost_score": cost_score(totals),
252
- "wall_clock_s": (closed - opened).total_seconds(),
253
- "session_id": sidecar.get("session_id"),
254
- "transcript": str(path),
255
- }
256
-
257
-
258
- def cost_score(totals: dict[str, int]) -> int:
259
- """Same weighting as scripts/audit-token-usage.py: input + 1.25*cache_creation
260
- + 0.10*cache_read + 5*output. Approximates billable $-weight for ranking."""
261
- return (
262
- totals.get("input_tokens", 0)
263
- + int(totals.get("cache_creation_input_tokens", 0) * 1.25)
264
- + int(totals.get("cache_read_input_tokens", 0) * 0.10)
265
- + totals.get("output_tokens", 0) * 5
266
- )
267
-
268
-
269
- def collect_session_rows(repo_paths: Iterable[Path], since: datetime | None = None) -> list[dict]:
270
- """Walk every transcript for the given repos and return one row per session."""
271
- out = []
272
- for f in discover_transcript_files(repo_paths):
273
- recs = load_session(f)
274
- if not recs:
275
- continue
276
- skill, subagents, halts = classify_session(recs)
277
- first, last = session_bounds(recs)
278
- if since is not None and last is not None and last < since:
279
- continue
280
- totals = tally_usage(recs)
281
- if totals.get("turns", 0) == 0:
282
- continue
283
- out.append({
284
- "file": f,
285
- "project_dir": f.parent.name,
286
- "skill": skill,
287
- "subagents": sorted(subagents),
288
- "halt_reasons": halts,
289
- "started_at": first,
290
- "ended_at": last,
291
- "totals": totals,
292
- "cost_score": cost_score(totals),
293
- })
294
- return out