@windyroad/jtbd 0.12.7 → 0.12.8-preview.802

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.
@@ -90,5 +90,5 @@
90
90
  }
91
91
  },
92
92
  "name": "wr-jtbd",
93
- "version": "0.12.7"
93
+ "version": "0.12.8"
94
94
  }
@@ -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/jtbd",
3
- "version": "0.12.7",
3
+ "version": "0.12.8-preview.802",
4
4
  "description": "Jobs-to-be-done enforcement for UI changes",
5
5
  "bin": {
6
6
  "windyroad-jtbd": "./bin/install.mjs"
@@ -25,6 +25,7 @@
25
25
  "skills/",
26
26
  "scripts/",
27
27
  ".claude-plugin/",
28
- "lib/"
28
+ "lib/",
29
+ "!agents/eval/"
29
30
  ]
30
31
  }
@@ -44,12 +44,16 @@ JTBD_DIR="${2:-docs/jtbd}"
44
44
  file=""
45
45
  if [ -f "$REF" ]; then
46
46
  file="$REF"
47
- elif printf '%s' "$REF" | grep -qiE 'JTBD-?[0-9]+|^[0-9]+$'; then
48
- # Job ref: JTBD-NNN or a bare numeric. Match the per-persona job file.
49
- num="$(printf '%s' "$REF" | grep -oE '[0-9]+' | head -1)"
50
- [ -n "$num" ] || { echo "is-job-or-persona-unconfirmed: cannot parse job id from '$REF'" >&2; exit 2; }
47
+ elif printf '%s' "$REF" | grep -qiE '^JTBD-|^[0-9]+$|^[A-Za-z]+-[0-9]+$'; then
48
+ # Job ref: JTBD-NNN | JTBD-M-NNN (maintainer alpha-infix scheme, P383) |
49
+ # M-NNN | bare NNN. Strip an optional JTBD- prefix to get the filename stem,
50
+ # PRESERVING any alpha infix (extracting only the numeric run would drop the
51
+ # `M` and mis-glob JTBD-NNN-*.md). Glob both the JTBD-prefixed and bare-stem
52
+ # filename shapes, first match wins.
53
+ stem="$(printf '%s' "$REF" | sed -E 's/^[Jj][Tt][Bb][Dd]-//')"
54
+ [ -n "$stem" ] || { echo "is-job-or-persona-unconfirmed: cannot parse job id from '$REF'" >&2; exit 2; }
51
55
  shopt -s nullglob
52
- for cand in "$JTBD_DIR"/*/JTBD-"$num"-*.md "$JTBD_DIR"/*/"$num"-*.md; do
56
+ for cand in "$JTBD_DIR"/*/JTBD-"$stem"-*.md "$JTBD_DIR"/*/"$stem"-*.md; do
53
57
  file="$cand"; break
54
58
  done
55
59
  shopt -u nullglob
@@ -95,6 +95,37 @@ mk_job() {
95
95
  [[ "$output" == *"JTBD-002-ship-with-confidence.md"* ]]
96
96
  }
97
97
 
98
+ # P383: maintainer JTBD-M-NNN alpha-infix scheme. The old number-only
99
+ # extraction dropped the `M` and globbed JTBD-NNN-*.md (wrong / no match).
100
+ @test "a maintainer JTBD-M-NNN job ref resolves (exit 0, preserves M infix)" {
101
+ mk_job "maintainer" "M-101" "release-plugins" "no"
102
+ run bash "$SCRIPT" "JTBD-M-101" "$DIR/docs/jtbd"
103
+ [ "$status" -eq 0 ]
104
+ [[ "$output" == *"JTBD-M-101-release-plugins.md"* ]]
105
+ }
106
+
107
+ @test "a confirmed maintainer JTBD-M-NNN job is confirmed (exit 1)" {
108
+ mk_job "maintainer" "M-202" "audit-suite" "yes"
109
+ run bash "$SCRIPT" "JTBD-M-202" "$DIR/docs/jtbd"
110
+ [ "$status" -eq 1 ]
111
+ }
112
+
113
+ @test "a bare M-NNN job ref variant resolves" {
114
+ mk_job "maintainer" "M-303" "scaffold-intake" "no"
115
+ run bash "$SCRIPT" "M-303" "$DIR/docs/jtbd"
116
+ [ "$status" -eq 0 ]
117
+ [[ "$output" == *"JTBD-M-303-scaffold-intake.md"* ]]
118
+ }
119
+
120
+ # Boundary lock (architect P383 review): a hyphenated persona NAME must still
121
+ # route to the persona branch, not the job branch — it has no trailing -NNN.
122
+ @test "a hyphenated persona name (smb-owner) routes to the persona branch" {
123
+ mk_persona "smb-owner" "no"
124
+ run bash "$SCRIPT" "smb-owner" "$DIR/docs/jtbd"
125
+ [ "$status" -eq 0 ]
126
+ [[ "$output" == *"smb-owner/persona.md"* ]]
127
+ }
128
+
98
129
  @test "a direct path ref resolves" {
99
130
  mk_persona "plugin-user" "no"
100
131
  run bash "$SCRIPT" "$DIR/docs/jtbd/plugin-user/persona.md"