@windyroad/itil 0.51.2 → 0.52.0-preview.763
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/bin/wr-itil-update-rfc-commits-section +51 -0
- package/hooks/hooks.json +8 -0
- package/hooks/itil-commit-trailer-transition-advisory.sh +79 -0
- package/hooks/itil-rfc-oversight-nudge.sh +46 -0
- package/hooks/test/itil-commit-trailer-transition-advisory.bats +70 -0
- package/hooks/test/itil-rfc-oversight-nudge.bats +78 -0
- package/lib/install-utils.mjs +99 -23
- package/package.json +1 -1
- package/scripts/detect-unoversighted-rfcs.sh +46 -0
- package/scripts/test/update-rfc-commits-section.bats +77 -0
- package/scripts/update-rfc-commits-section.sh +69 -0
- package/skills/capture-problem/SKILL.md +21 -12
- package/skills/capture-problem/test/capture-problem.bats +27 -34
- package/skills/capture-rfc/SKILL.md +1 -1
- package/skills/capture-story/SKILL.md +2 -2
- package/skills/capture-story/test/capture-story-behavioural.bats +1 -1
- package/skills/manage-incident/SKILL.md +1 -1
- package/skills/manage-problem/SKILL.md +1 -1
- package/skills/manage-rfc/SKILL.md +15 -0
- package/skills/manage-story/SKILL.md +1 -1
- package/skills/mitigate-incident/SKILL.md +1 -1
- package/skills/restore-incident/SKILL.md +1 -1
- package/skills/transition-problem/SKILL.md +1 -1
- package/skills/transition-problems/SKILL.md +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generated by scripts/sync-shim-wrappers.sh from
|
|
3
|
+
# packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
|
|
4
|
+
# shim files in packages/*/bin/wr-* directly; edit the template + run
|
|
5
|
+
# `npm run sync:shim-wrappers` to regenerate.
|
|
6
|
+
#
|
|
7
|
+
# Resolution (ADR-080):
|
|
8
|
+
# 1. If the wrapper's parent dir is semver-shaped, treat as installed-
|
|
9
|
+
# cache execution and resolve to the highest-version sibling's
|
|
10
|
+
# scripts/ entry below.
|
|
11
|
+
# 2. Otherwise (parent dir is e.g. `architect`), treat as source-
|
|
12
|
+
# monorepo execution and dispatch to own scripts/. The source-repo-
|
|
13
|
+
# guard `exec` is the anchor parsed by
|
|
14
|
+
# packages/retrospective/scripts/check-tarball-shipped-shims.sh.
|
|
15
|
+
# 3. If the cache parent contains zero semver-shaped siblings, exit
|
|
16
|
+
# 127 with a stderr message naming the cache parent (per SQ-080-2).
|
|
17
|
+
#
|
|
18
|
+
# @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
|
|
19
|
+
# @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
|
|
20
|
+
# @problem P343 (mid-session staleness window)
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
|
|
26
|
+
OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
|
|
27
|
+
CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
|
|
28
|
+
|
|
29
|
+
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
|
|
30
|
+
|
|
31
|
+
# Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
|
|
32
|
+
if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
|
|
33
|
+
exec "$SHIM_DIR/../scripts/update-rfc-commits-section.sh" "$@"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Cache execution: pick the highest-semver sibling under CACHE_PARENT.
|
|
37
|
+
HIGHEST=""
|
|
38
|
+
while IFS= read -r dir; do
|
|
39
|
+
name="$(basename "$dir")"
|
|
40
|
+
[[ "$name" =~ $SEMVER_RE ]] || continue
|
|
41
|
+
if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
|
|
42
|
+
HIGHEST="$name"
|
|
43
|
+
fi
|
|
44
|
+
done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
45
|
+
|
|
46
|
+
if [[ -z "$HIGHEST" ]]; then
|
|
47
|
+
printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
|
|
48
|
+
exit 127
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exec "$CACHE_PARENT/$HIGHEST/scripts/update-rfc-commits-section.sh" "$@"
|
package/hooks/hooks.json
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
{
|
|
6
6
|
"matcher": "startup",
|
|
7
7
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-pending-questions-surface.sh" }]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"matcher": "startup",
|
|
11
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-rfc-oversight-nudge.sh" }]
|
|
8
12
|
}
|
|
9
13
|
],
|
|
10
14
|
"UserPromptSubmit": [
|
|
@@ -50,6 +54,10 @@
|
|
|
50
54
|
"matcher": "Bash",
|
|
51
55
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-rfc-trailer-advisory.sh" }]
|
|
52
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"matcher": "Bash",
|
|
59
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-commit-trailer-transition-advisory.sh" }]
|
|
60
|
+
},
|
|
53
61
|
{
|
|
54
62
|
"matcher": "Write|Edit|MultiEdit",
|
|
55
63
|
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/itil-fictional-defer-detect.sh" }]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# wr-itil — PostToolUse:Bash hook (P378/RFC-030 Piece 2). The SHARED
|
|
3
|
+
# commit-trailer auto-transition trigger that ADR-060 (line 292/307) promised
|
|
4
|
+
# for BOTH the RFC and story tiers and that manage-rfc + manage-story both
|
|
5
|
+
# deferred to "a future commit-trailer-trigger hook" — never built. This is it.
|
|
6
|
+
#
|
|
7
|
+
# Per ADR-014, a hook MUST NOT perform the transition itself (git mv + Status
|
|
8
|
+
# edit + commit lands OUTSIDE the triggering commit's grain). So the hook
|
|
9
|
+
# DETECTS eligibility and ADVISES; the skill (/wr-itil:manage-rfc |
|
|
10
|
+
# /wr-itil:manage-story) — or an AFK orchestrator acting on the advisory —
|
|
11
|
+
# performs the transition. The detection is now self-firing; the false
|
|
12
|
+
# "auto-transitions on first non-capture commit" claims in both skills are
|
|
13
|
+
# corrected to describe this detect-then-perform shape.
|
|
14
|
+
#
|
|
15
|
+
# Trigger: a `git commit` whose HEAD message
|
|
16
|
+
# - carries a `Refs: RFC-<NNN>` or `Refs: STORY-<NNN>` trailer, AND
|
|
17
|
+
# - is NOT the artefact's capture commit (subject does not start with
|
|
18
|
+
# `docs(rfcs): capture RFC-` / `feat(itil): capture STORY-`), AND
|
|
19
|
+
# - the referenced artefact is in a pre-in-progress status
|
|
20
|
+
# (RFC: .proposed/.accepted ; story: .draft) on disk.
|
|
21
|
+
# → emit a stderr advisory naming the transition command. Silent otherwise.
|
|
22
|
+
#
|
|
23
|
+
# Advisory-only (ADR-013 Rule 6 fail-open; ADR-045 ≤300-byte band; exit 0).
|
|
24
|
+
# Bypass: BYPASS_TRANSITION_ADVISORY=1.
|
|
25
|
+
#
|
|
26
|
+
# @adr ADR-060 (line 292/307 auto-transition triggers) ADR-014 (hook detects,
|
|
27
|
+
# skill commits) ADR-045 (budget) ADR-013 (Rule 6 fail-open) ADR-052 (bats)
|
|
28
|
+
# @problem P378 @rfc RFC-030
|
|
29
|
+
|
|
30
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
31
|
+
# shellcheck source=lib/command-detect.sh
|
|
32
|
+
source "$SCRIPT_DIR/lib/command-detect.sh"
|
|
33
|
+
|
|
34
|
+
INPUT=$(cat)
|
|
35
|
+
TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c "import sys,json
|
|
36
|
+
try: print(json.load(sys.stdin).get('tool_name',''))
|
|
37
|
+
except: print('')" 2>/dev/null || echo "")
|
|
38
|
+
[ "$TOOL_NAME" = "Bash" ] || exit 0
|
|
39
|
+
|
|
40
|
+
COMMAND=$(printf '%s' "$INPUT" | python3 -c "import sys,json
|
|
41
|
+
try: print(json.load(sys.stdin).get('tool_input',{}).get('command',''))
|
|
42
|
+
except: print('')" 2>/dev/null || echo "")
|
|
43
|
+
command_invokes_git_commit "$COMMAND" || exit 0
|
|
44
|
+
[ "${BYPASS_TRANSITION_ADVISORY:-}" = "1" ] && exit 0
|
|
45
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
46
|
+
|
|
47
|
+
SUBJECT=$(git log -1 --format='%s' HEAD 2>/dev/null) || exit 0
|
|
48
|
+
BODY=$(git log -1 --format='%B' HEAD 2>/dev/null) || exit 0
|
|
49
|
+
|
|
50
|
+
# Skip capture commits — they CREATE the artefact, not advance it.
|
|
51
|
+
case "$SUBJECT" in
|
|
52
|
+
*"capture RFC-"*|*"capture STORY-"*) exit 0 ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
TRAILERS=$(printf '%s\n' "$BODY" | git interpret-trailers --parse 2>/dev/null \
|
|
56
|
+
| grep -oE 'RFC-[0-9]{3}|STORY-[0-9]{3}' | sort -u || true)
|
|
57
|
+
[ -n "$TRAILERS" ] || exit 0
|
|
58
|
+
|
|
59
|
+
advise() { echo "P378 ADVISORY: $1" >&2; }
|
|
60
|
+
|
|
61
|
+
while IFS= read -r id; do
|
|
62
|
+
[ -n "$id" ] || continue
|
|
63
|
+
case "$id" in
|
|
64
|
+
RFC-*)
|
|
65
|
+
[ -d "./docs/rfcs" ] || continue
|
|
66
|
+
shopt -s nullglob; files=(./docs/rfcs/${id}-*.proposed.md ./docs/rfcs/${id}-*.accepted.md); shopt -u nullglob
|
|
67
|
+
[ ${#files[@]} -gt 0 ] || continue
|
|
68
|
+
advise "${id} carries a non-capture commit but is still $(basename "${files[0]}" | grep -oE 'proposed|accepted'). It should advance to in-progress — run /wr-itil:manage-rfc ${id} in-progress (the skill performs the transition; a hook cannot, per ADR-014). Bypass: BYPASS_TRANSITION_ADVISORY=1."
|
|
69
|
+
;;
|
|
70
|
+
STORY-*)
|
|
71
|
+
[ -d "./docs/stories" ] || continue
|
|
72
|
+
shopt -s nullglob; files=(./docs/stories/draft/${id}-*.md ./docs/stories/${id}-*.draft.md); shopt -u nullglob
|
|
73
|
+
[ ${#files[@]} -gt 0 ] || continue
|
|
74
|
+
advise "${id} carries a non-capture commit but is still draft. It should advance to in-progress — run /wr-itil:manage-story ${id} in-progress. Bypass: BYPASS_TRANSITION_ADVISORY=1."
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done <<< "$TRAILERS"
|
|
78
|
+
|
|
79
|
+
exit 0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wr-itil — SessionStart hook (P378/RFC-030; ADR-066/068 oversight-nudge clone)
|
|
3
|
+
#
|
|
4
|
+
# Surfaces a one-line nudge when RFCs lack the human-oversight marker, so the
|
|
5
|
+
# user can ratify them via /wr-itil:manage-rfc <RFC-NNN> (the accepted
|
|
6
|
+
# transition ratifies RFC scope). Sibling of architect-oversight-nudge.sh
|
|
7
|
+
# (ADR-066) and jtbd-oversight-nudge.sh (ADR-068); same class-B SessionStart
|
|
8
|
+
# shape as the ADR-040 session-start surface — so ratification is auto-surfaced
|
|
9
|
+
# every session instead of leaning on the user's memory (P378).
|
|
10
|
+
#
|
|
11
|
+
# Detection is token-cheap: delegates to detect-unoversighted-rfcs.sh (a grep
|
|
12
|
+
# over docs/rfcs/ frontmatter — no body reads, no per-RFC LLM call). Silent
|
|
13
|
+
# when the unoversighted count is zero (steady state once ratified).
|
|
14
|
+
#
|
|
15
|
+
# AFK self-suppress: shares the suite-wide WR_SUPPRESS_OVERSIGHT_NUDGE guard
|
|
16
|
+
# with the architect + jtbd oversight nudges (ADR-068 § shared cross-plugin
|
|
17
|
+
# contracts). AFK orchestrators export it once and every oversight nudge
|
|
18
|
+
# self-suppresses — so this interactive ratify nudge never fires into an
|
|
19
|
+
# absent-user subprocess (JTBD-006 friction guard). Only the literal "1"
|
|
20
|
+
# suppresses. Silent-on-zero; fail-open; ADR-040 ≤2KB budget (one line).
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
if [ "${WR_SUPPRESS_OVERSIGHT_NUDGE:-}" = "1" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
29
|
+
RFCS_DIR="$PROJECT_DIR/docs/rfcs"
|
|
30
|
+
|
|
31
|
+
[ -d "$RFCS_DIR" ] || exit 0
|
|
32
|
+
|
|
33
|
+
DETECT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$0")/..}/scripts/detect-unoversighted-rfcs.sh"
|
|
34
|
+
[ -x "$DETECT" ] || DETECT="$(dirname "$0")/../scripts/detect-unoversighted-rfcs.sh"
|
|
35
|
+
[ -r "$DETECT" ] || exit 0
|
|
36
|
+
|
|
37
|
+
COUNT="$(bash "$DETECT" "$RFCS_DIR" 2>/dev/null | grep -c . || true)"
|
|
38
|
+
COUNT="${COUNT:-0}"
|
|
39
|
+
|
|
40
|
+
[ "$COUNT" -gt 0 ] 2>/dev/null || exit 0
|
|
41
|
+
|
|
42
|
+
if [ "$COUNT" -eq 1 ]; then
|
|
43
|
+
echo "[wr-itil] 1 RFC lacks human oversight — run /wr-itil:manage-rfc <RFC-NNN> to ratify it (the accepted transition confirms scope)."
|
|
44
|
+
else
|
|
45
|
+
echo "[wr-itil] $COUNT RFCs lack human oversight — run /wr-itil:manage-rfc <RFC-NNN> to ratify them (the accepted transition confirms scope)."
|
|
46
|
+
fi
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P378/RFC-030 Piece 2: itil-commit-trailer-transition-advisory.sh — the shared
|
|
4
|
+
# RFC+story commit-trailer auto-transition DETECTOR. Advises (ADR-014: does not
|
|
5
|
+
# perform) proposed/accepted RFC → in-progress and draft story → in-progress on
|
|
6
|
+
# the first non-capture commit carrying the Refs trailer. Behavioural.
|
|
7
|
+
|
|
8
|
+
setup() {
|
|
9
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
10
|
+
HOOK="$REPO_ROOT/packages/itil/hooks/itil-commit-trailer-transition-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/rfcs docs/stories/draft
|
|
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 proposed RFC → in-progress on a non-capture Refs commit" {
|
|
26
|
+
echo "---" > docs/rfcs/RFC-201-x.proposed.md; git add -A; commit "$(printf 'docs(rfcs): capture RFC-201 x\n\nRefs: RFC-201')"
|
|
27
|
+
commit "$(printf 'feat(itil): do slice 1\n\nRefs: RFC-201')"
|
|
28
|
+
run run_hook
|
|
29
|
+
[ "$status" -eq 0 ]
|
|
30
|
+
[[ "$output" == *"RFC-201"* ]]
|
|
31
|
+
[[ "$output" == *"in-progress"* ]]
|
|
32
|
+
[[ "$output" == *"manage-rfc"* ]]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test "silent on the capture commit itself" {
|
|
36
|
+
echo "---" > docs/rfcs/RFC-201-x.proposed.md; git add -A; commit "$(printf 'docs(rfcs): capture RFC-201 x\n\nRefs: RFC-201')"
|
|
37
|
+
run run_hook
|
|
38
|
+
[ "$status" -eq 0 ]
|
|
39
|
+
[ -z "$output" ]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "silent when the RFC is already in-progress (no .proposed/.accepted file)" {
|
|
43
|
+
echo "---" > docs/rfcs/RFC-201-x.in-progress.md; git add -A; commit "$(printf 'feat(itil): more\n\nRefs: RFC-201')"
|
|
44
|
+
run run_hook
|
|
45
|
+
[ "$status" -eq 0 ]
|
|
46
|
+
[ -z "$output" ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@test "advises draft story → in-progress on a non-capture Refs commit" {
|
|
50
|
+
echo "---" > docs/stories/draft/STORY-201-y.md; git add -A; commit "$(printf 'feat(itil): capture STORY-201 y\n\nRefs: STORY-201')"
|
|
51
|
+
commit "$(printf 'feat(itil): implement\n\nRefs: STORY-201')"
|
|
52
|
+
run run_hook
|
|
53
|
+
[ "$status" -eq 0 ]
|
|
54
|
+
[[ "$output" == *"STORY-201"* ]]
|
|
55
|
+
[[ "$output" == *"manage-story"* ]]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "bypass env var suppresses the advisory" {
|
|
59
|
+
echo "---" > docs/rfcs/RFC-201-x.proposed.md; git add -A; commit "$(printf 'feat: s\n\nRefs: RFC-201')"
|
|
60
|
+
run env BYPASS_TRANSITION_ADVISORY=1 bash -c 'printf "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m x\"}}" | bash "$0"' "$HOOK"
|
|
61
|
+
[ "$status" -eq 0 ]
|
|
62
|
+
[ -z "$output" ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "silent when commit carries no Refs trailer" {
|
|
66
|
+
echo "---" > docs/rfcs/RFC-201-x.proposed.md; git add -A; commit "chore: untagged"
|
|
67
|
+
run run_hook
|
|
68
|
+
[ "$status" -eq 0 ]
|
|
69
|
+
[ -z "$output" ]
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P378/RFC-030: itil-rfc-oversight-nudge.sh (SessionStart) emits a one-line
|
|
4
|
+
# nudge when RFCs lack the human-oversight marker, is silent when none do, and
|
|
5
|
+
# self-suppresses under the shared AFK guard (WR_SUPPRESS_OVERSIGHT_NUDGE=1).
|
|
6
|
+
# Behavioural — exercises the hook against fixture docs/rfcs/ trees. Clone of
|
|
7
|
+
# the architect/jtbd oversight-nudge contract (ADR-066/068).
|
|
8
|
+
|
|
9
|
+
setup() {
|
|
10
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
11
|
+
HOOK="$REPO_ROOT/packages/itil/hooks/itil-rfc-oversight-nudge.sh"
|
|
12
|
+
PLUGIN_ROOT="$REPO_ROOT/packages/itil"
|
|
13
|
+
DIR="$(mktemp -d)"
|
|
14
|
+
mkdir -p "$DIR/docs/rfcs"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
teardown() { rm -rf "$DIR"; }
|
|
18
|
+
|
|
19
|
+
mk_unconfirmed() {
|
|
20
|
+
{ echo "---"; echo "status: proposed"; echo "human-oversight: unconfirmed"; echo "---"; echo "# $1"; } \
|
|
21
|
+
> "$DIR/docs/rfcs/$1"
|
|
22
|
+
}
|
|
23
|
+
mk_confirmed() {
|
|
24
|
+
{ echo "---"; echo "status: proposed"; echo "human-oversight: confirmed"; echo "---"; echo "# $1"; } \
|
|
25
|
+
> "$DIR/docs/rfcs/$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
run_hook() { run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" "$@" bash "$HOOK"; }
|
|
29
|
+
|
|
30
|
+
@test "emits a count line when there are unoversighted RFCs" {
|
|
31
|
+
mk_unconfirmed "RFC-201-foo.proposed.md"
|
|
32
|
+
mk_unconfirmed "RFC-202-bar.proposed.md"
|
|
33
|
+
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
34
|
+
[ "$status" -eq 0 ]
|
|
35
|
+
[[ "$output" == *"2 RFCs lack human oversight"* ]]
|
|
36
|
+
[[ "$output" == *"/wr-itil:manage-rfc"* ]]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "singular wording for exactly one unoversighted RFC" {
|
|
40
|
+
mk_unconfirmed "RFC-201-foo.proposed.md"
|
|
41
|
+
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
42
|
+
[[ "$output" == *"1 RFC lacks human oversight"* ]]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "silent when every RFC is confirmed" {
|
|
46
|
+
mk_confirmed "RFC-201-foo.proposed.md"
|
|
47
|
+
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
48
|
+
[ "$status" -eq 0 ]
|
|
49
|
+
[ -z "$output" ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "excludes superseded and closed RFCs from the count" {
|
|
53
|
+
mk_unconfirmed "RFC-201-foo.superseded.md"
|
|
54
|
+
mk_unconfirmed "RFC-202-bar.closed.md"
|
|
55
|
+
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
56
|
+
[ "$status" -eq 0 ]
|
|
57
|
+
[ -z "$output" ]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test "shared AFK guard suppresses the nudge entirely" {
|
|
61
|
+
mk_unconfirmed "RFC-201-foo.proposed.md"
|
|
62
|
+
run env WR_SUPPRESS_OVERSIGHT_NUDGE=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
63
|
+
[ "$status" -eq 0 ]
|
|
64
|
+
[ -z "$output" ]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "guard value other than 1 does not suppress" {
|
|
68
|
+
mk_unconfirmed "RFC-201-foo.proposed.md"
|
|
69
|
+
run env WR_SUPPRESS_OVERSIGHT_NUDGE=yes CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
70
|
+
[[ "$output" == *"lack"*"human oversight"* ]] || [[ "$output" == *"lacks human oversight"* ]]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "silent when docs/rfcs does not exist (framework not adopted)" {
|
|
74
|
+
rm -rf "$DIR/docs/rfcs"
|
|
75
|
+
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
76
|
+
[ "$status" -eq 0 ]
|
|
77
|
+
[ -z "$output" ]
|
|
78
|
+
}
|
package/lib/install-utils.mjs
CHANGED
|
@@ -7,10 +7,12 @@ import { execSync } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
9
|
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
const CODEX_MARKETPLACE_PATH = ".";
|
|
11
|
+
const CODEX_MARKETPLACE_NAME = "windyroad-local";
|
|
10
12
|
|
|
11
13
|
let _dryRun = false;
|
|
12
14
|
|
|
13
|
-
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
15
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME, CODEX_MARKETPLACE_PATH, CODEX_MARKETPLACE_NAME };
|
|
14
16
|
|
|
15
17
|
export function setDryRun(value) {
|
|
16
18
|
_dryRun = value;
|
|
@@ -35,16 +37,34 @@ export function run(cmd, label) {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
function runtimesFor(runtime = "claude") {
|
|
41
|
+
if (runtime === "both") return ["claude", "codex"];
|
|
42
|
+
return [runtime];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkPrerequisites({ runtime = "claude" } = {}) {
|
|
39
46
|
if (_dryRun) return;
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
for (const currentRuntime of runtimesFor(runtime)) {
|
|
49
|
+
if (currentRuntime === "claude") {
|
|
50
|
+
try {
|
|
51
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(
|
|
54
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} else if (currentRuntime === "codex") {
|
|
59
|
+
try {
|
|
60
|
+
execSync("codex --version", { stdio: "pipe" });
|
|
61
|
+
} catch {
|
|
62
|
+
console.error(
|
|
63
|
+
"Error: 'codex' CLI not found. Install Codex CLI first:\n https://developers.openai.com/codex\n"
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
|
|
@@ -55,6 +75,13 @@ export function addMarketplace() {
|
|
|
55
75
|
);
|
|
56
76
|
}
|
|
57
77
|
|
|
78
|
+
export function addCodexMarketplace() {
|
|
79
|
+
return run(
|
|
80
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
81
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
59
86
|
return run(
|
|
60
87
|
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
@@ -62,6 +89,13 @@ export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
|
62
89
|
);
|
|
63
90
|
}
|
|
64
91
|
|
|
92
|
+
export function installCodexPlugin(pluginName) {
|
|
93
|
+
return run(
|
|
94
|
+
`codex plugin add ${pluginName}@${CODEX_MARKETPLACE_NAME}`,
|
|
95
|
+
pluginName
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
66
100
|
return run(
|
|
67
101
|
`claude plugin update "${pluginName}@${MARKETPLACE_NAME}" --scope ${scope}`,
|
|
@@ -69,18 +103,36 @@ export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
|
69
103
|
);
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
export function updateCodexMarketplace() {
|
|
107
|
+
return run(
|
|
108
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
109
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
72
113
|
export function uninstallPlugin(pluginName) {
|
|
73
114
|
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
74
115
|
}
|
|
75
116
|
|
|
117
|
+
export function uninstallCodexPlugin(pluginName) {
|
|
118
|
+
return run(`codex plugin remove ${pluginName}`, `Removing ${pluginName}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
/**
|
|
77
122
|
* Install a single package: marketplace add + plugin install.
|
|
78
123
|
*/
|
|
79
|
-
export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
|
|
124
|
+
export function installPackage(pluginName, { deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
80
125
|
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
if (runtime === "claude" || runtime === "both") {
|
|
128
|
+
addMarketplace();
|
|
129
|
+
installPlugin(pluginName, { scope });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (runtime === "codex" || runtime === "both") {
|
|
133
|
+
addCodexMarketplace();
|
|
134
|
+
installCodexPlugin(pluginName);
|
|
135
|
+
}
|
|
84
136
|
|
|
85
137
|
if (deps.length > 0) {
|
|
86
138
|
console.log(`\nNote: This plugin works best with:`);
|
|
@@ -90,34 +142,47 @@ export function installPackage(pluginName, { deps = [], scope = "project" } = {}
|
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
console.log(
|
|
93
|
-
`\nDone! Restart Claude Code to activate.\n`
|
|
145
|
+
`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to activate.\n`
|
|
94
146
|
);
|
|
95
147
|
}
|
|
96
148
|
|
|
97
149
|
/**
|
|
98
150
|
* Update a single package.
|
|
99
151
|
*/
|
|
100
|
-
export function updatePackage(pluginName, { scope = "project" } = {}) {
|
|
152
|
+
export function updatePackage(pluginName, { scope = "project", runtime = "claude" } = {}) {
|
|
101
153
|
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
102
154
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
if (runtime === "claude" || runtime === "both") {
|
|
156
|
+
run(
|
|
157
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
158
|
+
"Updating marketplace"
|
|
159
|
+
);
|
|
160
|
+
updatePlugin(pluginName, { scope });
|
|
161
|
+
}
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
if (runtime === "codex" || runtime === "both") {
|
|
164
|
+
updateCodexMarketplace();
|
|
165
|
+
installCodexPlugin(pluginName);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply updates.\n`);
|
|
110
169
|
}
|
|
111
170
|
|
|
112
171
|
/**
|
|
113
172
|
* Uninstall a single package.
|
|
114
173
|
*/
|
|
115
|
-
export function uninstallPackage(pluginName) {
|
|
174
|
+
export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
116
175
|
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
117
176
|
|
|
118
|
-
|
|
177
|
+
if (runtime === "claude" || runtime === "both") {
|
|
178
|
+
uninstallPlugin(pluginName);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (runtime === "codex" || runtime === "both") {
|
|
182
|
+
uninstallCodexPlugin(pluginName);
|
|
183
|
+
}
|
|
119
184
|
|
|
120
|
-
console.log(
|
|
185
|
+
console.log(`\nDone. Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply changes.\n`);
|
|
121
186
|
}
|
|
122
187
|
|
|
123
188
|
/**
|
|
@@ -131,6 +196,7 @@ export function parseStandardArgs(argv) {
|
|
|
131
196
|
update: args.includes("--update"),
|
|
132
197
|
dryRun: args.includes("--dry-run"),
|
|
133
198
|
scope: "project",
|
|
199
|
+
runtime: "claude",
|
|
134
200
|
};
|
|
135
201
|
const scopeIdx = args.indexOf("--scope");
|
|
136
202
|
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
@@ -142,5 +208,15 @@ export function parseStandardArgs(argv) {
|
|
|
142
208
|
process.exit(1);
|
|
143
209
|
}
|
|
144
210
|
}
|
|
211
|
+
const runtimeIdx = args.indexOf("--runtime");
|
|
212
|
+
if (runtimeIdx !== -1 && args[runtimeIdx + 1]) {
|
|
213
|
+
const val = args[runtimeIdx + 1];
|
|
214
|
+
if (["claude", "codex", "both"].includes(val)) {
|
|
215
|
+
flags.runtime = val;
|
|
216
|
+
} else {
|
|
217
|
+
console.error("--runtime requires: claude, codex, or both");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
145
221
|
return flags;
|
|
146
222
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wr-itil — detect RFCs lacking the human-oversight marker (P378/RFC-030).
|
|
3
|
+
#
|
|
4
|
+
# Clone of the architect/jtbd detect-unoversighted shape (ADR-066/068): greps
|
|
5
|
+
# each RFC's YAML frontmatter for `human-oversight: confirmed`. No body reads,
|
|
6
|
+
# no per-RFC LLM call. An RFC is "unoversighted" when its frontmatter does not
|
|
7
|
+
# carry that marker line (RFCs are born `human-oversight: unconfirmed` at
|
|
8
|
+
# capture-rfc; they are ratified at the /wr-itil:manage-rfc accepted transition
|
|
9
|
+
# where scope is confirmed).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# detect-unoversighted-rfcs.sh [RFCS_DIR] (default docs/rfcs)
|
|
13
|
+
# Output: one unoversighted RFC file path per line, sorted. Empty = all
|
|
14
|
+
# confirmed. Always exits 0 (detector, not a gate).
|
|
15
|
+
#
|
|
16
|
+
# Consumed by: itil-rfc-oversight-nudge.sh (SessionStart count). Marker
|
|
17
|
+
# contract: ADR-066 (human-oversight marker), as extended to RFCs by ADR-070/071.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
RFCS_DIR="${1:-docs/rfcs}"
|
|
22
|
+
|
|
23
|
+
[ -d "$RFCS_DIR" ] || exit 0
|
|
24
|
+
|
|
25
|
+
shopt -s nullglob
|
|
26
|
+
for f in "$RFCS_DIR"/*.md "$RFCS_DIR"/*/*.md; do
|
|
27
|
+
base="$(basename "$f")"
|
|
28
|
+
[ "$base" = "README.md" ] && continue
|
|
29
|
+
# Superseded + closed RFCs are retired/done — ratifying their scope is moot,
|
|
30
|
+
# so they are not part of the "needs oversight" set (keeps the nudge focused
|
|
31
|
+
# on live RFCs the user can still act on).
|
|
32
|
+
case "$base" in *.superseded.md|*.closed.md) continue ;; esac
|
|
33
|
+
|
|
34
|
+
fm="$(awk '
|
|
35
|
+
NR==1 && $0 != "---" { exit }
|
|
36
|
+
NR==1 { next }
|
|
37
|
+
/^---[[:space:]]*$/ { exit }
|
|
38
|
+
{ print }
|
|
39
|
+
' "$f")"
|
|
40
|
+
|
|
41
|
+
if printf '%s\n' "$fm" | grep -qiE '^human-oversight:[[:space:]]*confirmed[[:space:]]*$'; then
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "$f"
|
|
46
|
+
done | sort
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P378/RFC-030 Piece 1 (ADR-085): update-rfc-commits-section.sh renders the RFC
|
|
4
|
+
# `## Commits` section as a derived view from `git log --grep "Refs: RFC-NNN"`.
|
|
5
|
+
# Behavioural — exercises the renderer against a throwaway git repo.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
9
|
+
HELPER="$REPO_ROOT/packages/itil/scripts/update-rfc-commits-section.sh"
|
|
10
|
+
DIR="$(mktemp -d)"
|
|
11
|
+
cd "$DIR"
|
|
12
|
+
git init -q
|
|
13
|
+
git config user.email t@e.x; git config user.name t
|
|
14
|
+
mkdir -p docs/rfcs
|
|
15
|
+
cat > docs/rfcs/RFC-201-thing.proposed.md <<'EOF'
|
|
16
|
+
# RFC-201: thing
|
|
17
|
+
|
|
18
|
+
## Commits
|
|
19
|
+
|
|
20
|
+
(placeholder)
|
|
21
|
+
|
|
22
|
+
## Related
|
|
23
|
+
|
|
24
|
+
x
|
|
25
|
+
EOF
|
|
26
|
+
git add -A && git commit -qm "chore: seed"
|
|
27
|
+
}
|
|
28
|
+
teardown() { cd /; rm -rf "$DIR"; }
|
|
29
|
+
|
|
30
|
+
@test "renders commits carrying the Refs: RFC-NNN trailer, newest first" {
|
|
31
|
+
echo a > a; git add a; git commit -qm "$(printf 'feat: a\n\nRefs: RFC-201')"
|
|
32
|
+
echo b > b; git add b; git commit -qm "$(printf 'fix: b\n\nRefs: RFC-201')"
|
|
33
|
+
run bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
34
|
+
[ "$status" -eq 0 ]
|
|
35
|
+
run sed -n '/## Commits/,/## Related/p' docs/rfcs/RFC-201-thing.proposed.md
|
|
36
|
+
[[ "$output" == *"fix: b"* ]]
|
|
37
|
+
[[ "$output" == *"feat: a"* ]]
|
|
38
|
+
# newest first: fix: b appears before feat: a
|
|
39
|
+
[[ "$output" == *"fix: b"*"feat: a"* ]]
|
|
40
|
+
# placeholder body is gone
|
|
41
|
+
[[ "$output" != *"(placeholder)"* ]]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "does NOT include commits without the trailer" {
|
|
45
|
+
echo a > a; git add a; git commit -qm "$(printf 'feat: tagged\n\nRefs: RFC-201')"
|
|
46
|
+
echo b > b; git add b; git commit -qm "chore: untagged"
|
|
47
|
+
run bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
48
|
+
run sed -n '/## Commits/,/## Related/p' docs/rfcs/RFC-201-thing.proposed.md
|
|
49
|
+
[[ "$output" == *"feat: tagged"* ]]
|
|
50
|
+
[[ "$output" != *"chore: untagged"* ]]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "no trailer-bearing commits → renders a self-describing placeholder, not a false claim" {
|
|
54
|
+
run bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
55
|
+
[ "$status" -eq 0 ]
|
|
56
|
+
run sed -n '/## Commits/,/## Related/p' docs/rfcs/RFC-201-thing.proposed.md
|
|
57
|
+
[[ "$output" == *"rendered from"* ]] && [[ "$output" == *"git log"* ]]
|
|
58
|
+
[[ "$output" != *"maintained automatically"* ]]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "idempotent — second render is a byte-stable no-op" {
|
|
62
|
+
echo a > a; git add a; git commit -qm "$(printf 'feat: a\n\nRefs: RFC-201')"
|
|
63
|
+
bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
64
|
+
h1=$(shasum docs/rfcs/RFC-201-thing.proposed.md | cut -d' ' -f1)
|
|
65
|
+
bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
66
|
+
h2=$(shasum docs/rfcs/RFC-201-thing.proposed.md | cut -d' ' -f1)
|
|
67
|
+
[ "$h1" = "$h2" ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "preserves sections after ## Commits (## Related survives)" {
|
|
71
|
+
echo a > a; git add a; git commit -qm "$(printf 'feat: a\n\nRefs: RFC-201')"
|
|
72
|
+
bash "$HELPER" docs/rfcs/RFC-201-thing.proposed.md
|
|
73
|
+
run grep -c "## Related" docs/rfcs/RFC-201-thing.proposed.md
|
|
74
|
+
[ "$output" -eq 1 ]
|
|
75
|
+
run grep -c "^x$" docs/rfcs/RFC-201-thing.proposed.md
|
|
76
|
+
[ "$output" -eq 1 ]
|
|
77
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/update-rfc-commits-section.sh
|
|
3
|
+
#
|
|
4
|
+
# Renders an RFC's `## Commits` section as a DERIVED VIEW from the git history
|
|
5
|
+
# (P378/RFC-030 Piece 1; ADR-085). The commit log is the source of truth; the
|
|
6
|
+
# section is a projection of `git log --grep "Refs: RFC-NNN"`. Because nothing
|
|
7
|
+
# is written per-commit, there is no post-commit working-tree edit and no
|
|
8
|
+
# ADR-014 grain problem — the section is regenerated skill-side (manage-rfc on
|
|
9
|
+
# every transition/review; reconcile-rfcs) exactly like docs/problems/README.md
|
|
10
|
+
# is a rendered index (ADR-031 precedent).
|
|
11
|
+
#
|
|
12
|
+
# Usage: update-rfc-commits-section.sh <rfc-file>
|
|
13
|
+
# Idempotent: rewriting a current section is a byte-stable no-op.
|
|
14
|
+
#
|
|
15
|
+
# @adr ADR-085 (RFC ## Commits is a git-log-derived view, rendered skill-side)
|
|
16
|
+
# @adr ADR-031 (rendered-index precedent) ADR-014 (why not a post-commit hook)
|
|
17
|
+
# @problem P378
|
|
18
|
+
# @rfc RFC-030
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
RFC_FILE="${1:-}"
|
|
23
|
+
[ -n "$RFC_FILE" ] || { echo "ERROR: missing rfc-file argument" >&2; exit 1; }
|
|
24
|
+
[ -f "$RFC_FILE" ] || { echo "ERROR: rfc file not found: $RFC_FILE" >&2; exit 1; }
|
|
25
|
+
|
|
26
|
+
# RFC id from filename (RFC-NNN-...).
|
|
27
|
+
RFC_ID=$(basename "$RFC_FILE" | grep -oE 'RFC-[0-9]{3}' | head -1)
|
|
28
|
+
[ -n "$RFC_ID" ] || { echo "ERROR: cannot derive RFC id from $RFC_FILE" >&2; exit 1; }
|
|
29
|
+
|
|
30
|
+
# Render the commit list from git log (newest first). Fail-open to a sentinel
|
|
31
|
+
# line when not in a git tree (e.g. adopter tarball inspection).
|
|
32
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
33
|
+
RENDERED=$(git log --grep="Refs: ${RFC_ID}\b" --format='- `%h` %s — %ad' --date=short 2>/dev/null || true)
|
|
34
|
+
else
|
|
35
|
+
RENDERED=""
|
|
36
|
+
fi
|
|
37
|
+
if [ -z "$RENDERED" ]; then
|
|
38
|
+
RENDERED="(no commits yet — this section is rendered from \`git log --grep \"Refs: ${RFC_ID}\"\`)"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Rewrite the body of the `## Commits` section in place (everything between the
|
|
42
|
+
# `## Commits` heading and the next `## ` heading). awk preserves the rest. The
|
|
43
|
+
# rendered (multi-line) content is passed via a file — awk -v cannot carry
|
|
44
|
+
# embedded newlines.
|
|
45
|
+
TMP="$(mktemp)"
|
|
46
|
+
RENDERED_FILE="$(mktemp)"
|
|
47
|
+
printf '%s\n' "$RENDERED" > "$RENDERED_FILE"
|
|
48
|
+
awk -v rfile="$RENDERED_FILE" '
|
|
49
|
+
BEGIN { in_sec = 0 }
|
|
50
|
+
/^## Commits[[:space:]]*$/ {
|
|
51
|
+
print
|
|
52
|
+
print ""
|
|
53
|
+
while ((getline line < rfile) > 0) print line
|
|
54
|
+
close(rfile)
|
|
55
|
+
print ""
|
|
56
|
+
in_sec = 1
|
|
57
|
+
next
|
|
58
|
+
}
|
|
59
|
+
in_sec && /^## / { in_sec = 0 } # next section header ends the block
|
|
60
|
+
in_sec { next } # drop old body lines
|
|
61
|
+
{ print }
|
|
62
|
+
' "$RFC_FILE" > "$TMP"
|
|
63
|
+
rm -f "$RENDERED_FILE"
|
|
64
|
+
|
|
65
|
+
# Idempotent: only replace on change.
|
|
66
|
+
if ! cmp -s "$TMP" "$RFC_FILE"; then
|
|
67
|
+
cat "$TMP" > "$RFC_FILE"
|
|
68
|
+
fi
|
|
69
|
+
rm -f "$TMP"
|
|
@@ -35,8 +35,8 @@ This skill has **at most one direction-setting AskUserQuestion (the I12 derive-t
|
|
|
35
35
|
|----------|-----------|
|
|
36
36
|
| Duplicate-check | Mechanical 3-keyword title-only grep; matches listed in report; capture proceeds regardless. False-positives are cheaper than false-negatives (P155 line 24). |
|
|
37
37
|
| Hang-off arbitration (P346 Phase 3) | Mechanical pre-filter (≤5 candidates by shared ADR/RFC/SKILL/file signal) + fresh-context `wr-itil:hang-off-check` subagent dispatch (ADR-032 5th invocation pattern). Verdict-acts: `HANG_OFF: P<NNN>` halts capture + routes orchestrator to amend parent; `PROCEED_NEW` continues + appends rationale to `## Related`. AFK safe-default: ambiguous → PROCEED_NEW per subagent Rule 6 contract. JTBD-301 firewall: maintainer-side only. |
|
|
38
|
-
| Priority
|
|
39
|
-
| Effort
|
|
38
|
+
| Priority (Impact × Likelihood) | **Derived at capture** silently from the description per Step 4a (ADR-067 silent-derivation + ADR-026 grounding/sentinel; ADR-076 honest-likelihood). No deferred placeholder, no false-low. Category-4 silent-framework per ADR-044. |
|
|
39
|
+
| Effort | **Derived at capture** silently per Step 4a (ADR-067 t-shirt buckets + ADR-026 grounding/sentinel). No deferred placeholder. |
|
|
40
40
|
| Multi-concern split | Out of scope: capture-problem creates one ticket per invocation. Multi-concern observations route to `/wr-itil:manage-problem` (its Step 4b owns the split). |
|
|
41
41
|
| Empty `$ARGUMENTS` | Halt-with-stderr-directive: print "capture-problem requires a description in $ARGUMENTS — invoke /wr-itil:manage-problem instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-problem with empty arguments — caller-side contract. |
|
|
42
42
|
| JTBD-trace + persona derivation (I12 derive-then-ratify — ADR-060 Amendment 2026-06-02) | **Derive-then-ratify per ADR-044 category 1 (direction-setting) on the AskUserQuestion fallback path; silent-framework category 4 on the derive-success path.** Step 1.5b runs lexical detection (`\bJTBD-[0-9]+\b`) + flag pre-resolution (`--jtbd=` / `--persona=`) + cited-JTBD persona derivation. **Derive-success** → silent-proceed with derived values + stderr advisory. **Derive-failure or ambiguity** → AskUserQuestion proposes up-to-3 candidate persona+JTBD pairs + a Reject option (4-option cap per ADR-044 Rule 1). User response semantics: **REJECT** → halt-with-stderr-directive + exit non-zero (REJECT of proposed persona/JTBD = REJECT of the problem; no ticket created). **Option-pick** (acceptance of a proposed candidate as-is) → silent-proceed with picked values. **Free-text correction** → silent-proceed with corrected values (correction-as-acceptance). |
|
|
@@ -239,9 +239,9 @@ Log the renumber decision in the operation report if origin and local diverged.
|
|
|
239
239
|
|
|
240
240
|
**Status**: Open
|
|
241
241
|
**Reported**: <YYYY-MM-DD>
|
|
242
|
-
**Priority**:
|
|
242
|
+
**Priority**: <Severity = Impact × Likelihood> (<band>) — Impact: <1-5> × Likelihood: <1-5> — derived at capture from the description per Step 4a
|
|
243
243
|
**Origin**: internal
|
|
244
|
-
**Effort**: M
|
|
244
|
+
**Effort**: <S|M|L|XL> — derived at capture per Step 4a
|
|
245
245
|
**JTBD**: <jtbd_trace_value_as_comma_separated_list_OR_omit_line_when_empty>
|
|
246
246
|
**Persona**: <persona_value_OR_omit_line_when_empty>
|
|
247
247
|
|
|
@@ -268,7 +268,6 @@ Log the renumber decision in the operation report if origin and local diverged.
|
|
|
268
268
|
|
|
269
269
|
### Investigation Tasks
|
|
270
270
|
|
|
271
|
-
- [ ] Re-rate Priority and Effort at next /wr-itil:review-problems
|
|
272
271
|
- [ ] Investigate root cause
|
|
273
272
|
- [ ] Create reproduction test
|
|
274
273
|
|
|
@@ -283,7 +282,17 @@ Log the renumber decision in the operation report if origin and local diverged.
|
|
|
283
282
|
(captured via /wr-itil:capture-problem; expand at next investigation)
|
|
284
283
|
```
|
|
285
284
|
|
|
286
|
-
The deferred-placeholder
|
|
285
|
+
**Rate at capture, do not defer (P375 / ADR-032 amendment 2026-06-24).** The Priority (Impact × Likelihood) and Effort fields are **derived at capture** per Step 4a below — a real best-effort rating, ALWAYS. No `(deferred — re-rate…)` placeholder and **no "not estimated" / "no prior data" marker** — those are deferrals wearing a different hat (user correction 2026-06-24: *"that's just another deferral"*). The old deferred-placeholder default was the single largest uncadenced-deferral rot generator (P375): nothing self-fired `review-problems`, and the `Likelihood: 1` false-low buried every capture at the bottom of WSJF. Estimation under uncertainty produces an estimate, not a deferral: a thin description still gets a real number (rough is fine). `/wr-itil:review-problems` re-rates the whole backlog when it runs — captures need no per-ticket re-rate flag.
|
|
286
|
+
|
|
287
|
+
### 4a. Derive the rating at capture (P375 / ADR-032 amendment; ADR-067 + ADR-026 + ADR-076)
|
|
288
|
+
|
|
289
|
+
Derive Priority and Effort from the description **silently** — NO `AskUserQuestion` (ADR-067 item 1 silent-derivation, category-4 silent-framework per ADR-044; preserves the lightweight 3-4-turn capture cost shape). This is one inference over text the agent has already read, NOT the `/wr-itil:manage-problem` ceremony.
|
|
290
|
+
|
|
291
|
+
1. **Impact (1-5)** and **Likelihood (1-5)** — read the description against `RISK-POLICY.md`'s impact bands and assess how often / how broadly the harm manifests. `Severity = Impact × Likelihood`; `Priority` band follows the standard matrix. Likelihood is **honest field-risk only** — never inflate it to lift WSJF rank (ADR-076).
|
|
292
|
+
2. **Effort (S/M/L/XL)** — derive from the described scope (files touched, cross-package, migration-heavy → larger), per ADR-067's t-shirt buckets.
|
|
293
|
+
3. **Always a real value (ADR-026 grounding without deferral)** — when a comparable prior ticket informs the estimate, cite it inline (e.g. `— cf. P<NNN>`). When no comparable prior exists, **still commit to a real best-effort number** grounded in the description's own signals — do NOT append "not estimated" / "no prior data" or any other re-rate flag (that is a deferral; user correction 2026-06-24). A rough estimate from a thin description is honest estimation; a marker that says "I didn't really estimate this" is the rot this brick removes.
|
|
294
|
+
|
|
295
|
+
Write the derived values into the template's `**Priority**` and `**Effort**` lines (replacing the `<…>` placeholders). Every captured ticket leaves Step 4a with a real Impact × Likelihood and a real Effort bucket — never a placeholder, never a deferral marker.
|
|
287
296
|
|
|
288
297
|
### 5. Write the file
|
|
289
298
|
|
|
@@ -341,19 +350,19 @@ After the commit, report:
|
|
|
341
350
|
preflight_reason="$(wr-itil-check-deferred-placeholder-staleness "$PWD")"
|
|
342
351
|
```
|
|
343
352
|
|
|
344
|
-
See `/wr-itil:work-problems` SKILL.md Step 0c for the canonical contract on the two-axis trigger (count ≥ 3
|
|
353
|
+
See `/wr-itil:work-problems` SKILL.md Step 0c for the canonical contract on the two-axis trigger (count ≥ 3 ungrounded-rating tickets — the ADR-026 `not estimated` sentinel — AND README age > 7 days); the threshold constants live in the helper. <!-- DEFERRED-PLACEHOLDER-STALENESS-CONTRACT-SOURCE: packages/itil/lib/check-deferred-placeholder-staleness.sh -->
|
|
345
354
|
|
|
346
355
|
| `preflight_reason` | Trailing-pointer shape |
|
|
347
356
|
|---------------------------------------------------|--------------------------------------------------------------------------------------------------------|
|
|
348
|
-
| `no-deferred-placeholders` / `below-threshold ...` / `fresh-readme ...` | Default (low-priority) pointer: *"Run `/wr-itil:review-problems` next to re-rate the
|
|
349
|
-
| `no-readme count=<N>` | **Highlighted (actionable) pointer**: *"⚠ `<N>`
|
|
350
|
-
| `stale-readme count=<N> age=<X>s threshold=<Y>s` | **Highlighted (actionable) pointer**: *"⚠ `<N>`
|
|
357
|
+
| `no-deferred-placeholders` / `below-threshold ...` / `fresh-readme ...` | Default (low-priority) pointer: *"Run `/wr-itil:review-problems` next to re-rate the ungrounded (`not estimated — no prior data`) ratings."* (README is now refreshed inline at Step 6 per P199 Option 2 — the pointer no longer names a README-refresh action in the default shape.) |
|
|
358
|
+
| `no-readme count=<N>` | **Highlighted (actionable) pointer**: *"⚠ `<N>` ungrounded-rating ticket(s) (ADR-026 `not estimated` sentinel) have accumulated AND `docs/problems/README.md` is missing/malformed — run `/wr-itil:review-problems` NOW to rebuild the README and re-rate placeholders."* (Fires only when README is genuinely missing/malformed despite this skill's inline refresh — a drift class the helper still surfaces.) |
|
|
359
|
+
| `stale-readme count=<N> age=<X>s threshold=<Y>s` | **Highlighted (actionable) pointer**: *"⚠ `<N>` ungrounded-rating ticket(s) (ADR-026 `not estimated` sentinel) have accumulated AND the WSJF Rankings cadence is `<X>` days stale (> 7-day threshold) — run `/wr-itil:review-problems` NOW to re-rate placeholders."* (Inline refresh at Step 6 freshens the README mtime on every capture, so this shape now fires only when captures are absent for > 7 days — a real cadence-stale signal, not capture-driven drift.) |
|
|
351
360
|
|
|
352
|
-
**Why conditional, not auto-dispatch** (ADR-032 + JTBD-001): capture-problem is intentionally lightweight per ADR-032 P155 amendment. Auto-dispatching review-problems from capture would re-introduce the ~10-turn ceremony the lightweight aside is engineered to avoid. The conditional pointer surfaces the signal "
|
|
361
|
+
**Why conditional, not auto-dispatch** (ADR-032 + JTBD-001): capture-problem is intentionally lightweight per ADR-032 P155 amendment. Auto-dispatching review-problems from capture would re-introduce the ~10-turn ceremony the lightweight aside is engineered to avoid. The conditional pointer surfaces the signal "ungrounded-rating backlog has grown AND the cadence axis hit threshold" when both axes fire. The user picks when to absorb the re-rate cost.
|
|
353
362
|
|
|
354
363
|
**Fail-soft**: any error in the helper invocation MUST NOT block the report — fall back to the default pointer shape.
|
|
355
364
|
|
|
356
|
-
The trailing pointer is **not optional** — it is the user-visible signal for
|
|
365
|
+
The trailing pointer is **not optional** — it is the user-visible signal for ungrounded-rating re-rating cadence (P375 re-point: the count is now ADR-026 `not estimated` sentinels, not blanket deferred placeholders). The conditional highlight (P271) escalates the signal when the ungrounded-rating backlog AND the cadence axis both hit threshold. Drift here re-opens P271.
|
|
357
366
|
|
|
358
367
|
<!-- @jtbd JTBD-001 (Enforce Governance Without Slowing Down — conditional highlight escalates the signal without forcing a flow break) -->
|
|
359
368
|
|
|
@@ -198,30 +198,30 @@ EOF
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
# ---------------------------------------------------------------------------
|
|
201
|
-
# Skeleton-fill ticket shape — capture-problem
|
|
202
|
-
#
|
|
201
|
+
# Skeleton-fill ticket shape — capture-problem DERIVES the Priority/Effort
|
|
202
|
+
# rating at capture (P375 / ADR-032 amendment 2026-06-24). NO deferred false-
|
|
203
|
+
# low placeholder. The ADR-026 sentinel `not estimated — no prior data` is the
|
|
204
|
+
# residual re-rate signal, emitted only when no comparable prior grounds the
|
|
205
|
+
# rating.
|
|
203
206
|
# ---------------------------------------------------------------------------
|
|
204
207
|
|
|
205
|
-
@test "capture-problem: skeleton
|
|
206
|
-
# Fixture mirrors the SKILL.md Step 4-5 prescribed write target — per
|
|
207
|
-
# ADR-031 per-state-subdir layout (`docs/problems/open/<NNN>-<slug>.md`),
|
|
208
|
-
# NOT the pre-ADR-031 flat shape (`docs/problems/<NNN>-<slug>.open.md`).
|
|
209
|
-
# P281 (template-refresh sub-shape) corrected the SKILL.md drift; this
|
|
210
|
-
# fixture exercises the now-canonical write path.
|
|
208
|
+
@test "capture-problem: skeleton ticket carries a DERIVED rating, not a deferred false-low" {
|
|
211
209
|
mkdir -p "$TMPROOT/docs/problems/open"
|
|
212
210
|
TITLE="example-aside-finding"
|
|
213
211
|
ID="200"
|
|
214
212
|
TODAY=$(date -u +%Y-%m-%d)
|
|
215
213
|
DESCRIPTION="Quick observation worth a ticket but not blocking."
|
|
216
214
|
|
|
217
|
-
# Mirror the SKILL.md
|
|
215
|
+
# Mirror the SKILL.md Step 4a + Step 4 template: a derived rating (real
|
|
216
|
+
# Impact × Likelihood) — a real best-effort value, NO deferral marker of
|
|
217
|
+
# any kind (no "deferred — re-rate", no "not estimated" sentinel).
|
|
218
218
|
cat > "$TMPROOT/docs/problems/open/${ID}-${TITLE}.md" <<EOF
|
|
219
219
|
# Problem ${ID}: ${TITLE}
|
|
220
220
|
|
|
221
221
|
**Status**: Open
|
|
222
222
|
**Reported**: ${TODAY}
|
|
223
|
-
**Priority**:
|
|
224
|
-
**Effort**: M
|
|
223
|
+
**Priority**: 6 (Medium) — Impact: 3 × Likelihood: 2
|
|
224
|
+
**Effort**: M
|
|
225
225
|
|
|
226
226
|
## Description
|
|
227
227
|
|
|
@@ -231,49 +231,42 @@ ${DESCRIPTION}
|
|
|
231
231
|
|
|
232
232
|
(deferred to investigation)
|
|
233
233
|
|
|
234
|
-
## Workaround
|
|
235
|
-
|
|
236
|
-
(deferred to investigation)
|
|
237
|
-
|
|
238
|
-
## Impact Assessment
|
|
239
|
-
|
|
240
|
-
- **Who is affected**: (deferred to investigation)
|
|
241
|
-
- **Frequency**: (deferred to investigation)
|
|
242
|
-
- **Severity**: (deferred to investigation)
|
|
243
|
-
- **Analytics**: (deferred to investigation)
|
|
244
|
-
|
|
245
234
|
## Root Cause Analysis
|
|
246
235
|
|
|
247
236
|
### Investigation Tasks
|
|
248
237
|
|
|
249
|
-
- [ ] Re-rate Priority and Effort at next /wr-itil:review-problems
|
|
250
238
|
- [ ] Investigate root cause
|
|
251
239
|
- [ ] Create reproduction test
|
|
252
240
|
|
|
253
|
-
## Dependencies
|
|
254
|
-
|
|
255
|
-
- **Blocks**: (none)
|
|
256
|
-
- **Blocked by**: (none)
|
|
257
|
-
- **Composes with**: (none)
|
|
258
|
-
|
|
259
241
|
## Related
|
|
260
242
|
|
|
261
243
|
(captured via /wr-itil:capture-problem; expand at next investigation)
|
|
262
244
|
EOF
|
|
263
245
|
|
|
264
|
-
# Behavioural assertions: ticket file has the load-bearing fields.
|
|
265
246
|
TICKET="$TMPROOT/docs/problems/open/${ID}-${TITLE}.md"
|
|
266
247
|
[ -f "$TICKET" ]
|
|
267
248
|
run grep -F '**Status**: Open' "$TICKET"
|
|
268
249
|
[ "$status" -eq 0 ]
|
|
269
|
-
# Description survives verbatim
|
|
270
250
|
run grep -F "$DESCRIPTION" "$TICKET"
|
|
271
251
|
[ "$status" -eq 0 ]
|
|
272
|
-
|
|
252
|
+
|
|
253
|
+
# The dropped false-low default MUST NOT appear.
|
|
273
254
|
run grep -F 'deferred — re-rate at next /wr-itil:review-problems' "$TICKET"
|
|
274
|
-
[ "$status" -
|
|
275
|
-
# Investigation Tasks nudges user to re-rate
|
|
255
|
+
[ "$status" -ne 0 ]
|
|
276
256
|
run grep -F 'Re-rate Priority and Effort at next /wr-itil:review-problems' "$TICKET"
|
|
257
|
+
[ "$status" -ne 0 ]
|
|
258
|
+
run grep -F 'Likelihood: 1 (deferred' "$TICKET"
|
|
259
|
+
[ "$status" -ne 0 ]
|
|
260
|
+
# No "not estimated" sentinel either — that is just another deferral
|
|
261
|
+
# (user correction 2026-06-24). Every capture carries a real value.
|
|
262
|
+
run grep -F 'not estimated' "$TICKET"
|
|
263
|
+
[ "$status" -ne 0 ]
|
|
264
|
+
|
|
265
|
+
# A real derived Impact × Likelihood is present (not the buried false-low).
|
|
266
|
+
run grep -E 'Impact: [1-5] (×|x) Likelihood: [1-5]' "$TICKET"
|
|
267
|
+
[ "$status" -eq 0 ]
|
|
268
|
+
# And a real Effort bucket with no trailing deferral marker.
|
|
269
|
+
run grep -E '^\*\*Effort\*\*: (S|M|L|XL)$' "$TICKET"
|
|
277
270
|
[ "$status" -eq 0 ]
|
|
278
271
|
}
|
|
279
272
|
|
|
@@ -210,7 +210,7 @@ stories: [<from --stories flag — ordered execution sequence; or [] if --storie
|
|
|
210
210
|
|
|
211
211
|
## Commits
|
|
212
212
|
|
|
213
|
-
(
|
|
213
|
+
(rendered from `git log --grep "Refs: RFC-<NNN>"` by `/wr-itil:manage-rfc` + `wr-itil-reconcile-rfcs` per ADR-085 — a git-log-derived view, not stored per-commit. At capture there are no commits yet.)
|
|
214
214
|
|
|
215
215
|
## Related
|
|
216
216
|
|
|
@@ -201,7 +201,7 @@ problems: [P<NNN>, P<NNN>, ...]
|
|
|
201
201
|
jtbd: [JTBD-<NNN>, JTBD-<NNN>, ...]
|
|
202
202
|
rfcs: [<RFC-<NNN>, ...> or empty]
|
|
203
203
|
story-maps: [<STORY-MAP-<NNN>, ...> or empty]
|
|
204
|
-
estimated-effort:
|
|
204
|
+
estimated-effort: <S|M|L|XL — derived at capture per ADR-067 (real best-effort value, no deferral marker)>
|
|
205
205
|
---
|
|
206
206
|
|
|
207
207
|
# STORY-<NNN>: <Title>
|
|
@@ -212,7 +212,7 @@ estimated-effort: deferred
|
|
|
212
212
|
**JTBD**: <JTBD-<NNN> [, ...]>
|
|
213
213
|
**RFCs**: <RFC-<NNN> [, ...]> or (none — populate at accepted transition per I7)
|
|
214
214
|
**Story Maps**: <STORY-MAP-<NNN> [, ...]> or (none — populate at accepted transition per I8)
|
|
215
|
-
**Estimated effort**: deferred (
|
|
215
|
+
**Estimated effort**: <S|M|L|XL> — derived at capture as a real best-effort value (P375 / ADR-032 amendment 2026-06-24; ADR-067 silent-derivation). NO `deferred` default and no "not estimated" marker — those are deferrals (user correction 2026-06-24). Confirmed/refined at the accepted transition per I10 INVEST Estimable.
|
|
216
216
|
|
|
217
217
|
## User value (required, INVEST Valuable)
|
|
218
218
|
|
|
@@ -305,7 +305,7 @@ Commit the completed work per ADR-014 (governance skills commit their own work):
|
|
|
305
305
|
- Incident mitigated: `docs(incidents): I<NNN> mitigated — <mitigation summary>`
|
|
306
306
|
- Incident restored: `docs(incidents): I<NNN> restored — <action>`
|
|
307
307
|
- Incident closed: `docs(incidents): close I<NNN>`
|
|
308
|
-
4. If risk is above appetite:
|
|
308
|
+
4. If commit risk is above appetite: this is **framework-mediated, NOT a category-3 one-time-override ask** (P377/RFC-029 amendment 2026-06-24 — there is no incident carve-out). Per **ADR-042 Rule 1b (incident-context scoring)**: an active incident is a risk being realised (Likelihood already 5), so the incident-response change is scored against that live realised-risk baseline — weighing P(the change increases impact) vs P(it reduces impact & likelihood / restores service) + P(it introduces a new incident). If net risk-reducing, it takes the risk-reducing path (the `reducing` bypass — `RISK_BYPASS: reducing`) and proceeds, no ask. This is *better* for restore-service-fast (JTBD-201): a genuine hotfix clears via scoring with no consent gate mid-outage. If the change is NOT net-reducing, auto-remediate per ADR-042 Rule 1 or halt per Rule 5. **MUST NOT commit above appetite; MUST NOT `AskUserQuestion` "commit anyway".** If `AskUserQuestion` is unavailable, the ADR-013 Rule 6 fail-safe applies — skip the commit and report the uncommitted state.
|
|
309
309
|
|
|
310
310
|
### 15. Auto-release when changesets are queued (ADR-020)
|
|
311
311
|
|
|
@@ -1080,7 +1080,7 @@ Commit the completed work per ADR-014 (governance skills commit their own work):
|
|
|
1080
1080
|
- Problem closed: `docs(problems): close P<NNN> <title>`
|
|
1081
1081
|
- Review/re-rank: `docs(problems): review — re-rank priorities`
|
|
1082
1082
|
- Fix implemented: `fix(<scope>): <description> (closes P<NNN>)` — include problem file changes (rename to `.verifying.md` + `## Fix Released` section) in the same commit per ADR-022
|
|
1083
|
-
5. If risk is above appetite:
|
|
1083
|
+
5. If commit risk is above appetite: auto-apply scorer remediations per **ADR-042 Rule 1** incrementally until residual commit risk is within appetite (≤ 4/25), OR halt per ADR-042 Rule 5 if the scorer cannot converge. **The skill MUST NOT commit above appetite, and MUST NOT call `AskUserQuestion` to ask whether to commit anyway** (P377/RFC-029 amendment 2026-06-24 — above-appetite is framework-mediated, never a category-3 one-time-override; same invariant the push/release branch at Step 12 already enforces). The ADR-013 Rule 6 fail-safe (no `AskUserQuestion` available / non-interactive → skip the commit and report the uncommitted state) remains the terminal fallback. This applies only to the risk-above-appetite branch, not to the delegation-unavailable case above.
|
|
1084
1084
|
|
|
1085
1085
|
**Multi-commit slice changeset discipline (P141 Phase 2)**: when a single logical fix lands across multiple ADR-014-grain commits targeting the same plugin (e.g. helper extraction in commit 1, callers wired in commit 2, SKILL note + transition in commit 3 — all `packages/<plugin>/`), author ONE changeset on the first commit in the slice. Subsequent same-plugin commits do NOT need their own changeset — the `itil-changeset-discipline.sh` hook's Check 2b recognises any `.changeset/*.md` (or held-window `docs/changesets-holding/*.md` entry) already in the unpushed slice scope (`origin/<base>..HEAD` + untracked + modified-not-staged) that targets `"@windyroad/<plugin>": <any-bump>` and allows. This eliminates the per-commit changeset ceremony that previously produced N redundant `.changeset/*.md` files for one logical release entry (changesets-action collapses bump-class at version-package time, so per-commit changesets rendered N near-identical CHANGELOG bullets for one release). Once a changeset hits `origin/<base>` (drained at release time), it no longer counts — a fresh changeset is required for the next slice. Cross-plugin coverage is NOT permitted: an `@windyroad/itil` changeset does not satisfy a `packages/voice-tone/` commit.
|
|
1086
1086
|
|
|
@@ -135,6 +135,21 @@ git add docs/rfcs/RFC-<NNN>-<slug>.<new-status>.md
|
|
|
135
135
|
|
|
136
136
|
**P057 staging trap rule**: re-stage explicitly after the Edit tool runs. `git mv` alone stages only the rename, not subsequent content edits.
|
|
137
137
|
|
|
138
|
+
#### Auto-transition detection (P378/RFC-030 Piece 2; ADR-060 line 292)
|
|
139
|
+
|
|
140
|
+
`proposed → in-progress` is **detected** by `itil-commit-trailer-transition-advisory.sh` (PostToolUse:Bash; the shared RFC+story commit-trailer trigger): when the first non-capture commit carrying `Refs: RFC-<NNN>` lands while the RFC is still `proposed`/`accepted`, the hook emits a stderr advisory. Per ADR-014 the hook does NOT perform the `git mv` (that would land outside the commit grain) — this skill (or an AFK orchestrator acting on the advisory) performs the transition. Mirrors the manage-story draft→in-progress trigger; both share the one hook. (This closes the "future commit-trailer-trigger hook (deferred)" that manage-rfc and manage-story both named but never built.)
|
|
141
|
+
|
|
142
|
+
#### Render the `## Commits` section (P378/RFC-030 Piece 1; ADR-085)
|
|
143
|
+
|
|
144
|
+
On every transition AND every `review` (Step 9), render the affected RFC's `## Commits` section from git history — it is a derived view (`git log --grep "Refs: RFC-NNN"`), NOT stored per-commit, so it must be regenerated skill-side (a PostToolUse hook can't write it without breaking the ADR-014 commit grain). Invoke the helper before staging the RFC file:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
wr-itil-update-rfc-commits-section docs/rfcs/RFC-<NNN>-<slug>.<status>.md
|
|
148
|
+
git add docs/rfcs/RFC-<NNN>-<slug>.<status>.md
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`wr-itil-update-rfc-commits-section` is the ADR-049 `$PATH` shim for `packages/itil/scripts/update-rfc-commits-section.sh` (idempotent — a current section is a byte-stable no-op, so the `git add` is a no-op when nothing changed). This closes the never-built ADR-060 item 12; the section is honestly rendered-as-of-this-skill-run, never a false "maintained automatically" claim.
|
|
152
|
+
|
|
138
153
|
#### README refresh on every transition (P062 mirror)
|
|
139
154
|
|
|
140
155
|
After renaming + Editing + `git add`-ing the transitioned RFC file, regenerate `docs/rfcs/README.md` in-place reflecting the new filename set and the transitioned RFC's new Status. Stage the refreshed README with the same commit.
|
|
@@ -116,7 +116,7 @@ For any transition `<from> → <to>`:
|
|
|
116
116
|
|
|
117
117
|
The auto-transition logic fires in two contexts:
|
|
118
118
|
|
|
119
|
-
- **`draft → in-progress`**: when the FIRST commit AFTER the story's capture commit lands with a `Refs: STORY-<NNN>` trailer AND a commit subject NOT prefixed with `feat(itil): capture STORY-`. Detected by
|
|
119
|
+
- **`draft → in-progress`**: when the FIRST commit AFTER the story's capture commit lands with a `Refs: STORY-<NNN>` trailer AND a commit subject NOT prefixed with `feat(itil): capture STORY-`. **Detected by `itil-commit-trailer-transition-advisory.sh`** (PostToolUse:Bash; the shared RFC+story commit-trailer trigger — P378/RFC-030 Piece 2, no longer "a future deferred hook"). Per ADR-014 the hook DETECTS + emits a stderr advisory; it does NOT perform the `git mv` itself (that would land outside the commit grain). The transition is performed by `manage-story <NNN> in-progress` (run on the advisory) or by an AFK orchestrator acting on it.
|
|
120
120
|
|
|
121
121
|
- **`in-progress → done`**: when all `- [ ]` lines in `## Acceptance criteria` are ticked AND the linked RFC is `closed`. Detected at manage-rfc close-fire (the RFC's transition triggers a sweep of its `stories:` array; each in-progress story with all-criteria-ticked auto-transitions to `done`). Manual `manage-story <NNN> done` invocation works in the interim.
|
|
122
122
|
|
|
@@ -168,7 +168,7 @@ Per ADR-014, governance skills commit their own work.
|
|
|
168
168
|
1. `git add` the renamed / modified incident file.
|
|
169
169
|
2. Delegate to `wr-risk-scorer:pipeline` (subagent_type: `wr-risk-scorer:pipeline`) to assess the staged changes and create a bypass marker. If the subagent type is not available (spawned subagent surface), invoke `/wr-risk-scorer:assess-release` via the Skill tool instead — per ADR-015 it wraps the same pipeline subagent.
|
|
170
170
|
3. `git commit -m "docs(incidents): I<NNN> mitigated — <action summary>"`.
|
|
171
|
-
4. If risk is above appetite:
|
|
171
|
+
4. If commit risk is above appetite: this is **framework-mediated, NOT a category-3 one-time-override ask** (P377/RFC-029 — no incident carve-out). Per **ADR-042 Rule 1b (incident-context scoring)**: an active incident is a risk being realised (Likelihood already 5); score the mitigation against that live realised-risk baseline — P(increases impact) vs P(reduces impact & likelihood / restores service) + P(introduces a new incident). Net risk-reducing → it takes the risk-reducing path (`RISK_BYPASS: reducing`) and proceeds, no ask — exactly the restore-service-fast (JTBD-201) case, cleared via scoring not a consent gate. Not net-reducing → auto-remediate (ADR-042 Rule 1) or halt (Rule 5). **MUST NOT commit above appetite; MUST NOT `AskUserQuestion` "commit anyway".** If `AskUserQuestion` is unavailable, ADR-013 Rule 6 fail-safe — skip + report.
|
|
172
172
|
|
|
173
173
|
### 9. Auto-release when changesets are queued (ADR-020)
|
|
174
174
|
|
|
@@ -137,7 +137,7 @@ Per ADR-014, governance skills commit their own work.
|
|
|
137
137
|
1. `git add` the renamed / modified incident file.
|
|
138
138
|
2. Delegate to `wr-risk-scorer:pipeline` (subagent_type: `wr-risk-scorer:pipeline`) to assess the staged changes and create a bypass marker. If the subagent type is not available (spawned subagent surface), invoke `/wr-risk-scorer:assess-release` via the Skill tool instead — per ADR-015 it wraps the same pipeline subagent.
|
|
139
139
|
3. `git commit -m "docs(incidents): I<NNN> restored — <verification signal summary>"`.
|
|
140
|
-
4. If risk is above appetite:
|
|
140
|
+
4. If commit risk is above appetite: **framework-mediated, no ask** (P377/RFC-029 — no incident carve-out). Per **ADR-042 Rule 1b**: score the restore action against the live realised-risk baseline (incident Likelihood already 5) — net risk-reducing actions take the risk-reducing path (`RISK_BYPASS: reducing`) and proceed; otherwise auto-remediate (Rule 1) or halt (Rule 5). **MUST NOT commit above appetite; MUST NOT `AskUserQuestion` "commit anyway".** If `AskUserQuestion` is unavailable, ADR-013 Rule 6 fail-safe — skip the commit and report the uncommitted state.
|
|
141
141
|
|
|
142
142
|
### 9. Auto-release when changesets are queued (ADR-020)
|
|
143
143
|
|
|
@@ -307,7 +307,7 @@ Omit `docs/problems/README-history.md` from the path list when the P134 rotation
|
|
|
307
307
|
- Known Error → Verification Pending (standalone, no fix riding with it): `docs(problems): P<NNN> verification pending — <release marker>`
|
|
308
308
|
- Verification Pending → Closed: `docs(problems): close P<NNN> <title>`
|
|
309
309
|
|
|
310
|
-
If risk is above appetite
|
|
310
|
+
If commit risk is above appetite: auto-apply scorer remediations per **ADR-042 Rule 1** until residual risk is within appetite, OR halt per ADR-042 Rule 5 if the scorer cannot converge. **MUST NOT commit above appetite; MUST NOT call `AskUserQuestion` to ask whether to commit anyway** (P377/RFC-029 — above-appetite is framework-mediated, not a one-time-override). If `AskUserQuestion` is unavailable (AFK), the ADR-013 Rule 6 fail-safe applies as the terminal fallback: skip the commit and report the uncommitted state. This applies only to the risk-above-appetite branch, not to the delegation-unavailable case above.
|
|
311
311
|
|
|
312
312
|
### 9. Report the outcome
|
|
313
313
|
|
|
@@ -212,7 +212,7 @@ ONE commit covers: every surviving renamed ticket file + every Status edit + eve
|
|
|
212
212
|
- Mixed-destination batch: `docs(problems): batch transition — P063 close, P070 known-error, P094 verifying (3 tickets)`
|
|
213
213
|
- If the batch destination is `verifying` and rides with a fix commit, the commit-message scope follows the singular's pattern: `fix(<scope>): <description> (closes P063, P067)` — but the batch surface is unusual in the riding-with-fix shape; the canonical caller is verification-close housekeeping, not fix-release.
|
|
214
214
|
|
|
215
|
-
If risk is above appetite
|
|
215
|
+
If commit risk is above appetite: auto-apply scorer remediations per **ADR-042 Rule 1** until within appetite, OR halt per ADR-042 Rule 5. **MUST NOT commit above appetite; MUST NOT call `AskUserQuestion` to ask whether to commit anyway** (P377/RFC-029 — framework-mediated, not a one-time-override). If `AskUserQuestion` is unavailable (AFK), the ADR-013 Rule 6 fail-safe applies — skip the commit and report the uncommitted state — apply the same rule the singular does at the same gate.
|
|
216
216
|
|
|
217
217
|
### 5. Report the outcome
|
|
218
218
|
|