bigpowers 2.49.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.
- package/.pi/package.json +1 -1
- package/.pi/prompts/evolve-skill.md +7 -6
- package/.pi/prompts/plan-release.md +0 -1
- package/.pi/prompts/release-branch.md +74 -56
- package/.pi/prompts/session-state.md +2 -0
- package/.pi/prompts/survey-context.md +1 -1
- package/.pi/skills/evolve-skill/SKILL.md +7 -6
- package/.pi/skills/plan-release/SKILL.md +0 -1
- package/.pi/skills/release-branch/SKILL.md +74 -56
- package/.pi/skills/session-state/SKILL.md +2 -0
- package/.pi/skills/survey-context/SKILL.md +1 -1
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +20 -0
- package/CONVENTIONS.md +11 -0
- package/package.json +1 -1
- package/scripts/audit-compliance.sh +1 -4
- package/scripts/record-cycle-time.sh +340 -0
- package/scripts/run-golden-suite.sh +59 -1
- package/scripts/validate-okf.sh +198 -0
- package/scripts/wait-for-ci.sh +130 -0
- package/skills/align-grid/SKILL.md +1 -0
- package/skills/assess-impact/SKILL.md +1 -0
- package/skills/audit-code/SKILL.md +1 -0
- package/skills/audit-plan/SKILL.md +1 -0
- package/skills/build-epic/SKILL.md +1 -0
- package/skills/change-request/SKILL.md +1 -0
- package/skills/commit-message/SKILL.md +1 -0
- package/skills/compose-workflow/SKILL.md +1 -0
- package/skills/craft-skill/SKILL.md +1 -0
- package/skills/deepen-architecture/SKILL.md +1 -0
- package/skills/define-language/SKILL.md +1 -0
- package/skills/define-success/SKILL.md +1 -0
- package/skills/delegate-task/SKILL.md +1 -0
- package/skills/deploy/SKILL.md +1 -0
- package/skills/design-interface/SKILL.md +1 -0
- package/skills/develop-tdd/SKILL.md +1 -0
- package/skills/diagnose-root/SKILL.md +1 -0
- package/skills/dispatch-agents/SKILL.md +1 -0
- package/skills/edit-document/SKILL.md +1 -0
- package/skills/elaborate-spec/SKILL.md +1 -0
- package/skills/enforce-first/SKILL.md +1 -0
- package/skills/evolve-skill/SKILL.md +8 -6
- package/skills/execute-plan/SKILL.md +1 -0
- package/skills/extract-design/SKILL.md +1 -0
- package/skills/fix-bug/SKILL.md +1 -0
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/guard-git/SKILL.md +1 -0
- package/skills/hook-commits/SKILL.md +1 -0
- package/skills/inspect-quality/SKILL.md +1 -0
- package/skills/investigate-bug/SKILL.md +1 -0
- package/skills/kickoff-branch/SKILL.md +1 -0
- package/skills/map-codebase/SKILL.md +1 -0
- package/skills/migrate-spec/SKILL.md +1 -0
- package/skills/model-domain/SKILL.md +1 -0
- package/skills/orchestrate-project/SKILL.md +1 -0
- package/skills/organize-workspace/SKILL.md +1 -0
- package/skills/plan-refactor/SKILL.md +1 -0
- package/skills/plan-release/SKILL.md +1 -1
- package/skills/plan-work/SKILL.md +1 -0
- package/skills/publish-package/SKILL.md +1 -0
- package/skills/quick-fix/SKILL.md +1 -0
- package/skills/release-branch/REFERENCE.md +57 -54
- package/skills/release-branch/SKILL.md +20 -4
- package/skills/request-review/SKILL.md +1 -0
- package/skills/research-first/SKILL.md +1 -0
- package/skills/reset-baseline/SKILL.md +1 -0
- package/skills/respond-review/SKILL.md +1 -0
- package/skills/run-benchmark/SKILL.md +1 -0
- package/skills/run-evals/SKILL.md +1 -0
- package/skills/run-planning/SKILL.md +1 -0
- package/skills/scope-work/SKILL.md +1 -0
- package/skills/search-skills/SKILL.md +1 -0
- package/skills/security-review/SKILL.md +1 -0
- package/skills/seed-conventions/SKILL.md +1 -0
- package/skills/session-state/SKILL.md +3 -0
- package/skills/setup-environment/SKILL.md +1 -0
- package/skills/simulate-agents/SKILL.md +1 -0
- package/skills/slice-tasks/SKILL.md +1 -0
- package/skills/smoke-test/SKILL.md +1 -0
- package/skills/spike-prototype/SKILL.md +1 -0
- package/skills/stocktake-skills/SKILL.md +1 -0
- package/skills/survey-context/SKILL.md +2 -1
- package/skills/terse-mode/SKILL.md +1 -0
- package/skills/trace-requirement/SKILL.md +1 -0
- package/skills/using-bigpowers/SKILL.md +1 -0
- package/skills/validate-contracts/SKILL.md +1 -0
- package/skills/validate-fix/SKILL.md +1 -0
- package/skills/verify-work/SKILL.md +1 -0
- package/skills/visual-dashboard/SKILL.md +1 -0
- package/skills/wire-ci/SKILL.md +1 -0
- package/skills/wire-observability/SKILL.md +1 -0
- package/skills/write-document/SKILL.md +1 -0
- 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
|
|
@@ -15,12 +15,14 @@ REPORT_DIR="specs/benchmarks/reports"
|
|
|
15
15
|
BASELINE_FILE="$REPORT_DIR/BASELINE-GOLDEN.yaml"
|
|
16
16
|
DRY_RUN=false
|
|
17
17
|
BASELINE_MODE=false
|
|
18
|
+
CHECK_SIZE=false
|
|
18
19
|
|
|
19
20
|
for arg in "$@"; do
|
|
20
21
|
case "$arg" in
|
|
21
22
|
--dry-run) DRY_RUN=true ;;
|
|
22
23
|
--baseline) BASELINE_MODE=true ;;
|
|
23
|
-
|
|
24
|
+
--check-size) CHECK_SIZE=true ;;
|
|
25
|
+
*) echo "Unknown flag: $arg"; echo "Usage: run-golden-suite.sh [--dry-run] [--baseline] [--check-size]"; exit 2 ;;
|
|
24
26
|
esac
|
|
25
27
|
done
|
|
26
28
|
|
|
@@ -180,6 +182,62 @@ if $BASELINE_MODE; then
|
|
|
180
182
|
echo "Baseline pinned: $REPORT_FILE"
|
|
181
183
|
fi
|
|
182
184
|
|
|
185
|
+
# ── Size budget check (e31s04) ───────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
if $CHECK_SIZE; then
|
|
188
|
+
WARN_THRESHOLD=20
|
|
189
|
+
FAIL_THRESHOLD=50
|
|
190
|
+
|
|
191
|
+
if [[ ! -f "$BASELINE_FILE" ]]; then
|
|
192
|
+
echo "No baseline found. Run --baseline first."
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
echo ""
|
|
197
|
+
echo "Size Budget Check (+${WARN_THRESHOLD}% warn, +${FAIL_THRESHOLD}% fail):"
|
|
198
|
+
SIZE_WARNINGS=0
|
|
199
|
+
SIZE_FAILURES=0
|
|
200
|
+
NEW_SKILLS=0
|
|
201
|
+
|
|
202
|
+
for skill_md in skills/*/SKILL.md; do
|
|
203
|
+
skill_name=$(basename "$(dirname "$skill_md")")
|
|
204
|
+
current_bytes=$(wc -c < "$skill_md" | tr -d ' ')
|
|
205
|
+
|
|
206
|
+
# Get baseline size (best-effort grep from YAML)
|
|
207
|
+
baseline_bytes=$(grep "^ $skill_name:" "$BASELINE_FILE" 2>/dev/null | awk '{print $2}')
|
|
208
|
+
|
|
209
|
+
if [[ -z "$baseline_bytes" ]]; then
|
|
210
|
+
echo " ${YELLOW}NEW${NC} $skill_name: ${current_bytes}b (not in baseline)"
|
|
211
|
+
(( NEW_SKILLS += 1 )) || true
|
|
212
|
+
continue
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
if [[ "$baseline_bytes" -eq 0 ]]; then
|
|
216
|
+
baseline_bytes=1 # avoid division by zero
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
pct=$(( (current_bytes - baseline_bytes) * 100 / baseline_bytes ))
|
|
220
|
+
|
|
221
|
+
if [[ "$pct" -ge "$FAIL_THRESHOLD" ]]; then
|
|
222
|
+
echo " ${RED}FAIL${NC} $skill_name: ${current_bytes}b (+${pct}%)"
|
|
223
|
+
(( SIZE_FAILURES += 1 )) || true
|
|
224
|
+
elif [[ "$pct" -ge "$WARN_THRESHOLD" ]]; then
|
|
225
|
+
echo " ${YELLOW}WARN${NC} $skill_name: ${current_bytes}b (+${pct}%)"
|
|
226
|
+
(( SIZE_WARNINGS += 1 )) || true
|
|
227
|
+
else
|
|
228
|
+
echo " ${GREEN}OK${NC} $skill_name: ${current_bytes}b"
|
|
229
|
+
fi
|
|
230
|
+
done
|
|
231
|
+
|
|
232
|
+
echo ""
|
|
233
|
+
echo "Size summary: $SIZE_WARNINGS warnings, $SIZE_FAILURES failures, $NEW_SKILLS new"
|
|
234
|
+
|
|
235
|
+
if [[ "$SIZE_FAILURES" -gt 0 ]]; then
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
exit 0
|
|
239
|
+
fi
|
|
240
|
+
|
|
183
241
|
if [[ "$OVERALL" == "fail" ]]; then
|
|
184
242
|
exit 1
|
|
185
243
|
fi
|
|
@@ -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"
|