@windyroad/itil 0.56.3 → 0.57.0-preview.923

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.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "0.56.3"
500
+ "version": "0.57.0"
501
501
  }
package/hooks/hooks.json CHANGED
@@ -59,6 +59,10 @@
59
59
  "matcher": "Bash",
60
60
  "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-commit-trailer-transition-advisory.sh" }]
61
61
  },
62
+ {
63
+ "matcher": "Bash",
64
+ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-fix-title-lifecycle-advisory.sh" }]
65
+ },
62
66
  {
63
67
  "matcher": "Write|Edit|MultiEdit",
64
68
  "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-fictional-defer-detect.sh" }]
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # wr-itil — PostToolUse:Bash hook (P345/RFC-044/STORY-038). Detects the
3
+ # fix-without-paired-lifecycle-transition drift: a just-landed commit whose
4
+ # subject is fix-typed (`fix: ...` / `fix(<pkg>): ...`) and names P<NNN>
5
+ # ticket(s) that are still in `docs/problems/open/` — i.e. the commit claims
6
+ # to fix a ticket but carried no Open -> Known Error (or -> Verifying)
7
+ # transition in the same grain. The ticket then ages across release + CI
8
+ # verification until a catch-up cadence notices (P345; P334 witness chain).
9
+ #
10
+ # ADVISORY-ONLY per ADR-092: Open -> Known Error asserts a knowledge claim
11
+ # (root cause known + workaround documented) that a fix-titled commit does
12
+ # NOT establish, so this hook must never auto-fire the transition and never
13
+ # block the commit. The hook DETECTS and ADVISES; a skill
14
+ # (/wr-itil:transition-problem) — or the human — PERFORMS the transition
15
+ # (ADR-014 hook-detects-skill-commits grain). Sibling (copy-and-retarget) of
16
+ # itil-commit-trailer-transition-advisory.sh (P378): different signal —
17
+ # commit-title P<NNN> tokens vs Refs: trailers.
18
+ #
19
+ # Advisory-only (ADR-013 Rule 6 fail-open; ADR-045 ≤300-byte TOTAL emission
20
+ # even with multiple named tickets; exit 0 on every path).
21
+ # Bypass: BYPASS_FIX_TITLE_LIFECYCLE_ADVISORY=1.
22
+ #
23
+ # @adr ADR-092 (advisory-only posture + binding rule) ADR-014 (hook detects,
24
+ # skill commits) ADR-045 (budget) ADR-013 (Rule 6 fail-open) ADR-052 (bats)
25
+ # @problem P345 @rfc RFC-044 @story STORY-038
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
28
+ # shellcheck source=lib/command-detect.sh
29
+ source "$SCRIPT_DIR/lib/command-detect.sh"
30
+
31
+ INPUT=$(cat)
32
+ TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c "import sys,json
33
+ try: print(json.load(sys.stdin).get('tool_name',''))
34
+ except: print('')" 2>/dev/null || echo "")
35
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
36
+
37
+ COMMAND=$(printf '%s' "$INPUT" | python3 -c "import sys,json
38
+ try: print(json.load(sys.stdin).get('tool_input',{}).get('command',''))
39
+ except: print('')" 2>/dev/null || echo "")
40
+ command_invokes_git_commit "$COMMAND" || exit 0
41
+ [ "${BYPASS_FIX_TITLE_LIFECYCLE_ADVISORY:-}" = "1" ] && exit 0
42
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
43
+ [ -d "./docs/problems/open" ] || exit 0
44
+
45
+ SUBJECT=$(git log -1 --format='%s' HEAD 2>/dev/null) || exit 0
46
+ case "$SUBJECT" in
47
+ "fix: "*|"fix("*) ;;
48
+ *) exit 0 ;;
49
+ esac
50
+
51
+ PIDS=$(printf '%s' "$SUBJECT" | grep -oE 'P[0-9]{3}' | sort -u || true)
52
+ [ -n "$PIDS" ] || exit 0
53
+
54
+ OPEN_IDS=""
55
+ OPEN_COUNT=0
56
+ while IFS= read -r pid; do
57
+ [ -n "$pid" ] || continue
58
+ num="${pid#P}"
59
+ shopt -s nullglob; files=(./docs/problems/open/${num}-*.md); shopt -u nullglob
60
+ if [ ${#files[@]} -gt 0 ]; then
61
+ OPEN_COUNT=$((OPEN_COUNT + 1))
62
+ # Cap the named IDs at 3 so TOTAL emission stays ≤300 bytes (ADR-045).
63
+ if [ "$OPEN_COUNT" -le 3 ]; then
64
+ OPEN_IDS="${OPEN_IDS:+$OPEN_IDS,}$pid"
65
+ fi
66
+ fi
67
+ done <<< "$PIDS"
68
+ [ "$OPEN_COUNT" -gt 0 ] || exit 0
69
+ [ "$OPEN_COUNT" -gt 3 ] && OPEN_IDS="${OPEN_IDS}+$((OPEN_COUNT - 3))"
70
+
71
+ echo "P345 ADVISORY: fix-titled commit names ${OPEN_IDS} still Open — no paired lifecycle transition landed. Transition it (/wr-itil:transition-problem <NNN> known-error) or document why not. Bypass: BYPASS_FIX_TITLE_LIFECYCLE_ADVISORY=1." >&2
72
+ exit 0
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P345/RFC-044/STORY-038: itil-fix-title-lifecycle-advisory.sh — post-commit
4
+ # ADVISORY for fix-titled commits whose named P<NNN> ticket is still Open on
5
+ # disk (no paired lifecycle transition in the commit). Advisory-only per
6
+ # ADR-092: never blocks, exit 0 on every path. Behavioural (ADR-052).
7
+
8
+ setup() {
9
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
10
+ HOOK="$REPO_ROOT/packages/itil/hooks/itil-fix-title-lifecycle-advisory.sh"
11
+ DIR="$(mktemp -d)"; cd "$DIR"
12
+ git init -q; git config user.email t@e.x; git config user.name t
13
+ mkdir -p docs/problems/open docs/problems/known-error
14
+ echo x > seed; git add -A; git commit -qm "chore: seed"
15
+ }
16
+ teardown() { cd /; rm -rf "$DIR"; }
17
+
18
+ # Feed the hook a PostToolUse Bash payload for a `git commit` command.
19
+ run_hook() {
20
+ printf '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' | bash "$HOOK"
21
+ }
22
+
23
+ commit() { git commit -q --allow-empty -m "$1"; }
24
+
25
+ @test "advises when a fix-titled commit names a still-Open ticket" {
26
+ echo t > docs/problems/open/501-widget-breaks.md
27
+ commit "fix(itil): P501 stop the widget breaking"
28
+ run run_hook
29
+ [ "$status" -eq 0 ]
30
+ [[ "$output" == *"P501"* ]]
31
+ [[ "$output" == *"Open"* ]]
32
+ [[ "$output" == *"transition"* ]]
33
+ }
34
+
35
+ @test "silent when the named ticket is known-error (transition already happened)" {
36
+ echo t > docs/problems/known-error/501-widget-breaks.md
37
+ commit "fix(itil): P501 stop the widget breaking"
38
+ run run_hook
39
+ [ "$status" -eq 0 ]
40
+ [ -z "$output" ]
41
+ }
42
+
43
+ @test "silent when the named ticket does not exist on disk" {
44
+ commit "fix(itil): P999 phantom ticket"
45
+ run run_hook
46
+ [ "$status" -eq 0 ]
47
+ [ -z "$output" ]
48
+ }
49
+
50
+ @test "silent on a non-fix-typed subject naming a P token" {
51
+ echo t > docs/problems/open/501-widget-breaks.md
52
+ commit "docs(problems): P501 add investigation notes"
53
+ run run_hook
54
+ [ "$status" -eq 0 ]
55
+ [ -z "$output" ]
56
+ }
57
+
58
+ @test "silent on a fix-typed subject with no P token" {
59
+ echo t > docs/problems/open/501-widget-breaks.md
60
+ commit "fix(itil): tidy the widget"
61
+ run run_hook
62
+ [ "$status" -eq 0 ]
63
+ [ -z "$output" ]
64
+ }
65
+
66
+ @test "advises on the bare fix: form too" {
67
+ echo t > docs/problems/open/502-gadget-stuck.md
68
+ commit "fix: P502 unstick the gadget"
69
+ run run_hook
70
+ [ "$status" -eq 0 ]
71
+ [[ "$output" == *"P502"* ]]
72
+ }
73
+
74
+ @test "total emission stays at or under 300 bytes with many still-Open tickets" {
75
+ for n in 511 512 513 514 515 516; do
76
+ echo t > "docs/problems/open/${n}-thing-${n}.md"
77
+ done
78
+ commit "fix(itil): P511 P512 P513 P514 P515 P516 mega sweep"
79
+ run run_hook
80
+ [ "$status" -eq 0 ]
81
+ [ -n "$output" ]
82
+ [ "${#output}" -le 300 ]
83
+ }
84
+
85
+ @test "bypass env var suppresses the advisory" {
86
+ echo t > docs/problems/open/501-widget-breaks.md
87
+ commit "fix(itil): P501 stop the widget breaking"
88
+ run env BYPASS_FIX_TITLE_LIFECYCLE_ADVISORY=1 bash -c 'printf "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m x\"}}" | bash "$0"' "$HOOK"
89
+ [ "$status" -eq 0 ]
90
+ [ -z "$output" ]
91
+ }
92
+
93
+ @test "silent on non-Bash tool payloads" {
94
+ echo t > docs/problems/open/501-widget-breaks.md
95
+ commit "fix(itil): P501 stop the widget breaking"
96
+ run bash -c 'printf "{\"tool_name\":\"Write\",\"tool_input\":{}}" | bash "$0"' "$HOOK"
97
+ [ "$status" -eq 0 ]
98
+ [ -z "$output" ]
99
+ }
100
+
101
+ @test "silent on non-commit Bash commands" {
102
+ echo t > docs/problems/open/501-widget-breaks.md
103
+ commit "fix(itil): P501 stop the widget breaking"
104
+ run bash -c 'printf "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git log --oneline\"}}" | bash "$0"' "$HOOK"
105
+ [ "$status" -eq 0 ]
106
+ [ -z "$output" ]
107
+ }
108
+
109
+ @test "fail-open silent when docs/problems is absent" {
110
+ rm -rf docs/problems
111
+ commit "fix(itil): P501 stop the widget breaking"
112
+ run run_hook
113
+ [ "$status" -eq 0 ]
114
+ [ -z "$output" ]
115
+ }
116
+
117
+ @test "fail-open silent outside a git work tree" {
118
+ NOGIT="$(mktemp -d)"; cd "$NOGIT"
119
+ run run_hook
120
+ [ "$status" -eq 0 ]
121
+ [ -z "$output" ]
122
+ cd /; rm -rf "$NOGIT"
123
+ }
124
+
125
+ @test "fail-open silent on malformed JSON input" {
126
+ run bash -c 'printf "not json at all" | bash "$0"' "$HOOK"
127
+ [ "$status" -eq 0 ]
128
+ [ -z "$output" ]
129
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.56.3",
3
+ "version": "0.57.0-preview.923",
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"