@windyroad/itil 0.47.12-preview.593 → 0.47.12-preview.598

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.47.12-preview.593",
3
+ "version": "0.47.12-preview.598",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -509,10 +509,22 @@ rm -f "$ITER_JSON"
509
509
 
510
510
  **Iteration prompt body (self-contained — the subprocess has no prior conversation context):**
511
511
 
512
+ **Re-ground per iter (P211 — orchestrator-side construction invariant)**: each iter's prompt body MUST be re-grounded per iter against the CURRENT ticket's identity (ID + title) only. The orchestrator does NOT inline the target ticket's `## Fix Strategy` section verbatim into the dispatch prompt — the subprocess reads Fix Strategy from disk via `/wr-itil:manage-problem` inside its own context, where the design rationale travels with the ticket file and stays anchored to the correct ticket. Across iterations, no prior-iter content leaks into iter N's prompt body — specifically, prior ticket ID, prior Fix Strategy text, prior outcome reason, prior commit SHA, prior retro findings, and prior outstanding-question entries MUST NOT carry across the iter boundary into the new prompt. The construction is template-driven and reset per iter; no global accumulator carries from iter to iter. The "self-contained" opener above is a subprocess-side property (the subprocess has no prior conversation context); the re-grounding invariant is the symmetric orchestrator-side property (the orchestrator main turn does not carry prior-iter prompt content into the next iter's dispatch construction). P211 reported as inbound from downstream consumer bbstats as their P194 — without this invariant, an iter inherits a stale design-rationale frame and may land fixes anchored on the wrong ticket's intent, degrading the JTBD-006 audit trail. **`@jtbd JTBD-006`** (load-bearing).
513
+
512
514
  1. **Context**: this is one iteration of the AFK work-problems loop. The user is AFK. The orchestrator selected `P<NNN> (<title>)` as the highest-WSJF actionable ticket.
513
515
  2. **Task**: apply the `/wr-itil:manage-problem` workflow for `work highest WSJF problem that can be progressed non-interactively as the user is AFK`. Follow manage-problem SKILL.md verbatim, including architect / jtbd / style-guide / voice-tone gate reviews and the commit gate (manage-problem Step 11). Because this subprocess has the Agent tool in its own surface, the normal review-via-subagent paths work — no inline-verdict fallback needed.
514
516
  3. **Constraints**: commit the completed work per ADR-014. Do NOT push, do NOT run `push:watch`, do NOT run `release:watch` — the orchestrator's Step 6.5 owns release cadence. Do NOT invoke `capture-*` background skills mid-iter (AFK carve-out — ADR-032), **EXCEPT for retro-surfaced observations of recurring class-of-behaviour** — those route to `/wr-itil:capture-problem` per the **P342 mechanical-stage carve-out** (see retro-on-exit constraint #4 below; same trust-boundary as `/wr-retrospective:run-retro` Step 4a verification close-on-evidence — P342). Do NOT use `ScheduleWakeup` under any circumstance (P083 — iteration workers must not self-reschedule). **NEVER call `AskUserQuestion` mid-loop in AFK** (P135 / ADR-044): direction / deviation-approval / one-time-override / silent-framework observations queue at `ITERATION_SUMMARY.outstanding_questions` for loop-end batched presentation. **This includes the manage-problem substance-confirm-before-build guard (ADR-074 (Confirm a decision's substance before building dependent work)):** when the propose-fix step detects that the fix builds on a born-`proposed` decision whose substance is unconfirmed (via `wr-architect-is-decision-unconfirmed`), the iter does NOT implement on it and does NOT ask mid-loop — it queues a `category: "direction"` entry naming the unconfirmed ADR + its Decision Outcome for loop-end confirmation, and routes the ticket to `action: skipped`, `skip_reason_category: user-answerable`. Building on the unconfirmed substance instead (or guessing the choice) is the P315 failure this guard exists to prevent. The queued substance-confirm is a legitimate cat-1 direction ask — it is NOT counted as lazy in the Step 2d Ask Hygiene Pass (ADR-074 lazy-count exclusion). Per-iter `AskUserQuestion` calls are sub-contracting framework-resolved decisions back to the user (lazy deferral per Step 2d Ask Hygiene Pass classification). Non-interactive defaults apply per ADR-013 Rule 6 + ADR-044's framework-resolution boundary. **Treat the user as transient** (P130): even when observably present at orchestrator dispatch time, the user may answer one question and disappear for hours; presence is not a reliable signal and is not the goal. The iter's job is to progress the ticket and accumulate questions for batched surfacing — not to ask "is it OK to proceed?" at a mechanical-stage boundary. **Do NOT poll `bats` output with a bats-console-summary regex against TAP-format output** (P146 — bash until-loop-deadlock antipattern). The bats-console-summary line `<N> tests, <M> failures` is emitted ONLY by bats's *default* (non-TAP) formatter; `bats --tap` does not emit a console summary, so a polling loop of shape `until [ -f $OUT ] && grep -qE '^[0-9]+ tests?,' $OUT; do sleep 5; done` spins forever after bats completes (silent deadlock — no error, no exit; recovery requires manual SIGTERM with metadata loss per the P146/P147 stuck-before-emit subclass). When you need to wait on a backgrounded bats run, prefer `wait $bg_pid` (Unix idiom — completion signaled by process exit, no regex required) or, for the Bash tool, `run_in_background=true` + `BashOutput` polling on the tool's exit-state field rather than regex-poll on stdout. If you genuinely must regex-poll TAP output, anchor on the TAP plan line `^[0-9]+\.\.[0-9]+` (e.g. `1..1455`) — TAP's plan line is emitted on completion and is format-stable across bats versions; the bats-console-summary line is not. The console-summary vs TAP-format divergence is the load-bearing detail: `bats` and `bats --tap` produce structurally different stdout, and the antipattern assumes the former when iter dispatch typically uses the latter. **Do NOT poll subprocess completion with `pgrep -f '<pattern>'` inside an `until` / `while` loop** (P232 — self-referential pgrep deadlock; sibling variant of P146). `pgrep -f` matches against the FULL command line of every running process, so the polling loop's own `zsh -c` argument (which contains the literal `pgrep -f '<pattern>'` text) matches itself; with multiple concurrent polling loops, each loop matches the others and spins forever. Worked example of the antipattern: `until ! pgrep -f 'bats --recursive' > /dev/null 2>&1; do sleep 5; done` — the 2026-05-16 P232 deadlock witness; 4 concurrent polling loops each matched the others' command lines while no actual bats process ran; 45 min wall-clock + $20-30 wasted before manual SIGTERM. The same self-reference shape applies to `while pgrep -f ...; do sleep; done` and to `until ! pkill -0 -f '<pattern>'` / `while pkill -0 -f '<pattern>'` (signal-0 polling). The structural fix is the same as P146: prefer `wait $bg_pid` (Unix idiom — shell-native completion signal, no regex / no pgrep) or Bash-tool `run_in_background=true` + `BashOutput` polling (harness-tracked completion state). The hook `packages/itil/hooks/itil-bash-polling-antipattern-detect.sh` denies these shapes at PreToolUse:Bash, but the prompt rule belongs here too — structural enforcement + prompt discipline together close the class. **If the fix changes shippable code or package behaviour** (any path under `packages/<plugin>/{src,bin,hooks,skills,scripts,lib,agents}` excluding test paths — `test/`, `hooks/test/`, `scripts/test/` — and excluding `README.md` + `docs/*.md`), **the iter MUST author a `.changeset/*.md` entry in the same single ADR-014-grain commit as the fix** (the changeset names the bumping plugin via the YAML frontmatter `"@windyroad/<plugin>": <patch|minor|major>` per the changesets-action contract). **Doc-only changes** (under `docs/`, `*.md`) **and test-only changes** (under any `test/` path) **that ship no behaviour MAY omit the changeset**. The orchestrator's Step 6.5 release-cadence drain runs `release:watch` only when `.changeset/` is non-empty after push — without an iter-authored changeset, code-shape fixes accumulate without ever shipping to npm (violating JTBD-006's audit-trail expectation + JTBD-007's "Keep Plugins Current" closure dependency). Hook `packages/itil/hooks/itil-changeset-discipline.sh` (P141) provides hook-level enforcement at `git commit` time as defence-in-depth — but plugin hook execution depends on the marketplace cache carrying the current hook version, so the prompt-time constraint here MUST land independently (composes-with the hook; does NOT rely on the hook being installed). Inbound-reported from downstream consumer bbstats as their P195 — see [Related](#related) for `**Origin**: inbound-reported (bbstats#195)` per ADR-076. **`@jtbd JTBD-006`** (load-bearing) **`@jtbd JTBD-007`** (closure-dependent).
515
- 4. **Retro-on-exit (P086) + retro-surfaced observation classification (P342)**: before emitting `ITERATION_SUMMARY`, invoke `/wr-retrospective:run-retro`. Retro runs INSIDE this subprocess so its Step 2b pipeline-instability scan has access to the iteration's rich tool-call history (hook misbehaviour, repeat-workaround patterns, subagent-delegation friction, release-path instability). Retro may create tickets or update `docs/BRIEFING.md` run-retro commits its own work per ADR-014; any tickets it creates ride into either the iteration's own commit (if retro runs before the main commit) or a retro-owned follow-up commit, and the orchestrator picks them up on the next Step 1 scan. Proceed to `ITERATION_SUMMARY` emission regardless of retro findings — retro is non-blocking at the iter-subprocess layer (do not block on retro): if retro fails or surfaces findings, the iteration still returns a summary so the AFK loop does not silently halt on a flaky retro run. (Session-level retro at the orchestrator-main-turn layer per Step 2.4 gate (b) IS load-bearing — distinct surface; see Step 2.4 prose for the orchestrator-layer halt semantics.)
517
+ 4. **Retro-on-exit (P086) + retro-surfaced observation classification (P342) + iter-owned BRIEFING commit (P212)**: before emitting `ITERATION_SUMMARY`, invoke `/wr-retrospective:run-retro`. Retro runs INSIDE this subprocess so its Step 2b pipeline-instability scan has access to the iteration's rich tool-call history (hook misbehaviour, repeat-workaround patterns, subagent-delegation friction, release-path instability). Tickets retro creates ride a separate path: they delegate through `/wr-itil:manage-problem` which IS ADR-014 in-scope and self-commits each ticket per its own Step 11. Those commits land independently and the orchestrator picks them up on the next Step 1 scan.
518
+
519
+ **BRIEFING.md commit responsibility — iter owns, run-retro does not (P212).** run-retro is explicitly out-of-scope for self-commit per ADR-014's Scope section (which lists `packages/retrospective/skills/run-retro/SKILL.md` under "Out of scope for now"). Retro therefore EDITS but DOES NOT COMMIT `docs/BRIEFING.md` / `docs/briefing/*.md`. The iter subprocess (NOT run-retro, NOT the orchestrator main turn) owns the BRIEFING commit. After retro completes, run `git status --porcelain docs/BRIEFING.md docs/briefing/`. If non-empty, the iter:
520
+
521
+ 1. Stages the dirty BRIEFING paths (`git add docs/BRIEFING.md docs/briefing/`).
522
+ 2. Delegates to `wr-risk-scorer:pipeline` per ADR-014's `work → score → commit` ordering. The BRIEFING refresh is mechanical chore-class (derived retro output, no source-of-truth change) — within-appetite by construction, same risk shape as the `chore(problems): reconcile README ...` and `chore(problems): check upstream responses` precedents in ADR-014's commit-message convention table.
523
+ 3. Commits as `chore(briefing): refresh from iter retro (P<NNN>)` where `P<NNN>` is the ticket the iter was working.
524
+
525
+ Pre-P212, the orchestrator's Step 6.75 absorbed this as `dirty-for-a-known-reason` and added the commit at orchestrator-main-turn cost, invoking `wr-risk-scorer:pipeline` twice per iter (once for the ticket commit, once for the orchestrator-side hand-off). Shifting the commit into the iter subprocess preserves the audit trail (the same `chore(briefing)` commit lands), eliminates the orchestrator-main-turn hand-off, and moves the second scoring call from expensive main-turn context to cheaper iter-subprocess context. Step 6.75's table is amended below to classify dirty BRIEFING-at-iter-exit as a bug class rather than an expected hand-off.
526
+
527
+ Proceed to `ITERATION_SUMMARY` emission regardless of retro findings — retro is non-blocking at the iter-subprocess layer (do not block on retro): if retro fails or surfaces findings, the iteration still returns a summary so the AFK loop does not silently halt on a flaky retro run. The iter MUST verify `git status` is clean (no remaining BRIEFING dirty state) before emitting `ITERATION_SUMMARY`. (Session-level retro at the orchestrator-main-turn layer per Step 2.4 gate (b) IS load-bearing — distinct surface; see Step 2.4 prose for the orchestrator-layer halt semantics.)
516
528
 
517
529
  **P342 classification taxonomy — retro-surfaced observations.** When the iter-retro's Step 4b Stage 1 surfaces a ticketable observation, the routing depends on classification:
518
530
 
@@ -838,7 +850,7 @@ Before spawning the next iteration's subagent, verify the working tree state aga
838
850
  |---|---|---|
839
851
  | Clean (empty output) | The subagent committed successfully (the default happy path) | Proceed to Step 7 |
840
852
  | Dirty for a known reason | A deliberate hand-off to the next iteration (e.g. the subagent chose to skip the commit and report "uncommitted state" because risk was above appetite — per the Non-Interactive Decision Making table above). Reason MUST be stated in the iteration report. | Include the dirty state in the next iteration's subagent context and proceed to Step 7 |
841
- | Dirty for an unknown reason | Neither of the above — the subagent reported success but the tree is not clean, or the tree is dirty without a documented reason in the iteration report | **Halt the loop.** Report the `git status --porcelain` output, the last subagent's reported outcome, and the divergence. Do NOT spawn the next iteration. |
853
+ | Dirty for an unknown reason | Neither of the above — the subagent reported success but the tree is not clean, or the tree is dirty without a documented reason in the iteration report. **P212 case (no longer a hand-off)**: dirty `docs/BRIEFING.md` / `docs/briefing/*.md` at iter exit is a bug class — Step 5 retro-on-exit clause #4 now requires the iter to commit retro's BRIEFING edits as `chore(briefing): refresh from iter retro (P<NNN>)` before emitting `ITERATION_SUMMARY`. A dirty BRIEFING-at-iter-exit means the iter's retro-on-exit clause did not run to completion (retro hook failure, scoring failure, commit-gate rejection) and the orchestrator must NOT silently absorb it via a main-turn hand-off commit. | **Halt the loop.** Report the `git status --porcelain` output, the last subagent's reported outcome, and the divergence. Do NOT spawn the next iteration. |
842
854
 
843
855
  **Rationale**: the orchestrator previously treated the subagent's reported outcome as truth. Any lie, partial write, or silent failure in the subagent propagated into the summary. The `git status --porcelain` check is the cheapest possible independent verification — policy-authorised, no network, no judgement required — and it catches exactly the class of failure the subagent cannot self-report.
844
856
 
@@ -1023,6 +1035,7 @@ When every skipped ticket is in the `upstream-blocked` category (stop-condition
1023
1035
  - **ADR-022** (`docs/decisions/022-problem-verification-pending.proposed.md`) — iteration outcomes map into the return-summary's `outcome` field (`verifying` for a released fix, `known-error` for a root-cause-confirmed ticket awaiting release, etc.).
1024
1036
  - **ADR-032** (`docs/decisions/032-governance-skill-invocation-patterns.proposed.md`) — pattern taxonomy parent; Step 5 implements the AFK iteration-isolation wrapper — subprocess-boundary variant per the P084 amendment (2026-04-21), refining the P077 Agent-tool amendment. The P077 amendment remains in the ADR as the historical Agent-tool variant; the subprocess variant is the lead for new adopters.
1025
1037
  - **ADR-037** (`docs/decisions/037-skill-testing-strategy.proposed.md`) — doc-lint bats contract-assertion pattern used by `test/work-problems-step-5-delegation.bats`.
1038
+ - **P211** (`docs/problems/known-error/211-work-problems-orchestrator-carries-prior-ticket-fix-strategy-text-into-iter-dispatch-without-re-grounding.md`) — driver for Step 5 iteration-prompt-body's "Re-ground per iter" orchestrator-side construction invariant. The bug shape (reported as inbound from downstream consumer bbstats as their P194): the orchestrator builds each iter's dispatch prompt by reading the target ticket's `## Fix Strategy` section and citing it verbatim into the subprocess prompt; across iterations, prior-ticket Fix Strategy text leaks into subsequent dispatches without re-grounding in the new ticket's design intent, and iters land fixes anchored on the wrong design rationale. Fix: SKILL.md Step 5's "Iteration prompt body" section now carries an explicit re-grounding paragraph (immediately after the "self-contained" opener) that (a) names the per-iter re-ground invariant against current-ticket-ID + title only, (b) forbids inlining `## Fix Strategy` verbatim into the dispatch prompt (the subprocess reads it from disk via `/wr-itil:manage-problem`), (c) names the cross-iter leakage class (prior ticket ID, prior Fix Strategy text, prior outcome reason, prior commit SHA, prior retro findings, prior outstanding-questions), (d) names the construction shape (template-driven, reset per iter, no global accumulator). Behavioural second-source: `test/work-problems-step-5-prompt-body-re-grounding.bats` (structural-permitted per ADR-052 Surface 2; tdd-review comment in fixture cites P012 as harness-gap). Composes with P084 (subprocess-boundary isolation — re-grounding is the symmetric orchestrator-side property of the subprocess's "no prior conversation context"), ADR-032 (AFK iteration-isolation wrapper — re-grounding clarifies the wrapper's isolation intent on the orchestrator side), JTBD-006 (load-bearing — audit trail degrades if iters work the wrong ticket's design rationale).
1026
1039
  - **P206** (`docs/problems/known-error/206-work-problems-iter-workers-dont-add-changesets-fix-commits-accumulate-without-release.md`) — driver for Step 5 iter-prompt-body's explicit "if the fix changes shippable code, author a `.changeset/*.md` in the same commit" constraint (composes defence-in-depth with hook P141's `git commit`-time enforcement). Inbound-reported by downstream consumer **bbstats** as their P195 (`**Origin**: inbound-reported (bbstats#195)` per ADR-076 sort tier). Behavioural second-source: `test/work-problems-step-5-iter-changeset-required.bats` (structural-permitted per ADR-052; tdd-review comment in fixture).
1027
1040
  - **P141** (`docs/problems/verifying/141-iter-prompt-time-reminder-misses-40-percent-of-publishable-iters-hook-level-enforcement.md`) — sibling hook (`packages/itil/hooks/itil-changeset-discipline.sh`) that enforces the changeset-discipline rule at `git commit` time. The Step 5 iter-prompt-body constraint composes-with this hook; the prompt-time rule is load-bearing because plugin-hook execution depends on the marketplace cache carrying the current hook version (a fresh-cache adopter without P141 still gets the constraint via the prompt).
1028
1041
  - **JTBD-001**, **JTBD-006**, **JTBD-007**, **JTBD-101**, **JTBD-201** — personas whose reliability expectations the iteration-isolation wrapper restores. JTBD-006 (Progress the Backlog While I'm Away) + JTBD-007 (Keep Plugins Current Across Projects) are the load-bearing pair for the P206 changeset-discipline constraint — JTBD-006 requires the audit trail to stay accurate at release boundary; JTBD-007's closure depends on fixes actually shipping to npm.
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bats
2
+ # P211 — work-problems Step 5 iteration-prompt-body must EXPLICITLY re-ground
3
+ # each iter's dispatch prompt against the CURRENT ticket only. The orchestrator
4
+ # MUST NOT inline the ticket's `## Fix Strategy` text verbatim, and MUST NOT
5
+ # leak prior-iter content (prior ticket ID, prior Fix Strategy text, prior
6
+ # outcome reason, prior commit SHA, prior retro findings) across iterations.
7
+ #
8
+ # Reported as inbound from downstream consumer bbstats (their P194) on
9
+ # 2026-05-15; covered by ADR-076 Origin field tier.
10
+ #
11
+ # Behavioural mechanism for the bug: AFK iter subprocesses inherit a stale
12
+ # design-rationale frame and may attempt fixes anchored on the wrong ticket's
13
+ # intent. Workaround the ticket names: user-in-the-loop verification after
14
+ # each iter, reading the subprocess's commit and checking whether it cites
15
+ # the correct ticket's design rationale — a manual-policing burden the AFK
16
+ # loop is meant to eliminate. JTBD-006 (Progress the Backlog While I'm Away)
17
+ # is load-bearing: the audit trail and trust in the AFK loop degrade if iters
18
+ # work the wrong ticket's design rationale.
19
+ #
20
+ # tdd-review: structural-permitted (justification: SKILL.md is the named
21
+ # contract document under ADR-052; behavioural alternative would require a
22
+ # synthetic `claude -p` iter dispatch harness that simulates multiple
23
+ # sequential iters and asserts no cross-iter prompt-body content leakage —
24
+ # that harness sits outside the skill layer and depends on the Anthropic CLI
25
+ # binary. Same Permitted Exception precedent as
26
+ # `work-problems-step-5-iter-changeset-required.bats:14-21`,
27
+ # `work-problems-step-5-delegation.bats:99-105`, and the P083 / P086 / P089
28
+ # ScheduleWakeup / retro / stdin-redirect fixtures in the same directory.
29
+ # P012 is the harness-gap ticket).
30
+ #
31
+ # @problem P211
32
+ # @problem P012
33
+ # @jtbd JTBD-006
34
+ # @jtbd JTBD-001
35
+ #
36
+ # Cross-reference:
37
+ # P211 — this ticket (orchestrator carries prior-ticket Fix Strategy into
38
+ # next iter's dispatch context — pollutes the new iter's framing)
39
+ # bbstats#194 — inbound report from downstream consumer
40
+ # ADR-014 (single-commit grain — fix lands as one coherent commit)
41
+ # ADR-032 (governance skill invocation patterns — AFK iteration-isolation
42
+ # wrapper; re-grounding is a clarification of that isolation intent)
43
+ # ADR-052 (behavioural tests default; structural-permitted with comment)
44
+ # ADR-076 (inbound-reported problems rank ahead via sort tier — Origin
45
+ # field stamping)
46
+ # JTBD-006 (Progress the Backlog While I'm Away) — load-bearing
47
+
48
+ setup() {
49
+ SKILL_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
50
+ SKILL_FILE="${SKILL_DIR}/SKILL.md"
51
+ }
52
+
53
+ @test "SKILL.md cites P211 (re-grounding driver) in Related section" {
54
+ # Self-documenting contract — a future contributor weakening the
55
+ # re-grounding constraint reads P211 and understands why it exists.
56
+ run grep -nE 'P211' "$SKILL_FILE"
57
+ [ "$status" -eq 0 ]
58
+ }
59
+
60
+ @test "SKILL.md Step 5 iteration prompt body names re-grounding per iter explicitly" {
61
+ # The "self-contained" opener at line 510 is the existing weaker form; the
62
+ # stricter "re-ground per iter" phrasing names the construction invariant
63
+ # the orchestrator MUST satisfy on each iter dispatch. P211's bug shape is
64
+ # exactly the case where "self-contained" was read as a subprocess-side
65
+ # property only, with the orchestrator-side construction leaking prior-iter
66
+ # content into the new iter's prompt body.
67
+ run grep -niE "re.?ground.{0,40}per iter|re.?grounded.{0,40}per iter|per.?iter.{0,40}re.?ground" "$SKILL_FILE"
68
+ [ "$status" -eq 0 ]
69
+ }
70
+
71
+ @test "SKILL.md Step 5 iter prompt body forbids inlining Fix Strategy verbatim" {
72
+ # The bug shape: orchestrator reads target ticket's `## Fix Strategy` and
73
+ # cites it verbatim into the iteration subprocess's prompt body. The
74
+ # SKILL.md MUST explicitly forbid this so future contributors understand
75
+ # the subprocess reads Fix Strategy from disk via manage-problem inside
76
+ # its own context.
77
+ run grep -niE "(not|never|MUST NOT|does not).{0,40}inline.{0,40}Fix Strategy|Fix Strategy.{0,40}(not|never|MUST NOT|does not).{0,40}inline|do not.{0,40}cite.{0,40}Fix Strategy.{0,40}verbatim|Fix Strategy.{0,40}verbatim.{0,40}(not|never|forbid)" "$SKILL_FILE"
78
+ [ "$status" -eq 0 ]
79
+ }
80
+
81
+ @test "SKILL.md Step 5 iter prompt body explicitly forbids prior-iter content leakage" {
82
+ # The cross-iter leakage class names: prior ticket ID, prior Fix Strategy
83
+ # text, prior outcome reason, prior commit SHA, prior retro findings. The
84
+ # SKILL.md MUST name the no-leakage invariant explicitly so the orchestrator
85
+ # main turn's prompt construction is constrained on every iter.
86
+ run grep -niE "(no prior|not.{0,20}prior|prior.?iter.{0,40}(leak|carry|inherit)|leak.{0,40}prior|carry.{0,40}prior.{0,40}iter)" "$SKILL_FILE"
87
+ [ "$status" -eq 0 ]
88
+ }
89
+
90
+ @test "SKILL.md Step 5 names template-driven reset-per-iter construction" {
91
+ # The construction shape: template-driven, reset per iter, no global
92
+ # accumulator across iters. This is the structural invariant the
93
+ # orchestrator main turn must satisfy when building each iter's prompt.
94
+ run grep -niE "template.?driven|reset per iter|reset.{0,20}per.{0,20}iter|no.{0,20}(global )?accumulator" "$SKILL_FILE"
95
+ [ "$status" -eq 0 ]
96
+ }
97
+
98
+ @test "SKILL.md Step 5 iteration prompt body cites P211 inline" {
99
+ # The re-grounding clause must cite P211 inline so the contract document
100
+ # is self-documenting — a future contributor removing the clause reads the
101
+ # P211 reference and understands why it exists before deleting it. Same
102
+ # pattern as the P083 / P086 / P146 / P232 inline citations in the same
103
+ # block.
104
+ run grep -nE "re.?ground.{0,200}P211|P211.{0,200}re.?ground|P211.{0,200}Fix Strategy|Fix Strategy.{0,200}P211" "$SKILL_FILE"
105
+ [ "$status" -eq 0 ]
106
+ }
107
+
108
+ @test "SKILL.md re-grounding clause sits inside Step 5 iteration prompt body section" {
109
+ # Structural locality: the re-grounding clause must live INSIDE Step 5's
110
+ # Iteration prompt body section (after the "self-contained" opener at
111
+ # line 510), not free-floating elsewhere in SKILL.md. Locality matters
112
+ # because the rule is read alongside the rest of the prompt-body contract,
113
+ # and a future contributor refactoring Step 5 must encounter it inline.
114
+ # Assertion shape: the line containing "re-ground" sits after the line
115
+ # containing "Iteration prompt body" and before the line containing
116
+ # "Return-summary contract".
117
+ iter_line=$(grep -nE '^\*\*Iteration prompt body' "$SKILL_FILE" | head -1 | cut -d: -f1)
118
+ # Tightened regex: require the literal hyphenated form "re-ground" /
119
+ # "re-grounded" / "re-grounding" so partial-substring matches like
120
+ # "foreground" (line 33) don't satisfy the assertion.
121
+ reground_line=$(grep -niE "re-ground(ed|ing)?" "$SKILL_FILE" | head -1 | cut -d: -f1)
122
+ return_summary_line=$(grep -nE '^\*\*Return-summary contract' "$SKILL_FILE" | head -1 | cut -d: -f1)
123
+ [ -n "$iter_line" ]
124
+ [ -n "$reground_line" ]
125
+ [ -n "$return_summary_line" ]
126
+ [ "$reground_line" -gt "$iter_line" ]
127
+ [ "$reground_line" -lt "$return_summary_line" ]
128
+ }