@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.
@@ -497,5 +497,5 @@
497
497
  }
498
498
  },
499
499
  "name": "wr-itil",
500
- "version": "2.1.8"
500
+ "version": "2.2.0"
501
501
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-itil",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "description": "ITIL problem-management workflows for AI coding agents",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -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 all `gh pr merge` and routes to
27
- # `npm run release:watch` which subsequently runs `npm publish` — at
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "2.1.8",
3
+ "version": "2.2.0-preview.1182",
4
4
  "description": "ITIL-aligned IT service management for Claude Code and Codex",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -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 `MISMATCH`).
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
- # Read-only does NOT mutate the README. The /wr-itil:reconcile-readme
34
- # skill applies edits with narrative-aware preservation; this script's
35
- # only job is to report ground truth.
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 four shapes:
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` is read-only no live README mutation in the script layer (mutation only in this skill's Step 4, via the Edit tool).
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 four shapes:
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` is read-only no live README mutation in the script layer (mutation only in this skill's Step 4, via the Edit tool).
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).