hstack 0.16.0 → 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 (132) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +73 -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 +42 -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 +80 -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 +48 -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/data-architecture.md +38 -89
  36. package/template/templates/infrastructure.md +54 -148
  37. package/template/templates/roadmap.md +6 -12
  38. package/template/templates/story.md +7 -13
  39. package/template/templates/tech-debt.md +2 -37
  40. package/template/templates/tech-stack.md +2 -12
  41. package/template/.claude/agents/data-specialist.md +0 -81
  42. package/template/.claude/agents/implementer.md +0 -97
  43. package/template/.claude/agents/kernel-fit-analyst.md +0 -129
  44. package/template/.claude/agents/planner.md +0 -75
  45. package/template/.claude/agents/product-discovery.md +0 -129
  46. package/template/.claude/agents/product-manager.md +0 -76
  47. package/template/.claude/agents/researcher.md +0 -87
  48. package/template/.claude/agents/security-reviewer.md +0 -88
  49. package/template/.claude/agents/spec-author.md +0 -89
  50. package/template/.claude/agents/stack-architect.md +0 -108
  51. package/template/.claude/agents/ui-ux-briefer.md +0 -75
  52. package/template/.claude/agents/verifier.md +0 -81
  53. package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
  54. package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
  55. package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
  56. package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
  57. package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
  58. package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
  59. package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
  60. package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
  61. package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
  62. package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
  63. package/template/.claude/skills/hstack-help/SKILL.md +0 -153
  64. package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
  65. package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
  66. package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
  67. package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
  68. package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
  69. package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
  70. package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
  71. package/template/.claude/skills/hstack-research/SKILL.md +0 -115
  72. package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
  73. package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
  74. package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
  75. package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
  76. package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
  77. package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
  78. package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
  79. package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
  80. package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
  81. package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
  82. package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
  83. package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
  84. package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
  85. package/template/scripts/compute-merge-readiness.mjs +0 -780
  86. package/template/scripts/coord/coord_scan.py +0 -681
  87. package/template/scripts/run-gates.sh +0 -388
  88. package/template/scripts/telemetry/__init__.py +0 -6
  89. package/template/scripts/telemetry/insights/__init__.py +0 -0
  90. package/template/scripts/telemetry/insights/contract_drift.py +0 -137
  91. package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
  92. package/template/scripts/telemetry/insights/overengineering.py +0 -115
  93. package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
  94. package/template/scripts/telemetry/insights/token_economics.py +0 -302
  95. package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
  96. package/template/scripts/telemetry/parsers/__init__.py +0 -0
  97. package/template/scripts/telemetry/parsers/bodies.py +0 -87
  98. package/template/scripts/telemetry/parsers/commits.py +0 -219
  99. package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
  100. package/template/scripts/telemetry/parsers/sidecars.py +0 -61
  101. package/template/scripts/telemetry/parsers/transcripts.py +0 -294
  102. package/template/scripts/telemetry/render.py +0 -460
  103. package/template/scripts/telemetry/report.py +0 -150
  104. package/template/scripts/telemetry/run_kernel_fit.py +0 -95
  105. package/template/scripts/telemetry/session_id.py +0 -139
  106. package/template/scripts/validate-spec.mjs +0 -3303
  107. package/template/templates/adversarial-review.md +0 -54
  108. package/template/templates/bootstrap.md +0 -105
  109. package/template/templates/change-spec.md +0 -82
  110. package/template/templates/ci-cd.md +0 -41
  111. package/template/templates/coord-message.md +0 -56
  112. package/template/templates/data-review.md +0 -54
  113. package/template/templates/discovery/brainstorm.md +0 -69
  114. package/template/templates/discovery/forcing-questions.md +0 -76
  115. package/template/templates/discovery/project-brief.md +0 -53
  116. package/template/templates/figma-handoff.md +0 -38
  117. package/template/templates/glossary.md +0 -20
  118. package/template/templates/hardening-checklist.md +0 -73
  119. package/template/templates/incident-runbook.md +0 -57
  120. package/template/templates/kernel-fit-finding.md +0 -63
  121. package/template/templates/kernel-fit-flag.md +0 -52
  122. package/template/templates/module-spec.md +0 -49
  123. package/template/templates/persona.md +0 -38
  124. package/template/templates/plan.md +0 -53
  125. package/template/templates/product-brief.md +0 -56
  126. package/template/templates/security-review.md +0 -63
  127. package/template/templates/telemetry-sidecar.md +0 -227
  128. package/template/templates/test-plan.md +0 -119
  129. package/template/templates/threat-model.md +0 -54
  130. package/template/templates/ui-brief.md +0 -49
  131. package/template/templates/verification.md +0 -63
  132. 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