@windyroad/itil 0.19.1-preview.194 → 0.19.2-preview.196
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/hooks/hooks.json +6 -0
- package/hooks/lib/create-gate.sh +79 -0
- package/hooks/manage-problem-enforce-create.sh +121 -0
- package/hooks/test/manage-problem-enforce-create.bats +188 -0
- package/package.json +1 -1
- package/skills/manage-problem/SKILL.md +9 -0
- package/skills/report-upstream/SKILL.md +140 -4
- package/skills/report-upstream/test/report-upstream-contract.bats +89 -0
package/hooks/hooks.json
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
"UserPromptSubmit": [
|
|
7
7
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-gate.sh" }] }
|
|
8
8
|
],
|
|
9
|
+
"PreToolUse": [
|
|
10
|
+
{
|
|
11
|
+
"matcher": "Write",
|
|
12
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/manage-problem-enforce-create.sh" }]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
9
15
|
"Stop": [
|
|
10
16
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-assistant-output-review.sh" }] }
|
|
11
17
|
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared gate logic for new-ticket creation enforcement (P119).
|
|
3
|
+
#
|
|
4
|
+
# Sourced by manage-problem-enforce-create.sh. Provides:
|
|
5
|
+
# check_create_gate — returns 0 if Step 2 marker present (allow), 1 if absent (deny)
|
|
6
|
+
# create_gate_deny — emits PreToolUse deny JSON
|
|
7
|
+
# create_gate_parse_err — emits parse-error fallback (exit 0, no deny)
|
|
8
|
+
#
|
|
9
|
+
# Why a separate helper from lib/review-gate.sh:
|
|
10
|
+
# review-gate.sh enforces a per-session "policy was reviewed" marker with
|
|
11
|
+
# TTL + drift detection (the policy file's hash is stored alongside the
|
|
12
|
+
# marker; mismatch invalidates the gate). The Step-2 grep marker has
|
|
13
|
+
# different semantics — it records "this session has run the duplicate
|
|
14
|
+
# check at least once" — and has neither a policy file nor drift-relevant
|
|
15
|
+
# state. Per architect approval (P119): keep review-gate.sh untouched and
|
|
16
|
+
# add a sibling helper rather than overload review-gate semantics.
|
|
17
|
+
#
|
|
18
|
+
# Marker convention: /tmp/manage-problem-grep-${SESSION_ID}
|
|
19
|
+
#
|
|
20
|
+
# Per-session scope is intentional (per architect direction A): a single
|
|
21
|
+
# /wr-itil:manage-problem invocation may write multiple tickets (Step 4b
|
|
22
|
+
# multi-concern split writes 2-N consecutive .open.md files); per-grep
|
|
23
|
+
# scope would block split-create after the first Write.
|
|
24
|
+
#
|
|
25
|
+
# References:
|
|
26
|
+
# ADR-009 — gate marker lifecycle (covers session-scoped /tmp markers).
|
|
27
|
+
# ADR-038 — progressive disclosure (deny message stays terse + actionable).
|
|
28
|
+
# ADR-013 Rule 1 — deny redirects to /wr-itil:manage-problem where
|
|
29
|
+
# Step 2 fires AskUserQuestion if duplicates exist.
|
|
30
|
+
#
|
|
31
|
+
# Empty SESSION_ID fallback: returns 1 (no marker) — fail-closed by
|
|
32
|
+
# default. Hook callers may treat empty session_id as parse failure
|
|
33
|
+
# and exit 0 without deny (parity with jtbd-enforce-edit.sh).
|
|
34
|
+
|
|
35
|
+
# Returns 0 if the Step 2 grep marker exists for SESSION_ID; 1 otherwise.
|
|
36
|
+
# Empty SESSION_ID => returns 1 (no marker).
|
|
37
|
+
#
|
|
38
|
+
# Usage: if check_create_gate "$SESSION_ID"; then exit 0; fi
|
|
39
|
+
check_create_gate() {
|
|
40
|
+
local SESSION_ID="$1"
|
|
41
|
+
[ -n "$SESSION_ID" ] || return 1
|
|
42
|
+
[ -f "/tmp/manage-problem-grep-${SESSION_ID}" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Writes the Step 2 grep marker for SESSION_ID. Empty SESSION_ID => no-op.
|
|
46
|
+
# Idempotent — safe to call more than once per session.
|
|
47
|
+
#
|
|
48
|
+
# Usage: mark_step2_complete "$SESSION_ID"
|
|
49
|
+
mark_step2_complete() {
|
|
50
|
+
local SESSION_ID="$1"
|
|
51
|
+
[ -n "$SESSION_ID" ] || return 0
|
|
52
|
+
: > "/tmp/manage-problem-grep-${SESSION_ID}"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Emit fail-closed deny JSON for PreToolUse hooks.
|
|
56
|
+
# Usage: create_gate_deny "BLOCKED: <reason>"
|
|
57
|
+
create_gate_deny() {
|
|
58
|
+
local REASON="$1"
|
|
59
|
+
cat <<EOF
|
|
60
|
+
{
|
|
61
|
+
"hookSpecificOutput": {
|
|
62
|
+
"hookEventName": "PreToolUse",
|
|
63
|
+
"permissionDecision": "deny",
|
|
64
|
+
"permissionDecisionReason": "$REASON"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
EOF
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Emit fail-closed deny JSON for parse failures.
|
|
71
|
+
# Currently unused — empty session_id and empty file_path both exit 0
|
|
72
|
+
# without deny per parity with jtbd-enforce-edit.sh. Retained for
|
|
73
|
+
# symmetry with review-gate.sh in case a future caller wants strict mode.
|
|
74
|
+
create_gate_parse_error() {
|
|
75
|
+
cat <<'EOF'
|
|
76
|
+
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny",
|
|
77
|
+
"permissionDecisionReason": "BLOCKED: Could not parse hook input. Gate is fail-closed." } }
|
|
78
|
+
EOF
|
|
79
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# P119: PreToolUse:Write enforcement hook for new problem ticket creation.
|
|
3
|
+
#
|
|
4
|
+
# BLOCKS Write to docs/problems/<NNN>-*.<status>.md when the file does not
|
|
5
|
+
# yet exist on disk and the per-session Step-2 grep marker is absent.
|
|
6
|
+
# Marker is set by /wr-itil:manage-problem Step 2 (duplicate-check) at the
|
|
7
|
+
# end of its grep pass — present marker means the agent has run the
|
|
8
|
+
# duplicate-check for this session and may write new tickets.
|
|
9
|
+
#
|
|
10
|
+
# Out of scope (allow-listed):
|
|
11
|
+
# - docs/problems/README.md — regenerated by Steps 5/6/7 (chicken-and-egg)
|
|
12
|
+
# - Existing files — Edit-flow / status transitions are
|
|
13
|
+
# governed by /wr-itil:transition-problem,
|
|
14
|
+
# not new-ticket creation
|
|
15
|
+
# - Edit tool — only Write is gated; Edit on existing
|
|
16
|
+
# tickets is normal status-transition shape
|
|
17
|
+
# - Non-ticket basenames — only docs/problems/<NNN>-*.md ticket-shaped
|
|
18
|
+
# paths are gated (NOTES.md, archive/, etc.
|
|
19
|
+
# are project housekeeping, not tickets)
|
|
20
|
+
#
|
|
21
|
+
# ADR-031 forward-compat: matcher uses docs/problems/ prefix + numeric
|
|
22
|
+
# basename, agnostic to current suffix-based layout (docs/problems/NNN-*.<status>.md)
|
|
23
|
+
# vs. future per-state subdirectory layout (docs/problems/<state>/NNN-*.md).
|
|
24
|
+
#
|
|
25
|
+
# References:
|
|
26
|
+
# ADR-009 — gate marker lifecycle (per-session /tmp markers).
|
|
27
|
+
# ADR-013 Rule 1 — deny redirects to /wr-itil:manage-problem where
|
|
28
|
+
# Step 2 fires AskUserQuestion if duplicates exist.
|
|
29
|
+
# ADR-022 — problem lifecycle (status suffixes covered: open / known-error /
|
|
30
|
+
# verifying / parked).
|
|
31
|
+
# ADR-038 — progressive disclosure (deny message stays terse + actionable).
|
|
32
|
+
# P119 — agent bypasses Step 2 by writing tickets directly.
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
35
|
+
# shellcheck source=lib/create-gate.sh
|
|
36
|
+
source "$SCRIPT_DIR/lib/create-gate.sh"
|
|
37
|
+
|
|
38
|
+
INPUT=$(cat)
|
|
39
|
+
|
|
40
|
+
TOOL_NAME=$(echo "$INPUT" | python3 -c "
|
|
41
|
+
import sys, json
|
|
42
|
+
try:
|
|
43
|
+
data = json.load(sys.stdin)
|
|
44
|
+
print(data.get('tool_name', ''))
|
|
45
|
+
except:
|
|
46
|
+
print('')
|
|
47
|
+
" 2>/dev/null || echo "")
|
|
48
|
+
|
|
49
|
+
FILE_PATH=$(echo "$INPUT" | python3 -c "
|
|
50
|
+
import sys, json
|
|
51
|
+
try:
|
|
52
|
+
data = json.load(sys.stdin)
|
|
53
|
+
print(data.get('tool_input', {}).get('file_path', ''))
|
|
54
|
+
except:
|
|
55
|
+
print('')
|
|
56
|
+
" 2>/dev/null || echo "")
|
|
57
|
+
|
|
58
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "
|
|
59
|
+
import sys, json
|
|
60
|
+
try:
|
|
61
|
+
data = json.load(sys.stdin)
|
|
62
|
+
print(data.get('session_id', ''))
|
|
63
|
+
except:
|
|
64
|
+
print('')
|
|
65
|
+
" 2>/dev/null || echo "")
|
|
66
|
+
|
|
67
|
+
# Only gate the Write tool. Edit on existing tickets is the
|
|
68
|
+
# transition-problem surface, not the new-ticket surface.
|
|
69
|
+
if [ "$TOOL_NAME" != "Write" ]; then
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Empty file_path or session_id — fail-open (mirror jtbd-enforce-edit.sh).
|
|
74
|
+
# Empty session_id with a real file_path could be a hook harness call;
|
|
75
|
+
# do not deny on parse-incomplete input.
|
|
76
|
+
if [ -z "$FILE_PATH" ]; then
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if [ -z "$SESSION_ID" ]; then
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Match docs/problems/ paths only. Both absolute (project-root prefixed)
|
|
85
|
+
# and relative shapes are accepted because Claude Code passes absolute
|
|
86
|
+
# paths in tool_input.file_path but tests and direct invocations may
|
|
87
|
+
# use relative paths.
|
|
88
|
+
case "$FILE_PATH" in
|
|
89
|
+
*docs/problems/*) ;;
|
|
90
|
+
*) exit 0 ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
94
|
+
|
|
95
|
+
# Allow-list: docs/problems/README.md is regenerated by Steps 5/6/7.
|
|
96
|
+
# Gating it would chicken-and-egg the skill itself.
|
|
97
|
+
if [ "$BASENAME" = "README.md" ]; then
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Only gate ticket-shaped basenames: NNN-... (3-digit prefix).
|
|
102
|
+
# Non-ticket files (NOTES.md, archive/index.md, etc.) are project
|
|
103
|
+
# housekeeping and don't need the duplicate-check.
|
|
104
|
+
case "$BASENAME" in
|
|
105
|
+
[0-9][0-9][0-9]-*) ;;
|
|
106
|
+
*) exit 0 ;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
# Existing file — Edit-flow / status-transition path. Only block
|
|
110
|
+
# new-file creation (the Write that materialises a brand-new ticket).
|
|
111
|
+
if [ -f "$FILE_PATH" ]; then
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
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
|
+
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)"
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P119: manage-problem-enforce-create.sh PreToolUse hook must block new-file
|
|
4
|
+
# Write to docs/problems/<NNN>-*.<status>.md until /wr-itil:manage-problem
|
|
5
|
+
# Step 2 (duplicate-check) has run for the current session.
|
|
6
|
+
#
|
|
7
|
+
# Marker: /tmp/manage-problem-grep-${SESSION_ID} (set by Step 2 grep
|
|
8
|
+
# completion). Per-session scope so a single skill invocation can write
|
|
9
|
+
# multiple tickets (Step 4b multi-concern split) without re-grep blocking.
|
|
10
|
+
#
|
|
11
|
+
# Per feedback_behavioural_tests.md (P081): behavioural assertions —
|
|
12
|
+
# simulate the hook's payload on stdin and assert on emitted JSON
|
|
13
|
+
# permissionDecision and exit status. No source-grep on hook content.
|
|
14
|
+
#
|
|
15
|
+
# ADR-031 forward-compat: matcher uses docs/problems/ prefix +
|
|
16
|
+
# numeric-prefix basename test, agnostic to current suffix-based
|
|
17
|
+
# layout vs future per-state subdirectory layout.
|
|
18
|
+
|
|
19
|
+
setup() {
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
21
|
+
HOOK="$SCRIPT_DIR/manage-problem-enforce-create.sh"
|
|
22
|
+
ORIG_DIR="$PWD"
|
|
23
|
+
TEST_DIR=$(mktemp -d)
|
|
24
|
+
cd "$TEST_DIR"
|
|
25
|
+
mkdir -p docs/problems
|
|
26
|
+
SID="mp-create-test-$$-$RANDOM"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
teardown() {
|
|
30
|
+
cd "$ORIG_DIR"
|
|
31
|
+
rm -rf "$TEST_DIR"
|
|
32
|
+
rm -f "/tmp/manage-problem-grep-${SID}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Helper: run the hook with mock JSON for a Write tool call to file_path
|
|
36
|
+
run_write_hook() {
|
|
37
|
+
local file_path="$1"
|
|
38
|
+
local sid="$2"
|
|
39
|
+
local json
|
|
40
|
+
json=$(printf '{"tool_name":"Write","tool_input":{"file_path":"%s"},"session_id":"%s"}' "$file_path" "$sid")
|
|
41
|
+
echo "$json" | bash "$HOOK"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Helper: run the hook with mock JSON for an Edit tool call to file_path
|
|
45
|
+
run_edit_hook() {
|
|
46
|
+
local file_path="$1"
|
|
47
|
+
local sid="$2"
|
|
48
|
+
local json
|
|
49
|
+
json=$(printf '{"tool_name":"Edit","tool_input":{"file_path":"%s"},"session_id":"%s"}' "$file_path" "$sid")
|
|
50
|
+
echo "$json" | bash "$HOOK"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set_marker() {
|
|
54
|
+
: > "/tmp/manage-problem-grep-${SID}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# --- Core deny path: new-file Write without marker ---
|
|
58
|
+
|
|
59
|
+
@test "deny: Write to new docs/problems/999-foo.open.md without marker" {
|
|
60
|
+
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
61
|
+
[ "$status" -eq 0 ]
|
|
62
|
+
[[ "$output" == *"\"permissionDecision\": \"deny\""* ]]
|
|
63
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
64
|
+
[[ "$output" == *"manage-problem"* ]]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "deny message names the skill the agent must invoke" {
|
|
68
|
+
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
69
|
+
[ "$status" -eq 0 ]
|
|
70
|
+
[[ "$output" == *"/wr-itil:manage-problem"* ]]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# --- Allow paths ---
|
|
74
|
+
|
|
75
|
+
@test "allow: Write to new docs/problems/999-foo.open.md WITH marker" {
|
|
76
|
+
set_marker
|
|
77
|
+
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
78
|
+
[ "$status" -eq 0 ]
|
|
79
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
80
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@test "allow: marker permits multiple consecutive Writes (multi-concern split, Step 4b)" {
|
|
84
|
+
set_marker
|
|
85
|
+
run run_write_hook "$PWD/docs/problems/998-foo.open.md" "$SID"
|
|
86
|
+
[ "$status" -eq 0 ]
|
|
87
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
88
|
+
run run_write_hook "$PWD/docs/problems/999-bar.open.md" "$SID"
|
|
89
|
+
[ "$status" -eq 0 ]
|
|
90
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@test "allow: Write to docs/problems/README.md regardless of marker" {
|
|
94
|
+
# README is regenerated by Steps 5/6/7 — chicken-and-egg per ticket Out of Scope
|
|
95
|
+
run run_write_hook "$PWD/docs/problems/README.md" "$SID"
|
|
96
|
+
[ "$status" -eq 0 ]
|
|
97
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@test "allow: Edit (not Write) to existing ticket regardless of marker" {
|
|
101
|
+
echo "stub" > docs/problems/042-existing.open.md
|
|
102
|
+
run run_edit_hook "$PWD/docs/problems/042-existing.open.md" "$SID"
|
|
103
|
+
[ "$status" -eq 0 ]
|
|
104
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@test "allow: Write to existing ticket file (overwrite) regardless of marker" {
|
|
108
|
+
# Existing file = not a new ticket creation — Edit-flow / status transitions
|
|
109
|
+
# are governed by /wr-itil:transition-problem, not this hook.
|
|
110
|
+
echo "stub" > docs/problems/042-existing.open.md
|
|
111
|
+
run run_write_hook "$PWD/docs/problems/042-existing.open.md" "$SID"
|
|
112
|
+
[ "$status" -eq 0 ]
|
|
113
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@test "allow: Write to non-docs/problems path regardless of marker" {
|
|
117
|
+
run run_write_hook "$PWD/src/component.tsx" "$SID"
|
|
118
|
+
[ "$status" -eq 0 ]
|
|
119
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "allow: Write to docs/problems/ non-numeric basename (e.g. NOTES.md)" {
|
|
123
|
+
# Only docs/problems/<NNN>-*.md ticket-shaped paths are gated. Other
|
|
124
|
+
# docs in this directory (e.g. NOTES.md, archive/index.md) are not
|
|
125
|
+
# tickets and don't need the duplicate-check.
|
|
126
|
+
run run_write_hook "$PWD/docs/problems/NOTES.md" "$SID"
|
|
127
|
+
[ "$status" -eq 0 ]
|
|
128
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# --- Status-suffix coverage (ADR-022 lifecycle) ---
|
|
132
|
+
|
|
133
|
+
@test "deny: new .known-error.md without marker" {
|
|
134
|
+
run run_write_hook "$PWD/docs/problems/999-foo.known-error.md" "$SID"
|
|
135
|
+
[ "$status" -eq 0 ]
|
|
136
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@test "deny: new .verifying.md without marker" {
|
|
140
|
+
run run_write_hook "$PWD/docs/problems/999-foo.verifying.md" "$SID"
|
|
141
|
+
[ "$status" -eq 0 ]
|
|
142
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@test "deny: new .parked.md without marker" {
|
|
146
|
+
run run_write_hook "$PWD/docs/problems/999-foo.parked.md" "$SID"
|
|
147
|
+
[ "$status" -eq 0 ]
|
|
148
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# --- Tool-name and edge cases ---
|
|
152
|
+
|
|
153
|
+
@test "allow: empty session_id (fail-open for parse failure parity with sibling hooks)" {
|
|
154
|
+
# Mirror jtbd-enforce-edit.sh: empty session_id => exit 0 without deny.
|
|
155
|
+
run bash -c "echo '{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$PWD/docs/problems/999-foo.open.md\"}}' | bash $HOOK"
|
|
156
|
+
[ "$status" -eq 0 ]
|
|
157
|
+
# No deny — empty session is the parse-error / fallback shape
|
|
158
|
+
[[ "$output" != *"\"permissionDecision\": \"deny\""* ]] || skip "fail-closed parse error is also acceptable; assert in parse-failure test"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@test "allow: empty file_path exits 0 without action" {
|
|
162
|
+
run bash -c "echo '{\"tool_name\":\"Write\",\"tool_input\":{},\"session_id\":\"$SID\"}' | bash $HOOK"
|
|
163
|
+
[ "$status" -eq 0 ]
|
|
164
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# --- ADR-031 forward-compat: per-state subdirectory shape ---
|
|
168
|
+
|
|
169
|
+
@test "deny: hypothetical ADR-031 layout docs/problems/open/999-foo.md without marker" {
|
|
170
|
+
# If/when ADR-031 lands and tickets move to docs/problems/<state>/<NNN>-...md,
|
|
171
|
+
# the hook must still gate them. Matcher should be path-prefix +
|
|
172
|
+
# numeric-prefix basename, not suffix-based.
|
|
173
|
+
mkdir -p docs/problems/open
|
|
174
|
+
run run_write_hook "$PWD/docs/problems/open/999-foo.md" "$SID"
|
|
175
|
+
[ "$status" -eq 0 ]
|
|
176
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# --- Marker hygiene ---
|
|
180
|
+
|
|
181
|
+
@test "marker file presence is the gate signal — touching marker after first deny unblocks subsequent Write" {
|
|
182
|
+
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
183
|
+
[[ "$output" == *"BLOCKED"* ]]
|
|
184
|
+
set_marker
|
|
185
|
+
run run_write_hook "$PWD/docs/problems/999-foo.open.md" "$SID"
|
|
186
|
+
[ "$status" -eq 0 ]
|
|
187
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
188
|
+
}
|
package/package.json
CHANGED
|
@@ -221,9 +221,18 @@ Before creating, search existing problems for similar issues. The user may not k
|
|
|
221
221
|
- "I found existing problems that may be related: P011 (stuck saving, CLOSED), P023 (foul drawn garbled, OPEN). Would you like to: (a) Update an existing problem, (b) Create a new problem anyway, (c) Cancel?"
|
|
222
222
|
5. If the user chooses to update, switch to the update flow for that problem ID
|
|
223
223
|
6. If no matches found, proceed to create
|
|
224
|
+
7. **After the grep completes** (whether duplicates were found or not), write the per-session create-gate marker so the `PreToolUse:Write` hook (`packages/itil/hooks/manage-problem-enforce-create.sh`, P119) allows the subsequent Write of the new `.open.md` file. The marker is `/tmp/manage-problem-grep-${SESSION_ID}` and the agent should write it via Bash:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
: > "/tmp/manage-problem-grep-${CLAUDE_SESSION_ID:-$(echo "${CLAUDE_HOOK_SESSION_ID:-default}")}"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
In practice the session ID is supplied by the hook payload, not as an env var — the simplest portable pattern is to ask Claude Code to run a one-line Bash that touches the marker using whatever session_id is available in the current invocation. The exact command shape depends on the runtime; the contract is that the file `/tmp/manage-problem-grep-<session-id>` exists by the time Step 5's Write fires. Per architect direction, the marker is per-session (single marker covers all new tickets for the rest of this session), enabling Step 4b multi-concern splits and same-session unrelated-ticket creation without re-running the grep.
|
|
224
231
|
|
|
225
232
|
**Search strategy**: Search problem filenames AND file content. A match on the filename (kebab-case title) or the Description/Symptoms sections counts. Cast a wide net — false positives are cheap (user chooses), but false negatives mean duplicate problems.
|
|
226
233
|
|
|
234
|
+
**Hook contract (P119)**: writing a `.open.md` (or any `.<status>.md`) file under `docs/problems/` without first running this Step 2 grep + marker-touch is blocked by the `manage-problem-enforce-create.sh` PreToolUse hook with a `permissionDecision: deny` directing the agent back to this skill. Agents that try to bypass the skill (e.g. mid-retrospective inline capture, post-mortem wrap-up, or any "I'll just write it directly" shortcut) will hit the deny and be redirected here. Do not work around the deny by setting the marker manually — the marker exists to record that this Step 2 ran, and a marker without a grep is the audit-trail gap P119 closes.
|
|
235
|
+
|
|
227
236
|
### 3. For new problems: Assign the next ID
|
|
228
237
|
|
|
229
238
|
Compute the next ID as the **max of the local and origin highest IDs**, plus one, zero-padded to 3 digits. Comparing against `origin/<base>` is required by ADR-019 (confirmation criterion 2): without it, parallel sessions can mint the same ID for different problems and force a destructive surgical rebase on push (P040 incident).
|
|
@@ -12,6 +12,8 @@ This skill implements the contract documented in [ADR-024](../../../docs/decisio
|
|
|
12
12
|
|
|
13
13
|
[ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) (Report-upstream classifier is problem-first) partially supersedes ADR-024 Decision Outcome **Steps 3 and 5 only** — the classifier is problem-first with best-fit backward-compat fallback (per Step 3 below), and the structured default body is problem-shaped (per Step 5 below). ADR-024 Steps 1, 2, 4, 6, 7, 8 and all Consequences / Confirmation clauses remain in force unchanged.
|
|
14
14
|
|
|
15
|
+
The **ADR-024 amendment of 2026-04-25 (P070)** adds Step 4b (dedup check — own re-run + third-party search via `gh issue list --search` + inline LLM semantic match) and Step 5c (comment path — `gh issue comment` with cross-reference body when dedup match found). The maintainer-annoyance risk evaluator named in the P070 Direction decision is deferred to compose with the `wr-risk-scorer:external-comms` subagent declared in ADR-028 (per ADR-028 line 117 — third-evaluator extension point); the AFK auto-comment branch is on the interim **static heuristic** described in Step 4b until that evaluator lands. See Step 4b below.
|
|
16
|
+
|
|
15
17
|
## Invocation
|
|
16
18
|
|
|
17
19
|
```
|
|
@@ -110,7 +112,108 @@ The local ticket is **security-classified** if any of:
|
|
|
110
112
|
- The ticket body has a `## Security classification` section.
|
|
111
113
|
- The CLI `--classification security` argument was passed.
|
|
112
114
|
|
|
113
|
-
If security-classified, route to Step 6. Otherwise, route to Step 5 (public-issue path).
|
|
115
|
+
If security-classified, route to Step 6. Otherwise, route to Step 4b (dedup check) before Step 5 (public-issue path).
|
|
116
|
+
|
|
117
|
+
### 4b. Dedup check (P070)
|
|
118
|
+
|
|
119
|
+
This step is governed by the [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) 2026-04-25 amendment, which adds dedup checking to the Decision Outcome step list (P070). Two duplication windows close at the same insertion point: own re-run (4b.1) and third-party search (4b.2). Both branches share the same AskUserQuestion surface and the same AFK halt-and-save behaviour.
|
|
120
|
+
|
|
121
|
+
> **Serves**: JTBD-004 (cross-repo coordination — dedup is the difference between coordination and spam), JTBD-001 (solo developer "without slowing down" — dedup protects the user from policing upstream duplicates manually), JTBD-006 (AFK persona — halt-and-surface protects loops from duplicate-firing), JTBD-101 (clear pattern — pattern ships without a duplication hole).
|
|
122
|
+
|
|
123
|
+
#### 4b.1. Own re-run check
|
|
124
|
+
|
|
125
|
+
Detect whether the local ticket already records a previous upstream report. The `## Reported Upstream` section is written by Step 7 (cross-reference back-write) on a successful prior invocation; its presence means the skill has already filed (or commented) for this local ticket.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
LOCAL_URL=$(grep -A5 '^## Reported Upstream' "$LOCAL_TICKET" | grep -oE 'https?://[^ )]+' | head -1)
|
|
129
|
+
if [ -n "$LOCAL_URL" ]; then
|
|
130
|
+
echo "Local ticket P${LOCAL_ID} already records an existing upstream report: $LOCAL_URL"
|
|
131
|
+
# Branch interactive vs AFK below.
|
|
132
|
+
fi
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Interactive branch** — use `AskUserQuestion` per ADR-013 Rule 1:
|
|
136
|
+
|
|
137
|
+
- `header: "Existing upstream report"`
|
|
138
|
+
- `multiSelect: false`
|
|
139
|
+
- Options:
|
|
140
|
+
1. `Halt — local ticket already records ${LOCAL_URL}` (Recommended) — abort the invocation; the existing report is current.
|
|
141
|
+
2. `Comment on the existing upstream report` — route to Step 5c with the existing URL's issue number; appropriate when new evidence has emerged since the previous report.
|
|
142
|
+
3. `File a new upstream issue anyway (override)` — explicit override after user has reviewed the existing record and judged the second filing warranted (e.g. previous report was closed without resolution and a fresh tracker is needed).
|
|
143
|
+
|
|
144
|
+
**AFK / non-interactive branch** — apply the **interim static heuristic** (no subagent dispatch; the maintainer-annoyance risk evaluator is deferred per ADR-028 line 117 — see "AFK static heuristic" below). Default action: halt and save the drafted report to the local ticket's `## Drafted Upstream Report` section; do NOT auto-comment. The static heuristic remains in place until `wr-risk-scorer:external-comms` ships, at which point the AFK branch wires the gate combination (maintainer-annoyance + leak gate, both within appetite) per the ticket Direction decision (2026-04-21).
|
|
145
|
+
|
|
146
|
+
#### 4b.2. Third-party search
|
|
147
|
+
|
|
148
|
+
Detect whether a different reporter (or another agent in a parallel session) has already filed a similar issue against the upstream. The Direction decision (2026-04-21) pins a two-stage mechanism: a `gh issue list --search` pre-filter that trims candidates to ~5-10, followed by an **inline LLM semantic match** that judges each candidate's body against the proposed report.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Stage 1: gh-search pre-filter on title keywords (cheap, ~500ms-2s).
|
|
152
|
+
KEYWORDS=$(extract_3-5_keywords_from "$LOCAL_TICKET_TITLE + $LOCAL_TICKET_DESCRIPTION")
|
|
153
|
+
MATCHES=$(gh issue list \
|
|
154
|
+
--repo "$UPSTREAM_OWNER_REPO" \
|
|
155
|
+
--state all \
|
|
156
|
+
--search "$KEYWORDS" \
|
|
157
|
+
--json number,title,state,url \
|
|
158
|
+
--limit 10)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For each candidate returned by Stage 1, fetch the full body and run **Stage 2 — inline LLM semantic judgement**:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Stage 2: per-candidate body fetch + inline classification.
|
|
165
|
+
for n in $(echo "$MATCHES" | jq -r '.[].number'); do
|
|
166
|
+
CANDIDATE=$(gh issue view "$n" --repo "$UPSTREAM_OWNER_REPO" --json title,body,state,url)
|
|
167
|
+
# Inline LLM judgement: read {local ticket Description + Symptoms, candidate title + body}
|
|
168
|
+
# and return one of: same-problem | different-problem | uncertain.
|
|
169
|
+
# No subagent dispatch — Direction decision 2026-04-21 pins inline classification
|
|
170
|
+
# for simplicity. Promotion to a `wr-itil:dedup-check` subagent is a future
|
|
171
|
+
# ADR amendment if architect review later flags context-isolation concerns.
|
|
172
|
+
done
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Notes on inline LLM classification:
|
|
176
|
+
|
|
177
|
+
- **No subagent dispatch.** The skill's main-agent context already has the local ticket loaded (Step 1) and the candidate body in scope after `gh issue view`. The Direction decision (2026-04-21) pins inline classification to keep the dedup affordable; the gh-search pre-filter trims input to ~5-10 candidates so the inline reads stay bounded.
|
|
178
|
+
- **Verdicts**: `same-problem` (route to AskUserQuestion with the matched URL); `different-problem` (skip, continue); `uncertain` (always surface to user — never auto-resolve).
|
|
179
|
+
- **Heuristic for "same problem"**: same root cause described, overlapping symptoms, same affected component or scoped npm package. Different reproduction environment alone does NOT downgrade to `different-problem` — environment heterogeneity is normal.
|
|
180
|
+
|
|
181
|
+
If Stage 2 produces one or more `same-problem` matches, surface them to the user in interactive mode:
|
|
182
|
+
|
|
183
|
+
- `header: "Existing upstream issue may match"`
|
|
184
|
+
- `multiSelect: false`
|
|
185
|
+
- Options:
|
|
186
|
+
1. `Comment on #<N> (Recommended) — <title>` — one option per `same-problem` match; routes to Step 5c with that issue number.
|
|
187
|
+
2. `File a new upstream issue anyway (override)` — explicit override; user has reviewed the matches and judged them distinct.
|
|
188
|
+
3. `Cancel` — abort without filing or commenting.
|
|
189
|
+
|
|
190
|
+
`uncertain` matches surface alongside `same-problem` matches with their verdict labelled, so the user can review. The skill never auto-resolves an `uncertain` verdict.
|
|
191
|
+
|
|
192
|
+
**AFK / non-interactive branch** — apply the same interim static heuristic as 4b.1: halt and save the drafted report to the local ticket's `## Drafted Upstream Report` section. The third-party-match auto-comment path requires the deferred `wr-risk-scorer:external-comms` gate (maintainer-annoyance + leak), so the AFK branch must NOT auto-comment under the static heuristic.
|
|
193
|
+
|
|
194
|
+
#### AFK static heuristic (interim, until `wr-risk-scorer:external-comms` ships)
|
|
195
|
+
|
|
196
|
+
The Direction decision (2026-04-21) pins the AFK auto-comment branch on **two gates passing together**: the maintainer-annoyance risk evaluator AND the P064 external-comms leak gate, both within RISK-POLICY.md's commit-layer appetite (Low, ≤4/25). Neither gate exists yet — ADR-028 declares the `wr-risk-scorer:external-comms` subagent type but P064's implementation is open at WSJF 3.0 (Effort L), and the maintainer-annoyance evaluator was deferred by architect review on P070 to compose with the same subagent rather than ship as a separate evaluator (per ADR-028 line 117 — *"Third evaluator (licence-compliance, etc.) adding to the same gate — when it emerges, amend this ADR's evaluator list and the composite marker's `evaluator_set` component; no new ADR expected."*).
|
|
197
|
+
|
|
198
|
+
**Static heuristic, valid until both gates ship**: in AFK mode, both 4b.1 and 4b.2 default to **halt and save the drafted report**. No auto-comment, no auto-file. The drafted report is appended to the local ticket's `## Drafted Upstream Report` section so the user can review and act manually on return. This matches JTBD-006's "does not trust the agent to make judgement calls" stance — the conservative default is the right interim behaviour.
|
|
199
|
+
|
|
200
|
+
**Re-wire trigger**: when `wr-risk-scorer:external-comms` lands (ADR-028 implementation, P064 closure), amend this section to invoke both evaluators and proceed with auto-comment ONLY when both verdicts return PASS within appetite. Update the AFK behaviour summary table accordingly. Until then, the static heuristic stands.
|
|
201
|
+
|
|
202
|
+
**Drafted Upstream Report save format** (used by both 4b.1 and 4b.2 AFK halts; mirrors the security-path halt pattern from Step 6 per ADR-024 Consequences lines 116, 123):
|
|
203
|
+
|
|
204
|
+
```markdown
|
|
205
|
+
## Drafted Upstream Report
|
|
206
|
+
|
|
207
|
+
- **Drafted**: <YYYY-MM-DD>
|
|
208
|
+
- **Target upstream**: <upstream-repo-url>
|
|
209
|
+
- **Halt reason**: dedup match (own re-run | third-party `same-problem`) — interim static heuristic awaiting `wr-risk-scorer:external-comms` (ADR-028 / P064)
|
|
210
|
+
- **Matched URL(s)**: <existing-issue-or-report-URL(s)>
|
|
211
|
+
- **Drafted body**:
|
|
212
|
+
|
|
213
|
+
<the body that would have been posted as a `gh issue comment` or `gh issue create`, ready for manual copy-paste review>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The halt is a loop-stopping event for AFK orchestrators — same pattern as the security-path halt-and-surface branch — so the user sees the dedup match on return rather than the orchestrator silently auto-commenting.
|
|
114
217
|
|
|
115
218
|
### 5. Public-issue path
|
|
116
219
|
|
|
@@ -268,6 +371,37 @@ gh issue create \
|
|
|
268
371
|
|
|
269
372
|
Capture the returned issue URL. The voice-tone gate per ADR-028 may delegate-and-retry; treat this as expected (see "Voice-tone gate interaction" above). Proceed to Step 7 once the issue is created.
|
|
270
373
|
|
|
374
|
+
### 5c. Comment path (P070)
|
|
375
|
+
|
|
376
|
+
Used when Step 4b's dedup check (own re-run or third-party search) finds a match AND the user picks the "comment instead" option. Skips `gh issue create` and posts a cross-reference comment on the existing upstream issue:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
gh issue comment "${EXISTING_ISSUE_NUMBER}" \
|
|
380
|
+
--repo "${UPSTREAM_OWNER_REPO}" \
|
|
381
|
+
--body "${COMMENT_BODY}"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The comment body is a condensed cross-reference, not a full report restatement. Required structure:
|
|
385
|
+
|
|
386
|
+
```markdown
|
|
387
|
+
Seeing this from <downstream-repo-url>/<local-ticket-relative-path>.
|
|
388
|
+
|
|
389
|
+
## Additional context
|
|
390
|
+
|
|
391
|
+
- **Local ticket**: P<NNN> (<one-line title>)
|
|
392
|
+
- **Reproduction**: <if local has a fresh repro path the existing issue lacks; otherwise omit>
|
|
393
|
+
- **Environment**: <if differs materially from the existing issue; otherwise omit>
|
|
394
|
+
- **Hypothesis**: <if local has a contradictory or extending root-cause hypothesis; otherwise omit>
|
|
395
|
+
|
|
396
|
+
This issue is tracked locally as P<NNN> in the downstream project's `docs/problems/` directory.
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Empty subsections are skipped — the comment should add information, not restate what the existing issue already records. If none of the four "additional context" subsections has content, the comment defaults to a one-line acknowledgement: `Seeing this from <downstream-repo-url>/<local-ticket-relative-path>. Tracked locally as P<NNN>.` This is still useful — it tells the upstream maintainer they have a downstream witness — without spamming the thread with redundant content.
|
|
400
|
+
|
|
401
|
+
The voice-tone gate per ADR-028 also fires on `gh issue comment` (per the canonical hook's regex list at ADR-028 line 61); treat the deny-plus-delegate-and-retry as expected, same as Step 5.
|
|
402
|
+
|
|
403
|
+
Capture the returned comment URL (gh prints `https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>`). The Step 7 back-write records this as the cross-reference URL with disclosure path `commented-on-existing-issue`. Proceed to Step 7.
|
|
404
|
+
|
|
271
405
|
### 6. Security path
|
|
272
406
|
|
|
273
407
|
Fetch the upstream's `SECURITY.md`:
|
|
@@ -318,7 +452,7 @@ After the upstream issue or advisory is created (or drafted-and-saved in the sec
|
|
|
318
452
|
- **URL**: <upstream-issue-or-advisory-url>
|
|
319
453
|
- **Reported**: <YYYY-MM-DD>
|
|
320
454
|
- **Template used**: <template-name-or-"structured default">
|
|
321
|
-
- **Disclosure path**: <public issue | security advisory | drafted-and-saved (mailbox / out-of-band)>
|
|
455
|
+
- **Disclosure path**: <public issue | security advisory | drafted-and-saved (mailbox / out-of-band) | commented-on-existing-issue (Step 5c, P070)>
|
|
322
456
|
- **Cross-reference confirmed**: <yes/no — true once the upstream issue body contains the local ticket reference>
|
|
323
457
|
```
|
|
324
458
|
|
|
@@ -334,19 +468,21 @@ If the cumulative pipeline risk lands above appetite and `AskUserQuestion` is un
|
|
|
334
468
|
|
|
335
469
|
## AFK behaviour summary
|
|
336
470
|
|
|
337
|
-
|
|
471
|
+
Five distinct AFK branches per the architect reviews of ADR-024, ADR-013 Rule 6, and the P070 dedup amendment:
|
|
338
472
|
|
|
339
473
|
| Branch | AFK behaviour | Authority |
|
|
340
474
|
|---|---|---|
|
|
341
475
|
| Public-issue path (Step 5) | Proceeds. Voice-tone gate per ADR-028 may delegate-and-retry; that is the expected extra turn. | ADR-028 line 126 |
|
|
476
|
+
| Dedup match — Step 4b halt (own re-run OR third-party `same-problem`) | Save drafted report to local ticket's `## Drafted Upstream Report` section. **Halt the orchestrator** — loop-stopping event. Interim static heuristic; auto-comment branch deferred until `wr-risk-scorer:external-comms` ships (ADR-028 line 117). | ADR-024 amendment 2026-04-25 (P070); Direction decision 2026-04-21 |
|
|
342
477
|
| Security path with declared channel (Step 6, GitHub Advisories) | Proceeds via `gh api .../security-advisories`. | ADR-024 Decision Outcome step 6 |
|
|
343
478
|
| Security path with `security@` / other / missing-SECURITY.md (Step 6) | Save drafted report to local ticket's `## Drafted Upstream Report` section. **Halt the orchestrator** — loop-stopping event. AFK orchestrators must never auto-report a security-classified ticket. | ADR-024 Consequences lines 116, 123 |
|
|
344
479
|
| Above-appetite commit (Step 8) | Skip the commit, report uncommitted state. | ADR-013 Rule 6 |
|
|
345
480
|
|
|
346
481
|
## References
|
|
347
482
|
|
|
348
|
-
- [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill implements. Steps 1, 2, 4, 6, 7, 8 and all Consequences remain authoritative.
|
|
483
|
+
- [ADR-024](../../../docs/decisions/024-cross-project-problem-reporting-contract.proposed.md) — primary contract this skill implements. Steps 1, 2, 4, 6, 7, 8 and all Consequences remain authoritative; the 2026-04-25 amendment adds Step 4b (dedup) + Step 5c (comment path) for P070.
|
|
349
484
|
- [ADR-033](../../../docs/decisions/033-report-upstream-classifier-problem-first.proposed.md) — partially supersedes ADR-024 Decision Outcome Steps 3 + 5; governs the problem-first classifier and problem-shaped structured default body.
|
|
485
|
+
- [P070](../../../docs/problems/) — driver ticket for the Step 4b dedup check + Step 5c comment path; carries the 2026-04-21 Direction decision (gh search + inline LLM, no subagent dispatch) and the AFK static-heuristic interim behaviour.
|
|
350
486
|
- [ADR-027](../../../docs/decisions/027-governance-skill-auto-delegation.proposed.md) — Step-0 deferral rationale (held for reassessment).
|
|
351
487
|
- [ADR-028](../../../docs/decisions/028-voice-tone-gate-external-comms.proposed.md) — voice-tone gate on `gh issue create` and `gh api .../security-advisories`.
|
|
352
488
|
- [ADR-013](../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md) — interaction policy; Rule 1 governs Step 6 missing-SECURITY.md `AskUserQuestion`; Rule 6 governs the commit-gate AFK branch.
|
|
@@ -158,3 +158,92 @@ setup() {
|
|
|
158
158
|
[ "$status" -eq 0 ]
|
|
159
159
|
[ "${#lines[@]}" -ge 2 ]
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
# ─── P070 dedup contract (Step 4b + Step 5c + AFK static heuristic) ────────────
|
|
163
|
+
#
|
|
164
|
+
# P070 inserts a dedup check between security-path routing (Step 4) and the
|
|
165
|
+
# outbound `gh` call (Steps 5 / 6). Two duplication windows close at the same
|
|
166
|
+
# insertion point: own re-run (local ticket already has `## Reported Upstream`)
|
|
167
|
+
# and third-party search (different reporter filed similar). Step 5c adds a
|
|
168
|
+
# comment-on-existing-issue path used when the dedup branch finds a match.
|
|
169
|
+
#
|
|
170
|
+
# Per architect verdict on P070: the maintainer-annoyance risk evaluator is
|
|
171
|
+
# deferred until ADR-028 / P064's `wr-risk-scorer:external-comms` subagent
|
|
172
|
+
# ships (ADR-028 line 117 anticipates third evaluators). The interim AFK
|
|
173
|
+
# branch uses a static heuristic — no subagent dispatch — that defaults to
|
|
174
|
+
# halt-and-save.
|
|
175
|
+
|
|
176
|
+
@test "report-upstream: SKILL.md contains a Step 4b dedup check (P070)" {
|
|
177
|
+
run grep -nE '^### 4b\.' "$SKILL_MD"
|
|
178
|
+
[ "$status" -eq 0 ]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@test "report-upstream: SKILL.md Step 4b.1 detects own-re-run via ## Reported Upstream (P070)" {
|
|
182
|
+
# Own-re-run branch: grep the local ticket for an existing `## Reported
|
|
183
|
+
# Upstream` URL before firing a second upstream report.
|
|
184
|
+
run grep -nE 'Reported Upstream.*(grep|already|existing|previous)' "$SKILL_MD"
|
|
185
|
+
[ "$status" -eq 0 ]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "report-upstream: SKILL.md Step 4b.2 third-party search uses gh issue list --search (P070)" {
|
|
189
|
+
# Third-party branch: `gh issue list --repo ... --search ...` against the
|
|
190
|
+
# upstream's existing issues. Required tokens.
|
|
191
|
+
run grep -F 'gh issue list' "$SKILL_MD"
|
|
192
|
+
[ "$status" -eq 0 ]
|
|
193
|
+
# `--` before the pattern stops grep from treating `--search` as a flag.
|
|
194
|
+
run grep -F -- '--search' "$SKILL_MD"
|
|
195
|
+
[ "$status" -eq 0 ]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@test "report-upstream: SKILL.md Step 4b.2 uses inline LLM judge, not subagent dispatch (P070 Direction decision 2026-04-21)" {
|
|
199
|
+
# Direction decision pins inline LLM check inside the skill's own session.
|
|
200
|
+
# No `wr-itil:dedup-check` subagent dispatch; future promotion is a separate
|
|
201
|
+
# ADR amendment.
|
|
202
|
+
run grep -iE 'inline.*llm|inline semantic|inline.*judge|inline classification' "$SKILL_MD"
|
|
203
|
+
[ "$status" -eq 0 ]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@test "report-upstream: SKILL.md contains Step 5c comment path with gh issue comment (P070)" {
|
|
207
|
+
# Step 5c: when dedup match found AND user picks "comment instead", run
|
|
208
|
+
# `gh issue comment <number>` rather than `gh issue create`.
|
|
209
|
+
run grep -nE '^### 5c\.' "$SKILL_MD"
|
|
210
|
+
[ "$status" -eq 0 ]
|
|
211
|
+
run grep -F 'gh issue comment' "$SKILL_MD"
|
|
212
|
+
[ "$status" -eq 0 ]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@test "report-upstream: SKILL.md Step 5c records commented-on-existing-issue disclosure path (P070 + ADR-024 amendment)" {
|
|
216
|
+
# ADR-024 amendment extends the disclosure-path enumeration. The literal
|
|
217
|
+
# string MUST appear in the SKILL.md so the ## Reported Upstream back-write
|
|
218
|
+
# records the new path.
|
|
219
|
+
run grep -F 'commented-on-existing-issue' "$SKILL_MD"
|
|
220
|
+
[ "$status" -eq 0 ]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@test "report-upstream: SKILL.md AFK branch uses static heuristic, defers maintainer-annoyance evaluator (P070 interim per ADR-028 line 117)" {
|
|
224
|
+
# Architect verdict: maintainer-annoyance evaluator deferred. AFK branch
|
|
225
|
+
# uses a static heuristic and defaults to halt-and-save. The SKILL.md must
|
|
226
|
+
# name the deferral explicitly so future readers know why the static-heuristic
|
|
227
|
+
# path exists; once `wr-risk-scorer:external-comms` lands, this branch
|
|
228
|
+
# gets re-wired.
|
|
229
|
+
run grep -iE 'static heuristic|interim.*heuristic|heuristic.*interim' "$SKILL_MD"
|
|
230
|
+
[ "$status" -eq 0 ]
|
|
231
|
+
run grep -iE 'wr-risk-scorer:external-comms|external-comms.*evaluator' "$SKILL_MD"
|
|
232
|
+
[ "$status" -eq 0 ]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@test "report-upstream: SKILL.md AFK halt-and-save writes Drafted Upstream Report section (P070 + ADR-024 Consequences)" {
|
|
236
|
+
# AFK halt branch saves the drafted report to the local ticket's
|
|
237
|
+
# `## Drafted Upstream Report` section — same pattern as the security-path
|
|
238
|
+
# halt per ADR-024 Consequences (lines 116, 123).
|
|
239
|
+
run grep -F '## Drafted Upstream Report' "$SKILL_MD"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test "report-upstream: SKILL.md AFK behaviour summary table includes the dedup branch (P070)" {
|
|
244
|
+
# The "AFK behaviour summary" table at the bottom of the skill must list
|
|
245
|
+
# the new dedup-halt branch alongside the existing public-issue / security
|
|
246
|
+
# / above-appetite branches.
|
|
247
|
+
run grep -iE 'dedup.*halt|step 4b.*halt|halt.*dedup|halt-and-save.*dedup' "$SKILL_MD"
|
|
248
|
+
[ "$status" -eq 0 ]
|
|
249
|
+
}
|