@windyroad/itil 0.25.0 → 0.26.0-preview.291
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 +3 -0
- package/bin/wr-itil-reconcile-rfcs +2 -0
- package/hooks/hooks.json +6 -0
- package/hooks/itil-rfc-trailer-advisory.sh +198 -0
- package/hooks/lib/create-gate.sh +30 -0
- package/hooks/manage-problem-enforce-create.sh +89 -44
- package/hooks/test/itil-rfc-trailer-advisory.bats +273 -0
- package/hooks/test/manage-problem-enforce-create.bats +105 -1
- package/package.json +1 -1
- package/scripts/reconcile-rfcs.sh +329 -0
- package/scripts/test/reconcile-rfcs.bats +433 -0
- package/scripts/test/update-problem-rfcs-section.bats +242 -0
- package/scripts/update-problem-rfcs-section.sh +160 -0
- package/skills/capture-rfc/SKILL.md +276 -0
- package/skills/manage-rfc/SKILL.md +260 -0
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ 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 |
|
|
87
87
|
| `/wr-itil:reconcile-readme` | Detect and correct drift between `docs/problems/README.md` and on-disk ticket inventory |
|
|
88
88
|
| `/wr-itil:report-upstream` | Report a local problem as a structured issue against an upstream repository (ADR-024) |
|
|
89
|
+
| `/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) |
|
|
90
|
+
| `/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) |
|
|
89
91
|
| `/wr-itil:manage-incident` | Declare, triage, mitigate, and close an incident with evidence-first discipline |
|
|
90
92
|
| `/wr-itil:list-incidents` | Read-only display of active incidents by severity |
|
|
91
93
|
| `/wr-itil:mitigate-incident` / `/wr-itil:restore-incident` / `/wr-itil:close-incident` / `/wr-itil:link-incident` | Incident lifecycle transitions (ADR-011) |
|
|
@@ -106,6 +108,7 @@ This plugin serves the [Jobs to be Done](../../docs/jtbd/) below. Per [ADR-051](
|
|
|
106
108
|
### Solo developer
|
|
107
109
|
|
|
108
110
|
- **[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.
|
|
111
|
+
- **[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; the I1 trace-to-problem invariant is gate-enforced at capture-rfc time (P170 / ADR-060).
|
|
109
112
|
|
|
110
113
|
### Plugin user (currency anchor)
|
|
111
114
|
|
package/hooks/hooks.json
CHANGED
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-changeset-discipline.sh" }]
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
|
+
"PostToolUse": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "Bash",
|
|
43
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-rfc-trailer-advisory.sh" }]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
40
46
|
"Stop": [
|
|
41
47
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-review.sh" }] }
|
|
42
48
|
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# P170: PostToolUse:Bash hook — detects `git commit` invocations whose
|
|
3
|
+
# HEAD commit message carries a `Refs: RFC-<NNN>` trailer (the
|
|
4
|
+
# commit-message RFC trailer convention introduced by ADR-060 Phase 1
|
|
5
|
+
# item 12 + finding 8). For each such trailer, the hook checks whether
|
|
6
|
+
# the driving problem ticket(s) `## RFCs` reverse-trace section lists
|
|
7
|
+
# the RFC; emits a stderr advisory when stale.
|
|
8
|
+
#
|
|
9
|
+
# This is the SECONDARY surface for the auto-maintained `## RFCs` section
|
|
10
|
+
# contract. The PRIMARY surface is skill-side inline refresh in
|
|
11
|
+
# `/wr-itil:capture-rfc` Step 6 + `/wr-itil:manage-rfc` Step 7+9 (architect
|
|
12
|
+
# Q1 verdict). The hook is the drift-detection backstop for ARBITRARY
|
|
13
|
+
# commits — `feat(...)` / `fix(...)` / `chore(...)` commits that carry
|
|
14
|
+
# `Refs: RFC-<NNN>` trailers but were authored OUTSIDE the RFC skills
|
|
15
|
+
# and therefore did not run the inline refresh.
|
|
16
|
+
#
|
|
17
|
+
# Advisory-only per architect Q2 verdict + ADR-014 single-commit grain:
|
|
18
|
+
# the hook NEVER auto-fixes (no follow-up commit; no working-tree edit
|
|
19
|
+
# after the commit lands). It emits a stderr advisory pointing the
|
|
20
|
+
# user at `/wr-itil:manage-rfc <RFC-NNN>` to refresh the reverse-trace
|
|
21
|
+
# in a subsequent commit, OR at `wr-itil-reconcile-rfcs docs/rfcs
|
|
22
|
+
# docs/problems` to verify drift in batch.
|
|
23
|
+
#
|
|
24
|
+
# Allow paths (silent-on-pass per ADR-045 Pattern 1):
|
|
25
|
+
# - tool_name != "Bash" (only Bash invocations gated)
|
|
26
|
+
# - command lacks `git commit` (non-commit Bash bypasses)
|
|
27
|
+
# - BYPASS_RFC_TRAILER_ADVISORY=1 (env-var escape)
|
|
28
|
+
# - outside git work tree
|
|
29
|
+
# - no `./docs/rfcs/` (RFC framework not adopted)
|
|
30
|
+
# - no `./docs/problems/` (problem framework not adopted)
|
|
31
|
+
# - HEAD commit lacks Refs: RFC trailer
|
|
32
|
+
# - all referenced RFCs' driving problems' `## RFCs` tables are current
|
|
33
|
+
#
|
|
34
|
+
# Multi-`Refs:` malformed-per-finding-8 advisory: when HEAD's commit
|
|
35
|
+
# message carries multiple `Refs: RFC-<NNN>` trailer lines, the hook
|
|
36
|
+
# emits a malformed-per-finding-8 advisory directing the user to split
|
|
37
|
+
# the commit (one commit advances at most one RFC per ADR-060 finding
|
|
38
|
+
# 8). The split itself is user-side work; the hook flags only.
|
|
39
|
+
#
|
|
40
|
+
# Failmode (per ADR-013 Rule 6 fail-open): on parse error in the
|
|
41
|
+
# trailer, on a Refs: trailer pointing to a non-existent RFC file
|
|
42
|
+
# (could be a capture-rfc invocation in flight), on `git interpret-trailers`
|
|
43
|
+
# failure — exit 0 silently. The reconcile-rfcs.sh batch surface
|
|
44
|
+
# catches these cases at the next manage-rfc Step 0 preflight per
|
|
45
|
+
# Confirmation criterion 3.
|
|
46
|
+
#
|
|
47
|
+
# Cost: one git invocation per `git commit` Bash call (~30-60ms). No
|
|
48
|
+
# marker (per-invocation deterministic; mirrors P125 staging-detect.sh
|
|
49
|
+
# and P141 itil-changeset-discipline.sh precedent).
|
|
50
|
+
#
|
|
51
|
+
# References:
|
|
52
|
+
# ADR-005 — plugin testing strategy (hook bats live under hooks/test/).
|
|
53
|
+
# ADR-013 Rule 6 — fail-open on missing inputs / parse errors.
|
|
54
|
+
# ADR-014 — single-commit grain (this hook never auto-fixes via
|
|
55
|
+
# follow-up commit; advisory-only).
|
|
56
|
+
# ADR-038 — progressive disclosure / advisory band ≤300 bytes.
|
|
57
|
+
# ADR-045 — hook injection budget; Pattern 1 silent-on-pass.
|
|
58
|
+
# ADR-051 — load-bearing-from-the-start (drift detection ships at
|
|
59
|
+
# the same time as the convention; never deferred).
|
|
60
|
+
# ADR-052 — behavioural-tests default; bats live alongside.
|
|
61
|
+
# ADR-060 — Phase 1 item 12 + Confirmation criterion 3.
|
|
62
|
+
# P170 — driving problem ticket.
|
|
63
|
+
# P081 — behavioural tests preferred over structural greps.
|
|
64
|
+
# P125 — sibling per-invocation no-marker hook precedent.
|
|
65
|
+
# P141 — sibling commit-time gate hook precedent.
|
|
66
|
+
|
|
67
|
+
INPUT=$(cat)
|
|
68
|
+
|
|
69
|
+
TOOL_NAME=$(echo "$INPUT" | python3 -c "
|
|
70
|
+
import sys, json
|
|
71
|
+
try:
|
|
72
|
+
data = json.load(sys.stdin)
|
|
73
|
+
print(data.get('tool_name', ''))
|
|
74
|
+
except:
|
|
75
|
+
print('')
|
|
76
|
+
" 2>/dev/null || echo "")
|
|
77
|
+
|
|
78
|
+
# Only fire on Bash.
|
|
79
|
+
if [ "$TOOL_NAME" != "Bash" ]; then
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
COMMAND=$(echo "$INPUT" | python3 -c "
|
|
84
|
+
import sys, json
|
|
85
|
+
try:
|
|
86
|
+
data = json.load(sys.stdin)
|
|
87
|
+
print(data.get('tool_input', {}).get('command', ''))
|
|
88
|
+
except:
|
|
89
|
+
print('')
|
|
90
|
+
" 2>/dev/null || echo "")
|
|
91
|
+
|
|
92
|
+
# Only fire on `git commit` invocations.
|
|
93
|
+
case "$COMMAND" in
|
|
94
|
+
*"git commit"*) ;;
|
|
95
|
+
*) exit 0 ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
# Bypass via env var.
|
|
99
|
+
if [ "${BYPASS_RFC_TRAILER_ADVISORY:-}" = "1" ]; then
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Fail-open if not in a git work tree.
|
|
104
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
105
|
+
|
|
106
|
+
# Fail-open if RFC / problem framework not adopted.
|
|
107
|
+
[ -d "./docs/rfcs" ] || exit 0
|
|
108
|
+
[ -d "./docs/problems" ] || exit 0
|
|
109
|
+
|
|
110
|
+
# Read HEAD commit message.
|
|
111
|
+
COMMIT_MSG=$(git log -1 --format='%B' HEAD 2>/dev/null) || exit 0
|
|
112
|
+
[ -n "$COMMIT_MSG" ] || exit 0
|
|
113
|
+
|
|
114
|
+
# Parse `Refs:` trailers via git's native parser. The `key=Refs` filter
|
|
115
|
+
# extracts every Refs: line; we then keep only those naming RFC-<NNN>.
|
|
116
|
+
TRAILERS=$(printf '%s\n' "$COMMIT_MSG" | git interpret-trailers --parse 2>/dev/null \
|
|
117
|
+
| grep -E '^Refs:[[:space:]]+RFC-[0-9]{3}' || true)
|
|
118
|
+
|
|
119
|
+
# No RFC trailer → silent.
|
|
120
|
+
[ -n "$TRAILERS" ] || exit 0
|
|
121
|
+
|
|
122
|
+
# Multi-Refs: malformed-per-finding-8 advisory.
|
|
123
|
+
TRAILER_COUNT=$(printf '%s\n' "$TRAILERS" | wc -l | tr -d ' ')
|
|
124
|
+
if [ "$TRAILER_COUNT" -gt 1 ]; then
|
|
125
|
+
echo "P170 ADVISORY: HEAD commit carries ${TRAILER_COUNT} Refs: RFC trailers (malformed-per-finding-8 — one commit advances at most one RFC per ADR-060). Recovery: split the commit; rerun /wr-itil:manage-rfc on each RFC. Bypass: BYPASS_RFC_TRAILER_ADVISORY=1." >&2
|
|
126
|
+
exit 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Single trailer — extract RFC ID.
|
|
130
|
+
RFC_ID=$(printf '%s' "$TRAILERS" | grep -oE 'RFC-[0-9]{3}' | head -1)
|
|
131
|
+
[ -n "$RFC_ID" ] || exit 0
|
|
132
|
+
RFC_NUM="${RFC_ID#RFC-}"
|
|
133
|
+
|
|
134
|
+
# Locate the RFC file (any status suffix).
|
|
135
|
+
shopt -s nullglob
|
|
136
|
+
RFC_FILES=(./docs/rfcs/RFC-${RFC_NUM}-*.md)
|
|
137
|
+
shopt -u nullglob
|
|
138
|
+
if [ ${#RFC_FILES[@]} -eq 0 ]; then
|
|
139
|
+
# RFC file not yet on disk (capture-rfc may be in flight). Fail-open.
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
RFC_FILE="${RFC_FILES[0]}"
|
|
143
|
+
|
|
144
|
+
# Parse RFC frontmatter `problems: [P<NNN>, P<NNN>, ...]`.
|
|
145
|
+
RAW_PROBLEMS=$(awk '/^problems:/ { print; exit }' "$RFC_FILE")
|
|
146
|
+
INNER=$(echo "$RAW_PROBLEMS" | sed -E 's/^[[:space:]]*problems:[[:space:]]*\[//; s/\][[:space:]]*$//')
|
|
147
|
+
|
|
148
|
+
# Tokenise.
|
|
149
|
+
PIDS=()
|
|
150
|
+
while IFS= read -r tok; do
|
|
151
|
+
tok=$(echo "$tok" | tr -d ' "'\''')
|
|
152
|
+
case "$tok" in
|
|
153
|
+
P[0-9][0-9][0-9]) PIDS+=("$tok") ;;
|
|
154
|
+
esac
|
|
155
|
+
done <<< "$(echo "$INNER" | tr ',' '\n')"
|
|
156
|
+
|
|
157
|
+
# No claims → fail-open (RFC has no problem trace; trailer hook can't
|
|
158
|
+
# verify anything).
|
|
159
|
+
if [ ${#PIDS[@]} -eq 0 ]; then
|
|
160
|
+
exit 0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# For each PID, check whether the problem's `## RFCs` table lists RFC_ID.
|
|
164
|
+
STALE_PIDS=""
|
|
165
|
+
for pid in "${PIDS[@]}"; do
|
|
166
|
+
pnum="${pid#P}"
|
|
167
|
+
shopt -s nullglob
|
|
168
|
+
PFILES=(./docs/problems/${pnum}-*.md)
|
|
169
|
+
shopt -u nullglob
|
|
170
|
+
if [ ${#PFILES[@]} -eq 0 ]; then
|
|
171
|
+
# Problem file missing — fail-open (could be a stale frontmatter
|
|
172
|
+
# claim or a problem in flight).
|
|
173
|
+
continue
|
|
174
|
+
fi
|
|
175
|
+
pfile="${PFILES[0]}"
|
|
176
|
+
|
|
177
|
+
# Locate `## RFCs` section.
|
|
178
|
+
sec_start=$(awk '/^## RFCs[[:space:]]*$/ { print NR; exit }' "$pfile")
|
|
179
|
+
if [ -z "$sec_start" ]; then
|
|
180
|
+
STALE_PIDS="${STALE_PIDS:+$STALE_PIDS,}$pid"
|
|
181
|
+
continue
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Check if the section lists RFC_ID.
|
|
185
|
+
if ! awk -v start="$sec_start" -v rid="$RFC_ID" '
|
|
186
|
+
NR > start && /^## / { exit }
|
|
187
|
+
NR > start && index($0, rid) > 0 { print "found"; exit }
|
|
188
|
+
' "$pfile" | grep -q found; then
|
|
189
|
+
STALE_PIDS="${STALE_PIDS:+$STALE_PIDS,}$pid"
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
# No drift → silent.
|
|
194
|
+
[ -n "$STALE_PIDS" ] || exit 0
|
|
195
|
+
|
|
196
|
+
# Emit advisory (per ADR-045 ≤300 byte band; stderr; exit 0).
|
|
197
|
+
echo "P170 ADVISORY: HEAD commit ${RFC_ID} trailer; problem(s) ${STALE_PIDS} ## RFCs section stale (skill-side refresh missed). Recovery: /wr-itil:manage-rfc ${RFC_ID} OR wr-itil-reconcile-rfcs docs/rfcs docs/problems. Bypass: BYPASS_RFC_TRAILER_ADVISORY=1." >&2
|
|
198
|
+
exit 0
|
package/hooks/lib/create-gate.sh
CHANGED
|
@@ -52,6 +52,36 @@ mark_step2_complete() {
|
|
|
52
52
|
: > "/tmp/manage-problem-grep-${SESSION_ID}"
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
# Returns 0 if the RFC capture-step marker exists for SESSION_ID; 1 otherwise.
|
|
56
|
+
# Empty SESSION_ID => returns 1 (no marker).
|
|
57
|
+
#
|
|
58
|
+
# Sibling marker per architect verdict on capture-rfc sub-decision (a) —
|
|
59
|
+
# preserves audit-trail per-surface granularity (problem-tier vs RFC-tier
|
|
60
|
+
# capture). Marker name: /tmp/wr-itil-rfc-capture-grep-${SESSION_ID}.
|
|
61
|
+
#
|
|
62
|
+
# Usage: if check_rfc_capture_gate "$SESSION_ID"; then exit 0; fi
|
|
63
|
+
check_rfc_capture_gate() {
|
|
64
|
+
local SESSION_ID="$1"
|
|
65
|
+
[ -n "$SESSION_ID" ] || return 1
|
|
66
|
+
[ -f "/tmp/wr-itil-rfc-capture-grep-${SESSION_ID}" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Writes the RFC capture-step marker for SESSION_ID. Empty SESSION_ID => no-op.
|
|
70
|
+
# Idempotent — safe to call more than once per session.
|
|
71
|
+
#
|
|
72
|
+
# Per ADR-060 + capture-rfc Step 2: the marker records that capture-rfc has
|
|
73
|
+
# run its problem-trace validation pass (the RFC-tier analogue of the
|
|
74
|
+
# manage-problem Step 2 duplicate-grep). Per-session scope so a single
|
|
75
|
+
# capture-rfc invocation may write multiple RFC files (e.g. multi-problem
|
|
76
|
+
# trace splits) without re-validating.
|
|
77
|
+
#
|
|
78
|
+
# Usage: mark_rfc_capture_complete "$SESSION_ID"
|
|
79
|
+
mark_rfc_capture_complete() {
|
|
80
|
+
local SESSION_ID="$1"
|
|
81
|
+
[ -n "$SESSION_ID" ] || return 0
|
|
82
|
+
: > "/tmp/wr-itil-rfc-capture-grep-${SESSION_ID}"
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
# Emit fail-closed deny JSON for PreToolUse hooks.
|
|
56
86
|
# Usage: create_gate_deny "BLOCKED: <reason>"
|
|
57
87
|
create_gate_deny() {
|
|
@@ -1,35 +1,53 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# P119: PreToolUse:Write enforcement hook for new problem ticket creation.
|
|
3
|
+
# P170 / ADR-060: extended to also gate new RFC ticket creation under docs/rfcs/.
|
|
3
4
|
#
|
|
4
|
-
# BLOCKS Write to
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# BLOCKS Write to:
|
|
6
|
+
# - docs/problems/<NNN>-*.<status>.md (when file does not yet exist
|
|
7
|
+
# and Step-2 grep marker absent)
|
|
8
|
+
# - docs/rfcs/RFC-<NNN>-*.<status>.md (when file does not yet exist
|
|
9
|
+
# and capture-rfc marker absent)
|
|
10
|
+
#
|
|
11
|
+
# Marker convention:
|
|
12
|
+
# - /tmp/manage-problem-grep-${SESSION_ID} (problems tier;
|
|
13
|
+
# set by manage-problem /
|
|
14
|
+
# capture-problem Step 2)
|
|
15
|
+
# - /tmp/wr-itil-rfc-capture-grep-${SESSION_ID} (RFC tier;
|
|
16
|
+
# set by capture-rfc Step 2 /
|
|
17
|
+
# manage-rfc creation flow)
|
|
9
18
|
#
|
|
10
19
|
# Out of scope (allow-listed):
|
|
11
|
-
# - docs/problems/README.md — regenerated by Steps 5/6/7
|
|
20
|
+
# - docs/problems/README.md — regenerated by manage-problem Steps 5/6/7
|
|
21
|
+
# - docs/rfcs/README.md — regenerated by manage-rfc transitions / review
|
|
12
22
|
# - Existing files — Edit-flow / status transitions are
|
|
13
|
-
# governed by /wr-itil:transition-problem
|
|
14
|
-
#
|
|
23
|
+
# governed by /wr-itil:transition-problem and
|
|
24
|
+
# /wr-itil:manage-rfc respectively
|
|
15
25
|
# - Edit tool — only Write is gated; Edit on existing
|
|
16
26
|
# tickets is normal status-transition shape
|
|
17
|
-
# - Non-ticket basenames — only
|
|
18
|
-
#
|
|
19
|
-
#
|
|
27
|
+
# - Non-ticket basenames — only ticket-shaped paths are gated:
|
|
28
|
+
# problems: NNN-*.md (3-digit prefix)
|
|
29
|
+
# rfcs: RFC-NNN-*.md
|
|
30
|
+
# Other docs (NOTES.md, archive/, etc.)
|
|
31
|
+
# are project housekeeping, not tickets
|
|
20
32
|
#
|
|
21
|
-
# ADR-031 forward-compat: matcher uses
|
|
22
|
-
#
|
|
23
|
-
#
|
|
33
|
+
# ADR-031 forward-compat: matcher uses path-prefix + ticket-shape basename,
|
|
34
|
+
# agnostic to current suffix-based layout vs. future per-state subdirectory
|
|
35
|
+
# layout. Branch deny message names the right skill (manage-problem /
|
|
36
|
+
# capture-problem for problems-tier; capture-rfc / manage-rfc for RFC-tier).
|
|
24
37
|
#
|
|
25
38
|
# References:
|
|
26
|
-
# ADR-009
|
|
27
|
-
# ADR-013 Rule 1 — deny redirects to
|
|
28
|
-
#
|
|
29
|
-
# ADR-022
|
|
30
|
-
#
|
|
31
|
-
# ADR-038
|
|
32
|
-
#
|
|
39
|
+
# ADR-009 — gate marker lifecycle (per-session /tmp markers).
|
|
40
|
+
# ADR-013 Rule 1 — deny redirects to the relevant skill (manage-problem
|
|
41
|
+
# for problems; capture-rfc for RFCs) where Step 2 fires.
|
|
42
|
+
# ADR-022 — problem lifecycle (status suffixes covered: open / known-error /
|
|
43
|
+
# verifying / parked).
|
|
44
|
+
# ADR-038 — progressive disclosure (deny message stays terse + actionable).
|
|
45
|
+
# ADR-051 — load-bearing-from-the-start (RFC I1 trace-to-problem invariant
|
|
46
|
+
# ships as a hard-block from day one, not advisory-then-escalate).
|
|
47
|
+
# ADR-060 — Problem-RFC-Story framework; introduces RFC tier + I1
|
|
48
|
+
# hard-block at capture-rfc.
|
|
49
|
+
# P119 — agent bypasses Step 2 by writing tickets directly.
|
|
50
|
+
# P170 — driver problem ticket for the RFC framework introduction.
|
|
33
51
|
|
|
34
52
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
35
53
|
# shellcheck source=lib/create-gate.sh
|
|
@@ -81,29 +99,44 @@ if [ -z "$SESSION_ID" ]; then
|
|
|
81
99
|
exit 0
|
|
82
100
|
fi
|
|
83
101
|
|
|
84
|
-
# Match docs/problems/ paths only. Both absolute
|
|
85
|
-
# and relative shapes are accepted because
|
|
86
|
-
# paths in tool_input.file_path but tests
|
|
87
|
-
# use relative paths.
|
|
102
|
+
# Match docs/problems/ OR docs/rfcs/ paths only. Both absolute
|
|
103
|
+
# (project-root prefixed) and relative shapes are accepted because
|
|
104
|
+
# Claude Code passes absolute paths in tool_input.file_path but tests
|
|
105
|
+
# and direct invocations may use relative paths.
|
|
106
|
+
TIER=""
|
|
88
107
|
case "$FILE_PATH" in
|
|
89
|
-
*docs/problems/*) ;;
|
|
108
|
+
*docs/problems/*) TIER="problems" ;;
|
|
109
|
+
*docs/rfcs/*) TIER="rfcs" ;;
|
|
90
110
|
*) exit 0 ;;
|
|
91
111
|
esac
|
|
92
112
|
|
|
93
113
|
BASENAME=$(basename "$FILE_PATH")
|
|
94
114
|
|
|
95
|
-
# Allow-list:
|
|
96
|
-
#
|
|
115
|
+
# Allow-list: README.md regenerated by skill flows (chicken-and-egg).
|
|
116
|
+
# Applies to both tiers: docs/problems/README.md (manage-problem Steps 5/6/7)
|
|
117
|
+
# and docs/rfcs/README.md (manage-rfc transitions / review).
|
|
97
118
|
if [ "$BASENAME" = "README.md" ]; then
|
|
98
119
|
exit 0
|
|
99
120
|
fi
|
|
100
121
|
|
|
101
|
-
# Only gate ticket-shaped basenames
|
|
122
|
+
# Only gate ticket-shaped basenames per tier:
|
|
123
|
+
# problems: NNN-... (3-digit prefix)
|
|
124
|
+
# rfcs: RFC-NNN-...
|
|
102
125
|
# Non-ticket files (NOTES.md, archive/index.md, etc.) are project
|
|
103
|
-
# housekeeping and don't need the
|
|
104
|
-
case "$
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
# housekeeping and don't need the gate.
|
|
127
|
+
case "$TIER" in
|
|
128
|
+
problems)
|
|
129
|
+
case "$BASENAME" in
|
|
130
|
+
[0-9][0-9][0-9]-*) ;;
|
|
131
|
+
*) exit 0 ;;
|
|
132
|
+
esac
|
|
133
|
+
;;
|
|
134
|
+
rfcs)
|
|
135
|
+
case "$BASENAME" in
|
|
136
|
+
RFC-[0-9][0-9][0-9]-*) ;;
|
|
137
|
+
*) exit 0 ;;
|
|
138
|
+
esac
|
|
139
|
+
;;
|
|
107
140
|
esac
|
|
108
141
|
|
|
109
142
|
# Existing file — Edit-flow / status-transition path. Only block
|
|
@@ -112,19 +145,31 @@ if [ -f "$FILE_PATH" ]; then
|
|
|
112
145
|
exit 0
|
|
113
146
|
fi
|
|
114
147
|
|
|
115
|
-
# New ticket Write: gate on the Step-2 grep marker.
|
|
116
|
-
if check_create_gate "$SESSION_ID"; then
|
|
117
|
-
exit 0
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
148
|
# P142 / ADR-050: the runtime-SID instrumentation hook
|
|
121
149
|
# (itil-runtime-sid-marker.sh) writes the runtime stdin session_id to a
|
|
122
150
|
# per-machine marker on every PreToolUse:Bash|Write|Edit|Read event. The
|
|
123
151
|
# `get_current_session_id` helper reads that marker as the authoritative
|
|
124
|
-
# SID, so the marker `mark_step2_complete`
|
|
125
|
-
# session_id this hook will see on the
|
|
126
|
-
# denial is structurally eliminated; the
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
152
|
+
# SID, so the marker `mark_step2_complete` / `mark_rfc_capture_complete`
|
|
153
|
+
# writes is bound to the same session_id this hook will see on the
|
|
154
|
+
# subsequent Write. SID-mismatch denial is structurally eliminated; the
|
|
155
|
+
# only remaining deny path is the routine "duplicate-check / capture
|
|
156
|
+
# step has not run yet for this session" case.
|
|
157
|
+
|
|
158
|
+
# New ticket Write: gate on the tier-specific marker.
|
|
159
|
+
case "$TIER" in
|
|
160
|
+
problems)
|
|
161
|
+
if check_create_gate "$SESSION_ID"; then
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|
|
164
|
+
create_gate_deny "BLOCKED: Cannot Write '${BASENAME}' under docs/problems/ without running /wr-itil:manage-problem Step 2 (duplicate-check) first. New problem tickets MUST be created via the skill so the duplicate-prevention grep fires before the file lands. Invoke the Skill tool with skill='wr-itil:manage-problem' and a description of the new problem; Step 2 will grep for related existing tickets and surface any matches via AskUserQuestion before creating the new ticket. (P119)"
|
|
165
|
+
exit 0
|
|
166
|
+
;;
|
|
167
|
+
rfcs)
|
|
168
|
+
if check_rfc_capture_gate "$SESSION_ID"; then
|
|
169
|
+
exit 0
|
|
170
|
+
fi
|
|
171
|
+
create_gate_deny "BLOCKED: Cannot Write '${BASENAME}' under docs/rfcs/ without running /wr-itil:capture-rfc Step 2 (problem-trace validation) first. New RFC tickets MUST be created via the skill so the I1 trace-to-problem invariant is enforced before the file lands. Invoke the Skill tool with skill='wr-itil:capture-rfc' supplying the leading problem-trace argument (P<NNN> or P<NNN>,P<NNN>,...); Step 2 will validate that each traced problem exists. Without a problem-trace, capture-rfc emits a deny log entry to logs/rfc-capture-denials.jsonl per ADR-060 § Confirmation criterion 1 + § Reassessment trace-violation-rate criterion. (P170 / ADR-060)"
|
|
172
|
+
exit 0
|
|
173
|
+
;;
|
|
174
|
+
esac
|
|
130
175
|
exit 0
|