@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.
@@ -484,5 +484,5 @@
484
484
  }
485
485
  },
486
486
  "name": "wr-itil",
487
- "version": "0.35.7"
487
+ "version": "0.35.8"
488
488
  }
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 and [JTBD-201](../../docs/jtbd/tech-lead/JTBD-201-restore-service-fast.proposed.md) for the job this serves.
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; serves [JTBD-004](../../docs/jtbd/solo-developer/JTBD-004-connect-agents.proposed.md)) | 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) | 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ # ADR-049 $PATH shim — dispatches to the canonical effort-tally script.
3
+ exec "$(dirname "$0")/../scripts/effort-tally.sh" "$@"
@@ -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, or fail-open (non-git tree, parse error).
96
- TRAPPED_TICKET=$(detect_readme_refresh_required 2>/dev/null) && exit 0
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, or BYPASS env set, or fail-open (allow)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.35.7",
3
+ "version": "0.35.8-preview.393",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -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 \