@windyroad/itil 0.35.7 → 0.35.8-preview.393
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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -23
- package/bin/wr-itil-effort-tally +3 -0
- package/hooks/itil-readme-refresh-discipline.sh +4 -2
- package/hooks/lib/readme-refresh-detect.sh +64 -1
- package/hooks/test/itil-readme-refresh-discipline.bats +55 -0
- package/package.json +1 -1
- package/scripts/effort-tally.sh +74 -0
- package/scripts/test/effort-tally.bats +79 -0
- package/skills/work-problems/SKILL.md +8 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Supports creating new problems, updating root cause analysis, transitioning stat
|
|
|
56
56
|
|
|
57
57
|
Supports declaring new incidents, recording evidence-first observations and hypotheses, logging mitigation attempts, transitioning lifecycle (Investigating → Mitigating → Restored → Closed), and automatically handing off to `manage-problem` when service is restored.
|
|
58
58
|
|
|
59
|
-
See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for the incident-vs-problem split
|
|
59
|
+
See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for the incident-vs-problem split.
|
|
60
60
|
|
|
61
61
|
## How It Works
|
|
62
62
|
|
|
@@ -86,7 +86,7 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
|
|
|
86
86
|
| `/wr-itil:review-problems` | Re-rate every open and known-error ticket and refresh the WSJF ranking | Experimental |
|
|
87
87
|
| `/wr-itil:reconcile-readme` | Detect and correct drift between `docs/problems/README.md` and on-disk ticket inventory | Experimental |
|
|
88
88
|
| `/wr-itil:report-upstream` | Report a local problem as a structured issue against an upstream repository (ADR-024) | Experimental |
|
|
89
|
-
| `/wr-itil:check-upstream-responses` | Poll upstream issues we filed via `/wr-itil:report-upstream` and surface new comments / state changes / label changes since last check (P249 Phase 1; outbound symmetric counterpart to ADR-062 inbound discovery
|
|
89
|
+
| `/wr-itil:check-upstream-responses` | Poll upstream issues we filed via `/wr-itil:report-upstream` and surface new comments / state changes / label changes since last check (P249 Phase 1; outbound symmetric counterpart to ADR-062 inbound discovery) | Experimental |
|
|
90
90
|
| `/wr-itil:capture-rfc` | Lightweight RFC-capture skill — mandatory problem-trace per ADR-060 I1 invariant; opens a coordinated multi-commit change traceable to ≥ 1 driving problem (Phase 1 of the Problem-RFC-Story framework, P170 / ADR-060) | Experimental |
|
|
91
91
|
| `/wr-itil:manage-rfc` | Heavyweight RFC intake + lifecycle management — proposed → accepted → in-progress → verifying → closed; sibling to `manage-problem` at the RFC tier (ADR-060) | Experimental |
|
|
92
92
|
| `/wr-itil:capture-story` | Lightweight story-capture skill — mandatory problem-trace AND JTBD-trace per ADR-060 I6 + I9 invariants; optional `--rfc` / `--story-map` flags (I7 + I8 enforce at `accepted` transition); drafts an INVEST-shaped sub-workstream entity under a parent RFC (Phase 2 of the Problem-RFC-Story framework, P170 / ADR-060) | Experimental |
|
|
@@ -102,27 +102,6 @@ See [ADR-011](../../docs/decisions/011-manage-incident-skill.proposed.md) for th
|
|
|
102
102
|
| `/wr-itil:mitigate-incident` / `/wr-itil:restore-incident` / `/wr-itil:close-incident` / `/wr-itil:link-incident` | Incident lifecycle transitions (ADR-011) | Experimental |
|
|
103
103
|
| `/wr-itil:scaffold-intake` | Scaffold OSS intake surfaces (`.github/ISSUE_TEMPLATE/`, `SECURITY.md`, `SUPPORT.md`, `CONTRIBUTING.md`) for downstream adopters (ADR-036) | Experimental |
|
|
104
104
|
|
|
105
|
-
## Jobs to be Done
|
|
106
|
-
|
|
107
|
-
This plugin serves the [Jobs to be Done](../../docs/jtbd/) below. Per [ADR-051](../../docs/decisions/051-jtbd-anchored-readme-with-drift-advisory.proposed.md), the persona-grouped JTBD anchor is the canonical source of truth for the README's value framing.
|
|
108
|
-
|
|
109
|
-
### Plugin user
|
|
110
|
-
|
|
111
|
-
- **[JTBD-301 Report a Problem Without Pre-Classifying It](../../docs/jtbd/plugin-user/JTBD-301-report-problem-without-pre-classifying.proposed.md)** — adopters who hit a problem with an installed `@windyroad/*` plugin describe what they observed; `/wr-itil:scaffold-intake` provisions the intake template downstream so triage decides the category, not the reporter.
|
|
112
|
-
|
|
113
|
-
### Tech lead / consultant
|
|
114
|
-
|
|
115
|
-
- **[JTBD-201 Restore Service Fast with an Audit Trail](../../docs/jtbd/tech-lead/JTBD-201-restore-service-fast.proposed.md)** — the manage-incident skill carries an evidence-first lifecycle (investigating → mitigating → restored → closed), with handoff to manage-problem for the root-cause work.
|
|
116
|
-
|
|
117
|
-
### Solo developer
|
|
118
|
-
|
|
119
|
-
- **[JTBD-006 Progress the Backlog While I'm Away](../../docs/jtbd/solo-developer/JTBD-006-work-backlog-afk.proposed.md)** — `/wr-itil:work-problems` is the AFK orchestrator that loops through the WSJF-ranked backlog, working tickets without interactive input until quota or a stop condition fires.
|
|
120
|
-
- **[JTBD-008 Decompose a Fix Into Coordinated Changes](../../docs/jtbd/solo-developer/JTBD-008-decompose-fix-into-coordinated-changes.proposed.md)** — `/wr-itil:capture-rfc` + `/wr-itil:manage-rfc` are the capture-time decomposition surface for multi-commit coordinated changes traced to a driving problem (Phase 1); `/wr-itil:capture-story` is the INVEST-shaped sub-workstream surface for individual slices under those coordinated changes (Phase 2 — story tier). The I1 trace-to-problem invariant is gate-enforced at capture-rfc time; I6 + I9 problem-and-JTBD-trace invariants are gate-enforced at capture-story time (P170 / ADR-060).
|
|
121
|
-
|
|
122
|
-
### Plugin user (currency anchor)
|
|
123
|
-
|
|
124
|
-
- **[JTBD-302 Trust That the README Describes the Plugin I Just Installed](../../docs/jtbd/plugin-user/JTBD-302-trust-readme-describes-installed-behaviour.proposed.md)** — this README is anchored on current JTBD job IDs; drift between prose and shipped behaviour is detectable at retro time per ADR-051.
|
|
125
|
-
|
|
126
105
|
## Updating and Uninstalling
|
|
127
106
|
|
|
128
107
|
```bash
|
|
@@ -92,8 +92,10 @@ command_invokes_git_commit "$COMMAND" || exit 0
|
|
|
92
92
|
|
|
93
93
|
# Run detection. Helper echoes offending ticket path on stdout when
|
|
94
94
|
# detected; returns 1 in that case. Returns 0 (allow) on no-trap,
|
|
95
|
-
# bypass env,
|
|
96
|
-
|
|
95
|
+
# bypass env, a registered RISK_BYPASS commit-message trailer (P265 —
|
|
96
|
+
# `$COMMAND` is threaded in so the helper can inspect the trailer), or
|
|
97
|
+
# fail-open (non-git tree, parse error).
|
|
98
|
+
TRAPPED_TICKET=$(detect_readme_refresh_required "$COMMAND" 2>/dev/null) && exit 0
|
|
97
99
|
|
|
98
100
|
# Extract the leading ticket-ID digits from the basename so the deny
|
|
99
101
|
# names the ticket as `P<NNN>` rather than the full descriptive path
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
# `.claude/settings.json` env field or shell `export` before
|
|
43
43
|
# launching `claude` — inline-prefix syntax (`VAR=1 git commit ...`)
|
|
44
44
|
# does NOT propagate from a Bash subshell to PreToolUse hooks (P173).
|
|
45
|
+
# - Registered `RISK_BYPASS: <token>` commit-message trailer (P265) →
|
|
46
|
+
# return 0 (allow). Narrow allow-list (currently only
|
|
47
|
+
# `adr-031-migration`, the standalone ADR-031 layout-migration
|
|
48
|
+
# commit, which is a rename-only change that legitimately stages no
|
|
49
|
+
# README refresh). The trailer is read from the live `git commit`
|
|
50
|
+
# command string at PreToolUse time (the commit message is not yet
|
|
51
|
+
# written), matching the sibling `risk-score-commit-gate.sh`
|
|
52
|
+
# recognition (P170 T11) so one logical migration commit clears both
|
|
53
|
+
# gates. Registry of record: ADR-014 commit-message bypass-token
|
|
54
|
+
# table.
|
|
45
55
|
#
|
|
46
56
|
# Narrative-only short-circuit (P230):
|
|
47
57
|
# - When all staged ticket edits are purely narrative — no
|
|
@@ -101,21 +111,74 @@
|
|
|
101
111
|
# shape — per-invocation deterministic, no markers).
|
|
102
112
|
# P141 — sibling changeset-discipline helper (same shape).
|
|
103
113
|
# P165 — this helper.
|
|
114
|
+
# P265 — RISK_BYPASS trailer allow-list bypass (this addition).
|
|
115
|
+
|
|
116
|
+
# Allow-list of registered RISK_BYPASS commit-message trailer tokens
|
|
117
|
+
# (P265). A policy-authorised commit may carry `RISK_BYPASS: <token>` in
|
|
118
|
+
# its message body; when <token> is registered here, the README-refresh
|
|
119
|
+
# gate allows the commit even though no README refresh is staged. The
|
|
120
|
+
# allow-list keeps the bypass narrow and auditable — a generic
|
|
121
|
+
# `RISK_BYPASS:` match would let any commit self-exempt.
|
|
122
|
+
#
|
|
123
|
+
# Registered tokens:
|
|
124
|
+
# adr-031-migration — the standalone per-state-subdir layout-migration
|
|
125
|
+
# commit written by lib/migrate-problems-layout.sh. It is a pure
|
|
126
|
+
# rename (no README content change — the table references tickets by
|
|
127
|
+
# ID, not path), so requiring a README refresh would deadlock the
|
|
128
|
+
# migration (P265). The same token clears the sibling
|
|
129
|
+
# risk-score-commit-gate.sh (P170 T11); both gates recognise it via
|
|
130
|
+
# the identical grep below so one logical migration commit clears
|
|
131
|
+
# both. Registry of record: ADR-014 commit-message bypass-token table.
|
|
132
|
+
_README_REFRESH_BYPASS_TRAILERS=("adr-031-migration")
|
|
133
|
+
|
|
134
|
+
# Returns 0 if the given `git commit` command string carries a
|
|
135
|
+
# registered RISK_BYPASS trailer from the allow-list above. The grep
|
|
136
|
+
# pattern is kept byte-identical to risk-score-commit-gate.sh so both
|
|
137
|
+
# commit gates recognise the token the same way (P265 architect verdict).
|
|
138
|
+
_readme_refresh_command_has_bypass_trailer() {
|
|
139
|
+
local command="${1:-}"
|
|
140
|
+
[ -n "$command" ] || return 1
|
|
141
|
+
local token
|
|
142
|
+
for token in "${_README_REFRESH_BYPASS_TRAILERS[@]}"; do
|
|
143
|
+
if printf '%s' "$command" \
|
|
144
|
+
| grep -qE "RISK_BYPASS:[[:space:]]*${token}([^A-Za-z0-9_-]|\$)"; then
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
return 1
|
|
149
|
+
}
|
|
104
150
|
|
|
105
151
|
# Detect whether the current staged set requires a README refresh that
|
|
106
152
|
# is not staged.
|
|
107
153
|
#
|
|
154
|
+
# $1 (optional) — the `git commit` command string. Inspected for a
|
|
155
|
+
# registered RISK_BYPASS trailer (P265). Empty/absent → no trailer
|
|
156
|
+
# bypass (fail-safe; preserves pre-P265 behaviour for any caller that
|
|
157
|
+
# does not thread the command through).
|
|
158
|
+
#
|
|
108
159
|
# Echoes the offending ticket path on stdout when detected.
|
|
109
160
|
#
|
|
110
161
|
# Returns:
|
|
111
|
-
# 0 — no change required,
|
|
162
|
+
# 0 — no change required, BYPASS env set, a registered RISK_BYPASS
|
|
163
|
+
# trailer is present, or fail-open (allow)
|
|
112
164
|
# 1 — ticket change staged + README not staged (caller should deny)
|
|
113
165
|
detect_readme_refresh_required() {
|
|
166
|
+
local command="${1:-}"
|
|
167
|
+
|
|
114
168
|
# Bypass via env var — single most-common legitimate escape.
|
|
115
169
|
if [ "${BYPASS_README_REFRESH_GATE:-}" = "1" ]; then
|
|
116
170
|
return 0
|
|
117
171
|
fi
|
|
118
172
|
|
|
173
|
+
# Bypass via registered RISK_BYPASS commit-message trailer (P265).
|
|
174
|
+
# The ADR-031 layout-migration commit is a rename-only change that
|
|
175
|
+
# legitimately stages no README refresh; its `RISK_BYPASS:
|
|
176
|
+
# adr-031-migration` trailer carries the policy authorisation
|
|
177
|
+
# (ADR-031 § Backward Compatibility + ADR-013 Rule 6).
|
|
178
|
+
if _readme_refresh_command_has_bypass_trailer "$command"; then
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
|
|
119
182
|
# Fail-open if not inside a git working tree.
|
|
120
183
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
|
121
184
|
|
|
@@ -526,3 +526,58 @@ EOF
|
|
|
526
526
|
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
527
527
|
[ "${#output}" -eq 0 ]
|
|
528
528
|
}
|
|
529
|
+
|
|
530
|
+
# --- P265: RISK_BYPASS commit-message trailer allow-list bypass ---
|
|
531
|
+
#
|
|
532
|
+
# The ADR-031 layout-migration commit (lib/migrate-problems-layout.sh)
|
|
533
|
+
# is a pure rename (flat docs/problems/NNN-*.<state>.md → per-state
|
|
534
|
+
# subdir docs/problems/<state>/NNN-*.md) that legitimately stages NO
|
|
535
|
+
# README refresh — the rename does not change README content (the table
|
|
536
|
+
# references tickets by ID, not path). Its `RISK_BYPASS: adr-031-migration`
|
|
537
|
+
# trailer (written via sequential `-m` paragraphs, so the literal token
|
|
538
|
+
# appears in the `git commit` command argv) carries the policy
|
|
539
|
+
# authorisation (ADR-031 § Backward Compatibility + ADR-013 Rule 6).
|
|
540
|
+
# The hook must allow such commits silently. The bypass is an allow-list:
|
|
541
|
+
# only the registered token bypasses; an unregistered RISK_BYPASS token
|
|
542
|
+
# still denies. The recognition grep is kept identical to the sibling
|
|
543
|
+
# risk-score-commit-gate.sh (P170 T11 precedent) so both commit gates
|
|
544
|
+
# recognise the token the same way.
|
|
545
|
+
|
|
546
|
+
@test "P265 allow: migration rename + RISK_BYPASS: adr-031-migration trailer → allow silently" {
|
|
547
|
+
echo "# Problem 999 flat" > docs/problems/999-mig.open.md
|
|
548
|
+
git add docs/problems/999-mig.open.md
|
|
549
|
+
git -c commit.gpgsign=false commit --quiet -m "seed flat ticket"
|
|
550
|
+
git mv docs/problems/999-mig.open.md docs/problems/open/999-mig.md
|
|
551
|
+
run run_bash_hook "git commit -m 'docs(problems): auto-migrate to per-state subdirectory layout (ADR-031)' -m 'RISK_BYPASS: adr-031-migration'"
|
|
552
|
+
[ "$status" -eq 0 ]
|
|
553
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
554
|
+
# Bypass is an allow path — silent per ADR-045 Pattern 1.
|
|
555
|
+
[ "${#output}" -eq 0 ]
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
@test "P265 deny: same migration rename WITHOUT the trailer still denies (negative control)" {
|
|
559
|
+
echo "# Problem 999 flat" > docs/problems/999-mig.open.md
|
|
560
|
+
git add docs/problems/999-mig.open.md
|
|
561
|
+
git -c commit.gpgsign=false commit --quiet -m "seed flat ticket"
|
|
562
|
+
git mv docs/problems/999-mig.open.md docs/problems/open/999-mig.md
|
|
563
|
+
run run_bash_hook "git commit -m 'docs(problems): auto-migrate'"
|
|
564
|
+
[ "$status" -eq 0 ]
|
|
565
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@test "P265 deny: unregistered RISK_BYPASS token does NOT bypass (allow-list scope)" {
|
|
569
|
+
echo "# Problem 999" > docs/problems/open/999-x.md
|
|
570
|
+
git add docs/problems/open/999-x.md
|
|
571
|
+
run run_bash_hook "git commit -m 'feat' -m 'RISK_BYPASS: some-other-thing'"
|
|
572
|
+
[ "$status" -eq 0 ]
|
|
573
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
@test "P265 allow: registered trailer bypasses a newly-staged ticket too (bypass is staged-shape-agnostic)" {
|
|
577
|
+
echo "# Problem 999" > docs/problems/open/999-x.md
|
|
578
|
+
git add docs/problems/open/999-x.md
|
|
579
|
+
run run_bash_hook "git commit -m 'docs(problems): auto-migrate' -m 'RISK_BYPASS: adr-031-migration'"
|
|
580
|
+
[ "$status" -eq 0 ]
|
|
581
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
582
|
+
[ "${#output}" -eq 0 ]
|
|
583
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wr-itil — per-ticket effort tally from AFK iteration cost metadata (ADR-067, P248)
|
|
3
|
+
#
|
|
4
|
+
# @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK actuals feed WSJF calibration)
|
|
5
|
+
# @jtbd JTBD-202 (Run Pre-Flight Governance Checks — structured, auditable effort tally)
|
|
6
|
+
#
|
|
7
|
+
# Attributes `.afk-run-state/iter*.json` actuals back to their source ticket
|
|
8
|
+
# (the `pNNN` token in the filename) and emits one tally line per ticket in the
|
|
9
|
+
# `## Effort Tally` schema. The reusable core shared by:
|
|
10
|
+
# - the backfill (seed historical tickets — ADR-067 Decision Outcome item 4)
|
|
11
|
+
# - go-forward per-iter append (work-problems — ADR-067 item 2)
|
|
12
|
+
#
|
|
13
|
+
# Authority hierarchy (P089 Gap 2 — load-bearing): `total_cost_usd` is the
|
|
14
|
+
# AUTHORITATIVE actual (session-cumulative by CLI contract; reliable token-spend
|
|
15
|
+
# proxy). `duration_ms` is reliable wall-clock. Raw `usage.*` token counts are
|
|
16
|
+
# BEST-EFFORT (undercount when a subprocess exits on a background-ack turn) and
|
|
17
|
+
# are emitted with a `~` best-effort marker.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# effort-tally.sh [AFK_DIR]
|
|
21
|
+
# AFK_DIR defaults to .afk-run-state
|
|
22
|
+
#
|
|
23
|
+
# Output (stdout): one line per ticket, sorted by descending cost:
|
|
24
|
+
# P<NNN> | iters=<N> | cost_usd=<authoritative> | minutes=<reliable> | tokens=~<best-effort>M
|
|
25
|
+
# Always exits 0.
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
AFK_DIR="${1:-.afk-run-state}"
|
|
30
|
+
[ -d "$AFK_DIR" ] || exit 0
|
|
31
|
+
|
|
32
|
+
python3 - "$AFK_DIR" <<'PY'
|
|
33
|
+
import json, glob, os, re, sys
|
|
34
|
+
from collections import defaultdict
|
|
35
|
+
|
|
36
|
+
afk_dir = sys.argv[1]
|
|
37
|
+
|
|
38
|
+
def cost_obj(d):
|
|
39
|
+
"""Return the dict carrying total_cost_usd, whether d is a dict or an event list."""
|
|
40
|
+
if isinstance(d, dict):
|
|
41
|
+
return d if d.get("total_cost_usd") is not None else None
|
|
42
|
+
if isinstance(d, list):
|
|
43
|
+
for item in reversed(d):
|
|
44
|
+
if isinstance(item, dict) and item.get("total_cost_usd") is not None:
|
|
45
|
+
return item
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
agg = defaultdict(lambda: {"cost": 0.0, "dur_ms": 0, "tokens": 0, "iters": 0})
|
|
49
|
+
for path in glob.glob(os.path.join(afk_dir, "*.json")):
|
|
50
|
+
m = re.search(r'p(\d{3})', os.path.basename(path))
|
|
51
|
+
if not m:
|
|
52
|
+
continue
|
|
53
|
+
tid = "P" + m.group(1)
|
|
54
|
+
try:
|
|
55
|
+
with open(path) as fh:
|
|
56
|
+
d = json.load(fh)
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
o = cost_obj(d)
|
|
60
|
+
if not o:
|
|
61
|
+
continue
|
|
62
|
+
a = agg[tid]
|
|
63
|
+
a["cost"] += float(o.get("total_cost_usd") or 0)
|
|
64
|
+
a["dur_ms"] += int(o.get("duration_ms") or 0)
|
|
65
|
+
u = o.get("usage") or {}
|
|
66
|
+
a["tokens"] += sum(int(u.get(k) or 0) for k in
|
|
67
|
+
("input_tokens", "output_tokens",
|
|
68
|
+
"cache_creation_input_tokens", "cache_read_input_tokens"))
|
|
69
|
+
a["iters"] += 1
|
|
70
|
+
|
|
71
|
+
for tid, a in sorted(agg.items(), key=lambda kv: kv[1]["cost"], reverse=True):
|
|
72
|
+
print(f"{tid} | iters={a['iters']} | cost_usd={a['cost']:.2f} | "
|
|
73
|
+
f"minutes={a['dur_ms']/60000:.1f} | tokens=~{a['tokens']/1e6:.1f}M")
|
|
74
|
+
PY
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# ADR-067 / P248: effort-tally.sh attributes .afk-run-state/iter*.json actuals
|
|
4
|
+
# back to their source ticket (pNNN filename token) and emits the per-ticket
|
|
5
|
+
# tally. Behavioural — exercises the script against fixture iter-JSON trees.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
9
|
+
SCRIPT="$REPO_ROOT/packages/itil/scripts/effort-tally.sh"
|
|
10
|
+
DIR="$(mktemp -d)"
|
|
11
|
+
mkdir -p "$DIR/.afk-run-state"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
teardown() { rm -rf "$DIR"; }
|
|
15
|
+
|
|
16
|
+
mk_iter() { # mk_iter <filename> <cost> <duration_ms> <input_tokens>
|
|
17
|
+
cat > "$DIR/.afk-run-state/$1" <<EOF
|
|
18
|
+
{"total_cost_usd": $2, "duration_ms": $3, "usage": {"input_tokens": $4, "output_tokens": 0, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@test "attributes a single iter to its ticket via the pNNN filename token" {
|
|
23
|
+
mk_iter "iter1-p087.json" 12.50 600000 1000000
|
|
24
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
25
|
+
[ "$status" -eq 0 ]
|
|
26
|
+
[[ "$output" == *"P087"* ]]
|
|
27
|
+
[[ "$output" == *"cost_usd=12.50"* ]]
|
|
28
|
+
[[ "$output" == *"minutes=10.0"* ]]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "sums multiple iters for the same ticket" {
|
|
32
|
+
mk_iter "iter1-p087.json" 10.00 300000 500000
|
|
33
|
+
mk_iter "iter2-p087.json" 5.00 300000 500000
|
|
34
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
35
|
+
[[ "$output" == *"P087 | iters=2 | cost_usd=15.00"* ]]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "authoritative cost comes from total_cost_usd; tokens flagged best-effort with ~" {
|
|
39
|
+
mk_iter "iter1-p100.json" 7.00 60000 2000000
|
|
40
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
41
|
+
[[ "$output" == *"cost_usd=7.00"* ]]
|
|
42
|
+
[[ "$output" == *"tokens=~2.0M"* ]]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "tickets are sorted by descending cost" {
|
|
46
|
+
mk_iter "iter1-p010.json" 3.00 60000 100000
|
|
47
|
+
mk_iter "iter1-p020.json" 30.00 60000 100000
|
|
48
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
49
|
+
# P020 (30.00) must appear before P010 (3.00)
|
|
50
|
+
[[ "$(echo "$output" | grep -n P020 | cut -d: -f1)" -lt "$(echo "$output" | grep -n P010 | cut -d: -f1)" ]]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "handles JSON-array (event-stream) shape, not just a single object" {
|
|
54
|
+
cat > "$DIR/.afk-run-state/iter1-p050.json" <<'EOF'
|
|
55
|
+
[{"type":"system"},{"type":"result","total_cost_usd":9.00,"duration_ms":120000,"usage":{"input_tokens":3000000,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}]
|
|
56
|
+
EOF
|
|
57
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
58
|
+
[[ "$output" == *"P050"* ]]
|
|
59
|
+
[[ "$output" == *"cost_usd=9.00"* ]]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "files without a pNNN token are ignored" {
|
|
63
|
+
mk_iter "drain-push.json" 99.00 60000 100000
|
|
64
|
+
cp "$DIR/.afk-run-state/drain-push.json" "$DIR/.afk-run-state/work-problems-session-totals.json"
|
|
65
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
66
|
+
[ -z "$output" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "files without total_cost_usd are skipped" {
|
|
70
|
+
echo '{"pid": 1234, "start": 999}' > "$DIR/.afk-run-state/iter1-p077.json"
|
|
71
|
+
run bash "$SCRIPT" "$DIR/.afk-run-state"
|
|
72
|
+
[ -z "$output" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "missing afk dir exits 0 with no output" {
|
|
76
|
+
run bash "$SCRIPT" "$DIR/nonexistent"
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
[ -z "$output" ]
|
|
79
|
+
}
|
|
@@ -317,6 +317,14 @@ IDLE_TIMEOUT_S="${WORK_PROBLEMS_IDLE_TIMEOUT_S:-3600}"
|
|
|
317
317
|
# into iter subprocesses' first turn.
|
|
318
318
|
export WR_SUPPRESS_PENDING_QUESTIONS=1
|
|
319
319
|
|
|
320
|
+
# AFK-iter oversight-nudge suppression (ADR-066): the architect plugin's
|
|
321
|
+
# SessionStart oversight nudge ("N decisions lack human oversight — run
|
|
322
|
+
# /wr-architect:review-decisions") is an interactive batch-confirm prompt. It
|
|
323
|
+
# must NOT fire into an absent-user iter subprocess. architect-oversight-nudge.sh
|
|
324
|
+
# self-suppresses when this env var is set — same discipline as the
|
|
325
|
+
# pending-questions guard above (JTBD-006 friction guard).
|
|
326
|
+
export WR_SUPPRESS_OVERSIGHT_NUDGE=1
|
|
327
|
+
|
|
320
328
|
claude -p \
|
|
321
329
|
--permission-mode bypassPermissions \
|
|
322
330
|
--output-format json \
|