bigpowers 2.50.0 → 2.51.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 (90) hide show
  1. package/.pi/package.json +1 -1
  2. package/.pi/prompts/plan-release.md +0 -1
  3. package/.pi/prompts/release-branch.md +74 -56
  4. package/.pi/prompts/session-state.md +2 -0
  5. package/.pi/prompts/survey-context.md +1 -1
  6. package/.pi/skills/plan-release/SKILL.md +0 -1
  7. package/.pi/skills/release-branch/SKILL.md +74 -56
  8. package/.pi/skills/session-state/SKILL.md +2 -0
  9. package/.pi/skills/survey-context/SKILL.md +1 -1
  10. package/CHANGELOG.md +16 -0
  11. package/CLAUDE.md +9 -0
  12. package/package.json +1 -1
  13. package/scripts/audit-compliance.sh +1 -4
  14. package/scripts/record-cycle-time.sh +340 -0
  15. package/scripts/validate-okf.sh +198 -0
  16. package/scripts/wait-for-ci.sh +130 -0
  17. package/skills/align-grid/SKILL.md +1 -0
  18. package/skills/assess-impact/SKILL.md +1 -0
  19. package/skills/audit-code/SKILL.md +1 -0
  20. package/skills/audit-plan/SKILL.md +1 -0
  21. package/skills/build-epic/SKILL.md +1 -0
  22. package/skills/change-request/SKILL.md +1 -0
  23. package/skills/commit-message/SKILL.md +1 -0
  24. package/skills/compose-workflow/SKILL.md +1 -0
  25. package/skills/craft-skill/SKILL.md +1 -0
  26. package/skills/deepen-architecture/SKILL.md +1 -0
  27. package/skills/define-language/SKILL.md +1 -0
  28. package/skills/define-success/SKILL.md +1 -0
  29. package/skills/delegate-task/SKILL.md +1 -0
  30. package/skills/deploy/SKILL.md +1 -0
  31. package/skills/design-interface/SKILL.md +1 -0
  32. package/skills/develop-tdd/SKILL.md +1 -0
  33. package/skills/diagnose-root/SKILL.md +1 -0
  34. package/skills/dispatch-agents/SKILL.md +1 -0
  35. package/skills/edit-document/SKILL.md +1 -0
  36. package/skills/elaborate-spec/SKILL.md +1 -0
  37. package/skills/enforce-first/SKILL.md +1 -0
  38. package/skills/evolve-skill/SKILL.md +1 -0
  39. package/skills/execute-plan/SKILL.md +1 -0
  40. package/skills/extract-design/SKILL.md +1 -0
  41. package/skills/fix-bug/SKILL.md +1 -0
  42. package/skills/grill-me/SKILL.md +1 -0
  43. package/skills/grill-with-docs/SKILL.md +1 -0
  44. package/skills/guard-git/SKILL.md +1 -0
  45. package/skills/hook-commits/SKILL.md +1 -0
  46. package/skills/inspect-quality/SKILL.md +1 -0
  47. package/skills/investigate-bug/SKILL.md +1 -0
  48. package/skills/kickoff-branch/SKILL.md +1 -0
  49. package/skills/map-codebase/SKILL.md +1 -0
  50. package/skills/migrate-spec/SKILL.md +1 -0
  51. package/skills/model-domain/SKILL.md +1 -0
  52. package/skills/orchestrate-project/SKILL.md +1 -0
  53. package/skills/organize-workspace/SKILL.md +1 -0
  54. package/skills/plan-refactor/SKILL.md +1 -0
  55. package/skills/plan-release/SKILL.md +1 -1
  56. package/skills/plan-work/SKILL.md +1 -0
  57. package/skills/publish-package/SKILL.md +1 -0
  58. package/skills/quick-fix/SKILL.md +1 -0
  59. package/skills/release-branch/REFERENCE.md +57 -54
  60. package/skills/release-branch/SKILL.md +20 -4
  61. package/skills/request-review/SKILL.md +1 -0
  62. package/skills/research-first/SKILL.md +1 -0
  63. package/skills/reset-baseline/SKILL.md +1 -0
  64. package/skills/respond-review/SKILL.md +1 -0
  65. package/skills/run-benchmark/SKILL.md +1 -0
  66. package/skills/run-evals/SKILL.md +1 -0
  67. package/skills/run-planning/SKILL.md +1 -0
  68. package/skills/scope-work/SKILL.md +1 -0
  69. package/skills/search-skills/SKILL.md +1 -0
  70. package/skills/security-review/SKILL.md +1 -0
  71. package/skills/seed-conventions/SKILL.md +1 -0
  72. package/skills/session-state/SKILL.md +3 -0
  73. package/skills/setup-environment/SKILL.md +1 -0
  74. package/skills/simulate-agents/SKILL.md +1 -0
  75. package/skills/slice-tasks/SKILL.md +1 -0
  76. package/skills/smoke-test/SKILL.md +1 -0
  77. package/skills/spike-prototype/SKILL.md +1 -0
  78. package/skills/stocktake-skills/SKILL.md +1 -0
  79. package/skills/survey-context/SKILL.md +2 -1
  80. package/skills/terse-mode/SKILL.md +1 -0
  81. package/skills/trace-requirement/SKILL.md +1 -0
  82. package/skills/using-bigpowers/SKILL.md +1 -0
  83. package/skills/validate-contracts/SKILL.md +1 -0
  84. package/skills/validate-fix/SKILL.md +1 -0
  85. package/skills/verify-work/SKILL.md +1 -0
  86. package/skills/visual-dashboard/SKILL.md +1 -0
  87. package/skills/wire-ci/SKILL.md +1 -0
  88. package/skills/wire-observability/SKILL.md +1 -0
  89. package/skills/write-document/SKILL.md +1 -0
  90. package/skills-lock.json +72 -72
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # record-cycle-time.sh — deterministic, git-derived delivery metrics.
4
+ #
5
+ # Replaces the agent's hand-written story_start/story_end timestamps and
6
+ # hand-arithmetic (which produced fabricated / wall-clock-inflated rows in
7
+ # specs/metrics/cycle-times.yaml). Two metrics, honestly separated:
8
+ #
9
+ # effort_hours ADDITIVE. Idle-stripped estimated effort. Computed as a
10
+ # GLOBAL PARTITION of the commit stream so that
11
+ # Σ(per-story effort) == whole-repo effort, exactly.
12
+ # Overnight / weekend / UAT gaps are dropped by construction.
13
+ #
14
+ # lead_time_minutes NON-ADDITIVE. Calendar latency, first-commit → merge.
15
+ # Aggregate by median/p75 over many stories. NEVER sum.
16
+ #
17
+ # Effort model (git-hours, kimmobrunfeldt/git-hours src/index.js:15,18):
18
+ # * commits sorted by author-date across the WHOLE range;
19
+ # * gap < IDLE (120 min) -> real elapsed time, credited to the *later*
20
+ # commit's story (the work that produced it);
21
+ # * gap >= IDLE -> session boundary: credit a flat PAD (120 min)
22
+ # to the later commit's story, drop the idle time;
23
+ # * the very first commit of the stream is never padded (no left neighbour).
24
+ #
25
+ # Why not run git-hours per story and sum? Because it doesn't sum: each
26
+ # per-story run re-pads its own first commit and drops its own leading gap,
27
+ # so the sum leaks both ways. The global partition above is additive by
28
+ # construction. Attribution of a shared <120-min window to "the later commit's
29
+ # story" is a documented CONVENTION, not a physical truth — it is the only way
30
+ # to keep the numbers summable without double-counting.
31
+ #
32
+ # Story attribution: a `Story: <id>` trailer in the commit message. Commits
33
+ # with no trailer land in the `unattributed` bucket (surfaced, never dropped).
34
+ # Merge commits are excluded (--no-merges); duplicate SHAs cannot occur in a
35
+ # single range.
36
+ #
37
+ # Usage:
38
+ # scripts/record-cycle-time.sh report [--range <git-range>] [--story-key <key>]
39
+ # scripts/record-cycle-time.sh append --story <id> --bcps <n> \
40
+ # [--range <git-range>] [--merged-at <epoch>] [--file <path>] [--story-key <key>]
41
+ #
42
+ # `report` prints the per-story partition + an additivity self-check.
43
+ # `append` recomputes the partition and appends one row for <id> to the ledger.
44
+ #
45
+ set -euo pipefail
46
+
47
+ IDLE_SECONDS=$(( 120 * 60 )) # git-hours maxCommitDiffInMinutes
48
+ PAD_SECONDS=$(( 120 * 60 )) # git-hours firstCommitAdditionInMinutes
49
+ STORY_KEY="Story"
50
+
51
+ die() { echo "record-cycle-time: $*" >&2; exit 1; }
52
+
53
+ usage() {
54
+ cat <<'EOF'
55
+ Usage: scripts/record-cycle-time.sh <command> [flags]
56
+
57
+ Commands:
58
+ report Compute per-story effort partition + additivity self-check.
59
+ Flags: --range <git-range> --story-key <trailer-key>
60
+ append Recompute partition and append one row for a story.
61
+ Flags: --story <id> --bcps <n> [--range <git-range>]
62
+ [--merged-at <epoch>] [--file <path>] [--story-key <trailer-key>]
63
+ self-test Run report, then exit 0 on additivity PASS; exit 1 on FAIL.
64
+ Flags: --range <git-range> --story-key <trailer-key>
65
+ help Show this message.
66
+
67
+ Global flags:
68
+ --help Show this message (works with any subcommand).
69
+
70
+ Description:
71
+ Computes git-derived, additive effort from commit history.
72
+ Two metrics, separated: effort_hours (ADDITIVE, Σ = total) and
73
+ lead_time_minutes (calendar latency, median-aggregate, NEVER sum).
74
+
75
+ Uses the git-hours model: commits sorted by author-date across the
76
+ whole range; gaps < 120 min counted as effort; gaps >= 120 min
77
+ start a new session with a 120-min pad.
78
+ EOF
79
+ exit 0
80
+ }
81
+
82
+ command -v git >/dev/null 2>&1 || die "git not found"
83
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repo"
84
+
85
+ # ---- engine ---------------------------------------------------------------
86
+ # Emits, per story bucket, a TSV line: story <TAB> effort_hours <TAB> commit_count <TAB> first_epoch <TAB> last_epoch
87
+ # Reads the whole range once and partitions globally, so the lines are additive.
88
+ partition() {
89
+ local range="$1" key="$2"
90
+ # US (\x1f) between fields; one commit per line, oldest→newest by author date.
91
+ git -C "$ROOT" log --no-merges "$range" \
92
+ --pretty=format:"%at%x1f%H%x1f%(trailers:key=${key},valueonly,separator=%x2C)" \
93
+ | sort -n \
94
+ | awk -F '\x1f' -v IDLE="$IDLE_SECONDS" -v PAD="$PAD_SECONDS" '
95
+ {
96
+ ts = $1 + 0
97
+ story = $3
98
+ gsub(/[[:space:]]/, "", story)
99
+ if (story == "") story = "unattributed"
100
+ count[story]++
101
+ seen[story] = 1
102
+ if (NR > 1) {
103
+ gap = ts - prev
104
+ if (gap < 0) gap = 0 # guard against author-date skew
105
+ if (gap < IDLE) eff[story] += gap # real elapsed effort -> later commit
106
+ else eff[story] += PAD # new session pad -> later commit
107
+ }
108
+ if (!(story in first)) first[story] = ts
109
+ last[story] = ts
110
+ prev = ts
111
+ }
112
+ END {
113
+ for (s in seen)
114
+ printf "%s\t%.4f\t%d\t%d\t%d\n", s, eff[s] / 3600.0, count[s], first[s], last[s]
115
+ }' \
116
+ | sort
117
+ }
118
+
119
+ # Whole-range effort in one bucket — the independent additivity oracle.
120
+ whole_range_hours() {
121
+ local range="$1"
122
+ git -C "$ROOT" log --no-merges "$range" --pretty=format:"%at" \
123
+ | sort -n \
124
+ | awk -v IDLE="$IDLE_SECONDS" -v PAD="$PAD_SECONDS" '
125
+ { ts=$1+0; if(NR>1){gap=ts-prev; if(gap<0)gap=0; if(gap<IDLE)e+=gap; else e+=PAD} prev=ts }
126
+ END { printf "%.4f\n", e/3600.0 }'
127
+ }
128
+
129
+ cmd_report() {
130
+ local range="HEAD" key="$STORY_KEY"
131
+ while [ $# -gt 0 ]; do
132
+ case "$1" in
133
+ --range) range="$2"; shift 2 ;;
134
+ --story-key) key="$2"; shift 2 ;;
135
+ *) die "unknown flag: $1" ;;
136
+ esac
137
+ done
138
+
139
+ local rows; rows="$(partition "$range" "$key")"
140
+ [ -n "$rows" ] || die "no commits in range '$range'"
141
+
142
+ printf '%-16s %12s %8s %s\n' "story" "effort_hours" "commits" "coding_span"
143
+ printf '%-16s %12s %8s %s\n' "----------------" "------------" "--------" "-----------------"
144
+ local sum
145
+ sum="$(printf '%s\n' "$rows" | awk -F'\t' '
146
+ {
147
+ span = ($5 - $4) / 60.0
148
+ printf "%-16s %12.2f %8d %.0f min\n", $1, $2, $3, span
149
+ total += $2
150
+ }
151
+ END { printf "__TOTAL__ %.4f\n", total > "/dev/stderr" }
152
+ ' 2> >(cat >&2))"
153
+ printf '%s\n' "$sum"
154
+
155
+ # Additivity self-check: Σ(per-story) must equal the whole-range oracle.
156
+ local part_total whole
157
+ part_total="$(printf '%s\n' "$rows" | awk -F'\t' '{t+=$2} END{printf "%.4f\n", t}')"
158
+ whole="$(whole_range_hours "$range")"
159
+ printf -- '----\n'
160
+ printf 'Σ(per-story effort_hours) = %s\n' "$part_total"
161
+ printf 'whole-range effort_hours = %s (independent oracle)\n' "$whole"
162
+ # Compare with a rounding tolerance (per-story values are rounded before summing).
163
+ if awk -v a="$part_total" -v b="$whole" 'BEGIN{d=a-b; if(d<0)d=-d; exit !(d < 0.01)}'; then
164
+ printf 'ADDITIVITY: PASS — the partition sums to the whole (within rounding).\n'
165
+ else
166
+ printf 'ADDITIVITY: FAIL — Σ(%s) != whole(%s)\n' "$part_total" "$whole" >&2
167
+ return 1
168
+ fi
169
+ }
170
+
171
+ cmd_append() {
172
+ local range="HEAD" key="$STORY_KEY" story="" bcps="" merged_at="" file="$ROOT/specs/metrics/cycle-times.yaml"
173
+ local telemetry="" # path to harness telemetry JSON (GSD-2/gsd-pi format, optional)
174
+ while [ $# -gt 0 ]; do
175
+ case "$1" in
176
+ --story) story="$2"; shift 2 ;;
177
+ --bcps) bcps="$2"; shift 2 ;;
178
+ --range) range="$2"; shift 2 ;;
179
+ --merged-at) merged_at="$2"; shift 2 ;;
180
+ --file) file="$2"; shift 2 ;;
181
+ --story-key) key="$2"; shift 2 ;;
182
+ --telemetry) telemetry="$2"; shift 2 ;;
183
+ *) die "unknown flag: $1" ;;
184
+ esac
185
+ done
186
+ [ -n "$story" ] || die "append requires --story <id>"
187
+ [ -n "$bcps" ] || die "append requires --bcps <n>"
188
+ [ -n "$merged_at" ] || merged_at="$(date -u +%s)"
189
+
190
+ local rows line eff count first last
191
+ rows="$(partition "$range" "$key")"
192
+ line="$(printf '%s\n' "$rows" | awk -F'\t' -v s="$story" '$1==s')"
193
+ [ -n "$line" ] || die "story '$story' has no attributable commits in range '$range' (check the 'Story: $story' trailer)"
194
+
195
+ eff="$(printf '%s' "$line" | cut -f2)"
196
+ count="$(printf '%s' "$line" | cut -f3)"
197
+ first="$(printf '%s' "$line" | cut -f4)"
198
+ last="$(printf '%s' "$line" | cut -f5)"
199
+
200
+ # lead time: first commit of the story -> merge (calendar latency; NEVER summed).
201
+ local lead_min
202
+ lead_min="$(awk -v m="$merged_at" -v f="$first" 'BEGIN{printf "%.0f", (m-f)/60.0}')"
203
+
204
+ local first_iso range_lo range_hi
205
+ first_iso="$(date -u -d "@$first" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -r "$first" +%Y-%m-%dT%H:%M:%SZ)"
206
+ range_lo="$(git -C "$ROOT" log --no-merges "$range" --pretty=%h --reverse 2>/dev/null \
207
+ | awk 'NR==1{print}')"
208
+ range_hi="$(git -C "$ROOT" rev-parse --short HEAD)"
209
+
210
+ local generated_at
211
+ generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
212
+
213
+ # ---- telemetry ingestion (GSD-2/gsd-pi style; e40s04) ----
214
+ # If --telemetry <json-file> is provided, read the harness data and populate
215
+ # agent.* fields. Otherwise, agent.* stays null (git effort is the fallback).
216
+ local agent_cost agent_tokens_in agent_tokens_out agent_tokens_cache_read
217
+ local agent_tokens_cache_write agent_tokens_total agent_cache_hit
218
+ local agent_compression agent_tool_calls agent_api_requests
219
+ local agent_duration_ms agent_model agent_tier agent_downgraded agent_skills
220
+ agent_cost="null"; agent_tokens_total="null"; agent_cache_hit="null"
221
+ agent_compression="null"; agent_tool_calls="null"; agent_api_requests="null"
222
+ agent_duration_ms="null"; agent_model="null"; agent_tier="null"
223
+ agent_downgraded="null"; agent_skills="[]"
224
+
225
+ if [ -n "$telemetry" ] && [ -f "$telemetry" ]; then
226
+ # GSD-2/gsd-pi snapshotUnitMetrics JSON format:
227
+ # { cost_usd, tokens: {input, output, cache_read, cache_write, total},
228
+ # cache_hit_rate_pct, compression_savings_pct, tool_calls, api_requests,
229
+ # agent_duration_ms, model, tier, model_downgraded, skills[] }
230
+ if command -v jq >/dev/null 2>&1; then
231
+ agent_cost="$(jq -r '.cost_usd // "null"' "$telemetry")"
232
+ agent_tokens_total="$(jq -r '.tokens.total // "null"' "$telemetry")"
233
+ agent_cache_hit="$(jq -r '.cache_hit_rate_pct // "null"' "$telemetry")"
234
+ agent_compression="$(jq -r '.compression_savings_pct // "null"' "$telemetry")"
235
+ agent_tool_calls="$(jq -r '.tool_calls // "null"' "$telemetry")"
236
+ agent_api_requests="$(jq -r '.api_requests // "null"' "$telemetry")"
237
+ agent_duration_ms="$(jq -r '.agent_duration_ms // "null"' "$telemetry")"
238
+ agent_model="$(jq -r '.model // "null"' "$telemetry")"
239
+ agent_tier="$(jq -r '.tier // "null"' "$telemetry")"
240
+ agent_downgraded="$(jq -r '.model_downgraded // "null"' "$telemetry")"
241
+ agent_skills="$(jq -c '.skills // []' "$telemetry")"
242
+ agent_tokens_in="$(jq -r '.tokens.input // "null"' "$telemetry")"
243
+ agent_tokens_out="$(jq -r '.tokens.output // "null"' "$telemetry")"
244
+ agent_tokens_cache_read="$(jq -r '.tokens.cache_read // "null"' "$telemetry")"
245
+ agent_tokens_cache_write="$(jq -r '.tokens.cache_write // "null"' "$telemetry")"
246
+ else
247
+ echo "record-cycle-time: jq not found — telemetry file ignored (git effort fallback)" >&2
248
+ fi
249
+ fi
250
+
251
+ # Emit OKF bundle (YAML frontmatter + markdown body) per story.
252
+ # This is the canonical format — see specs/templates/story-metrics.okf.md.
253
+ # Fields not yet populated (quality.*, flow.*) carry null placeholders.
254
+ {
255
+ printf -- '---\n'
256
+ printf -- 'okf_kind: story-metrics\n'
257
+ printf -- 'okf_version: "0.1"\n'
258
+ printf -- 'id: %s\n' "$story"
259
+ printf -- 'epic: %s\n' "${story%%s*}" # e40s03 → e40
260
+ printf -- 'bcps: %s\n' "$bcps"
261
+ printf -- 'commit_range: "%s..%s"\n' "${range_lo:-$range_hi}" "$range_hi"
262
+ printf -- 'source: measured\n'
263
+ printf -- 'generated_at: %s\n' "$generated_at"
264
+ printf -- 'generator: scripts/record-cycle-time.sh\n'
265
+ printf -- '\n'
266
+ printf -- '# DORA — the four keys (market standard). ⌀ median aggregate.\n'
267
+ printf -- 'dora:\n'
268
+ printf -- ' lead_time_for_changes_min: %s # ⌀ first commit → merge\n' "$lead_min"
269
+ printf -- ' deployment_frequency: null # %% populated by e40s05\n'
270
+ printf -- ' change_failure: null # %% populated by e40s05\n'
271
+ printf -- ' time_to_restore_min: null # ⌀ populated by e40s05\n'
272
+ printf -- '\n'
273
+ printf -- '# Agent code-generation telemetry (GSD-2 style). Σ additive.\n'
274
+ printf -- 'agent:\n'
275
+ printf -- ' cost_usd: %s # Σ harness telemetry (null = git effort fallback)\n' "$agent_cost"
276
+ printf -- ' tokens:\n'
277
+ printf -- ' input: %s\n' "$agent_tokens_in"
278
+ printf -- ' output: %s\n' "$agent_tokens_out"
279
+ printf -- ' cache_read: %s\n' "$agent_tokens_cache_read"
280
+ printf -- ' cache_write: %s\n' "$agent_tokens_cache_write"
281
+ printf -- ' total: %s\n' "$agent_tokens_total"
282
+ printf -- ' cache_hit_rate_pct: %s # %%\n' "$agent_cache_hit"
283
+ printf -- ' compression_savings_pct: %s # %%\n' "$agent_compression"
284
+ printf -- ' tool_calls: %s # Σ\n' "$agent_tool_calls"
285
+ printf -- ' api_requests: %s # Σ\n' "$agent_api_requests"
286
+ printf -- ' agent_duration_ms: %s # Σ (excludes human UAT wait)\n' "$agent_duration_ms"
287
+ printf -- ' model: %s # •\n' "$agent_model"
288
+ printf -- ' tier: %s # • light|standard|heavy\n' "$agent_tier"
289
+ printf -- ' model_downgraded: %s # •\n' "$agent_downgraded"
290
+ printf -- ' skills: %s # Σ\n' "$agent_skills"
291
+ printf -- '\n'
292
+ printf -- '# Effort (git-derived, idle-stripped, ADDITIVE).\n'
293
+ printf -- 'effort:\n'
294
+ printf -- ' effort_hours: %s # Σ idle-stripped (git-hours, 120-min threshold)\n' "$eff"
295
+ printf -- ' idle_threshold_min: 120 # • recorded for interpretability\n'
296
+ printf -- '\n'
297
+ printf -- '# Quality gate. ⌀ median / • static.\n'
298
+ printf -- 'quality:\n'
299
+ printf -- ' audit_score_pct: null # ⌀ populated by audit-code\n'
300
+ printf -- ' coverage_pct: null # ⌀ populated by test suite\n'
301
+ printf -- ' tests_passed: null # • populated by test suite\n'
302
+ printf -- ' verify_status: null # pass|waived\n'
303
+ printf -- ' rework_count: null # Σ reopens → feeds change_failure\n'
304
+ printf -- '\n'
305
+ printf -- '# Flow (worktree telemetry).\n'
306
+ printf -- 'flow:\n'
307
+ printf -- ' merge_duration_ms: null # ⌀ p50/p95 across stories\n'
308
+ printf -- ' merge_conflicts: null # Σ\n'
309
+ printf -- ' worktree_orphaned: null # •\n'
310
+ printf -- '---\n'
311
+ printf -- '\n'
312
+ printf -- '# Story metrics — %s\n' "$story"
313
+ printf -- '\n'
314
+ printf -- 'Effort derived from git commit history via the git-hours model\n'
315
+ printf -- '(src: kimmobrunfeldt/git-hours, 120-min idle threshold, 120-min\n'
316
+ printf -- 'first-commit pad). Lead time is calendar latency (first commit → merge).\n'
317
+ printf -- '\n'
318
+ printf -- 'Aggregation tags: Σ additive (sum → total) · ⌀ median/p95 (never sum)\n'
319
+ printf -- '· %% rate (ratio over window) · • static (identity).\n'
320
+ } >> "$file"
321
+
322
+ echo "record-cycle-time: OKF bundle written for $story → $file (effort=$eff h, lead=$lead_min min, commits=$count)"
323
+ }
324
+
325
+ [ $# -ge 1 ] || { usage; }
326
+
327
+ # Trap --help before subcommand dispatch
328
+ for a in "$@"; do
329
+ if [ "$a" = "--help" ] || [ "$a" = "-h" ]; then usage; fi
330
+ done
331
+
332
+ sub="$1"; shift
333
+ case "$sub" in
334
+ report) cmd_report "$@" ;;
335
+ append) cmd_append "$@" ;;
336
+ self-test) cmd_report "$@" ;;
337
+ help) usage ;;
338
+ --help) usage ;;
339
+ *) die "unknown subcommand '$sub' (run 'record-cycle-time.sh help' for usage)" ;;
340
+ esac
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # validate-okf.sh — provenance gate for OKF story-metrics bundles (e40/e31 style).
4
+ #
5
+ # A bundle is valid iff:
6
+ # 1. The generator ran (generator field matches scripts/record-cycle-time.sh).
7
+ # 2. commit_range resolves to real commits in the repo.
8
+ # 3. Source enum is valid (measured|estimated|backfilled).
9
+ # 4. Required keys are present with correct aggregation tags.
10
+ #
11
+ # This script gates on PROVENANCE, NEVER on a specific metric value.
12
+ # Gate on freshness (did the right generator run?) not on outcomes.
13
+ #
14
+ # Usage:
15
+ # scripts/validate-okf.sh [--dir <path>] [--help]
16
+ # scripts/validate-okf.sh --bundle <file> # validate a single bundle
17
+ #
18
+ set -euo pipefail
19
+
20
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "validate-okf: not inside a git repo"; exit 1; }
21
+ METRICS_DIR="$ROOT/specs/metrics"
22
+ EXIT_CODE=0
23
+ REQUIRED_KEYS="id epic bcps commit_range source generated_at generator"
24
+ VALID_SOURCES="measured estimated backfilled"
25
+
26
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
27
+
28
+ usage() {
29
+ cat <<'EOF'
30
+ Usage: scripts/validate-okf.sh [flags]
31
+
32
+ Flags:
33
+ --dir <path> Validate all OKF bundles in directory (default: specs/metrics/)
34
+ --bundle <file> Validate a single OKF bundle
35
+ --help Show this message
36
+
37
+ Description:
38
+ Provenance gate for OKF story-metrics bundles. Validates that:
39
+ - Required keys are present (id, epic, bcps, commit_range, source, generated_at, generator)
40
+ - Source enum is valid (measured|estimated|backfilled)
41
+ - Generator matches scripts/record-cycle-time.sh
42
+ - commit_range resolves to real commits
43
+ - Aggregation tags are present (Σ, ⌀, %, •)
44
+
45
+ NEVER gates on a specific metric value — only on provenance and freshness.
46
+ EOF
47
+ exit 0
48
+ }
49
+
50
+ # Parse a single YAML frontmatter block from an OKF bundle .md file.
51
+ # Returns the frontmatter as JSON via Python (which handles YAML safely).
52
+ parse_frontmatter() {
53
+ local file="$1"
54
+ # Extract YAML frontmatter between --- markers
55
+ python3 -c "
56
+ import yaml, sys
57
+ content = open('$file').read()
58
+ parts = content.split('---')
59
+ if len(parts) < 3:
60
+ sys.exit(1)
61
+ try:
62
+ fm = yaml.safe_load(parts[1])
63
+ if fm is None:
64
+ sys.exit(1)
65
+ # Emit as JSON for bash
66
+ import json
67
+ print(json.dumps(fm))
68
+ except Exception as e:
69
+ print(f'validate-okf: YAML parse error in $file: {e}', file=sys.stderr)
70
+ sys.exit(1)
71
+ " 2>/dev/null
72
+ }
73
+
74
+ validate_bundle() {
75
+ local file="$1"
76
+ local bundle_name; bundle_name="$(basename "$file")"
77
+
78
+ # Parse frontmatter
79
+ local fm
80
+ fm="$(parse_frontmatter "$file")" || {
81
+ printf "${RED}FAIL${NC} %s: cannot parse YAML frontmatter\n" "$bundle_name"
82
+ EXIT_CODE=1
83
+ return
84
+ }
85
+
86
+ # Check okf_kind
87
+ local kind
88
+ kind="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('okf_kind',''))" 2>/dev/null)"
89
+ if [ "$kind" != "story-metrics" ]; then
90
+ printf "${YELLOW}SKIP${NC} %s: not a story-metrics bundle (kind=%s)\n" "$bundle_name" "${kind:-none}"
91
+ return
92
+ fi
93
+
94
+ # Check required keys
95
+ for key in $REQUIRED_KEYS; do
96
+ local val
97
+ val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
98
+ if [ -z "$val" ] || [ "$val" = "null" ] || [ "$val" = "None" ]; then
99
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$bundle_name" "$key"
100
+ EXIT_CODE=1
101
+ return
102
+ fi
103
+ done
104
+
105
+ # Check source enum
106
+ local source
107
+ source="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['source'])" 2>/dev/null)"
108
+ if ! echo "$VALID_SOURCES" | grep -qw "$source"; then
109
+ printf "${RED}FAIL${NC} %s: invalid source '%s' — must be one of: %s\n" "$bundle_name" "$source" "$VALID_SOURCES"
110
+ EXIT_CODE=1
111
+ return
112
+ fi
113
+
114
+ # Check generator
115
+ local generator
116
+ generator="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['generator'])" 2>/dev/null)"
117
+ if [ "$generator" != "scripts/record-cycle-time.sh" ]; then
118
+ printf "${RED}FAIL${NC} %s: wrong generator '%s' — expected scripts/record-cycle-time.sh\n" "$bundle_name" "$generator"
119
+ EXIT_CODE=1
120
+ return
121
+ fi
122
+
123
+ # Check commit_range resolves
124
+ local commit_range
125
+ commit_range="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['commit_range'])" 2>/dev/null)"
126
+ if ! git -C "$ROOT" log --oneline -1 "$commit_range" >/dev/null 2>&1; then
127
+ printf "${RED}FAIL${NC} %s: commit_range '%s' does not resolve\n" "$bundle_name" "$commit_range"
128
+ EXIT_CODE=1
129
+ return
130
+ fi
131
+
132
+ # Check aggregation tags exist (not empty, but don't validate correctness)
133
+ local effort_hours
134
+ effort_hours="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); e=d.get('effort',{}); print(e.get('effort_hours',''))" 2>/dev/null)"
135
+ if [ -z "$effort_hours" ] || [ "$effort_hours" = "null" ]; then
136
+ printf "${RED}FAIL${NC} %s: effort.effort_hours missing or null\n" "$bundle_name"
137
+ EXIT_CODE=1
138
+ return
139
+ fi
140
+
141
+ # Bounds sanity (effort can't be negative)
142
+ python3 -c "
143
+ import json, sys
144
+ d = json.load(sys.stdin)
145
+ eff = d.get('effort', {}).get('effort_hours')
146
+ if eff is not None and eff < 0:
147
+ sys.exit(1)
148
+ " <<< "$fm" 2>/dev/null || {
149
+ printf "${RED}FAIL${NC} %s: effort_hours out of bounds\n" "$bundle_name"
150
+ EXIT_CODE=1
151
+ return
152
+ }
153
+
154
+ printf "${GREEN}PASS${NC} %s (source=%s, generator=%s, commit_range resolves)\n" "$bundle_name" "$source" "$generator"
155
+ }
156
+
157
+ # ---- main ----------------------------------------------------------------
158
+ DIR=""
159
+ BUNDLE=""
160
+
161
+ [ $# -ge 1 ] || set -- "--dir" "$METRICS_DIR" # default: validate specs/metrics/
162
+
163
+ while [ $# -gt 0 ]; do
164
+ case "$1" in
165
+ --dir) DIR="$2"; shift 2 ;;
166
+ --bundle) BUNDLE="$2"; shift 2 ;;
167
+ --help|-h) usage ;;
168
+ *) echo "validate-okf: unknown flag: $1" >&2; usage ;;
169
+ esac
170
+ done
171
+
172
+ echo "validate-okf: scanning for OKF bundles..."
173
+
174
+ if [ -n "$BUNDLE" ]; then
175
+ validate_bundle "$BUNDLE"
176
+ elif [ -n "$DIR" ]; then
177
+ if [ ! -d "$DIR" ]; then
178
+ printf "${YELLOW}SKIP${NC} directory not found: %s\n" "$DIR"
179
+ exit 0
180
+ fi
181
+ count=0
182
+ while IFS= read -r -d '' f; do
183
+ # Only process .md files with YAML frontmatter
184
+ if head -1 "$f" 2>/dev/null | grep -q '^---$'; then
185
+ validate_bundle "$f"
186
+ count=$((count + 1))
187
+ fi
188
+ done < <(find "$DIR" -maxdepth 1 -name '*.md' -print0 2>/dev/null || true)
189
+ if [ "$count" -eq 0 ]; then
190
+ printf "${YELLOW}SKIP${NC} no OKF bundles found in %s\n" "$DIR"
191
+ else
192
+ echo "validate-okf: scanned $count bundle(s)"
193
+ fi
194
+ else
195
+ echo "validate-okf: nothing to validate" >&2
196
+ fi
197
+
198
+ exit "$EXIT_CODE"
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # wait-for-ci.sh — Poll GitHub Actions until all workflow runs complete
3
+ # Usage: wait-for-ci.sh [--timeout SECONDS] [--interval SECONDS] [--branch BRANCH]
4
+ # --timeout Max seconds to wait (default: 600)
5
+ # --interval Seconds between polls (default: 30)
6
+ # --branch Branch to check (default: current branch)
7
+ # --commit Specific commit SHA to verify (default: HEAD)
8
+ # --json Output machine-readable JSON on completion
9
+ # --help Show this message
10
+ #
11
+ # Exit codes: 0 = all workflows green, 1 = any workflow failed, 2 = timeout, 3 = no gh CLI
12
+
13
+ set -euo pipefail
14
+
15
+ TIMEOUT=600
16
+ INTERVAL=30
17
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
18
+ COMMIT=""
19
+ OUTPUT_JSON=false
20
+
21
+ usage() {
22
+ sed -n '2,12p' "$0" | sed 's/^# //'
23
+ }
24
+
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --timeout) TIMEOUT="$2"; shift 2 ;;
28
+ --interval) INTERVAL="$2"; shift 2 ;;
29
+ --branch) BRANCH="$2"; shift 2 ;;
30
+ --commit) COMMIT="$2"; shift 2 ;;
31
+ --json) OUTPUT_JSON=true; shift ;;
32
+ --help) usage; exit 0 ;;
33
+ *) echo "Unknown flag: $1"; usage; exit 2 ;;
34
+ esac
35
+ done
36
+
37
+ COMMIT="${COMMIT:-$(git rev-parse HEAD)}"
38
+ COMMIT_SHORT="${COMMIT:0:7}"
39
+
40
+ # ── Git-only fallback ─────────────────────────────────────────────────
41
+ if ! command -v gh >/dev/null 2>&1; then
42
+ echo "⚠️ gh CLI not found — using git-only fallback (cannot verify CI status)"
43
+ git fetch origin "$BRANCH" 2>/dev/null || true
44
+ REMOTE_SHA=$(git rev-parse "origin/$BRANCH" 2>/dev/null || echo "")
45
+ if [[ -n "$REMOTE_SHA" ]] && [[ "$REMOTE_SHA" == "$COMMIT" ]]; then
46
+ echo "✅ Push confirmed: remote $BRANCH matches $COMMIT_SHORT"
47
+ echo "⚠️ CI status UNVERIFIED — gh CLI required for workflow polling"
48
+ exit 0
49
+ else
50
+ echo "❌ Push unconfirmed: remote $BRANCH does not match $COMMIT_SHORT"
51
+ exit 1
52
+ fi
53
+ fi
54
+
55
+ # ── Verify gh auth ────────────────────────────────────────────────────
56
+ if ! gh auth status 2>/dev/null; then
57
+ echo "⚠️ gh CLI not authenticated — falling back to git-only"
58
+ git fetch origin "$BRANCH" 2>/dev/null || true
59
+ REMOTE_SHA=$(git rev-parse "origin/$BRANCH" 2>/dev/null || echo "")
60
+ if [[ -n "$REMOTE_SHA" ]] && [[ "$REMOTE_SHA" == "$COMMIT" ]]; then
61
+ echo "✅ Push confirmed: remote $BRANCH matches $COMMIT_SHORT"
62
+ echo "⚠️ CI status UNVERIFIED — gh auth required for workflow polling"
63
+ exit 0
64
+ else
65
+ echo "❌ Push unconfirmed"
66
+ exit 1
67
+ fi
68
+ fi
69
+
70
+ # ── Poll CI ───────────────────────────────────────────────────────────
71
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "unknown")
72
+ echo "==> Polling CI for $BRANCH ($COMMIT_SHORT) in $REPO"
73
+ echo " Timeout: ${TIMEOUT}s, Interval: ${INTERVAL}s"
74
+
75
+ ELAPSED=0
76
+ FAILED_WORKFLOWS=()
77
+ PASSED_WORKFLOWS=()
78
+
79
+ while [[ $ELAPSED -lt $TIMEOUT ]]; do
80
+ # Fetch all runs for the branch
81
+ RUNS_JSON=$(gh run list --branch "$BRANCH" --limit 20 --json workflowName,status,conclusion,headSha,databaseId,displayTitle 2>/dev/null || echo "[]")
82
+
83
+ # Filter to runs matching our commit
84
+ MATCHING_RUNS=$(echo "$RUNS_JSON" | jq --arg sha "$COMMIT" '[.[] | select(.headSha == $sha)]' 2>/dev/null || echo "[]")
85
+
86
+ RUN_COUNT=$(echo "$MATCHING_RUNS" | jq 'length' 2>/dev/null || echo "0")
87
+
88
+ if [[ "$RUN_COUNT" -eq 0 ]]; then
89
+ echo " (${ELAPSED}s) No runs found for $COMMIT_SHORT — waiting for CI to start..."
90
+ sleep "$INTERVAL"
91
+ ELAPSED=$((ELAPSED + INTERVAL))
92
+ continue
93
+ fi
94
+
95
+ # Check completion
96
+ INCOMPLETE=$(echo "$MATCHING_RUNS" | jq '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo "0")
97
+ FAILURES=$(echo "$MATCHING_RUNS" | jq '[.[] | select(.status == "completed" and .conclusion == "failure")]' 2>/dev/null || echo "[]")
98
+ FAILURE_COUNT=$(echo "$FAILURES" | jq 'length' 2>/dev/null || echo "0")
99
+ SUCCESSES=$(echo "$MATCHING_RUNS" | jq '[.[] | select(.status == "completed" and .conclusion == "success")]' 2>/dev/null || echo "[]")
100
+ SUCCESS_COUNT=$(echo "$SUCCESSES" | jq 'length' 2>/dev/null || echo "0")
101
+
102
+ echo " (${ELAPSED}s) $RUN_COUNT runs: $SUCCESS_COUNT passed, $FAILURE_COUNT failed, $INCOMPLETE pending"
103
+
104
+ # All complete?
105
+ if [[ "$INCOMPLETE" -eq 0 ]]; then
106
+ if [[ "$FAILURE_COUNT" -eq 0 ]]; then
107
+ echo "✅ All $SUCCESS_COUNT workflow(s) passed for $COMMIT_SHORT"
108
+ if $OUTPUT_JSON; then
109
+ echo "$SUCCESSES" | jq '{status:"success",commit:$sha,workflows:.}' --arg sha "$COMMIT_SHORT"
110
+ fi
111
+ exit 0
112
+ else
113
+ echo "❌ $FAILURE_COUNT workflow(s) FAILED:"
114
+ echo "$FAILURES" | jq -r '.[] | " \(.workflowName) — https://github.com/'"$REPO"'/actions/runs/\(.databaseId)"'
115
+ echo ""
116
+ echo "Handoff: set handoff.next_skill = fix-bug in specs/state.yaml"
117
+ if $OUTPUT_JSON; then
118
+ echo "$FAILURES" | jq '{status:"failure",commit:$sha,failed_workflows:.}' --arg sha "$COMMIT_SHORT"
119
+ fi
120
+ exit 1
121
+ fi
122
+ fi
123
+
124
+ sleep "$INTERVAL"
125
+ ELAPSED=$((ELAPSED + INTERVAL))
126
+ done
127
+
128
+ echo "❌ Timeout: CI did not complete within ${TIMEOUT}s"
129
+ echo " Branch: $BRANCH, Commit: $COMMIT_SHORT"
130
+ exit 2
@@ -2,6 +2,7 @@
2
2
  name: align-grid
3
3
  description: "Build editorial/magazine/report webpages on a GENUINE Müller-Brockmann modular grid (International Typographic Style) — not a decorative one. Encodes the discipline (columns + modules + baseline, grotesque type, flush-left, restrained black/white/red palette) AND the hard-won front-end engineering to make the grid real, visible, and verified: one CSS-variable source of truth, an interactive grid-toggle overlay that lives in the SAME content box as the content, subgrid \"bands\" so every element snaps to a column line, an 8px baseline lock, and runtime OPTICAL ALIGNMENT that puts display type's ink (not its box) on the line. Ships with a scaffold generator and a Puppeteer verification harness that proves 0px adherence."
4
4
  model: sonnet
5
+ effort: standard
5
6
  ---
6
7
 
7
8
  # Müller-Brockmann Grid Systems — built real, visible, and verified