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,388 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # hstack gate runner — the verifier's machine hands, and the thing
4
- # {{TODO-SCRIPT: hstack/scripts/run-gates.sh}} stood in for.
5
- #
6
- # hstack/scripts/run-gates.sh --change <change-id>
7
- # hstack/scripts/run-gates.sh --change <id> --suite unit --suite lint
8
- # hstack/scripts/run-gates.sh --list
9
- # hstack/scripts/run-gates.sh --change <id> --json
10
- #
11
- # It reads the canonical commands declared in hstack/context/ci-cd.md, runs
12
- # every one of them, captures combined stdout/stderr to the pointer file the
13
- # verification artifact references, and emits an observed-test-count PER SUITE
14
- # so V-05 ("a suite that executed zero tests cannot be recorded as pass") is a
15
- # measurement rather than a paragraph of parsing instructions in a prompt.
16
- #
17
- # Exit codes:
18
- # 0 every suite ran, exited 0, and every test suite observed > 0 tests
19
- # 1 a suite failed, or a test suite observed zero tests (V-05)
20
- # 2 usage / environment error (no ci-cd.md, no canonical-commands block)
21
- #
22
- # Dependency-free by construction: POSIX tools only, no jq, no node. The
23
- # consuming repo has no node_modules for hstack — same constraint that made
24
- # validate-spec.mjs plain ESM (ADR-0001).
25
-
26
- set -uo pipefail
27
-
28
- # ---------------------------------------------------------------------------
29
- # 1. Argument parsing
30
- # ---------------------------------------------------------------------------
31
-
32
- CHANGE_ID=""
33
- OUT=""
34
- ROOT=""
35
- JSON=0
36
- LIST=0
37
- SUITES_REQUESTED=""
38
-
39
- usage() {
40
- cat <<'EOF'
41
- hstack run-gates — run the canonical test / lint / typecheck suites
42
-
43
- hstack/scripts/run-gates.sh [options]
44
-
45
- Options
46
- --change ID change-spec id; the pointer file defaults to
47
- hstack/specs/changes/<ID>/test-output.txt
48
- --out PATH pointer file path (overrides --change)
49
- --suite NAME run only this suite; repeatable
50
- --list print the parsed canonical commands and exit
51
- --json emit the per-suite summary as JSON on stdout
52
- --root DIR repo root to resolve hstack/ from (default: search upward)
53
- -h, --help this text
54
-
55
- Exit codes: 0 all green with a non-zero test count per test suite,
56
- 1 a suite failed or observed zero tests (V-05), 2 usage / environment error.
57
- EOF
58
- }
59
-
60
- while [ $# -gt 0 ]; do
61
- case "$1" in
62
- --change) CHANGE_ID="${2:-}"; shift 2 ;;
63
- --out) OUT="${2:-}"; shift 2 ;;
64
- --suite) SUITES_REQUESTED="$SUITES_REQUESTED ${2:-}"; shift 2 ;;
65
- --root) ROOT="${2:-}"; shift 2 ;;
66
- --json) JSON=1; shift ;;
67
- --list) LIST=1; shift ;;
68
- -h|--help) usage; exit 0 ;;
69
- *) echo "run-gates: unknown option $1" >&2; usage >&2; exit 2 ;;
70
- esac
71
- done
72
-
73
- # ---------------------------------------------------------------------------
74
- # 2. Locate the hstack tree
75
- # ---------------------------------------------------------------------------
76
-
77
- find_hstack_root() {
78
- dir="${1:-$PWD}"
79
- dir=$(cd "$dir" 2>/dev/null && pwd) || return 1
80
- while :; do
81
- if [ -f "$dir/hstack/KERNEL.md" ] || [ -f "$dir/hstack/CLAUDE.md" ] || [ -f "$dir/hstack/config.yaml" ]; then
82
- printf '%s\n' "$dir"
83
- return 0
84
- fi
85
- parent=$(dirname "$dir")
86
- [ "$parent" = "$dir" ] && return 1
87
- dir="$parent"
88
- done
89
- }
90
-
91
- REPO_ROOT=$(find_hstack_root "${ROOT:-$PWD}") || {
92
- echo "run-gates: no hstack/ tree found (looked for hstack/KERNEL.md upward from ${ROOT:-$PWD}). Pass --root <repo>." >&2
93
- exit 2
94
- }
95
- HSTACK="$REPO_ROOT/hstack"
96
- CI_CD="$HSTACK/context/ci-cd.md"
97
-
98
- [ -f "$CI_CD" ] || {
99
- echo "run-gates: $CI_CD not found. The canonical commands live there; run \`/hstack:configure --interview ci-cd\` first." >&2
100
- exit 2
101
- }
102
-
103
- # ---------------------------------------------------------------------------
104
- # 3. Parse the canonical commands
105
- # ---------------------------------------------------------------------------
106
- #
107
- # ci-cd.md declares them in a fenced block with the info string `hstack-gates`,
108
- # one `suite: command` pair per line. The fence is the contract: everything
109
- # else in ci-cd.md is prose written for humans, and a runner that guessed at
110
- # prose would produce a confident wrong answer about what the repo's tests are.
111
-
112
- CANONICAL=$(awk '
113
- /^```[[:space:]]*hstack-gates[[:space:]]*$/ { inblock=1; next }
114
- inblock && /^```/ { inblock=0; next }
115
- inblock {
116
- line=$0
117
- sub(/#.*$/, "", line) # trailing comment
118
- if (line ~ /^[[:space:]]*$/) next
119
- idx = index(line, ":")
120
- if (idx == 0) next
121
- key = substr(line, 1, idx-1)
122
- val = substr(line, idx+1)
123
- gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
124
- gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
125
- if (val == "" || val == "none" || val == "null") next # declared absent
126
- printf "%s\t%s\n", key, val
127
- }
128
- ' "$CI_CD")
129
-
130
- if [ -z "$CANONICAL" ]; then
131
- echo "run-gates: no \`hstack-gates\` fenced block in hstack/context/ci-cd.md." >&2
132
- echo " Declare the canonical commands there — see hstack/templates/ci-cd.md § Canonical Commands." >&2
133
- exit 2
134
- fi
135
-
136
- # Suites that are evidence of behaviour, and therefore subject to V-05. Lint
137
- # and typecheck are exempt: both produce a diagnostic count whose floor is
138
- # naturally zero on a clean repo, so zero is not a signal of a skipped run.
139
- is_test_suite() {
140
- case "$1" in
141
- unit|integration|e2e) return 0 ;;
142
- *) return 1 ;;
143
- esac
144
- }
145
-
146
- wanted() {
147
- [ -z "$SUITES_REQUESTED" ] && return 0
148
- for s in $SUITES_REQUESTED; do [ "$s" = "$1" ] && return 0; done
149
- return 1
150
- }
151
-
152
- if [ "$LIST" -eq 1 ]; then
153
- echo "hstack run-gates — canonical commands from hstack/context/ci-cd.md"
154
- echo ""
155
- printf '%s\n' "$CANONICAL" | while IFS="$(printf '\t')" read -r suite cmd; do
156
- printf ' %-12s %s\n' "$suite" "$cmd"
157
- done
158
- exit 0
159
- fi
160
-
161
- # ---------------------------------------------------------------------------
162
- # 4. Pointer file
163
- # ---------------------------------------------------------------------------
164
-
165
- if [ -z "$OUT" ]; then
166
- if [ -n "$CHANGE_ID" ]; then
167
- OUT="$HSTACK/specs/changes/$CHANGE_ID/test-output.txt"
168
- [ -d "$HSTACK/specs/changes/$CHANGE_ID" ] || {
169
- echo "run-gates: no change folder at hstack/specs/changes/$CHANGE_ID/" >&2
170
- exit 2
171
- }
172
- else
173
- OUT="$REPO_ROOT/hstack-gates-output.txt"
174
- fi
175
- fi
176
- mkdir -p "$(dirname "$OUT")" || exit 2
177
- : > "$OUT" || { echo "run-gates: cannot write $OUT" >&2; exit 2; }
178
-
179
- {
180
- echo "hstack run-gates"
181
- echo "repo: $REPO_ROOT"
182
- echo "source: hstack/context/ci-cd.md"
183
- echo "====================================================================="
184
- } >> "$OUT"
185
-
186
- # ---------------------------------------------------------------------------
187
- # 5. Observed-test-count extraction
188
- # ---------------------------------------------------------------------------
189
- #
190
- # One awk pass per suite over that suite's captured output. The runners hstack
191
- # meets in practice all print a summary line; the point is not to understand
192
- # every runner, it is to answer one question honestly: did this suite execute
193
- # anything? When no known pattern matches, the answer is "unknown" — and
194
- # unknown is treated as zero, because a count nobody could read is not evidence.
195
- #
196
- # "Executed" is passed + failed, NOT total. A run that collected fifteen tests
197
- # and skipped all fifteen executed nothing, and V-05 exists precisely for that
198
- # case: `Tests: 15 skipped, 15 total` is a non-zero total with zero assertions.
199
- #
200
- # Jest / Vitest Tests: 12 passed, 3 skipped, 15 total | No tests found
201
- # Mocha 12 passing / 3 pending / 1 failing
202
- # Playwright 12 passed (4.2s) / 1 failed / 3 skipped
203
- # pytest collected 15 items | 12 passed, 3 skipped | no tests ran
204
- # go test ok pkg 0.4s | testing: warning: no tests to run
205
-
206
- count_tests() {
207
- # $1 = file holding this suite's output
208
- awk '
209
- function num(s) { return s + 0 }
210
- # --- explicit zero-collection statements, strongest signal ----------------
211
- /[Nn]o tests found/ { zero=1 }
212
- /collected 0 items/ { zero=1; seen=1 }
213
- /no tests ran/ { zero=1; seen=1 }
214
- /no tests to run/ { zero=1 }
215
- /^[[:space:]]*Test Files[[:space:]]+no tests/ { zero=1 }
216
-
217
- # --- Jest / Vitest summary ----------------------------------------------
218
- /^[[:space:]]*Tests:?[[:space:]]/ {
219
- seen=1
220
- line=$0
221
- if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
222
- if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
223
- if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
224
- if (match(line, /[0-9]+ todo/)) { s=substr(line, RSTART, RLENGTH); skipped+=num(s) }
225
- if (match(line, /[0-9]+ total/)) { s=substr(line, RSTART, RLENGTH); total=num(s) }
226
- }
227
-
228
- # --- pytest short summary ------------------------------------------------
229
- /=+ .*(passed|failed|error|skipped).* =+/ {
230
- seen=1
231
- line=$0
232
- if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
233
- if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
234
- if (match(line, /[0-9]+ error/)) { s=substr(line, RSTART, RLENGTH); failed+=num(s) }
235
- if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
236
- }
237
- /collected [0-9]+ item/ {
238
- seen=1
239
- if (match($0, /collected [0-9]+/)) { s=substr($0, RSTART+10, RLENGTH-10); collected=num(s) }
240
- }
241
-
242
- # --- Mocha ---------------------------------------------------------------
243
- /^[[:space:]]*[0-9]+ passing/ { seen=1; if (match($0, /[0-9]+/)) { passed=num(substr($0, RSTART, RLENGTH)) } }
244
- /^[[:space:]]*[0-9]+ pending/ { seen=1; if (match($0, /[0-9]+/)) { skipped=num(substr($0, RSTART, RLENGTH)) } }
245
- /^[[:space:]]*[0-9]+ failing/ { seen=1; if (match($0, /[0-9]+/)) { failed=num(substr($0, RSTART, RLENGTH)) } }
246
-
247
- # --- Playwright ("12 passed (4.2s)") and bare runner tallies -------------
248
- # No \b here: POSIX awk reads it as a backspace, not a word boundary.
249
- /^[[:space:]]*[0-9]+ (passed|failed|skipped|flaky)([^a-z]|$)/ {
250
- seen=1
251
- line=$0
252
- if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
253
- if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
254
- if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
255
- if (match(line, /[0-9]+ flaky/)) { s=substr(line, RSTART, RLENGTH); passed+=num(s) }
256
- }
257
-
258
- END {
259
- if (total == 0) total = passed + failed + skipped
260
- if (total == 0 && collected > 0) { total = collected }
261
- if (zero) { total = 0; passed = 0; failed = 0 }
262
- executed = passed + failed
263
- # `known` says whether any pattern matched at all. An unreadable summary
264
- # is reported as unknown and treated as zero downstream — a count nobody
265
- # could read is not evidence that tests ran.
266
- known = (seen || zero) ? 1 : 0
267
- printf "%d %d %d %d %d %d\n", passed+0, failed+0, skipped+0, total+0, executed+0, known
268
- }
269
- ' "$1"
270
- }
271
-
272
- # ---------------------------------------------------------------------------
273
- # 6. Run
274
- # ---------------------------------------------------------------------------
275
-
276
- TMPDIR_RUN=$(mktemp -d "${TMPDIR:-/tmp}/hstack-run-gates.XXXXXX") || exit 2
277
- trap 'rm -rf "$TMPDIR_RUN"' EXIT
278
-
279
- SUMMARY="$TMPDIR_RUN/summary"
280
- : > "$SUMMARY"
281
- OVERALL=0
282
- RAN_ANY=0
283
-
284
- while IFS="$(printf '\t')" read -r suite cmd; do
285
- [ -n "$suite" ] || continue
286
- wanted "$suite" || continue
287
- RAN_ANY=1
288
-
289
- suite_out="$TMPDIR_RUN/$suite.out"
290
- {
291
- echo ""
292
- echo "--- suite: $suite ------------------------------------------------"
293
- echo "\$ $cmd"
294
- } >> "$OUT"
295
-
296
- # stdin from /dev/null, not inherited: the loop below is fed by a heredoc of
297
- # the canonical commands, and a suite that reads stdin (an interactive watch
298
- # mode, a prompt) would otherwise eat the remaining suites.
299
- ( cd "$REPO_ROOT" && eval "$cmd" ) > "$suite_out" 2>&1 < /dev/null
300
- code=$?
301
- cat "$suite_out" >> "$OUT"
302
-
303
- if is_test_suite "$suite"; then
304
- read -r passed failed skipped total executed known <<EOF
305
- $(count_tests "$suite_out")
306
- EOF
307
- else
308
- passed=0; failed=0; skipped=0; total=0; executed=0; known=1
309
- fi
310
-
311
- # V-05: zero executed tests is `not-run`, never `pass`. "Zero failures" is
312
- # not evidence of correctness when there were zero assertions to fail.
313
- if [ "$code" -ne 0 ]; then
314
- verdict="fail"
315
- reason="command exited $code"
316
- OVERALL=1
317
- elif is_test_suite "$suite" && [ "$executed" -eq 0 ]; then
318
- verdict="not-run"
319
- if [ "$known" -eq 0 ]; then
320
- reason="no test count could be read from the runner's output (unrecognised summary format)"
321
- elif [ "$skipped" -gt 0 ]; then
322
- reason="the runner reported zero executed tests — $skipped skipped of $total collected (all-skipped, or a filter that collapsed the set)"
323
- else
324
- reason="the runner reported zero executed tests (env-gated, empty-collection, or filter-collapse)"
325
- fi
326
- OVERALL=1
327
- else
328
- verdict="pass"
329
- reason=""
330
- fi
331
-
332
- printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
333
- "$suite" "$cmd" "$code" "$verdict" "$passed" "$failed" "$skipped" "$total" "$executed" "$reason" >> "$SUMMARY"
334
-
335
- {
336
- echo "--- suite: $suite → $verdict (exit $code, $executed of $total test(s) executed)"
337
- } >> "$OUT"
338
- done <<EOF
339
- $CANONICAL
340
- EOF
341
-
342
- if [ "$RAN_ANY" -eq 0 ]; then
343
- echo "run-gates: no suite matched --suite${SUITES_REQUESTED}" >&2
344
- exit 2
345
- fi
346
-
347
- # ---------------------------------------------------------------------------
348
- # 7. Report
349
- # ---------------------------------------------------------------------------
350
-
351
- REL_OUT="${OUT#"$REPO_ROOT"/}"
352
-
353
- if [ "$JSON" -eq 1 ]; then
354
- printf '{\n'
355
- printf ' "ok": %s,\n' "$([ "$OVERALL" -eq 0 ] && echo true || echo false)"
356
- printf ' "test-output": "%s",\n' "$REL_OUT"
357
- printf ' "suites": [\n'
358
- first=1
359
- while IFS="$(printf '\t')" read -r suite cmd code verdict passed failed skipped total executed reason; do
360
- [ "$first" -eq 1 ] || printf ',\n'
361
- first=0
362
- esc_cmd=$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')
363
- esc_reason=$(printf '%s' "$reason" | sed 's/\\/\\\\/g; s/"/\\"/g')
364
- printf ' {"suite": "%s", "command": "%s", "exit": %s, "verdict": "%s", "observed": {"passed": %s, "failed": %s, "skipped": %s, "total": %s, "executed": %s}, "reason": "%s"}' \
365
- "$suite" "$esc_cmd" "$code" "$verdict" "$passed" "$failed" "$skipped" "$total" "$executed" "$esc_reason"
366
- done < "$SUMMARY"
367
- printf '\n ]\n}\n'
368
- else
369
- echo ""
370
- echo "hstack run-gates — $REPO_ROOT"
371
- echo ""
372
- printf ' %-12s %-9s %-6s %s\n' "suite" "verdict" "exit" "observed (passed/failed/skipped/total)"
373
- while IFS="$(printf '\t')" read -r suite cmd code verdict passed failed skipped total executed reason; do
374
- printf ' %-12s %-9s %-6s %s/%s/%s/%s\n' "$suite" "$verdict" "$code" "$passed" "$failed" "$skipped" "$total"
375
- [ -n "$reason" ] && printf ' %s\n' "$reason"
376
- done < "$SUMMARY"
377
- echo ""
378
- echo " captured output: $REL_OUT"
379
- echo " → verification.artifacts.test-output: $REL_OUT"
380
- echo ""
381
- if [ "$OVERALL" -eq 0 ]; then
382
- echo "run-gates: all suites green with a non-zero executed-test count."
383
- else
384
- echo "run-gates: at least one suite failed or executed zero tests (V-05). status: passed is blocked."
385
- fi
386
- fi
387
-
388
- exit "$OVERALL"
@@ -1,6 +0,0 @@
1
- """hstack-telemetry — retrospective observability for the hstack workflow.
2
-
3
- Reads on-disk artifacts (frontmatter + bodies), git history, and Claude Code
4
- transcripts to compute a small set of metrics that drive the hstack
5
- learning/improvement loop. Pure derivation; never the authoritative source.
6
- """
File without changes
@@ -1,137 +0,0 @@
1
- """Contract-drift insights: module-spec staleness, ADR supersession lag,
2
- tech-debt half-life by exit path."""
3
-
4
- from __future__ import annotations
5
-
6
- from collections import defaultdict
7
- from datetime import date, datetime
8
-
9
-
10
- def compute(commits: list[dict], changes: dict, tech_debt: list[dict],
11
- adrs: list[dict], module_specs: list[dict]) -> dict:
12
- return {
13
- "module_spec_staleness": _module_staleness(module_specs, commits),
14
- "adr_supersession_lag": _adr_lag(adrs),
15
- "tech_debt_half_life": _td_half_life(tech_debt),
16
- }
17
-
18
-
19
- def _parse_date(value) -> date | None:
20
- if value is None:
21
- return None
22
- if isinstance(value, date):
23
- return value
24
- if isinstance(value, datetime):
25
- return value.date()
26
- if isinstance(value, str):
27
- try:
28
- return datetime.fromisoformat(value).date()
29
- except ValueError:
30
- return None
31
- return None
32
-
33
-
34
- def _module_staleness(module_specs: list[dict], commits: list[dict]) -> dict:
35
- """Per module: spec status + count of recent commits touching that module's
36
- files. A `needs-refresh` module with high recent activity is the drift
37
- pathology."""
38
- # Count commits touching each module dir.
39
- module_activity: dict[str, int] = defaultdict(int)
40
- for c in commits:
41
- for f in c.get("files", []):
42
- for ms in module_specs:
43
- module = ms["module"]
44
- # Use a heuristic — count when a commit touches files outside
45
- # hstack/ in directories named like the module. Imperfect, but
46
- # avoids requiring a module→file-path map.
47
- if module in f and not f.startswith("hstack/"):
48
- module_activity[module] += 1
49
- break
50
- rows = []
51
- for ms in module_specs:
52
- status = ms["fm"].get("status", "-")
53
- updated = _parse_date(ms["fm"].get("updated"))
54
- rows.append({
55
- "module": ms["module"],
56
- "status": status,
57
- "updated": str(updated) if updated else "-",
58
- "recent_commits_touching_module": module_activity.get(ms["module"], 0),
59
- "drift_flag": status == "needs-refresh" and module_activity.get(ms["module"], 0) > 0,
60
- })
61
- rows.sort(key=lambda r: -r["recent_commits_touching_module"])
62
- return {"rows": rows}
63
-
64
-
65
- def _adr_lag(adrs: list[dict]) -> dict:
66
- """Among ADRs at `superseded` status, how long they lived before the
67
- superseding ADR landed."""
68
- by_id = {a["fm"].get("id") or a["path"].stem: a for a in adrs}
69
- lags = []
70
- for a in adrs:
71
- status = a["fm"].get("status")
72
- if status != "superseded":
73
- continue
74
- sup = a["fm"].get("superseded-by")
75
- a_created = _parse_date(a["fm"].get("created"))
76
- if not sup or not a_created:
77
- continue
78
- sup_adr = by_id.get(sup)
79
- if not sup_adr:
80
- continue
81
- sup_created = _parse_date(sup_adr["fm"].get("created"))
82
- if not sup_created:
83
- continue
84
- days = (sup_created - a_created).days
85
- lags.append({
86
- "adr": a["fm"].get("id"),
87
- "superseded_by": sup,
88
- "lag_days": days,
89
- })
90
- return {"rows": lags}
91
-
92
-
93
- def _td_half_life(tech_debt: list[dict]) -> dict:
94
- """For each TD, compute days from created → exit (resolved | wontfix |
95
- stale-no-longer-reproducible). Surface by exit-path histogram + per-TD
96
- rows."""
97
- today = date.today()
98
- by_exit = defaultdict(list)
99
- rows = []
100
- for td in tech_debt:
101
- fm = td["fm"]
102
- status = fm.get("status")
103
- created = _parse_date(fm.get("created"))
104
- if not created:
105
- continue
106
- exit_date = None
107
- if status == "resolved":
108
- exit_date = _parse_date(fm.get("updated"))
109
- elif status == "wontfix":
110
- exit_date = _parse_date(fm.get("updated"))
111
- elif status == "stale-no-longer-reproducible":
112
- exit_date = _parse_date(fm.get("stale-verified-at") or fm.get("updated"))
113
- if exit_date:
114
- days = (exit_date - created).days
115
- by_exit[status].append(days)
116
- elif status == "open" or status == "in-progress":
117
- days = (today - created).days
118
- by_exit[f"{status} (still open)"].append(days)
119
- exit_date = None
120
- rows.append({
121
- "id": fm.get("id"),
122
- "status": status,
123
- "created": str(created),
124
- "days_to_exit": (exit_date - created).days if exit_date else (today - created).days,
125
- "exit_path": status if exit_date else None,
126
- })
127
- summary = {}
128
- for exit_path, days_list in by_exit.items():
129
- if not days_list:
130
- continue
131
- days_list.sort()
132
- summary[exit_path] = {
133
- "count": len(days_list),
134
- "median_days": days_list[len(days_list) // 2],
135
- "max_days": max(days_list),
136
- }
137
- return {"summary": summary, "rows": rows}