@windyroad/retrospective 0.25.0 → 0.26.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.
@@ -78,5 +78,5 @@
78
78
  }
79
79
  },
80
80
  "name": "wr-retrospective",
81
- "version": "0.25.0"
81
+ "version": "0.26.0"
82
82
  }
package/hooks/hooks.json CHANGED
@@ -17,6 +17,15 @@
17
17
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start-briefing.sh"
18
18
  }
19
19
  ]
20
+ },
21
+ {
22
+ "matcher": "startup",
23
+ "hooks": [
24
+ {
25
+ "type": "command",
26
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/retrospective-deferral-census.sh"
27
+ }
28
+ ]
20
29
  }
21
30
  ],
22
31
  "PreToolUse": [
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # P375 — single source of truth for the deferred-work marker vocabulary.
3
+ #
4
+ # A "deferred-work marker" is prose that parks work against a named-but-not-
5
+ # self-firing re-entry point (a /skill, a lifecycle transition, "next review").
6
+ # Per P375 the rot test is: does the trigger chain reach a SELF-FIRING event?
7
+ # This vocabulary is what the SessionStart census (retrospective-deferral-
8
+ # census.sh) greps for so the parked work cannot silently rot.
9
+ #
10
+ # Sourced, not executed. Defines DEFERRAL_MARKER_RE (grep -E, case-insensitive
11
+ # at the call site).
12
+ #
13
+ # Convergence note: itil-fictional-defer-detect.sh keeps its own retro-specific
14
+ # DEFER_RATIONALE_RE for now; folding it onto this vocabulary is a tracked P375
15
+ # investigation task, NOT refactored cross-plugin here (ADR-002/003 plugin
16
+ # self-containment — a retrospective lib must not become an itil runtime dep).
17
+ # P378 fold: slice/phase-deferral phrasing ("lands in Slice N", "future slice",
18
+ # "deferred to a hook-source slice", "<lands|deferred> ... in a future <slice|
19
+ # pass|iter>") was MISSED by the original vocabulary — that blind spot let the
20
+ # never-built RFC commit-trailer executor ("lands in Slice 3 task B5.T9") hide
21
+ # from the census. Added so slice/phase-deferred-and-unbuilt work surfaces too.
22
+ DEFERRAL_MARKER_RE='\(deferred|deferred to|pending review|re-rate at next|: deferred|lands in [Ss]lice|future slice|hook-source slice|in a future (slice|pass|iter|session)'
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # wr-retrospective — SessionStart hook (ADR-040 class-B surface; P375)
3
+ #
4
+ # The "self-firing deferral census". Every session start, count deferred-work
5
+ # markers across docs/ + packages/ (.md only) that name a re-entry point nothing
6
+ # self-fires, and surface a bounded census so the parked work cannot silently
7
+ # rot (P375). Clones the class-B oversight-nudge shape (architect/jtbd nudges,
8
+ # ADR-040 session-start surface): silent-on-zero, fail-open, advisory stdout.
9
+ #
10
+ # .md-only scope is the code-comment false-positive guard — shipped-skill
11
+ # deferrals live in SKILL.md, not source comments. Marker vocabulary is the
12
+ # single source of truth in lib/deferral-markers.sh.
13
+ #
14
+ # AFK self-suppress: WR_SUPPRESS_DEFERRAL_CENSUS=1 (distinct from the interactive
15
+ # oversight nudges' WR_SUPPRESS_OVERSIGHT_NUDGE — the census is advisory-stdout-
16
+ # never-halts and is valuable under AFK, so the orchestrator suppresses it on a
17
+ # separate axis). Only the literal "1" suppresses.
18
+ #
19
+ # Fail-open: never aborts session startup (ADR-013 Rule 6). Missing dirs,
20
+ # unsourceable lib, or zero matches all exit 0 silently. ADR-040 Tier-1 budget
21
+ # (<=2KB) honoured by capping the worst-offender list at 5 rows.
22
+ #
23
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down) primary;
24
+ # JTBD-006 (Progress the Backlog While I'm Away) secondary.
25
+
26
+ set -o pipefail
27
+
28
+ [ "${WR_SUPPRESS_DEFERRAL_CENSUS:-}" = "1" ] && exit 0
29
+
30
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
31
+
32
+ # Resolve the marker vocabulary (CLAUDE_PLUGIN_ROOT in installs; dirname fallback
33
+ # in the source monorepo / tests). Fail-open if neither resolves.
34
+ LIB="${CLAUDE_PLUGIN_ROOT:-$(dirname "$0")/..}/hooks/lib/deferral-markers.sh"
35
+ [ -r "$LIB" ] || LIB="$(dirname "$0")/lib/deferral-markers.sh"
36
+ # shellcheck source=/dev/null
37
+ . "$LIB" 2>/dev/null || exit 0
38
+ [ -n "${DEFERRAL_MARKER_RE:-}" ] || exit 0
39
+
40
+ # Scan .md files under docs/ and packages/ only.
41
+ SCAN_DIRS=""
42
+ [ -d "$PROJECT_DIR/docs" ] && SCAN_DIRS="$SCAN_DIRS $PROJECT_DIR/docs"
43
+ [ -d "$PROJECT_DIR/packages" ] && SCAN_DIRS="$SCAN_DIRS $PROJECT_DIR/packages"
44
+ [ -n "$SCAN_DIRS" ] || exit 0
45
+
46
+ # Per-file match-line counts; keep only files with >0 matches. grep no-match
47
+ # exits 1 → `|| true` keeps the hook fail-open.
48
+ # shellcheck disable=SC2086
49
+ COUNTS="$(grep -rIcE --include='*.md' --exclude='CHANGELOG.md' --exclude='*-history.md' "$DEFERRAL_MARKER_RE" $SCAN_DIRS 2>/dev/null | awk -F: '$NF>0' || true)"
50
+ [ -n "$COUNTS" ] || exit 0
51
+
52
+ TOTAL=$(printf '%s\n' "$COUNTS" | awk -F: '{s+=$NF} END{print s+0}')
53
+ FILES=$(printf '%s\n' "$COUNTS" | grep -c . 2>/dev/null || true)
54
+ [ "${TOTAL:-0}" -gt 0 ] 2>/dev/null || exit 0
55
+
56
+ # Top 5 worst offenders (bounds output to the ADR-040 Tier-1 budget).
57
+ TOP=$(printf '%s\n' "$COUNTS" | sort -t: -k2 -nr | head -5)
58
+
59
+ echo "[wr-retrospective] ${TOTAL} deferred-work marker(s) across ${FILES} file(s) name a re-entry point that nothing self-fires — they rot until someone runs a command (P375). Top offenders:"
60
+ while IFS=: read -r f c; do
61
+ [ -n "$f" ] || continue
62
+ rel="${f#"$PROJECT_DIR"/}"
63
+ echo " • ${rel}: ${c}"
64
+ done <<< "$TOP"
65
+ echo " Drain: /wr-retrospective:run-retro, /wr-itil:review-problems, or work the backlog. Advisory only."
66
+
67
+ exit 0
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # ADR-040 / P375: retrospective-deferral-census.sh (SessionStart) emits a one-line
4
+ # census + bounded top-offenders list when deferred-work markers exist that lack a
5
+ # self-firing trigger, is silent at zero, self-suppresses under the AFK guard
6
+ # (WR_SUPPRESS_DEFERRAL_CENSUS=1), respects the ADR-040 Tier-1 ≤2KB budget, scans
7
+ # BOTH docs/ and packages/ (.md only — code-comment false-positive guard), and is
8
+ # fail-open (never aborts session startup). Behavioural — exercises the hook against
9
+ # fixture trees. Marker vocabulary is the single source of truth in
10
+ # lib/deferral-markers.sh (P375 "shared vocabulary").
11
+
12
+ setup() {
13
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
14
+ HOOK="$REPO_ROOT/packages/retrospective/hooks/retrospective-deferral-census.sh"
15
+ PLUGIN_ROOT="$REPO_ROOT/packages/retrospective"
16
+ DIR="$(mktemp -d)"
17
+ mkdir -p "$DIR/docs/problems" "$DIR/packages/itil/skills/foo"
18
+ }
19
+
20
+ teardown() { rm -rf "$DIR"; }
21
+
22
+ mk() { mkdir -p "$(dirname "$DIR/$1")"; printf '%s\n' "$2" > "$DIR/$1"; }
23
+
24
+ @test "emits a census line with the total marker count" {
25
+ mk "docs/problems/001-x.md" "Priority: 3 (deferred — re-rate at next /wr-itil:review-problems)"
26
+ mk "docs/problems/002-y.md" "**Severity**: (deferred to investigation)"
27
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
28
+ [ "$status" -eq 0 ]
29
+ [[ "$output" == *"deferred-work marker"* ]]
30
+ [[ "$output" == *"[wr-retrospective]"* ]]
31
+ }
32
+
33
+ @test "lists the worst-offender files" {
34
+ mk "docs/problems/001-x.md" "$(printf '(deferred to A)\n(deferred to B)\n(deferred to C)')"
35
+ mk "docs/problems/002-y.md" "(deferred to D)"
36
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
37
+ [[ "$output" == *"001-x.md"* ]]
38
+ }
39
+
40
+ @test "scans BOTH docs/ and packages/ .md files" {
41
+ mk "packages/itil/skills/foo/SKILL.md" "section deferred to /wr-itil:manage-rfc accepted transition"
42
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
43
+ [ "$status" -eq 0 ]
44
+ [[ "$output" == *"SKILL.md"* ]]
45
+ }
46
+
47
+ @test "silent when there are no deferred-work markers" {
48
+ mk "docs/problems/001-x.md" "Everything here is rated and tracked."
49
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
50
+ [ "$status" -eq 0 ]
51
+ [ -z "$output" ]
52
+ }
53
+
54
+ @test "fail-open: silent + exit 0 when neither docs/ nor packages/ exists" {
55
+ EMPTY="$(mktemp -d)"
56
+ run env CLAUDE_PROJECT_DIR="$EMPTY" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
57
+ rm -rf "$EMPTY"
58
+ [ "$status" -eq 0 ]
59
+ [ -z "$output" ]
60
+ }
61
+
62
+ @test "fail-open: marker lib unreadable → exit 0, no output, no abort" {
63
+ mk "docs/problems/001-x.md" "(deferred to investigation)"
64
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="/nonexistent/plugin/root" bash "$HOOK"
65
+ [ "$status" -eq 0 ]
66
+ }
67
+
68
+ @test "AFK guard WR_SUPPRESS_DEFERRAL_CENSUS=1 suppresses entirely" {
69
+ mk "docs/problems/001-x.md" "(deferred to investigation)"
70
+ run env WR_SUPPRESS_DEFERRAL_CENSUS=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
71
+ [ "$status" -eq 0 ]
72
+ [ -z "$output" ]
73
+ }
74
+
75
+ @test "guard value other than 1 does not suppress" {
76
+ mk "docs/problems/001-x.md" "(deferred to investigation)"
77
+ run env WR_SUPPRESS_DEFERRAL_CENSUS=yes CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
78
+ [[ "$output" == *"deferred-work marker"* ]]
79
+ }
80
+
81
+ @test "output respects the ADR-040 Tier-1 budget (<= 2048 bytes) even with many offenders" {
82
+ for i in $(seq 1 50); do
83
+ mk "docs/problems/$(printf '%03d' "$i")-ticket.md" "$(printf '(deferred to X)\n(deferred to Y)\n(deferred to Z)')"
84
+ done
85
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
86
+ [ "$status" -eq 0 ]
87
+ bytes=$(printf '%s' "$output" | wc -c | tr -d ' ')
88
+ [ "$bytes" -le 2048 ]
89
+ }
90
+
91
+ @test "excludes archival records (CHANGELOG.md, *-history.md) — they are not rotting work" {
92
+ mk "packages/itil/CHANGELOG.md" "$(printf '(deferred to A)\n(deferred to B)')"
93
+ mk "docs/problems/README-history.md" "(deferred to investigation)"
94
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
95
+ [ "$status" -eq 0 ]
96
+ [ -z "$output" ]
97
+ }
98
+
99
+ @test "catches slice/phase-deferral phrasing (P378 fold) — 'lands in Slice N' etc." {
100
+ mk "packages/itil/skills/foo/SKILL.md" "the executor lands in Slice 3 task B5.T9"
101
+ mk "packages/itil/skills/bar/SKILL.md" "deferred to a hook-source slice"
102
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
103
+ [ "$status" -eq 0 ]
104
+ [[ "$output" == *"deferred-work marker"* ]]
105
+ [[ "$output" == *"SKILL.md"* ]]
106
+ }
107
+
108
+ @test "points at a drain path (run-retro / backlog) and cites P375" {
109
+ mk "docs/problems/001-x.md" "(deferred to investigation)"
110
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
111
+ [[ "$output" == *"P375"* ]]
112
+ }
@@ -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
- export function checkPrerequisites() {
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
- try {
42
- execSync("claude --version", { stdio: "pipe" });
43
- } catch {
44
- console.error(
45
- "Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
46
- );
47
- process.exit(1);
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
- addMarketplace();
83
- installPlugin(pluginName, { scope });
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
- run(
104
- `claude plugin marketplace update ${MARKETPLACE_NAME}`,
105
- "Updating marketplace"
106
- );
107
- updatePlugin(pluginName, { scope });
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
- console.log("\nDone! Restart Claude Code to apply updates.\n");
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
- uninstallPlugin(pluginName);
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("\nDone. Restart Claude Code to apply changes.\n");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/retrospective",
3
- "version": "0.25.0",
3
+ "version": "0.26.0-preview.763",
4
4
  "description": "Session retrospectives that update briefings and create problem tickets",
5
5
  "bin": {
6
6
  "windyroad-retrospective": "./bin/install.mjs"