@brunosps00/dev-workflow 1.1.2 → 1.2.0
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/README.md +22 -15
- package/bin/dev-workflow.js +1 -1
- package/lib/constants.js +6 -2
- package/lib/doctor.js +3 -1
- package/lib/install-deps.js +3 -3
- package/lib/setup-wsl-browser.js +10 -50
- package/lib/skill-registry.js +41 -0
- package/package.json +1 -1
- package/scaffold/en/agent-instructions.md +5 -2
- package/scaffold/en/commands/dw-analyze-project.md +1 -1
- package/scaffold/en/commands/dw-brainstorm.md +4 -1
- package/scaffold/en/commands/dw-functional-doc.md +3 -3
- package/scaffold/en/commands/dw-harness-audit.md +4 -2
- package/scaffold/en/commands/dw-help.md +12 -7
- package/scaffold/en/commands/dw-opportunities.md +131 -0
- package/scaffold/en/commands/dw-refactor.md +54 -0
- package/scaffold/en/commands/dw-review.md +1 -1
- package/scaffold/en/commands/dw-skill-health.md +11 -3
- package/scaffold/en/references/refactoring-catalog.md +1 -1
- package/scaffold/en/templates/constitution-template.md +1 -1
- package/scaffold/pt-br/agent-instructions.md +5 -2
- package/scaffold/pt-br/commands/dw-analyze-project.md +1 -1
- package/scaffold/pt-br/commands/dw-brainstorm.md +4 -1
- package/scaffold/pt-br/commands/dw-functional-doc.md +3 -3
- package/scaffold/pt-br/commands/dw-harness-audit.md +4 -2
- package/scaffold/pt-br/commands/dw-help.md +14 -7
- package/scaffold/pt-br/commands/dw-opportunities.md +131 -0
- package/scaffold/pt-br/commands/dw-refactor.md +54 -0
- package/scaffold/pt-br/commands/dw-review.md +1 -1
- package/scaffold/pt-br/commands/dw-skill-health.md +11 -3
- package/scaffold/pt-br/references/refactoring-catalog.md +1 -1
- package/scaffold/pt-br/templates/constitution-template.md +1 -1
- package/scaffold/scripts/cdp-relay/Cargo.toml +1 -1
- package/scaffold/scripts/cdp-relay/bin/cdp-relay.exe +0 -0
- package/scaffold/scripts/cdp-relay/src/main.rs +120 -23
- package/scaffold/scripts/functional-doc/run-playwright-flow.mjs +55 -24
- package/scaffold/scripts/lib/capture-screenshots.mjs +56 -17
- package/scaffold/scripts/lib/resolve-browser.mjs +314 -82
- package/scaffold/skill-registry.json +1 -1
- package/scaffold/skills/api-testing-recipes/SKILL.md +12 -0
- package/scaffold/skills/docker-compose-recipes/SKILL.md +12 -0
- package/scaffold/skills/dw-codebase-intel/SKILL.md +13 -1
- package/scaffold/skills/dw-context-budget/SKILL.md +12 -0
- package/scaffold/skills/dw-council/SKILL.md +12 -0
- package/scaffold/skills/dw-debug-protocol/SKILL.md +12 -0
- package/scaffold/skills/dw-execute-phase/SKILL.md +12 -0
- package/scaffold/skills/dw-git-discipline/SKILL.md +12 -0
- package/scaffold/skills/dw-incident-response/SKILL.md +12 -0
- package/scaffold/skills/dw-llm-eval/SKILL.md +12 -0
- package/scaffold/skills/dw-memory/SKILL.md +12 -0
- package/scaffold/skills/dw-review-rigor/SKILL.md +14 -2
- package/scaffold/skills/dw-search-first/SKILL.md +12 -0
- package/scaffold/skills/dw-silent-failure/SKILL.md +12 -0
- package/scaffold/skills/dw-simplification/SKILL.md +18 -6
- package/scaffold/skills/dw-simplification/references/deep-modules.md +1 -1
- package/scaffold/skills/dw-source-grounding/SKILL.md +12 -0
- package/scaffold/skills/dw-testing-discipline/SKILL.md +12 -0
- package/scaffold/skills/dw-testing-discipline/references/playwright-recipes.md +18 -19
- package/scaffold/skills/dw-ui-discipline/SKILL.md +12 -0
- package/scaffold/skills/dw-verify/SKILL.md +10 -0
- package/scaffold/skills/humanizer/SKILL.md +12 -0
- package/scaffold/skills/remotion-best-practices/SKILL.md +12 -0
- package/scaffold/skills/security-review/SKILL.md +12 -0
- package/scaffold/skills/vercel-react-best-practices/SKILL.md +12 -0
- package/scaffold/skills/vercel-react-best-practices/references/perf-discipline.md +1 -1
|
@@ -3,26 +3,23 @@
|
|
|
3
3
|
// Resolves which browser to drive for dev-workflow's browser-automation flows
|
|
4
4
|
// (dw-qa, dw-functional-doc, dw-redesign-ui), with WSL resilience.
|
|
5
5
|
//
|
|
6
|
-
// Resolution order:
|
|
7
|
-
//
|
|
8
|
-
// Chrome) and launches it in remote-debugging mode so flows connect over CDP instead
|
|
9
|
-
// of opening WSLg / losing layout fidelity in the bundled Linux Chromium.
|
|
6
|
+
// Resolution order: Playwright Chromium primary -> optional CDP fallback from env
|
|
7
|
+
// BROWSER_TEST or .dw/config.json (browserTest).
|
|
10
8
|
//
|
|
11
9
|
// Returns one of:
|
|
12
|
-
// { mode: "
|
|
13
|
-
// { mode: "
|
|
10
|
+
// { mode: "launch", launchOptions, source, fallback?, openFallback? } // let Playwright launch first
|
|
11
|
+
// { mode: "cdp", endpoint, source, cleanup } // resolved fallback descriptor
|
|
14
12
|
//
|
|
15
|
-
// DEFAULT
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// DEFAULT: full Playwright Chromium in headless via channel:"chromium", which uses the
|
|
14
|
+
// new headless mode (desktop-faithful rendering, not the lighter headless-shell) and
|
|
15
|
+
// never opens WSLg. BROWSER_TEST can provide a CDP/Windows-browser fallback when the
|
|
16
|
+
// primary Playwright launch fails.
|
|
19
17
|
//
|
|
20
18
|
// NOTE on WSL networking: the Chromium debug port binds Windows loopback only. In mirrored
|
|
21
19
|
// networking mode WSL shares that loopback, so 127.0.0.1 connects directly. In NAT mode the
|
|
22
|
-
// helper starts the prebuilt cdp-relay.exe on Windows
|
|
23
|
-
// and connects
|
|
24
|
-
//
|
|
25
|
-
// rule). Without it, the helper falls back to the local headless Chromium.
|
|
20
|
+
// helper starts the prebuilt cdp-relay.exe on Windows in reverse mode: the Windows process
|
|
21
|
+
// connects outbound to a WSL broker, and Playwright connects to a local WSL proxy. The one-time
|
|
22
|
+
// setup only installs the relay in the Windows user profile; no admin/firewall rule is needed.
|
|
26
23
|
|
|
27
24
|
import fs from "node:fs";
|
|
28
25
|
import net from "node:net";
|
|
@@ -35,6 +32,8 @@ const BUNDLED_LAUNCH = { channel: "chromium" };
|
|
|
35
32
|
|
|
36
33
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
34
|
const dedupe = (items) => [...new Set(items.filter(Boolean))];
|
|
35
|
+
const REVERSE_RELAY_POOL = 8;
|
|
36
|
+
const CLIENT_WAIT_TIMEOUT_MS = 5000;
|
|
38
37
|
|
|
39
38
|
export function detectWsl() {
|
|
40
39
|
if (process.env.WSL_DISTRO_NAME) {
|
|
@@ -96,6 +95,42 @@ function getFreePort() {
|
|
|
96
95
|
});
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
function listen(server, port, host) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const onError = (error) => {
|
|
101
|
+
server.off("listening", onListening);
|
|
102
|
+
reject(error);
|
|
103
|
+
};
|
|
104
|
+
const onListening = () => {
|
|
105
|
+
server.off("error", onError);
|
|
106
|
+
resolve(server.address().port);
|
|
107
|
+
};
|
|
108
|
+
server.once("error", onError);
|
|
109
|
+
server.listen(port, host, onListening);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeItem(items, item) {
|
|
114
|
+
const index = items.indexOf(item);
|
|
115
|
+
if (index >= 0) items.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function destroyQuietly(item) {
|
|
119
|
+
try {
|
|
120
|
+
item.destroy();
|
|
121
|
+
} catch {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function closeServerQuietly(server) {
|
|
127
|
+
try {
|
|
128
|
+
server.close();
|
|
129
|
+
} catch {
|
|
130
|
+
// ignore
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
function windowsHostIp() {
|
|
100
135
|
// In WSL2 NAT mode the Windows host is the default-route gateway (the vEthernet
|
|
101
136
|
// adapter), NOT the /etc/resolv.conf nameserver (which can be a DNS-tunneling proxy
|
|
@@ -120,6 +155,27 @@ function windowsHostIp() {
|
|
|
120
155
|
return null;
|
|
121
156
|
}
|
|
122
157
|
|
|
158
|
+
function wslBrokerHosts() {
|
|
159
|
+
const hosts = ["127.0.0.1"];
|
|
160
|
+
try {
|
|
161
|
+
const out = spawnSync("ip", ["-4", "-o", "addr", "show", "dev", "eth0", "scope", "global"], { encoding: "utf8" }).stdout ?? "";
|
|
162
|
+
const match = out.match(/\binet\s+([0-9.]+)\//);
|
|
163
|
+
if (match) hosts.push(match[1]);
|
|
164
|
+
} catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
if (hosts.length === 1) {
|
|
168
|
+
try {
|
|
169
|
+
const out = spawnSync("hostname", ["-I"], { encoding: "utf8" }).stdout ?? "";
|
|
170
|
+
const fallback = out.split(/\s+/).find((ip) => /^[0-9.]+$/.test(ip) && !ip.startsWith("127."));
|
|
171
|
+
if (fallback) hosts.push(fallback);
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return dedupe(hosts);
|
|
177
|
+
}
|
|
178
|
+
|
|
123
179
|
function httpGetJson(url, timeoutMs = 1000) {
|
|
124
180
|
return new Promise((resolve) => {
|
|
125
181
|
const request = http.get(url, { timeout: timeoutMs }, (response) => {
|
|
@@ -190,21 +246,19 @@ function windowsTempDir() {
|
|
|
190
246
|
return wslPath ? { winPath, wslPath } : null;
|
|
191
247
|
}
|
|
192
248
|
|
|
193
|
-
// Fixed port the Windows-side relay listens on, so a single Hyper-V firewall rule covers it.
|
|
194
|
-
const DEFAULT_RELAY_PORT = 39222;
|
|
195
|
-
|
|
196
249
|
function relayPort(projectRoot) {
|
|
197
250
|
const fromEnv = Number(process.env.DW_RELAY_PORT);
|
|
198
|
-
if (fromEnv) return fromEnv;
|
|
251
|
+
if (Number.isInteger(fromEnv) && fromEnv > 0 && fromEnv < 65536) return fromEnv;
|
|
199
252
|
if (projectRoot) {
|
|
200
253
|
try {
|
|
201
254
|
const cfg = JSON.parse(fs.readFileSync(path.join(projectRoot, ".dw", "config.json"), "utf8"));
|
|
202
|
-
|
|
255
|
+
const fromConfig = Number(cfg?.relayPort);
|
|
256
|
+
if (Number.isInteger(fromConfig) && fromConfig > 0 && fromConfig < 65536) return fromConfig;
|
|
203
257
|
} catch {
|
|
204
258
|
// ignore
|
|
205
259
|
}
|
|
206
260
|
}
|
|
207
|
-
return
|
|
261
|
+
return 0;
|
|
208
262
|
}
|
|
209
263
|
|
|
210
264
|
// The prebuilt Windows relay (cdp-relay.exe), installed by `setup-wsl-browser`. Order:
|
|
@@ -237,13 +291,183 @@ function findRelayExe(projectRoot) {
|
|
|
237
291
|
return null;
|
|
238
292
|
}
|
|
239
293
|
|
|
294
|
+
async function startReverseRelayBroker({ relay, debugPort, projectRoot }) {
|
|
295
|
+
const idleRelays = [];
|
|
296
|
+
const pendingClients = [];
|
|
297
|
+
const relaySockets = [];
|
|
298
|
+
const localSockets = [];
|
|
299
|
+
const relayChildren = [];
|
|
300
|
+
const relayWaiters = [];
|
|
301
|
+
let closed = false;
|
|
302
|
+
|
|
303
|
+
const notifyRelayReady = () => {
|
|
304
|
+
while (idleRelays.length > 0 && relayWaiters.length > 0) {
|
|
305
|
+
const waiter = relayWaiters.shift();
|
|
306
|
+
clearTimeout(waiter.timer);
|
|
307
|
+
waiter.resolve(true);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const waitForRelayReady = (timeoutMs) => {
|
|
312
|
+
if (idleRelays.length > 0) return Promise.resolve(true);
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
const waiter = { resolve, timer: null };
|
|
315
|
+
waiter.timer = setTimeout(() => {
|
|
316
|
+
removeItem(relayWaiters, waiter);
|
|
317
|
+
resolve(false);
|
|
318
|
+
}, timeoutMs);
|
|
319
|
+
relayWaiters.push(waiter);
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const removePendingClient = (pending) => {
|
|
324
|
+
clearTimeout(pending.timer);
|
|
325
|
+
removeItem(pendingClients, pending);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const pairSockets = (client, relaySocket) => {
|
|
329
|
+
const closePair = () => {
|
|
330
|
+
destroyQuietly(client);
|
|
331
|
+
destroyQuietly(relaySocket);
|
|
332
|
+
};
|
|
333
|
+
client.on("error", closePair);
|
|
334
|
+
relaySocket.on("error", closePair);
|
|
335
|
+
client.on("close", () => destroyQuietly(relaySocket));
|
|
336
|
+
relaySocket.on("close", () => destroyQuietly(client));
|
|
337
|
+
relaySocket.write("GO\n", (error) => {
|
|
338
|
+
if (error) closePair();
|
|
339
|
+
});
|
|
340
|
+
client.pipe(relaySocket);
|
|
341
|
+
relaySocket.pipe(client);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const drainPending = () => {
|
|
345
|
+
while (pendingClients.length > 0 && idleRelays.length > 0) {
|
|
346
|
+
const pending = pendingClients.shift();
|
|
347
|
+
clearTimeout(pending.timer);
|
|
348
|
+
const relaySocket = idleRelays.shift();
|
|
349
|
+
if (pending.client.destroyed || relaySocket.destroyed) {
|
|
350
|
+
destroyQuietly(pending.client);
|
|
351
|
+
destroyQuietly(relaySocket);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
pairSockets(pending.client, relaySocket);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const brokerServer = net.createServer((socket) => {
|
|
359
|
+
if (closed) {
|
|
360
|
+
socket.destroy();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
socket.setNoDelay(true);
|
|
364
|
+
relaySockets.push(socket);
|
|
365
|
+
idleRelays.push(socket);
|
|
366
|
+
socket.on("error", () => destroyQuietly(socket));
|
|
367
|
+
socket.on("close", () => {
|
|
368
|
+
removeItem(relaySockets, socket);
|
|
369
|
+
removeItem(idleRelays, socket);
|
|
370
|
+
});
|
|
371
|
+
notifyRelayReady();
|
|
372
|
+
drainPending();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const localServer = net.createServer((client) => {
|
|
376
|
+
if (closed) {
|
|
377
|
+
client.destroy();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
client.setNoDelay(true);
|
|
381
|
+
localSockets.push(client);
|
|
382
|
+
client.on("close", () => removeItem(localSockets, client));
|
|
383
|
+
client.on("error", () => destroyQuietly(client));
|
|
384
|
+
const pending = {
|
|
385
|
+
client,
|
|
386
|
+
timer: setTimeout(() => {
|
|
387
|
+
removePendingClient(pending);
|
|
388
|
+
destroyQuietly(client);
|
|
389
|
+
}, CLIENT_WAIT_TIMEOUT_MS),
|
|
390
|
+
};
|
|
391
|
+
client.on("close", () => removePendingClient(pending));
|
|
392
|
+
pendingClients.push(pending);
|
|
393
|
+
drainPending();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const cleanup = async () => {
|
|
397
|
+
closed = true;
|
|
398
|
+
for (const waiter of relayWaiters.splice(0)) {
|
|
399
|
+
clearTimeout(waiter.timer);
|
|
400
|
+
waiter.resolve(false);
|
|
401
|
+
}
|
|
402
|
+
for (const pending of pendingClients.splice(0)) {
|
|
403
|
+
clearTimeout(pending.timer);
|
|
404
|
+
destroyQuietly(pending.client);
|
|
405
|
+
}
|
|
406
|
+
for (const socket of [...localSockets, ...relaySockets]) destroyQuietly(socket);
|
|
407
|
+
for (const child of relayChildren) {
|
|
408
|
+
try {
|
|
409
|
+
child.kill();
|
|
410
|
+
} catch {
|
|
411
|
+
// ignore
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
closeServerQuietly(localServer);
|
|
415
|
+
closeServerQuietly(brokerServer);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
let localPort;
|
|
419
|
+
let brokerPort;
|
|
420
|
+
try {
|
|
421
|
+
localPort = await listen(localServer, relayPort(projectRoot), "127.0.0.1");
|
|
422
|
+
brokerPort = await listen(brokerServer, 0, "0.0.0.0");
|
|
423
|
+
} catch (error) {
|
|
424
|
+
await cleanup();
|
|
425
|
+
throw new Error(`Could not start the WSL reverse relay broker: ${error.message}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let selectedHost = null;
|
|
429
|
+
for (const brokerHost of wslBrokerHosts()) {
|
|
430
|
+
const child = spawn(
|
|
431
|
+
relay.wslPath,
|
|
432
|
+
["--reverse", brokerHost, String(brokerPort), String(debugPort), String(REVERSE_RELAY_POOL)],
|
|
433
|
+
{ detached: true, stdio: "ignore" },
|
|
434
|
+
);
|
|
435
|
+
relayChildren.push(child);
|
|
436
|
+
child.unref();
|
|
437
|
+
|
|
438
|
+
if (await waitForRelayReady(brokerHost === "127.0.0.1" ? 1800 : 3000)) {
|
|
439
|
+
selectedHost = brokerHost;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
child.kill();
|
|
445
|
+
} catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!selectedHost) {
|
|
451
|
+
await cleanup();
|
|
452
|
+
throw new Error(
|
|
453
|
+
"WSL is in NAT mode, but the Windows relay could not connect back to the WSL broker. " +
|
|
454
|
+
"Run `npx @brunosps00/dev-workflow setup-wsl-browser` to reinstall the relay, or enable mirrored networking.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
hosts: ["127.0.0.1", "localhost"],
|
|
460
|
+
port: localPort,
|
|
461
|
+
relayHost: selectedHost,
|
|
462
|
+
cleanup,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
240
466
|
// Launches the Windows browser in remote-debugging mode and returns a CDP descriptor.
|
|
241
467
|
// - mirrored networking: WSL shares the Windows loopback (Hyper-V LoopbackEnabled), connect 127.0.0.1.
|
|
242
|
-
// - NAT networking: the prebuilt cdp-relay.exe
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// inbound to Block. The browser echoes the request Host into webSocketDebuggerUrl, so the ws stays
|
|
246
|
-
// reachable through the relay.
|
|
468
|
+
// - NAT networking: the prebuilt cdp-relay.exe runs on Windows in reverse mode. It connects outbound
|
|
469
|
+
// to a WSL broker, which exposes a local proxy for Playwright. The browser echoes the request Host
|
|
470
|
+
// into webSocketDebuggerUrl, so the ws stays reachable through the local proxy.
|
|
247
471
|
// --remote-allow-origins=* is required or Chromium drops the DevTools ws (anti-DNS-rebinding).
|
|
248
472
|
async function launchWindowsBrowser(exePath, { headless, projectRoot }) {
|
|
249
473
|
const mirrored = isMirroredNetworking();
|
|
@@ -282,58 +506,85 @@ async function launchWindowsBrowser(exePath, { headless, projectRoot }) {
|
|
|
282
506
|
}
|
|
283
507
|
};
|
|
284
508
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
509
|
+
try {
|
|
510
|
+
let hosts;
|
|
511
|
+
let port;
|
|
512
|
+
let sourceSuffix = "";
|
|
513
|
+
if (mirrored) {
|
|
514
|
+
hosts = ["127.0.0.1", "localhost"];
|
|
515
|
+
port = debugPort;
|
|
516
|
+
} else {
|
|
517
|
+
const relay = findRelayExe(projectRoot);
|
|
518
|
+
if (!relay) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
"WSL is in NAT mode; reaching the Windows browser needs cdp-relay.exe. Run " +
|
|
521
|
+
"`npx @brunosps00/dev-workflow setup-wsl-browser` to install the prebuilt relay in your Windows user profile. " +
|
|
522
|
+
"Alternatively enable mirrored networking or set BROWSER_TEST to a CDP URL.",
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const reverseRelay = await startReverseRelayBroker({ relay, debugPort, projectRoot });
|
|
526
|
+
cleanups.unshift(reverseRelay.cleanup);
|
|
527
|
+
hosts = reverseRelay.hosts;
|
|
528
|
+
port = reverseRelay.port;
|
|
529
|
+
sourceSuffix = "+reverse-relay";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const found = await waitForCdp(hosts, port);
|
|
533
|
+
if (!found) {
|
|
294
534
|
throw new Error(
|
|
295
|
-
|
|
296
|
-
"`npx @brunosps00/dev-workflow setup-wsl-browser`
|
|
297
|
-
"Hyper-V firewall rule). Alternatively enable mirrored networking or set BROWSER_TEST to a CDP URL.",
|
|
535
|
+
`Launched ${path.basename(exePath)} but its CDP endpoint was not reachable (${hosts.join("/")}:${port}). ` +
|
|
536
|
+
"Run `npx @brunosps00/dev-workflow setup-wsl-browser` to reinstall the user-level relay, or enable mirrored networking.",
|
|
298
537
|
);
|
|
299
538
|
}
|
|
300
|
-
const rport = relayPort(projectRoot);
|
|
301
|
-
const relayChild = spawn(relay.wslPath, [String(rport), String(debugPort)], { detached: true, stdio: "ignore" });
|
|
302
|
-
relayChild.unref();
|
|
303
|
-
cleanups.unshift(() => relayChild.kill());
|
|
304
|
-
hosts = [windowsHostIp(), "127.0.0.1"].filter(Boolean);
|
|
305
|
-
port = rport;
|
|
306
|
-
}
|
|
307
539
|
|
|
308
|
-
|
|
309
|
-
|
|
540
|
+
return { mode: "cdp", endpoint: found.base, source: `launched:${path.basename(exePath)}${sourceSuffix}`, cleanup };
|
|
541
|
+
} catch (error) {
|
|
310
542
|
await cleanup();
|
|
311
|
-
throw
|
|
312
|
-
`Launched ${path.basename(exePath)} but its CDP endpoint was not reachable (${hosts.join("/")}:${port}). ` +
|
|
313
|
-
"The Hyper-V firewall rule may be missing — run `npx @brunosps00/dev-workflow setup-wsl-browser`.",
|
|
314
|
-
);
|
|
543
|
+
throw error;
|
|
315
544
|
}
|
|
316
|
-
|
|
317
|
-
return { mode: "cdp", endpoint: found.base, source: `launched:${path.basename(exePath)}${mirrored ? "" : "+relay"}`, cleanup };
|
|
318
545
|
}
|
|
319
546
|
|
|
320
547
|
const noopCleanup = async () => {};
|
|
321
548
|
|
|
322
|
-
function bundledDescriptor(headless, source = "
|
|
549
|
+
function bundledDescriptor(headless, source = "playwright-default") {
|
|
323
550
|
return { mode: "launch", launchOptions: { ...BUNDLED_LAUNCH, headless }, source };
|
|
324
551
|
}
|
|
325
552
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
553
|
+
function cdpFallbackInfo({ kind, source, value }) {
|
|
554
|
+
return { mode: "cdp", kind, source, value };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function resolveCdpFallback({ kind, value, source, projectRoot, headless }) {
|
|
558
|
+
if (kind === "cdp") {
|
|
559
|
+
const url = new URL(value.replace(/^cdp:\/\//i, "http://"));
|
|
560
|
+
const port = Number(url.port) || 9222;
|
|
561
|
+
const found = await findReachableCdp(port, [url.hostname]);
|
|
562
|
+
if (!found) {
|
|
563
|
+
throw new Error(`CDP fallback '${value}' is not reachable (start the browser with --remote-debugging-port=${port}).`);
|
|
564
|
+
}
|
|
565
|
+
return { mode: "cdp", endpoint: found.base, source: `fallback:${source}`, cleanup: noopCleanup };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (kind === "exe") {
|
|
569
|
+
if (!fs.existsSync(value)) {
|
|
570
|
+
throw new Error(`CDP fallback points to '${value}', but that file does not exist.`);
|
|
571
|
+
}
|
|
572
|
+
const descriptor = await launchWindowsBrowser(value, { headless, projectRoot });
|
|
573
|
+
return { ...descriptor, source: `fallback:${descriptor.source}` };
|
|
332
574
|
}
|
|
333
|
-
|
|
575
|
+
|
|
576
|
+
throw new Error(`Unsupported CDP fallback kind '${kind}'.`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function withCdpFallback(primary, fallbackArgs) {
|
|
580
|
+
return {
|
|
581
|
+
...primary,
|
|
582
|
+
fallback: cdpFallbackInfo(fallbackArgs),
|
|
583
|
+
openFallback: () => resolveCdpFallback(fallbackArgs),
|
|
584
|
+
};
|
|
334
585
|
}
|
|
335
586
|
|
|
336
|
-
export async function resolveBrowser({ projectRoot, headless = true
|
|
587
|
+
export async function resolveBrowser({ projectRoot, headless = true } = {}) {
|
|
337
588
|
const { value, source } = readConfigValue(projectRoot);
|
|
338
589
|
|
|
339
590
|
if (!value) {
|
|
@@ -343,30 +594,11 @@ export async function resolveBrowser({ projectRoot, headless = true, allowFallba
|
|
|
343
594
|
const kind = classifyValue(value);
|
|
344
595
|
|
|
345
596
|
if (kind === "cdp") {
|
|
346
|
-
|
|
347
|
-
const port = Number(url.port) || 9222;
|
|
348
|
-
const found = await findReachableCdp(port, [url.hostname]);
|
|
349
|
-
if (!found) {
|
|
350
|
-
return degradeOrThrow(
|
|
351
|
-
`BROWSER_TEST endpoint '${value}' is not reachable (start the browser with --remote-debugging-port=${port}).`,
|
|
352
|
-
{ headless, allowFallback },
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
return { mode: "cdp", endpoint: found.base, source, cleanup: noopCleanup };
|
|
597
|
+
return withCdpFallback(bundledDescriptor(headless), { kind, value, source, projectRoot, headless });
|
|
356
598
|
}
|
|
357
599
|
|
|
358
600
|
if (kind === "exe") {
|
|
359
|
-
|
|
360
|
-
return degradeOrThrow(`BROWSER_TEST points to '${value}', but that file does not exist.`, {
|
|
361
|
-
headless,
|
|
362
|
-
allowFallback,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
try {
|
|
366
|
-
return await launchWindowsBrowser(value, { headless, projectRoot });
|
|
367
|
-
} catch (error) {
|
|
368
|
-
return degradeOrThrow(error.message, { headless, allowFallback });
|
|
369
|
-
}
|
|
601
|
+
return withCdpFallback(bundledDescriptor(headless), { kind, value, source, projectRoot, headless });
|
|
370
602
|
}
|
|
371
603
|
|
|
372
604
|
// channel: chrome | msedge | chromium
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"trigger": "Security review, sensitive code, OWASP checks, auth/payment/input surfaces",
|
|
241
241
|
"expected_output": "High-confidence security findings or explicit pass.",
|
|
242
242
|
"load_policy": "lazy-references",
|
|
243
|
-
"context_limit":
|
|
243
|
+
"context_limit": 14000,
|
|
244
244
|
"bundled": true
|
|
245
245
|
},
|
|
246
246
|
{
|
|
@@ -102,3 +102,15 @@ Per-recipe details in `references/auth-patterns.md`.
|
|
|
102
102
|
## Inspired by
|
|
103
103
|
|
|
104
104
|
Hand-curated by dev-workflow. `.http` syntax follows the JetBrains HTTP Client / VSCode REST Client conventions. Per-stack recipes adapt patterns from each ecosystem's official testing docs (FastAPI testing tutorial, NestJS testing recipes, Microsoft.AspNetCore.Mvc.Testing docs, Axum testing examples).
|
|
105
|
+
|
|
106
|
+
## Structured Return
|
|
107
|
+
|
|
108
|
+
When invoked directly or by a harness, return or merge this block:
|
|
109
|
+
|
|
110
|
+
- **Status:** `PASS` when a recipe and evidence path are selected, `FINDINGS` when API test gaps remain, `BLOCKED` when the stack or auth flow is unknown, `NOT_APPLICABLE` when no HTTP/API surface is in scope.
|
|
111
|
+
- **Scope:** API surface, requirement/RF, auth mode, and selected recipe.
|
|
112
|
+
- **Evidence:** route/schema/source files read and request/response logs or planned log path.
|
|
113
|
+
- **Artifacts:** `.http` file, test file, matrix, or log path to create/update.
|
|
114
|
+
- **Decisions:** why this recipe fits the stack and why alternatives were rejected.
|
|
115
|
+
- **Risks:** credentials, environment dependencies, flaky external calls, or uncovered negative cases.
|
|
116
|
+
- **Next Step:** exact command or file edit needed to execute the recipe.
|
|
@@ -82,3 +82,15 @@ Each row points to `services/<name>.yml`.
|
|
|
82
82
|
## Inspired by
|
|
83
83
|
|
|
84
84
|
Hand-curated by dev-workflow. Service defaults follow upstream documentation (Docker Hub `postgres`, `redis`, `mailhog/mailhog`, `minio/minio`, `getmeili/meilisearch`, `jaegertracing/all-in-one`, `traefik`). Healthcheck patterns adapted from `docker-library/healthcheck` and the official compose docs.
|
|
85
|
+
|
|
86
|
+
## Structured Return
|
|
87
|
+
|
|
88
|
+
When invoked directly or by a harness, return or merge this block:
|
|
89
|
+
|
|
90
|
+
- **Status:** `PASS` when service recipes and compose guidance are selected, `FINDINGS` when compose gaps remain, `BLOCKED` when required ports/secrets/envs are unknown, `NOT_APPLICABLE` when no local infrastructure service is in scope.
|
|
91
|
+
- **Scope:** services, project mode (`dev`/`prod`), ports, volumes, and env files touched.
|
|
92
|
+
- **Evidence:** existing compose files, service references, healthcheck needs, and conflicting ports.
|
|
93
|
+
- **Artifacts:** compose snippets/files, `.env` keys, healthchecks, and follow-up reference files.
|
|
94
|
+
- **Decisions:** selected service recipe, image tag policy, and any prod/dev transform.
|
|
95
|
+
- **Risks:** secret leakage, readiness ambiguity, data persistence, mail routing, or destructive defaults.
|
|
96
|
+
- **Next Step:** exact compose edit and validation command.
|
|
@@ -11,7 +11,7 @@ allowed-tools:
|
|
|
11
11
|
|
|
12
12
|
# dw-codebase-intel
|
|
13
13
|
|
|
14
|
-
Bundled skill that gives dev-workflow native **codebase intelligence** — a queryable knowledge base of the project's stack, file graph, API surface, dependencies, and architecture. Other commands (`/dw-plan prd`, `/dw-plan techspec`, `/dw-review --code-only`, `/dw-
|
|
14
|
+
Bundled skill that gives dev-workflow native **codebase intelligence** — a queryable knowledge base of the project's stack, file graph, API surface, dependencies, and architecture. Other commands (`/dw-plan prd`, `/dw-plan techspec`, `/dw-review --code-only`, `/dw-opportunities`, `/dw-refactor`, `/dw-brainstorm`, etc.) read from this index instead of re-exploring the codebase on every invocation.
|
|
15
15
|
|
|
16
16
|
## Why a skill (not inline)
|
|
17
17
|
|
|
@@ -111,3 +111,15 @@ If no `.dw/intel/` exists at all, `/dw-intel` falls back to `.dw/rules/` (seeded
|
|
|
111
111
|
Adapted from [`get-shit-done-cc`](https://github.com/gsd-build/get-shit-done) (`gsd-intel-updater`) by gsd-build (MIT license). Core schemas (`files.json`, `apis.json`, `deps.json`, `stack.json`, `arch.md`) and incremental update protocol preserved. Path conventions changed from `.planning/intel/` to `.dw/intel/`. CLI tooling (`gsd-sdk query intel.*`) replaced by agent-driven inline operations (no separate runtime). The companion `gsd-codebase-mapper` agent (human-readable analysis docs) was NOT ported — its scope overlaps with the existing `/dw-analyze-project` command which writes to `.dw/rules/`.
|
|
112
112
|
|
|
113
113
|
The `bugfixes.json` index and the `bugfix-history` / `risk-area` query shapes (see `references/query-patterns.md`) were added in v1.0.2 to enable cross-skill awareness of `.dw/bugfixes/` — adapted from the operational-memory pattern in [`tech-leads-club/agent-skills/tlc-spec-driven`](https://github.com/tech-leads-club/agent-skills/tree/main/packages/skills-catalog/skills/(development)/tlc-spec-driven) (CC-BY-4.0, Felipe Rodrigues). The original skill maintains a single `STATE.md`; dev-workflow splits this into project-level `.dw/STATE.md` (session continuity) and a queryable `bugfixes.json` (historical fixes), which compose with the existing per-PRD `MEMORY.md`.
|
|
114
|
+
|
|
115
|
+
## Structured Return
|
|
116
|
+
|
|
117
|
+
When invoked directly or by a harness, return or merge this block:
|
|
118
|
+
|
|
119
|
+
- **Status:** `PASS` when indexes are updated or the query answer is cited, `FINDINGS` when intel is stale/incomplete, `BLOCKED` when required project files cannot be read, `NOT_APPLICABLE` when the task does not need codebase intel.
|
|
120
|
+
- **Scope:** index files, query type, module, and paths inspected.
|
|
121
|
+
- **Evidence:** cited `.dw/intel/*` entries, source files, timestamps, or stale-index indicators.
|
|
122
|
+
- **Artifacts:** updated index files, query answer, codemap, or bugfix index path.
|
|
123
|
+
- **Decisions:** whether to rebuild, incrementally update, or answer from existing intel.
|
|
124
|
+
- **Risks:** stale docs, missing generated files, broad reads, or uncited conclusions.
|
|
125
|
+
- **Next Step:** exact index update, query refinement, or source read required.
|
|
@@ -43,3 +43,15 @@ Return:
|
|
|
43
43
|
5. What must remain loaded because commands depend on it.
|
|
44
44
|
|
|
45
45
|
Inspired by ECC's `context-budget` and `strategic-compact` patterns; adapted for dev-workflow's `.dw/` scaffold and cross-platform wrappers.
|
|
46
|
+
|
|
47
|
+
## Structured Return
|
|
48
|
+
|
|
49
|
+
When invoked directly or by a harness, return or merge this block:
|
|
50
|
+
|
|
51
|
+
- **Status:** `PASS` when the budget is acceptable, `FINDINGS` when concrete savings exist, `BLOCKED` when files cannot be measured, `NOT_APPLICABLE` when context budget is not relevant.
|
|
52
|
+
- **Scope:** commands, skills, agents, MCPs, and instruction files measured.
|
|
53
|
+
- **Evidence:** largest files, duplicate content, loaded references, and approximate byte/token cost.
|
|
54
|
+
- **Artifacts:** report path or inline budget table.
|
|
55
|
+
- **Decisions:** what stays loaded, what moves behind lazy references, and what can merge/remove.
|
|
56
|
+
- **Risks:** losing trigger clarity, hiding required instructions, or creating stale references.
|
|
57
|
+
- **Next Step:** top compaction action with expected savings.
|
|
@@ -180,3 +180,15 @@ Adaptations for dev-workflow:
|
|
|
180
180
|
- Integration points map to dev-workflow commands (`dw-brainstorm`, `dw-create-techspec`) instead of Compozy's idea-factory pipeline
|
|
181
181
|
|
|
182
182
|
Credit: Compozy project (https://github.com/compozy/compozy).
|
|
183
|
+
|
|
184
|
+
## Structured Return
|
|
185
|
+
|
|
186
|
+
When invoked directly or by a harness, return or merge this block:
|
|
187
|
+
|
|
188
|
+
- **Status:** `PASS` when advisor positions and a recommendation are complete, `FINDINGS` when unresolved tensions remain, `BLOCKED` when inputs or constraints are missing, `NOT_APPLICABLE` when no high-impact tradeoff exists.
|
|
189
|
+
- **Scope:** decision, constraints, advisors consulted, and artifact path.
|
|
190
|
+
- **Evidence:** each advisor's cited position and the facts they used.
|
|
191
|
+
- **Artifacts:** council report, debate notes, or decision summary path.
|
|
192
|
+
- **Decisions:** recommended path, rejected alternatives, and what would change the call.
|
|
193
|
+
- **Risks:** unresolved disagreement, missing stakeholder data, or unvalidated assumptions.
|
|
194
|
+
- **Next Step:** concrete follow-up: accept, run experiment, gather data, or escalate.
|
|
@@ -106,3 +106,15 @@ After 60 minutes stuck at step 2 (localize) or step 4 (root cause), surface the
|
|
|
106
106
|
- Don't silently spin. Fresh eyes find what stuck eyes miss.
|
|
107
107
|
|
|
108
108
|
This is not failure — it's protocol. Stuck > 1 hour is a signal, not a flaw.
|
|
109
|
+
|
|
110
|
+
## Structured Return
|
|
111
|
+
|
|
112
|
+
When invoked directly or by a harness, return or merge this block:
|
|
113
|
+
|
|
114
|
+
- **Status:** `PASS` when the bug is reproduced, root-caused, guarded, and verified, `FINDINGS` when a confirmed defect remains, `BLOCKED` when reproduction/localization is impossible, `NOT_APPLICABLE` when the task is not bug-shaped.
|
|
115
|
+
- **Scope:** symptom, affected surface, environment, and triage step reached.
|
|
116
|
+
- **Evidence:** reproduction command, failing output, localized files, and root-cause proof.
|
|
117
|
+
- **Artifacts:** bugfix task/report, regression test, logs, or verification output.
|
|
118
|
+
- **Decisions:** chosen fix path and rejected hypotheses.
|
|
119
|
+
- **Risks:** non-reproducibility, partial guard coverage, rollback gaps, or hidden adjacent defects.
|
|
120
|
+
- **Next Step:** next triage step or verification command.
|
|
@@ -131,3 +131,15 @@ If the executor exhausts its context budget mid-phase OR the user signals stop:
|
|
|
131
131
|
## Inspired by
|
|
132
132
|
|
|
133
133
|
Adapted from [`get-shit-done-cc`](https://github.com/gsd-build/get-shit-done) (`gsd-executor`, `gsd-plan-checker`) by gsd-build (MIT license). Core protocols (goal-backward verification, atomic commits, deviation handling, checkpoint resume) preserved. Path conventions changed from `.planning/<phase>/` to `.dw/spec/prd-<slug>/`. SDK CLI calls (`gsd-sdk query init.execute-phase`) replaced by inline operations. The companion `gsd-debugger` agent (1452 lines) was NOT ported — its scope overlaps with the existing `/dw-bugfix` and `/dw-qa --fix` commands.
|
|
134
|
+
|
|
135
|
+
## Structured Return
|
|
136
|
+
|
|
137
|
+
When invoked directly or by a harness, return or merge this block:
|
|
138
|
+
|
|
139
|
+
- **Status:** `PASS` when plan-check and execution wave complete, `FINDINGS` when deviations or failed tasks remain, `BLOCKED` when plan-check returns `REVISE`/`BLOCK`, `NOT_APPLICABLE` when no task execution is in scope.
|
|
140
|
+
- **Scope:** PRD/spec slug, wave, tasks, dependencies, and agent roles.
|
|
141
|
+
- **Evidence:** plan-check verdict, task files read, command outputs, and deviation notes.
|
|
142
|
+
- **Artifacts:** changed files, task memory, checkpoint, deviations report, or commits.
|
|
143
|
+
- **Decisions:** wave ordering, task split, and accepted deviations.
|
|
144
|
+
- **Risks:** dependency drift, uncommitted work, skipped verification, or partial checkpoint.
|
|
145
|
+
- **Next Step:** next task, commit, checkpoint, or revision needed.
|
|
@@ -121,3 +121,15 @@ Real-world software can't always be perfect:
|
|
|
121
121
|
- [ ] No `--no-verify`
|
|
122
122
|
- [ ] No amend of pushed commit
|
|
123
123
|
- [ ] Branch name follows convention
|
|
124
|
+
|
|
125
|
+
## Structured Return
|
|
126
|
+
|
|
127
|
+
When invoked directly or by a harness, return or merge this block:
|
|
128
|
+
|
|
129
|
+
- **Status:** `PASS` when the commit/PR plan is atomic and verified, `FINDINGS` when git hygiene gaps remain, `BLOCKED` when repository state is ambiguous, `NOT_APPLICABLE` when no git action is in scope.
|
|
130
|
+
- **Scope:** branch, staged/unstaged files, intended commit/PR, and remote state.
|
|
131
|
+
- **Evidence:** `git status`, diff summary, verification commands, and branch checks.
|
|
132
|
+
- **Artifacts:** commit plan, commit hash, PR body, or branch hygiene report.
|
|
133
|
+
- **Decisions:** commit split, message type/scope, and files intentionally excluded.
|
|
134
|
+
- **Risks:** unrelated dirty files, skipped checks, pushed-history rewrite, or mixed intent.
|
|
135
|
+
- **Next Step:** exact stage/commit/push/PR command or cleanup action.
|
|
@@ -166,3 +166,15 @@ Files are committed to the repo alongside code — incidents are part of the pro
|
|
|
166
166
|
## Why this skill exists
|
|
167
167
|
|
|
168
168
|
dev-workflow's existing surface is "build feature → ship." Nothing covered "production broke, what now?" Teams improvised postmortems, action items got lost, and the same bug recurred. This skill closes that loop: structured response in the moment, blameless reflection after, and cross-incident learning that feeds back into the project's constitution.
|
|
169
|
+
|
|
170
|
+
## Structured Return
|
|
171
|
+
|
|
172
|
+
When invoked directly or by a harness, return or merge this block:
|
|
173
|
+
|
|
174
|
+
- **Status:** `PASS` when incident triage, resolution, comms, and follow-up are complete, `FINDINGS` when incident work remains, `BLOCKED` when severity/ownership/evidence is missing, `NOT_APPLICABLE` when no incident workflow is in scope.
|
|
175
|
+
- **Scope:** incident id, severity, service, timeline window, and owner.
|
|
176
|
+
- **Evidence:** symptoms, customer impact, logs/metrics, mitigation, and verification.
|
|
177
|
+
- **Artifacts:** incident files, communication draft, action items, or postmortem path.
|
|
178
|
+
- **Decisions:** severity, mitigation path, rollback/fix choice, and communication cadence.
|
|
179
|
+
- **Risks:** recurring cause, incomplete customer notice, weak action items, or missing monitoring.
|
|
180
|
+
- **Next Step:** next incident phase or postmortem/action-item owner.
|