@windyroad/itil 2.1.8 → 2.2.0-preview.1182
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/.codex-plugin/plugin.json +1 -1
- package/hooks/pre-publish-intake-gate.sh +11 -3
- package/package.json +1 -1
- package/scripts/classify-readme-drift.sh +22 -1
- package/scripts/reconcile-readme.sh +203 -3
- package/skills/reconcile-readme/SKILL.md +27 -2
- package/skills-codex/reconcile-readme/SKILL.md +27 -2
|
@@ -23,9 +23,17 @@
|
|
|
23
23
|
#
|
|
24
24
|
# This hook composes with risk-scorer's git-push-gate.sh: both fire on
|
|
25
25
|
# Bash with `gh pr merge` matchers, and either may deny independently.
|
|
26
|
-
# In practice git-push-gate denies
|
|
27
|
-
# `npm run release:watch
|
|
28
|
-
# which point this hook fires.
|
|
26
|
+
# In practice git-push-gate denies `gh pr merge` of the changeset release
|
|
27
|
+
# PR and routes to `npm run release:watch`, which subsequently runs
|
|
28
|
+
# `npm publish` — at which point this hook fires. An ordinary
|
|
29
|
+
# feature-branch merge is denied by neither gate.
|
|
30
|
+
#
|
|
31
|
+
# The two hooks identify the release PR differently: this one greps the
|
|
32
|
+
# command text for `changeset-release/`, while git-push-gate resolves the
|
|
33
|
+
# PR's head branch through `gh pr view`. So a `gh pr merge <number>` or a
|
|
34
|
+
# bare `gh pr merge` against the release PR is caught there and not here,
|
|
35
|
+
# because the command text names no branch. No gap opens today — the
|
|
36
|
+
# push gate denies first — but the divergence is worth knowing.
|
|
29
37
|
|
|
30
38
|
set -euo pipefail
|
|
31
39
|
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# <drift-stdout-file>: path to a file containing the captured stdout of
|
|
14
14
|
# `reconcile-readme.sh` (one structured drift line per row — `DRIFT`,
|
|
15
|
-
# `MISSING`, `STALE`, or `
|
|
15
|
+
# `MISSING`, `STALE`, `MISMATCH`, or `CLASH`).
|
|
16
16
|
#
|
|
17
17
|
# <problems-dir>: defaults to ./docs/problems. Used by `git status
|
|
18
18
|
# --porcelain` to scope the staged-rename probe.
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
# committed cross-session drift OR
|
|
28
28
|
# mixed; route to
|
|
29
29
|
# /wr-itil:reconcile-readme.
|
|
30
|
+
# HALT_ROUTE_RECONCILE clash=<N> — <N> IDs are each claimed by more
|
|
31
|
+
# than one ticket file. Never
|
|
32
|
+
# deferrable: no working-tree
|
|
33
|
+
# rename can repair a clash, and
|
|
34
|
+
# the drift rows it causes describe
|
|
35
|
+
# the wrong file. Route to
|
|
36
|
+
# /wr-itil:reconcile-readme, whose
|
|
37
|
+
# --fix-clashes repairs it (duplicate-ticket-ids-are-silently-swallowed-by-the-readme-reconciler-problem).
|
|
30
38
|
#
|
|
31
39
|
# Exit codes:
|
|
32
40
|
# 0 = INLINE_REFRESH
|
|
@@ -68,6 +76,19 @@ fi
|
|
|
68
76
|
# MISSING P<NNN> verification-queue: ...
|
|
69
77
|
# STALE P<NNN> verification-queue: ...
|
|
70
78
|
# MISMATCH P<NNN> closed: ...
|
|
79
|
+
# CLASH P<NNN> <state>/<basename> <state>/<basename>
|
|
80
|
+
#
|
|
81
|
+
# A CLASH short-circuits ahead of the rename-coverage probe below. It is a
|
|
82
|
+
# structural defect — two tickets holding one number — and no rename in the
|
|
83
|
+
# working tree can cover it. Left to the probe, a clashing ID that happened
|
|
84
|
+
# to be a rename destination would classify as INLINE_REFRESH and the clash
|
|
85
|
+
# would vanish exactly as it does today (duplicate-ticket-ids-are-silently-swallowed-by-the-readme-reconciler-problem).
|
|
86
|
+
CLASH_COUNT="$(grep -cE '^CLASH' "$DRIFT_FILE" 2>/dev/null || true)"
|
|
87
|
+
if [ "${CLASH_COUNT:-0}" -gt 0 ]; then
|
|
88
|
+
echo "HALT_ROUTE_RECONCILE clash=${CLASH_COUNT}"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
71
92
|
DRIFT_IDS="$(grep -oE 'P[0-9]{3}' "$DRIFT_FILE" 2>/dev/null | sort -u)"
|
|
72
93
|
|
|
73
94
|
if [ -z "$DRIFT_IDS" ]; then
|
|
@@ -18,24 +18,72 @@
|
|
|
18
18
|
# the per-state subdir wins — problem-ticket-directory-layout-per-state-subdirectories-under-docs-problems-architecture-rule §"Authoritative state signal"
|
|
19
19
|
# treats subdirectory as the post-migration ground truth.
|
|
20
20
|
#
|
|
21
|
+
# Usage with the repair flag:
|
|
22
|
+
# reconcile-readme.sh --fix-clashes [<problems-dir>]
|
|
23
|
+
#
|
|
21
24
|
# Exit codes:
|
|
22
25
|
# 0 = clean (README matches filesystem)
|
|
23
26
|
# 1 = drift detected (structured diff to stdout)
|
|
24
27
|
# 2 = parse error (README missing or malformed)
|
|
25
28
|
#
|
|
29
|
+
# With --fix-clashes, detection re-runs after the renumber and the exit
|
|
30
|
+
# code reports what survives: 0 when the clash was the only drift, 1 when
|
|
31
|
+
# ordinary drift remains. So a caller branching on 1 learns what is
|
|
32
|
+
# actually left rather than reading a stale 1.
|
|
33
|
+
#
|
|
26
34
|
# Output format on drift (one line per drift entry, ≤ 150 bytes per
|
|
27
35
|
# progressive-disclosure-once-per-session-budget-for-userpromptsubmit-governance-prose-architecture-rule progressive-disclosure budget):
|
|
28
36
|
# DRIFT <ID> wsjf-rankings: claims=<status> actual=<status>
|
|
29
37
|
# MISSING <ID> wsjf-rankings: actual=<status> file=<basename>
|
|
30
38
|
# STALE <ID> verification-queue: actual=<status>
|
|
31
39
|
# MISMATCH <ID> closed: actual=<status>
|
|
40
|
+
# CLASH <ID> <state>/<basename> <state>/<basename>
|
|
41
|
+
#
|
|
42
|
+
# Read-only by default — does NOT mutate the README. The
|
|
43
|
+
# /wr-itil:reconcile-readme skill applies README edits with
|
|
44
|
+
# narrative-aware preservation; this script's job is to report ground
|
|
45
|
+
# truth.
|
|
46
|
+
#
|
|
47
|
+
# --fix-clashes is the one exception: it moves the later-claiming ticket
|
|
48
|
+
# file to a free ID and rewrites references to it. It never touches the
|
|
49
|
+
# README, so the narrative-preservation contract above is intact.
|
|
32
50
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
51
|
+
# ── Reference-rewrite boundary (--fix-clashes) ──────────────────────────────
|
|
52
|
+
#
|
|
53
|
+
# Rewritten: docs/problems/**/*.md and docs/stories/**/*.md. Scoped to
|
|
54
|
+
# *.md so ID-keyed machine state (.outbound-responses-cache.json,
|
|
55
|
+
# .upstream-cache.json) is never touched.
|
|
56
|
+
#
|
|
57
|
+
# Reported, never rewritten — every other file under docs/ naming the ID,
|
|
58
|
+
# for three distinct reasons:
|
|
59
|
+
#
|
|
60
|
+
# docs/decisions/** A ratified decision body changes only by
|
|
61
|
+
# supersession (ratified-decisions-change-only-by-supersession-architecture-rule).
|
|
62
|
+
# docs/story-maps/** The grid is generated from its JSON island; a
|
|
63
|
+
# text rewrite desynchronises the render and the
|
|
64
|
+
# oversight fingerprint (story-maps-render-from-json-through-a-canonical-template-architecture-rule, a-story-map-card-stores-no-value-a-story-file-already-carries-architecture-rule,
|
|
65
|
+
# the-grid-ships-in-the-file-a-map-is-readable-with-no-script-engine-architecture-rule). Edit via story-map-edit and re-render.
|
|
66
|
+
# docs/jtbd/** Jobs and personas carry a ratification marker
|
|
67
|
+
# whose integrity depends on edits being
|
|
68
|
+
# human-confirmed (jtbd-persona-human-oversight-marker-wr-jtbd-confirm-jobs-and-personas-drain-sibling-of-architecture-rule, a-ratification-marker-can-only-be-written-when-someone-actually-ratified-architecture-rule). NOT excluded
|
|
69
|
+
# as historical — this is a live surface.
|
|
70
|
+
# docs/retros/** Each entry records what was true when written,
|
|
71
|
+
# docs/incidents/** so re-pointing its identifiers rewrites history
|
|
72
|
+
# docs/audits/** rather than repairing it.
|
|
73
|
+
# docs/briefing/**
|
|
74
|
+
# docs/risks/** Live surfaces, neither historical nor
|
|
75
|
+
# docs/plans/** ratification-bearing. Out of scope for this
|
|
76
|
+
# docs/rfcs/** change; widening the rewrite set to them is a
|
|
77
|
+
# separate decision.
|
|
36
78
|
#
|
|
37
79
|
# @problem docs-problems-readme-md-drifts-from-filesystem-truth-across-sessions-despite-refresh-on-create-and-refresh-on-transition-both-closed-problem
|
|
38
80
|
# @problem problem-tickets-strain-as-fixes-decompose-into-multiple-coordinated-changes-need-an-rfc-framework-that-ties-all-changes-back-to-problems-and-unifies-technical-with-user-business-problems-problem (docs-problems-flat-layout-migration-per-state-subdirs-adopter-auto-migration-release-design — dual-tolerant migration window)
|
|
81
|
+
# @problem duplicate-ticket-ids-are-silently-swallowed-by-the-readme-reconciler-problem (duplicate ticket IDs silently swallowed by the ID-keyed map)
|
|
82
|
+
# @story see-the-id-clash-instead-of-the-drift-it-causes-delivery-story (See the ID clash instead of the drift it causes)
|
|
83
|
+
# @adr afk-orchestrator-preflight-get-the-repo-into-a-clean-state-before-starting-architecture-rule (next-ID allocation is max(local, origin) + 1)
|
|
84
|
+
# @adr ratified-decisions-change-only-by-supersession-architecture-rule (ratified decisions change only by supersession)
|
|
85
|
+
# @adr story-maps-render-from-json-through-a-canonical-template-architecture-rule / a-story-map-card-stores-no-value-a-story-file-already-carries-architecture-rule / the-grid-ships-in-the-file-a-map-is-readable-with-no-script-engine-architecture-rule (story maps render from their island)
|
|
86
|
+
# @adr jtbd-persona-human-oversight-marker-wr-jtbd-confirm-jobs-and-personas-drain-sibling-of-architecture-rule / a-ratification-marker-can-only-be-written-when-someone-actually-ratified-architecture-rule (jobs and personas carry a ratification marker)
|
|
39
87
|
# @problem reconcile-readme-false-positive-parses-inbound-upstream-reports-matched-local-ticket-column-as-verification-queue-problem (Inbound Upstream Reports rows mis-attributed to VQ)
|
|
40
88
|
# @adr governance-skills-commit-their-own-completed-work-architecture-rule (Reconciliation as preflight robustness layer)
|
|
41
89
|
# @adr problem-lifecycle-add-a-verification-pending-status-between-known-error-and-closed-architecture-rule (Verification Pending lifecycle excludes from WSJF Rankings)
|
|
@@ -49,8 +97,23 @@
|
|
|
49
97
|
|
|
50
98
|
set -uo pipefail
|
|
51
99
|
|
|
100
|
+
FIX_CLASHES=0
|
|
101
|
+
if [ "${1:-}" = "--fix-clashes" ]; then
|
|
102
|
+
FIX_CLASHES=1
|
|
103
|
+
shift
|
|
104
|
+
fi
|
|
105
|
+
|
|
52
106
|
PROBLEMS_DIR="${1:-docs/problems}"
|
|
53
107
|
README="${PROBLEMS_DIR}/README.md"
|
|
108
|
+
# The docs root, for the --fix-clashes report scan. Only trusted when the
|
|
109
|
+
# tickets really do live in `<docs>/problems`; otherwise the parent is
|
|
110
|
+
# some unrelated directory (a test fixture, a temp dir) and scanning it
|
|
111
|
+
# would sweep the whole filesystem around it.
|
|
112
|
+
if [ "$(basename "$PROBLEMS_DIR")" = "problems" ]; then
|
|
113
|
+
DOCS_DIR="$(dirname "$PROBLEMS_DIR")"
|
|
114
|
+
else
|
|
115
|
+
DOCS_DIR="$PROBLEMS_DIR"
|
|
116
|
+
fi
|
|
54
117
|
|
|
55
118
|
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
56
119
|
|
|
@@ -71,6 +134,15 @@ fi
|
|
|
71
134
|
# migration race; per-state is the migration target per problem-ticket-directory-layout-per-state-subdirectories-under-docs-problems-architecture-rule).
|
|
72
135
|
|
|
73
136
|
declare -A FS_STATUS
|
|
137
|
+
# Every file that claimed each ID, newline-joined, kept per layout half.
|
|
138
|
+
# FS_STATUS is keyed by ID and so is last-writer-wins: correct for the
|
|
139
|
+
# cross-layout migration race it was built for, wrong for two genuinely
|
|
140
|
+
# distinct tickets — the second disappears and the drift check then
|
|
141
|
+
# blames whichever survived (duplicate-ticket-ids-are-silently-swallowed-by-the-readme-reconciler-problem). Keeping the halves apart lets a
|
|
142
|
+
# flat+subdir pair stay the migration transient it is while two files in
|
|
143
|
+
# the SAME half are reported as the clash they are.
|
|
144
|
+
declare -A FLAT_FILES
|
|
145
|
+
declare -A SUB_FILES
|
|
74
146
|
shopt -s nullglob
|
|
75
147
|
# Flat layout: docs/problems/<NNN>-<title>.<state>.md
|
|
76
148
|
# Status classified from filename suffix.
|
|
@@ -94,6 +166,7 @@ for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
|
94
166
|
*) continue ;;
|
|
95
167
|
esac
|
|
96
168
|
FS_STATUS["$id"]="$ticket_status"
|
|
169
|
+
FLAT_FILES["$id"]+="$f"$'\n'
|
|
97
170
|
done
|
|
98
171
|
# Per-state subdir layout: docs/problems/<state>/<NNN>-<title>.md
|
|
99
172
|
# Status derived from parent directory name (the subdirectory IS the
|
|
@@ -105,10 +178,19 @@ for ticket_status in open known-error verifying closed parked; do
|
|
|
105
178
|
num="${base%%-*}"
|
|
106
179
|
id="P${num}"
|
|
107
180
|
FS_STATUS["$id"]="$ticket_status"
|
|
181
|
+
SUB_FILES["$id"]+="$f"$'\n'
|
|
108
182
|
done
|
|
109
183
|
done
|
|
110
184
|
shopt -u nullglob
|
|
111
185
|
|
|
186
|
+
# Per-state wins on cross-layout collision, so an ID present in both
|
|
187
|
+
# halves is the problem-ticket-directory-layout-per-state-subdirectories-under-docs-problems-architecture-rule migration transient and not a clash. Collapse to
|
|
188
|
+
# the winning half; a clash is then simply more than one file left.
|
|
189
|
+
declare -A FS_FILES
|
|
190
|
+
for id in "${!FLAT_FILES[@]}" "${!SUB_FILES[@]}"; do
|
|
191
|
+
FS_FILES["$id"]="${SUB_FILES[$id]:-${FLAT_FILES[$id]}}"
|
|
192
|
+
done
|
|
193
|
+
|
|
112
194
|
# ── Parse README sections into ID buckets ───────────────────────────────────
|
|
113
195
|
# We use the section-header line numbers to slice the file into ranges.
|
|
114
196
|
|
|
@@ -234,6 +316,124 @@ for id in "${!FS_STATUS[@]}"; do
|
|
|
234
316
|
esac
|
|
235
317
|
done
|
|
236
318
|
|
|
319
|
+
# (5) An ID claimed by more than one file is a clash, not drift. It is
|
|
320
|
+
# reported ahead of everything else because the drift rows it causes
|
|
321
|
+
# describe the wrong file (duplicate-ticket-ids-are-silently-swallowed-by-the-readme-reconciler-problem).
|
|
322
|
+
|
|
323
|
+
# `<state>/<basename>`, basename truncated to 40 chars, so the row stays
|
|
324
|
+
# inside the per-row budget with this repo's long ticket filenames.
|
|
325
|
+
clash_label() {
|
|
326
|
+
local path="$1" base parent
|
|
327
|
+
base="$(basename "$path")"
|
|
328
|
+
parent="$(basename "$(dirname "$path")")"
|
|
329
|
+
[ ${#base} -gt 40 ] && base="${base:0:40}..."
|
|
330
|
+
printf '%s/%s' "$parent" "$base"
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
CLASH_IDS=()
|
|
334
|
+
for id in "${!FS_FILES[@]}"; do
|
|
335
|
+
mapfile -t claimants < <(printf '%s' "${FS_FILES[$id]}" | grep -v '^$')
|
|
336
|
+
[ "${#claimants[@]}" -gt 1 ] || continue
|
|
337
|
+
CLASH_IDS+=("$id")
|
|
338
|
+
DRIFT_LINES+=("CLASH ${id} $(clash_label "${claimants[0]}") $(clash_label "${claimants[1]}")")
|
|
339
|
+
done
|
|
340
|
+
|
|
341
|
+
# ── Repair (--fix-clashes only) ─────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
if [ "$FIX_CLASHES" = "1" ] && [ "${#CLASH_IDS[@]}" -gt 0 ]; then
|
|
344
|
+
# afk-orchestrator-preflight-get-the-repo-into-a-clean-state-before-starting-architecture-rule allocates against origin as well as the working tree, so a
|
|
345
|
+
# renumber cannot land on an ID created on the remote and recreate the
|
|
346
|
+
# clash it is repairing. The base ref is resolved rather than assumed:
|
|
347
|
+
# an adopter's default branch may not be `main`, and assuming it is the
|
|
348
|
+
# same class of defect as the gates hardcoded to the home-repo shape.
|
|
349
|
+
BASE_REF="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" || BASE_REF=""
|
|
350
|
+
BASE_REF="${BASE_REF#origin/}"
|
|
351
|
+
[ -n "$BASE_REF" ] || BASE_REF="main"
|
|
352
|
+
|
|
353
|
+
local_max=$(ls "$PROBLEMS_DIR"/*.md "$PROBLEMS_DIR"/*/*.md 2>/dev/null \
|
|
354
|
+
| sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1)
|
|
355
|
+
origin_max=$(git ls-tree -r --name-only "origin/${BASE_REF}" "$PROBLEMS_DIR/" 2>/dev/null \
|
|
356
|
+
| sed 's|.*/||' | grep -oE '^[0-9]+' | sort -n | tail -1)
|
|
357
|
+
if [ -z "$origin_max" ]; then
|
|
358
|
+
echo "NOTE no origin/${BASE_REF} — allocating from the working tree only, ordering by mtime"
|
|
359
|
+
fi
|
|
360
|
+
next=$(( 10#$(printf '%s\n%s\n' "${local_max:-0}" "${origin_max:-0}" | sort -n | tail -1) ))
|
|
361
|
+
|
|
362
|
+
# First-add commit on the resolved base ref decides who claimed the
|
|
363
|
+
# number first. Outside a repository (or for a file never committed)
|
|
364
|
+
# this yields nothing and mtime stands in.
|
|
365
|
+
first_seen() {
|
|
366
|
+
local path="$1" t
|
|
367
|
+
t=$(git log --diff-filter=A --format=%at "$BASE_REF" -- "$path" 2>/dev/null | tail -1)
|
|
368
|
+
[ -n "$t" ] || t=$(git log --diff-filter=A --format=%at -- "$path" 2>/dev/null | tail -1)
|
|
369
|
+
# GNU stat first: on a Mac with coreutils installed, `-f` means
|
|
370
|
+
# "filesystem status" and happily prints a block-count dump instead
|
|
371
|
+
# of failing, which then gets treated as a timestamp.
|
|
372
|
+
[ -n "$t" ] || t=$(stat -c %Y "$path" 2>/dev/null)
|
|
373
|
+
[ -n "$t" ] || t=$(stat -f %m "$path" 2>/dev/null)
|
|
374
|
+
printf '%s' "${t:-0}"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for id in "${CLASH_IDS[@]}"; do
|
|
378
|
+
mapfile -t claimants < <(printf '%s' "${FS_FILES[$id]}" | grep -v '^$')
|
|
379
|
+
# Earliest claimant keeps the number; everyone after it is renumbered.
|
|
380
|
+
mapfile -t ordered < <(
|
|
381
|
+
for c in "${claimants[@]}"; do printf '%s\t%s\n' "$(first_seen "$c")" "$c"; done \
|
|
382
|
+
| sort -n -k1,1 | cut -f2-
|
|
383
|
+
)
|
|
384
|
+
for loser in "${ordered[@]:1}"; do
|
|
385
|
+
[ -f "$loser" ] || continue
|
|
386
|
+
next=$(( next + 1 ))
|
|
387
|
+
new_num=$(printf '%03d' "$next")
|
|
388
|
+
old_num="${id#P}"
|
|
389
|
+
new_id="P${new_num}"
|
|
390
|
+
dir="$(dirname "$loser")"
|
|
391
|
+
new_path="${dir}/${new_num}-$(basename "$loser" | cut -d- -f2-)"
|
|
392
|
+
|
|
393
|
+
if git ls-files --error-unmatch "$loser" >/dev/null 2>&1; then
|
|
394
|
+
git mv "$loser" "$new_path"
|
|
395
|
+
else
|
|
396
|
+
mv "$loser" "$new_path"
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
# perl, not sed: BSD sed has no `\b`, and without a word boundary
|
|
400
|
+
# renumbering P23 would also rewrite phase-3b-readme-badge-renderer-wr-itil-plugin-maturity-render-advisory-drift-detector-check-plugin-maturity-drift-sh-problem.
|
|
401
|
+
rewrite_id_in() {
|
|
402
|
+
perl -pi -e "s/\\bP${old_num}\\b/${new_id}/g; s/\\b(Problem|problem) ${old_num}\\b/\$1 ${new_num}/g" "$1"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# The ticket's own references follow it — unambiguous, they are
|
|
406
|
+
# inside the file being renumbered.
|
|
407
|
+
rewrite_id_in "$new_path"
|
|
408
|
+
|
|
409
|
+
# A story the ticket names in its `## Stories` section, and which
|
|
410
|
+
# names the ticket back, is the one foreign reference that resolves
|
|
411
|
+
# without guessing: the two-way link says which of the two
|
|
412
|
+
# claimants it meant. Every other reference to the old ID is
|
|
413
|
+
# ambiguous by construction — both tickets carried it — so it is
|
|
414
|
+
# reported below rather than guessed at.
|
|
415
|
+
while IFS= read -r story_id; do
|
|
416
|
+
story_file=$(grep -rl "^# ${story_id}:" --include='*.md' "$DOCS_DIR/stories" 2>/dev/null | head -1)
|
|
417
|
+
[ -n "$story_file" ] || continue
|
|
418
|
+
grep -qE "\\bP${old_num}\\b" "$story_file" || continue
|
|
419
|
+
rewrite_id_in "$story_file"
|
|
420
|
+
echo "RETRACE ${new_id} in ${story_file#"$DOCS_DIR"/}"
|
|
421
|
+
done < <(sed -n '/^## Stories/,/^## /p' "$new_path" | grep -oE '\bSTORY-[0-9]+\b' | sort -u)
|
|
422
|
+
|
|
423
|
+
echo "RENUMBER ${id} -> ${new_id} $(clash_label "$new_path")"
|
|
424
|
+
|
|
425
|
+
# Everything else under docs/ that names the old ID is reported,
|
|
426
|
+
# never rewritten — see the boundary in this file's docblock.
|
|
427
|
+
while IFS= read -r stranded; do
|
|
428
|
+
echo "MANUAL ${id} still named in ${stranded#"$DOCS_DIR"/} — ambiguous, both claimants held it"
|
|
429
|
+
done < <(grep -rl -E "\\bP${old_num}\\b" "$DOCS_DIR" 2>/dev/null | sort)
|
|
430
|
+
done
|
|
431
|
+
done
|
|
432
|
+
|
|
433
|
+
# Re-run detection so the exit code reports what actually survives.
|
|
434
|
+
exec "$0" "$PROBLEMS_DIR"
|
|
435
|
+
fi
|
|
436
|
+
|
|
237
437
|
# ── Report ──────────────────────────────────────────────────────────────────
|
|
238
438
|
|
|
239
439
|
if [ ${#DRIFT_LINES[@]} -eq 0 ]; then
|
|
@@ -51,9 +51,34 @@ Exit codes:
|
|
|
51
51
|
- `1` — drift detected. The script prints one structured row per drift entry to stdout (each ≤ 150 bytes per the "Progressive disclosure + once-per-session budget for UserPromptSubmit governance prose" architecture rule progressive-disclosure budget). Continue to Step 2.
|
|
52
52
|
- `2` — parse error. README is missing, malformed, or section headers are absent. Halt with the parse-error message; this is a deeper repair that needs investigation, not mechanical reconciliation. In AFK mode (the "Structured User Interaction for Governance-Skill Decisions" architecture rule Rule 6), halt-with-report; do not attempt edits.
|
|
53
53
|
|
|
54
|
+
### Step 1a. Repair any ID clash before reading the rest of the drift
|
|
55
|
+
|
|
56
|
+
If the output carries one or more `CLASH` rows, two tickets are claiming one number. Nothing below can be trusted until that is repaired: the filesystem-truth map is keyed by ID, so one claimant was dropped and the `DRIFT` / `STALE` / `MISSING` rows alongside it describe the wrong file.
|
|
57
|
+
|
|
58
|
+
This is mechanical — the framework has already decided who keeps the number (the earlier claimant, by first-add commit on the base ref) and what the other becomes (`max(local, origin) + 1`). Do NOT ask. Re-run with the repair flag:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
wr-itil-reconcile-readme --fix-clashes docs/problems
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The renumber moves the later claimant, rewrites its own references, and follows the two-way link to any story it names. It then re-runs detection, so its exit code and remaining rows describe what actually survives — continue from there.
|
|
65
|
+
|
|
66
|
+
Two kinds of line come back alongside the drift:
|
|
67
|
+
|
|
68
|
+
- `RENUMBER P<old> -> P<new> <path>` — what moved. Relay it; the number people have been using has changed.
|
|
69
|
+
- `MANUAL P<old> still named in <path>` — a reference the repair would have had to guess at, since both claimants held that number. Surface every one of them for a person to resolve. Do NOT rewrite them, do NOT offer to rewrite them, and do NOT bulk search-and-replace the old ID anywhere. Say why, in plain words, for whichever trees the rows name:
|
|
70
|
+
|
|
71
|
+
| Where the reference lives | Why it is left alone |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `docs/decisions/` | A ratified decision changes only by supersession. Its body records what was decided; a script editing it in place is the one edit that rule forbids. |
|
|
74
|
+
| `docs/story-maps/` | The grid is generated from the map's own data island and carries an oversight fingerprint. Rewriting the rendered HTML desynchronises the two and leaves the map reading as ratified against content nobody re-confirmed. Change it through `story-map-edit` and re-render. |
|
|
75
|
+
| `docs/jtbd/` | Jobs and personas carry a ratification marker whose integrity depends on edits being human-confirmed. A script rewrite moves content underneath a marker nobody re-confirmed. |
|
|
76
|
+
| `docs/retros/`, `docs/incidents/`, `docs/audits/`, `docs/briefing/` | Each entry records what was true when it was written. Re-pointing its identifiers rewrites history rather than repairing it. |
|
|
77
|
+
| `docs/risks/`, `docs/plans/`, `docs/rfcs/` | Live surfaces, but out of scope for this repair. Widening it to them is a separate decision, not a call to make here. |
|
|
78
|
+
|
|
54
79
|
### Step 2. Bucket the drift entries by section
|
|
55
80
|
|
|
56
|
-
Each drift line is one of
|
|
81
|
+
Each drift line is one of five shapes (`CLASH` is handled at Step 1a, before this bucketing):
|
|
57
82
|
|
|
58
83
|
| Marker | Meaning | Required edit |
|
|
59
84
|
|--------|---------|---------------|
|
|
@@ -143,7 +168,7 @@ The "Structured User Interaction for Governance-Skill Decisions" architecture ru
|
|
|
143
168
|
## Confirmation
|
|
144
169
|
|
|
145
170
|
This skill's contract holds when:
|
|
146
|
-
1. The script `packages/itil/scripts/reconcile-readme.sh`
|
|
171
|
+
1. The script `packages/itil/scripts/reconcile-readme.sh` never mutates the README — that happens only in this skill's Step 4, via the Edit tool. Its one write path is `--fix-clashes`, which moves a clashing ticket file and rewrites references to it, and touches no README.
|
|
147
172
|
2. Each agent-applied edit preserves the README's narrative content (prose paragraph at top, Closed section free text).
|
|
148
173
|
3. After Step 4 + Step 5, a re-run of the script reports exit 0 (clean).
|
|
149
174
|
4. The reconciled README rides a single commit (Step 6 single-purpose commit) regardless of invocation mode — interactive and AFK behave identically per the "Governance Skills Commit Their Own Completed Work" architecture rule governance-skill commit contract (the "Skill contract "interactive vs AFK" commit-gating anti-pattern contradicts" problem).
|
|
@@ -62,9 +62,34 @@ Exit codes:
|
|
|
62
62
|
- `1` — drift detected. The script prints one structured row per drift entry to stdout (each ≤ 150 bytes per the "Progressive disclosure + once-per-session budget for UserPromptSubmit governance prose" architecture rule progressive-disclosure budget). Continue to Step 2.
|
|
63
63
|
- `2` — parse error. README is missing, malformed, or section headers are absent. Halt with the parse-error message; this is a deeper repair that needs investigation, not mechanical reconciliation. In AFK mode (the "Structured User Interaction for Governance-Skill Decisions" architecture rule Rule 6), halt-with-report; do not attempt edits.
|
|
64
64
|
|
|
65
|
+
### Step 1a. Repair any ID clash before reading the rest of the drift
|
|
66
|
+
|
|
67
|
+
If the output carries one or more `CLASH` rows, two tickets are claiming one number. Nothing below can be trusted until that is repaired: the filesystem-truth map is keyed by ID, so one claimant was dropped and the `DRIFT` / `STALE` / `MISSING` rows alongside it describe the wrong file.
|
|
68
|
+
|
|
69
|
+
This is mechanical — the framework has already decided who keeps the number (the earlier claimant, by first-add commit on the base ref) and what the other becomes (`max(local, origin) + 1`). Do NOT ask. Re-run with the repair flag:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
<itil-plugin-root>/bin/wr-itil-reconcile-readme --fix-clashes docs/problems
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The renumber moves the later claimant, rewrites its own references, and follows the two-way link to any story it names. It then re-runs detection, so its exit code and remaining rows describe what actually survives — continue from there.
|
|
76
|
+
|
|
77
|
+
Two kinds of line come back alongside the drift:
|
|
78
|
+
|
|
79
|
+
- `RENUMBER P<old> -> P<new> <path>` — what moved. Relay it; the number people have been using has changed.
|
|
80
|
+
- `MANUAL P<old> still named in <path>` — a reference the repair would have had to guess at, since both claimants held that number. Surface every one of them for a person to resolve. Do NOT rewrite them, do NOT offer to rewrite them, and do NOT bulk search-and-replace the old ID anywhere. Say why, in plain words, for whichever trees the rows name:
|
|
81
|
+
|
|
82
|
+
| Where the reference lives | Why it is left alone |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `docs/decisions/` | A ratified decision changes only by supersession. Its body records what was decided; a script editing it in place is the one edit that rule forbids. |
|
|
85
|
+
| `docs/story-maps/` | The grid is generated from the map's own data island and carries an oversight fingerprint. Rewriting the rendered HTML desynchronises the two and leaves the map reading as ratified against content nobody re-confirmed. Change it through `story-map-edit` and re-render. |
|
|
86
|
+
| `docs/jtbd/` | Jobs and personas carry a ratification marker whose integrity depends on edits being human-confirmed. A script rewrite moves content underneath a marker nobody re-confirmed. |
|
|
87
|
+
| `docs/retros/`, `docs/incidents/`, `docs/audits/`, `docs/briefing/` | Each entry records what was true when it was written. Re-pointing its identifiers rewrites history rather than repairing it. |
|
|
88
|
+
| `docs/risks/`, `docs/plans/`, `docs/rfcs/` | Live surfaces, but out of scope for this repair. Widening it to them is a separate decision, not a call to make here. |
|
|
89
|
+
|
|
65
90
|
### Step 2. Bucket the drift entries by section
|
|
66
91
|
|
|
67
|
-
Each drift line is one of
|
|
92
|
+
Each drift line is one of five shapes (`CLASH` is handled at Step 1a, before this bucketing):
|
|
68
93
|
|
|
69
94
|
| Marker | Meaning | Required edit |
|
|
70
95
|
|--------|---------|---------------|
|
|
@@ -154,7 +179,7 @@ The "Structured User Interaction for Governance-Skill Decisions" architecture ru
|
|
|
154
179
|
## Confirmation
|
|
155
180
|
|
|
156
181
|
This skill's contract holds when:
|
|
157
|
-
1. The script `<itil-plugin-root>/scripts/reconcile-readme.sh`
|
|
182
|
+
1. The script `<itil-plugin-root>/scripts/reconcile-readme.sh` never mutates the README — that happens only in this skill's Step 4, via the Edit tool. Its one write path is `--fix-clashes`, which moves a clashing ticket file and rewrites references to it, and touches no README.
|
|
158
183
|
2. Each agent-applied edit preserves the README's narrative content (prose paragraph at top, Closed section free text).
|
|
159
184
|
3. After Step 4 + Step 5, a re-run of the script reports exit 0 (clean).
|
|
160
185
|
4. The reconciled README rides a single commit (Step 6 single-purpose commit) regardless of invocation mode — interactive and AFK behave identically per the "Governance Skills Commit Their Own Completed Work" architecture rule governance-skill commit contract (the "Skill contract "interactive vs AFK" commit-gating anti-pattern contradicts" problem).
|