@brunosps00/dev-workflow 2.4.0 → 2.6.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +109 -0
- package/README.md +10 -6
- package/bin/dev-workflow.js +1 -1
- package/lib/constants.js +2 -0
- package/lib/doctor.js +46 -9
- package/lib/hooks.js +112 -27
- package/lib/init.js +11 -5
- package/lib/install-deps.js +3 -0
- package/lib/install-quality-tools.js +185 -0
- package/lib/uninstall.js +2 -2
- package/package.json +1 -1
- package/scaffold/en/commands/dw-analyze-project.md +27 -2
- package/scaffold/en/commands/dw-autopilot.md +19 -9
- package/scaffold/en/commands/dw-generate-pr.md +3 -0
- package/scaffold/en/commands/dw-help.md +10 -5
- package/scaffold/en/commands/dw-quality-gate.md +397 -0
- package/scaffold/en/commands/dw-review.md +13 -4
- package/scaffold/en/commands/dw-run.md +1 -0
- package/scaffold/en/references/command-routing.md +1 -0
- package/scaffold/en/references/invariants.md +3 -2
- package/scaffold/pt-br/commands/dw-analyze-project.md +26 -2
- package/scaffold/pt-br/commands/dw-autopilot.md +19 -9
- package/scaffold/pt-br/commands/dw-generate-pr.md +3 -0
- package/scaffold/pt-br/commands/dw-help.md +10 -5
- package/scaffold/pt-br/commands/dw-quality-gate.md +401 -0
- package/scaffold/pt-br/commands/dw-review.md +13 -4
- package/scaffold/pt-br/commands/dw-run.md +1 -0
- package/scaffold/pt-br/references/command-routing.md +1 -0
- package/scaffold/pt-br/references/invariants.md +3 -2
- package/scaffold/scripts/hooks/git-guardrails.mjs +3 -2
- package/scaffold/scripts/hooks/statusline.mjs +14 -11
- package/scaffold/skill-registry.json +1 -1
- package/scaffold/skills/dw-simplification/SKILL.md +1 -0
- package/scaffold/skills/dw-simplification/references/complexity-metrics.md +5 -3
- package/scaffold/skills/dw-simplification/references/quality-gate-tools.md +178 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-workflow",
|
|
3
3
|
"description": "Standalone, à-la-carte skills from the dev-workflow pipeline: minimalism, search-first, simplification, verification, security review, and more.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bruno Santos"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,115 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
10
10
|
> those versions were released, so they are summaries of what shipped, not
|
|
11
11
|
> contemporaneous release notes. `git log` remains the authoritative record.
|
|
12
12
|
|
|
13
|
+
## [2.6.0] — 2026-10-07
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Git guardrails under Codex.** `init` and `update` also write the git-guardrails
|
|
18
|
+
`PreToolUse` hook to `.codex/hooks.json` — same payload and deny contract as Claude Code,
|
|
19
|
+
matcher `Bash`. It is the only hook Codex gets: the session-cost tracker parses Claude Code
|
|
20
|
+
transcripts and Codex has no statusLine command. Codex runs project hooks only after the
|
|
21
|
+
project and the hook are trusted. Merge-aware like the Claude side: a malformed
|
|
22
|
+
`.codex/hooks.json` is left untouched, and `uninstall` removes only our entry, deleting the
|
|
23
|
+
file when nothing else is left. `doctor` warns when the Codex guardrail is missing or
|
|
24
|
+
outdated.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Hooks stopped running whenever the shell left the project root.** The hook and statusLine
|
|
29
|
+
commands were cwd-relative (`node .dw/scripts/hooks/…`). Claude Code runs hooks in the
|
|
30
|
+
session's current directory, which follows every `cd`, so from any subdirectory they failed
|
|
31
|
+
with `Cannot find module` — and a failing `PreToolUse` hook is non-blocking, so Bash ran with
|
|
32
|
+
no git guardrail at all. In one real session, 843 of 1,243 Bash calls ran unguarded this way.
|
|
33
|
+
Commands now resolve `${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel || pwd)}`:
|
|
34
|
+
Claude Code exports the variable to hooks, and Codex, which exports none, gets the git root.
|
|
35
|
+
`update` rewrites the old entries in place, and `doctor` reports a cwd-relative guardrail as
|
|
36
|
+
an issue.
|
|
37
|
+
- The statusline read `.dw/` state (active spec, minimalism mode, today's spend) from the
|
|
38
|
+
current directory. It now reads it from the payload's `workspace.project_dir`.
|
|
39
|
+
- The guardrail's deny message pointed only at `.claude/settings.json`; it now names
|
|
40
|
+
`.codex/hooks.json` too, as does the invariants floor.
|
|
41
|
+
|
|
42
|
+
## [2.5.0] — 2026-09-28
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`/dw-quality-gate` — a measured quality gate on new code.** SonarQube's "Clean as You
|
|
47
|
+
Code" idea without a server: the diff's new code is measured for complexity, duplication,
|
|
48
|
+
new lint issues and coverage of changed lines, and each changed file is compared with its
|
|
49
|
+
own merge-base version measured by the same engine. Only what the diff introduces or makes
|
|
50
|
+
worse blocks; pre-existing debt a diff merely touches is advisory. Verdicts are `APPROVED`,
|
|
51
|
+
`APPROVED WITH CAVEATS`, `REJECTED` and `UNMEASURED` (no complexity engine ran, which
|
|
52
|
+
blocks: complexity is the one required layer).
|
|
53
|
+
- Engines are layered like the Security Gate: qlty for cognitive complexity with lizard as
|
|
54
|
+
fallback, jscpd over the whole source tree for duplication, qlty check or the project's
|
|
55
|
+
own lint, and the project's own coverage report. A missing engine degrades one layer.
|
|
56
|
+
- A diff never configures its own gate: `.dw/quality/gate.json` (thresholds, `exclude`,
|
|
57
|
+
waivers), `.qlty/qlty.toml`, the linter's config and ignore files, `.jscpd.json`, and the
|
|
58
|
+
lint and coverage commands are read from the base branch (a changed file is swapped for
|
|
59
|
+
its base version during measurement and restored byte for byte), and a diff that loosens them alongside code is a blocking `config-loosening`
|
|
60
|
+
finding. A configuration-only PR (gate.json, qlty.toml, ADRs) is the sanctioned route:
|
|
61
|
+
its loosening is listed for the owner's review instead of blocking.
|
|
62
|
+
- Blanket or file-wide suppressions added by the diff block; a suppression that names the
|
|
63
|
+
rule and gives a checkable reason is advisory. Coverage exclusions follow the same rule
|
|
64
|
+
when a coverage threshold is set.
|
|
65
|
+
- Complexity is the one required layer. lizard (cyclomatic, blind to nesting) is only the
|
|
66
|
+
engine for languages qlty cannot parse; a qlty-supported file measured without qlty is
|
|
67
|
+
`UNMEASURED`.
|
|
68
|
+
- New code is measured against the working tree, including untracked files, so
|
|
69
|
+
uncommitted `/dw-run` output counts; duplication
|
|
70
|
+
scans every tracked source file with `--min-lines 0`, since jscpd otherwise drops dense
|
|
71
|
+
clones on fewer than five lines.
|
|
72
|
+
- Refutation can reject a blocking candidate only for named reasons (generated code, flat
|
|
73
|
+
enum switch, engine mis-parse, required boilerplate); an unsure refutation leaves the
|
|
74
|
+
finding standing. Waivers need a reason, an existing ADR for HIGH rules, and may cap the
|
|
75
|
+
value with `max`.
|
|
76
|
+
- Coverage never blocks without a project threshold (`coverage.newCode`); with one, a
|
|
77
|
+
missing report fails the layer instead of skipping it.
|
|
78
|
+
- **`/dw-quality-gate --full`** — a complete, advisory project report: complexity
|
|
79
|
+
distribution, worst functions, duplication by language and directory, lint, coverage,
|
|
80
|
+
hotspots (churn × complexity), trend against the previous report and the baseline, and a
|
|
81
|
+
prioritized debt backlog routed to `/dw-refactor`, tests or lint fixes.
|
|
82
|
+
`--update-baseline` writes `.dw/quality/baseline.json` from the same measurement on the
|
|
83
|
+
base branch.
|
|
84
|
+
- **`install-deps` installs the quality engines** instead of only printing instructions:
|
|
85
|
+
qlty from its pinned GitHub release with the SHA-256 verified, lizard into a private venv
|
|
86
|
+
(sidestepping PEP 668), and a warmed npx cache for jscpd — all under `~/.dw`, never inside
|
|
87
|
+
a project and without editing the shell profile.
|
|
88
|
+
- **`/dw-analyze-project` Step 5.2** runs `/dw-quality-gate --full --no-write-config` (or
|
|
89
|
+
`--update-baseline` on the base branch), cites the measured values in the anti-patterns and
|
|
90
|
+
the concerns map, and records a `## Quality Baseline` in `.dw/rules/index.md`.
|
|
91
|
+
`--no-write-config` keeps that documentation-only command from writing `.qlty/qlty.toml`.
|
|
92
|
+
- `dw-simplification/references/quality-gate-tools.md` — engine commands and output shapes,
|
|
93
|
+
each verified by running it (qlty 0.649, lizard 1.24, jscpd 5.3).
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- **The Quality Gate is mandatory, in every language.** After updating, `/dw-review`
|
|
98
|
+
(Level 3 step 9) returns REJECTED when `.dw/quality/quality-summary.md` is missing, stale,
|
|
99
|
+
`REJECTED` or `UNMEASURED`; `/dw-generate-pr` (hard gate 3) re-runs the gate against the
|
|
100
|
+
PR's target branch before every push, on a clean tree, instead of trusting a stored
|
|
101
|
+
summary, and copies suppressions, configuration changes and degraded layers into the PR
|
|
102
|
+
description; and
|
|
103
|
+
`/dw-autopilot` runs it next to the Security Gate. Run
|
|
104
|
+
`npx @brunosps00/dev-workflow install-deps` so the engines are present; without any engine
|
|
105
|
+
the verdict is `UNMEASURED` and PRs block.
|
|
106
|
+
- `/dw-autopilot`: the Security Gate's non-SECRET `REJECTED` gets one fix pass and a re-scan,
|
|
107
|
+
like the Quality Gate, before stop 3; a SECRET still stops at once. A fix pass invalidates
|
|
108
|
+
the review and the scan. Stop 5 applies to findings still `high`/`critical` after the
|
|
109
|
+
goal's correction, and an ADR the run wrote itself does not count. Stops now persist
|
|
110
|
+
`status: blocked`, `blocked_reason` and `question`, with a `blocked` resume row, and never
|
|
111
|
+
record a secret value.
|
|
112
|
+
- `/dw-autopilot` now reads `.dw/references/untrusted-input.md` before external text enters
|
|
113
|
+
the run.
|
|
114
|
+
- `/dw-run` gains stop 8: the review stays REJECTED on something a code correction cannot
|
|
115
|
+
clear (quality gate `UNMEASURED`, a SECRET, a decision needing an ADR or waiver).
|
|
116
|
+
- `/dw-review`'s escapes are stated per gate instead of "ADR is the only escape": an ADR for
|
|
117
|
+
constitution high+critical and non-SECRET security findings, a `gate.json` waiver for the
|
|
118
|
+
quality gate, none for SECRET or verify.
|
|
119
|
+
- `complexity-metrics.md` splits cognitive 16+ into 16-25 and 26+, and distinguishes
|
|
120
|
+
whole-repo CI gates from new-code gates.
|
|
121
|
+
|
|
13
122
|
## [2.4.0] — 2026-09-21
|
|
14
123
|
|
|
15
124
|
### Added
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This will:
|
|
|
16
16
|
5. Install bundled skills (`dw-verify`, `dw-memory`, `dw-review-rigor`, `dw-ui-discipline`, `dw-testing-discipline`, `security-review`, etc.) to `.agents/skills/`
|
|
17
17
|
6. Generate skill wrappers and project agents for Claude Code, Codex, Copilot, and OpenCode
|
|
18
18
|
7. Configure MCP servers (Context7 + Playwright)
|
|
19
|
-
8. Install enforcement hooks (git guardrails + a `SessionEnd` token-cost tracker → `.dw/metrics/costs.jsonl`) + a statusline (branch · active spec · minimalism mode · today's estimated spend) into `.claude/settings.json` (merge-aware — your own hooks/statusline are never overwritten)
|
|
19
|
+
8. Install enforcement hooks (git guardrails + a `SessionEnd` token-cost tracker → `.dw/metrics/costs.jsonl`) + a statusline (branch · active spec · minimalism mode · today's estimated spend) into `.claude/settings.json`, and the git guardrails into `.codex/hooks.json` for Codex (merge-aware — your own hooks/statusline are never overwritten; Codex runs project hooks only after you trust them)
|
|
20
20
|
|
|
21
21
|
> **Compozy-inspired disciplines.** Since 0.5.0, dev-workflow bundles three primitives — `dw-verify`, `dw-memory`, `dw-review-rigor` — adapted from the [Compozy](https://github.com/compozy/compozy) project and invoked internally by existing commands. See [docs/compozy-integration.md](docs/compozy-integration.md) for what was ported and what was not.
|
|
22
22
|
|
|
@@ -42,7 +42,7 @@ Exportable skills (no `.dw/` pipeline required): `dw-minimalism`, `dw-search-fir
|
|
|
42
42
|
|
|
43
43
|
## Commands
|
|
44
44
|
|
|
45
|
-
dev-workflow v2.3.0 ships **
|
|
45
|
+
dev-workflow v2.3.0 ships **42 commands** organized into four tiers. Most users only invoke Tier 1 + Tier 2.
|
|
46
46
|
|
|
47
47
|
### Tier 1 — Gateway (4)
|
|
48
48
|
|
|
@@ -90,7 +90,7 @@ Use these when you want step-by-step control instead of `/dw-autopilot`.
|
|
|
90
90
|
| **`/dw-install-azure-skills`** | **Opt-in.** Clones curated Azure skills from [`MicrosoftDocs/Agent-Skills`](https://github.com/MicrosoftDocs/Agent-Skills) (CC-BY-4.0) into `.agents/skills/azure/` and registers the [Microsoft Learn MCP Server](https://learn.microsoft.com/en-us/training/support/mcp-get-started) (HTTP, no-auth). Interactive category selection (Compute / Data & Storage / AI & ML / Networking / Identity & Security / DevOps / Observability / Integration / Architecture / All). Re-run to refresh from upstream. Also available as CLI: `npx @brunosps00/dev-workflow install-azure-skills`. |
|
|
91
91
|
| **`/dw-install-aws-skills`** | **Opt-in.** Clones curated AWS skills from [`aws/agent-toolkit-for-aws`](https://github.com/aws/agent-toolkit-for-aws) (Apache 2.0) into `.agents/skills/aws/` and registers the unified [AWS MCP Server](https://docs.aws.amazon.com/aws-mcp/) (stdio via `uvx mcp-proxy-for-aws@latest`). **Requires `uv`, `aws cli ≥ 2.32.0`, and AWS credentials.** Interactive category selection (Core / Analytics / Database / EC2 / Migration / Networking / Operations / Security / Serverless / Storage / All). The agent gains `aws___call_aws` (executes 15,000+ AWS APIs) and `aws___run_script` (Python sandboxed) — review `.dw/references/aws-mcp-instructions.md` for the destructive-operations protocol. Also available as CLI: `npx @brunosps00/dev-workflow install-aws-skills [--region=<aws-region>]`. |
|
|
92
92
|
|
|
93
|
-
### Tier 4 — Hidden/Internal (
|
|
93
|
+
### Tier 4 — Hidden/Internal (13)
|
|
94
94
|
|
|
95
95
|
These are auto-invoked by Tier 1-3 commands. Available standalone via `/dw-help --advanced`.
|
|
96
96
|
|
|
@@ -99,6 +99,7 @@ These are auto-invoked by Tier 1-3 commands. Available standalone via `/dw-help
|
|
|
99
99
|
| **`/dw-adr "decision"`** | Records an Architecture Decision Record at `.dw/spec/<prd>/adrs/`. | `/dw-plan techspec --council`; deviations from constitution |
|
|
100
100
|
| **`/dw-intel "question"`** | Query codebase intelligence (`.dw/intel/`). `--build` (re)creates the index. | `/dw-plan`, `/dw-review`, `/dw-bugfix` |
|
|
101
101
|
| **`/dw-secure-audit`** | **Security Gate** (phase after review/QA, before commit/PR): OWASP + Semgrep SAST (diff/generated code) + gitleaks secrets + Trivy SCA/IaC + lockfile + supply-chain + outdated. Rigoroso: secrets/CRITICAL/HIGH block (secrets have no ADR escape). Flags: `--scan-only`, `--plan`, `--execute`. See [docs/security-gate.md](docs/security-gate.md). | `/dw-review`, `/dw-autopilot` (explicit step), enforced by `/dw-generate-pr` |
|
|
102
|
+
| **`/dw-quality-gate`** | **Quality Gate** (measured, SonarQube-style "Clean as You Code" without a server): complexity, duplication, new lint issues and coverage of the diff's new code, each changed file compared with its merge-base version; thresholds come from the base branch, so a PR cannot loosen its own gate. qlty (or lizard) for complexity, jscpd for duplication; no complexity engine = `UNMEASURED`, which blocks. Pre-existing debt never blocks. `--full` writes a complete project report (distributions, hotspots, trend, prioritized debt backlog). Flags: `--since`, `--full`, `--update-baseline`, `--scan-only`. | `/dw-review`, `/dw-autopilot` (next to the Security Gate), enforced by `/dw-generate-pr` |
|
|
102
103
|
| **`/dw-goal "<objective>"`** | Durable objective contract. Uses Codex native `/goal` when available and `.dw/goals/` everywhere for Codex, Copilot, Claude Code, and OpenCode. | `/dw-autopilot` after planning |
|
|
103
104
|
| **`/dw-claude-run`** | Fire `claude -p` (headless) in a dedicated git worktree to implement a prepared prompt/spec. Claude adapter over the `dw-cli-run` protocol: durable audit log, resumable session via `--session-id`, 0–10 dual evaluation, STOP for the gate. Never the main checkout; never merges. | `/dw-plan tasks` cross-tool assignment, once approved; or manually |
|
|
104
105
|
| **`/dw-codex-run`** | Fire `codex exec` in a dedicated git worktree to implement a prepared prompt/spec. Codex adapter over the `dw-cli-run` protocol: durable audit log, resumable per-task session, 0–10 dual evaluation, STOP for the gate. Never the main checkout; never merges. | `/dw-plan tasks` cross-tool assignment, once approved; or manually |
|
|
@@ -113,17 +114,17 @@ These are auto-invoked by Tier 1-3 commands. Available standalone via `/dw-help
|
|
|
113
114
|
|
|
114
115
|
```
|
|
115
116
|
/dw-autopilot "wish" ------> PRD → TechSpec → Tasks + execution assignments → approval
|
|
116
|
-
→ /dw-goal → Security Gate → validated delivery → authorized publication
|
|
117
|
+
→ /dw-goal → Security Gate + Quality Gate → validated delivery → authorized publication
|
|
117
118
|
--- OR step-by-step ---
|
|
118
119
|
|
|
119
120
|
/dw-opportunities --> /dw-brainstorm --> /dw-plan --> .dw/spec/prd-{name}/{prd,techspec,tasks}.md
|
|
120
121
|
|
|
|
121
|
-
/dw-goal --> /dw-run → /dw-review → /dw-qa → /dw-review → /dw-secure-audit
|
|
122
|
+
/dw-goal --> /dw-run → /dw-review → /dw-qa → /dw-review → /dw-secure-audit → /dw-quality-gate
|
|
122
123
|
|
|
|
123
124
|
/dw-commit + /dw-generate-pr
|
|
124
125
|
|
|
125
126
|
Manual alternative after /dw-plan:
|
|
126
|
-
/dw-run → /dw-review → /dw-qa → /dw-review → /dw-secure-audit → /dw-commit → /dw-generate-pr
|
|
127
|
+
/dw-run → /dw-review → /dw-qa → /dw-review → /dw-secure-audit → /dw-quality-gate → /dw-commit → /dw-generate-pr
|
|
127
128
|
|
|
128
129
|
Shortcuts:
|
|
129
130
|
/dw-intel "question" Query codebase intelligence
|
|
@@ -330,6 +331,9 @@ Installed via `npx @brunosps00/dev-workflow install-deps`:
|
|
|
330
331
|
| **Semgrep** | SAST layer for `/dw-secure-audit`, focused on generated-code diffs with pinned security rulesets. `install-deps` detects presence and prints OS-specific install instructions. | [semgrep.dev](https://semgrep.dev/) |
|
|
331
332
|
| **gitleaks** | Dedicated secret scanner for `/dw-secure-audit`; any confirmed secret blocks without ADR exception. `install-deps` detects presence and prints OS-specific install instructions. | [github.com/gitleaks/gitleaks](https://github.com/gitleaks/gitleaks) |
|
|
332
333
|
| **syft** | Optional SBOM/license inventory layer for `/dw-secure-audit`; advisory and non-blocking. `install-deps` detects presence and prints OS-specific install instructions. | [github.com/anchore/syft](https://github.com/anchore/syft) |
|
|
334
|
+
| **qlty** | Primary complexity engine of `/dw-quality-gate` (cognitive + cyclomatic per function). Fair Source (BSL 1.1), free including commercial use. `install-deps` downloads the pinned release into `~/.dw/bin` and verifies its SHA-256. | [github.com/qltysh/qlty](https://github.com/qltysh/qlty) |
|
|
335
|
+
| **lizard** | Complexity fallback for `/dw-quality-gate` (~30 languages, MIT). `install-deps` installs the pinned version into a private venv under `~/.dw` (needs Python 3). | [github.com/terryyin/lizard](https://github.com/terryyin/lizard) |
|
|
336
|
+
| **jscpd** | Duplication engine for `/dw-quality-gate` (150+ formats, MIT), run as `npx -y jscpd@5`; `install-deps` warms the npx cache. | [github.com/kucherenko/jscpd](https://github.com/kucherenko/jscpd) |
|
|
333
337
|
| **Docker + Docker Compose** | Required by `/dw-new-project` and `/dw-dockerize` for dev dependency seeding and image generation. `install-deps` detects presence and prints OS-specific install instructions — does not install automatically. | [docs.docker.com](https://docs.docker.com/engine/install/) |
|
|
334
338
|
|
|
335
339
|
## Options
|
package/bin/dev-workflow.js
CHANGED
|
@@ -46,7 +46,7 @@ const HELP_TEXT = `
|
|
|
46
46
|
doctor Check managed files, wrappers, agents, and MCP configuration
|
|
47
47
|
repair Reconcile managed files using the recorded install state
|
|
48
48
|
subtask Create, complete, consume, and list local subagent handoffs
|
|
49
|
-
install-deps Install system dependencies (Playwright browsers, MCP servers)
|
|
49
|
+
install-deps Install system dependencies (Playwright browsers, MCP servers, quality-gate engines)
|
|
50
50
|
setup-wsl-browser (WSL) Install the optional user-level CDP fallback relay for the real Windows browser without admin
|
|
51
51
|
install-azure-skills Opt-in: clone curated Azure skills from MicrosoftDocs/Agent-Skills
|
|
52
52
|
into .agents/skills/azure/ and register the Microsoft Learn MCP
|
package/lib/constants.js
CHANGED
|
@@ -27,6 +27,7 @@ const COMMANDS = {
|
|
|
27
27
|
{ name: 'dw-pause', description: 'Trigger when user says "pause work", "end session", or "save where we are". Consolidates open loops, decisions, blockers, and todos into .dw/STATE.md so the next session can resume.' },
|
|
28
28
|
{ name: 'dw-plan', description: 'Produce PRD, architecture and tasks, with local/cross-tool executor, model, effort and agent proposals by task complexity.' },
|
|
29
29
|
{ name: 'dw-qa', description: 'Trigger when user wants to validate behavior beyond unit tests. Mode-aware (UI / API / --ai / --uat). --fix enters the iterative QA + fix-retest loop. --bugfix <slug> targets a .dw/bugfixes/ entry.' },
|
|
30
|
+
{ name: 'dw-quality-gate', description: 'Measure the new code of a diff (complexity, duplication, lint, coverage) with local engines against a versioned baseline; block what the diff introduces or worsens.' },
|
|
30
31
|
{ name: 'dw-redesign-ui', description: 'Audits a frontend page, proposes design directions you choose from, then ships the redesign.' },
|
|
31
32
|
{ name: 'dw-refactor', description: 'Audits a target for refactor opportunities using the refactor-audit protocol, Fowler smells, deep-modules analysis, and behavior-preserving test gates.' },
|
|
32
33
|
{ name: 'dw-report', description: 'Report progress periodically during authorized long-running work; persist updates and stop reporting when the work ends.' },
|
|
@@ -70,6 +71,7 @@ const COMMANDS = {
|
|
|
70
71
|
{ name: 'dw-pause', description: 'Trigger quando usuario diz "pausa o trabalho", "encerra a sessao" ou "salva onde paramos". Consolida pontas soltas, decisoes, bloqueios e todos em .dw/STATE.md para a proxima sessao retomar.' },
|
|
71
72
|
{ name: 'dw-plan', description: 'Produz PRD, arquitetura e tasks com propostas de executor local/cruzado, modelo, esforço e agentes por complexidade.' },
|
|
72
73
|
{ name: 'dw-qa', description: 'Trigger quando usuario quer validar comportamento alem de unit tests. Mode-aware (UI / API / --ai / --uat). --fix entra no loop iterativo QA + fix-retest. --bugfix <slug> aponta para uma entrada em .dw/bugfixes/.' },
|
|
74
|
+
{ name: 'dw-quality-gate', description: 'Mede o código novo de um diff (complexidade, duplicação, lint, cobertura) com engines locais contra um baseline versionado; bloqueia o que o diff introduz ou piora.' },
|
|
73
75
|
{ name: 'dw-redesign-ui', description: 'Audita uma pagina frontend, propoe direcoes de design que voce escolhe, e entrega o redesign.' },
|
|
74
76
|
{ name: 'dw-refactor', description: 'Audita um target por oportunidades de refactor usando o protocolo refactor-audit, smells Fowler, deep-modules e gates de teste preservando comportamento.' },
|
|
75
77
|
{ name: 'dw-report', description: 'Reporta progresso periódico durante trabalho longo autorizado; persiste atualizações e encerra quando o trabalho termina.' },
|
package/lib/doctor.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
} = require('./agents');
|
|
11
11
|
const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
|
|
12
12
|
const { validateSkillRegistry, readSkillRegistry, validateSkillStructuredReturn } = require('./skill-registry');
|
|
13
|
-
const { GIT_GUARDRAILS_CMD, STATUSLINE_CMD, HOOKS_DIR_MARKER } = require('./hooks');
|
|
13
|
+
const { GIT_GUARDRAILS_SCRIPT, GIT_GUARDRAILS_CMD, STATUSLINE_CMD, HOOKS_DIR_MARKER, CODEX_HOOKS_FILE } = require('./hooks');
|
|
14
14
|
const removedCommands = require('./removed-commands');
|
|
15
15
|
|
|
16
16
|
function run() {
|
|
@@ -212,6 +212,8 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
212
212
|
if (!fs.existsSync(file)) issues.push(`Missing hook script: ${path.relative(projectRoot, file)}`);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
checkCodexHooks(projectRoot, warnings);
|
|
216
|
+
|
|
215
217
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
216
218
|
if (!fs.existsSync(settingsPath)) return; // checkMcp already reports this
|
|
217
219
|
|
|
@@ -222,17 +224,52 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
222
224
|
return; // checkMcp already reports malformed settings
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
(
|
|
228
|
-
)
|
|
229
|
-
|
|
227
|
+
const guardrailCommands = guardrailCommandsOf(settings);
|
|
228
|
+
if (guardrailCommands.length === 0) {
|
|
229
|
+
issues.push('Missing git-guardrails PreToolUse hook in .claude/settings.json');
|
|
230
|
+
} else if (!guardrailCommands.includes(GIT_GUARDRAILS_CMD)) {
|
|
231
|
+
// Pre-2.5.1 installs wired a cwd-relative path: it fails (non-blocking) and stops
|
|
232
|
+
// guarding as soon as the shell leaves the project root.
|
|
233
|
+
issues.push('Outdated git-guardrails PreToolUse hook in .claude/settings.json. Run `dev-workflow update`.');
|
|
234
|
+
}
|
|
230
235
|
|
|
231
236
|
const statusLine = settings.statusLine;
|
|
237
|
+
const statusCommand = String((statusLine && statusLine.command) || '');
|
|
232
238
|
if (!statusLine) {
|
|
233
239
|
issues.push('Missing statusLine in .claude/settings.json');
|
|
234
|
-
} else if (
|
|
235
|
-
warnings.push(
|
|
240
|
+
} else if (!statusCommand.includes(HOOKS_DIR_MARKER)) {
|
|
241
|
+
warnings.push(`Custom statusLine set; dev-workflow statusline is not active (add \`${STATUSLINE_CMD}\`).`);
|
|
242
|
+
} else if (statusCommand !== STATUSLINE_CMD) {
|
|
243
|
+
warnings.push('Outdated dev-workflow statusLine in .claude/settings.json. Run `dev-workflow update`.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function guardrailCommandsOf(config) {
|
|
248
|
+
const preToolUse = (config && config.hooks && config.hooks.PreToolUse) || [];
|
|
249
|
+
return preToolUse.flatMap((g) =>
|
|
250
|
+
g && Array.isArray(g.hooks)
|
|
251
|
+
? g.hooks.map((h) => h && h.command).filter((c) => typeof c === 'string' && c.includes(GIT_GUARDRAILS_SCRIPT))
|
|
252
|
+
: [],
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Codex is one of several supported agents, so a missing Codex guardrail is a
|
|
257
|
+
// warning rather than an issue.
|
|
258
|
+
function checkCodexHooks(projectRoot, warnings) {
|
|
259
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
260
|
+
let commands = [];
|
|
261
|
+
if (fs.existsSync(file)) {
|
|
262
|
+
try {
|
|
263
|
+
commands = guardrailCommandsOf(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
264
|
+
} catch {
|
|
265
|
+
warnings.push(`Malformed ${CODEX_HOOKS_FILE}; Codex cannot load the git guardrail.`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (commands.length === 0) {
|
|
270
|
+
warnings.push(`Missing git-guardrails hook in ${CODEX_HOOKS_FILE} (Bash under Codex is unguarded). Run \`dev-workflow update\`.`);
|
|
271
|
+
} else if (!commands.includes(GIT_GUARDRAILS_CMD)) {
|
|
272
|
+
warnings.push(`Outdated git-guardrails hook in ${CODEX_HOOKS_FILE}. Run \`dev-workflow update\`.`);
|
|
236
273
|
}
|
|
237
274
|
}
|
|
238
275
|
|
|
@@ -245,4 +282,4 @@ function checkLegacyCommands(projectRoot, warnings) {
|
|
|
245
282
|
}
|
|
246
283
|
}
|
|
247
284
|
|
|
248
|
-
module.exports = { run };
|
|
285
|
+
module.exports = { run, checkHooks };
|
package/lib/hooks.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { readSettings, writeSettings } = require('./mcp');
|
|
2
4
|
|
|
3
5
|
// dev-workflow-owned hook/statusline scripts live under this path. The substring
|
|
4
|
-
// is the marker used to identify OUR entries in .claude/settings.json
|
|
5
|
-
// add/refresh them on update without ever touching
|
|
6
|
+
// is the marker used to identify OUR entries in .claude/settings.json and
|
|
7
|
+
// .codex/hooks.json so we can add/refresh them on update without ever touching
|
|
8
|
+
// the user's own hooks.
|
|
6
9
|
const HOOKS_DIR_MARKER = '.dw/scripts/hooks/';
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// Hooks run in the session's *current* directory, which in Claude Code follows
|
|
12
|
+
// every `cd` in the Bash tool. A cwd-relative `node .dw/...` breaks with "Cannot
|
|
13
|
+
// find module" as soon as the shell sits in a subdirectory, and a failing
|
|
14
|
+
// PreToolUse hook is non-blocking: the command runs unguarded. Claude Code exports
|
|
15
|
+
// $CLAUDE_PROJECT_DIR to hooks; Codex exports no project variable, so fall back to
|
|
16
|
+
// the git root (a worktree's own root), then to the cwd. The fallback is only
|
|
17
|
+
// evaluated when the variable is unset.
|
|
18
|
+
const PROJECT_ROOT = '${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}';
|
|
19
|
+
const hookCommand = (script) => `node "${PROJECT_ROOT}/${HOOKS_DIR_MARKER}${script}"`;
|
|
20
|
+
|
|
21
|
+
// Codex reads project hooks from .codex/hooks.json (same shape as Claude Code's
|
|
22
|
+
// "hooks" block; PreToolUse matches tool_name "Bash"). It only runs them once the
|
|
23
|
+
// project's .codex/ layer and the hook itself are trusted in Codex.
|
|
24
|
+
const CODEX_HOOKS_FILE = path.join('.codex', 'hooks.json');
|
|
25
|
+
|
|
9
26
|
const GIT_GUARDRAILS_SCRIPT = 'git-guardrails.mjs';
|
|
10
|
-
const
|
|
27
|
+
const GIT_GUARDRAILS_CMD = hookCommand(GIT_GUARDRAILS_SCRIPT);
|
|
28
|
+
const STATUSLINE_CMD = hookCommand('statusline.mjs');
|
|
11
29
|
const SESSION_COST_SCRIPT = 'session-cost.mjs';
|
|
30
|
+
const SESSION_COST_CMD = hookCommand(SESSION_COST_SCRIPT);
|
|
12
31
|
|
|
13
32
|
function isOurs(command) {
|
|
14
33
|
return typeof command === 'string' && command.includes(HOOKS_DIR_MARKER);
|
|
@@ -25,8 +44,11 @@ function isOurs(command) {
|
|
|
25
44
|
* - leaves a user-defined statusLine untouched (reports it so the caller can
|
|
26
45
|
* surface a post-update note).
|
|
27
46
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
47
|
+
* Also reconciles the git guardrail into .codex/hooks.json (see installCodexHooks).
|
|
48
|
+
*
|
|
49
|
+
* Returns { hooks, sessionCost, statusLine, codex } where each is 'added' |
|
|
50
|
+
* 'updated' | 'unchanged' (statusLine may also be 'skipped-user', codex
|
|
51
|
+
* 'skipped-malformed').
|
|
30
52
|
*/
|
|
31
53
|
function installHooks(projectRoot) {
|
|
32
54
|
const settings = readSettings(projectRoot);
|
|
@@ -40,7 +62,44 @@ function installHooks(projectRoot) {
|
|
|
40
62
|
writeSettings(projectRoot, settings);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
hooks: hooksResult,
|
|
67
|
+
sessionCost: sessionCostResult,
|
|
68
|
+
statusLine: statusLineResult,
|
|
69
|
+
codex: installCodexHooks(projectRoot),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readCodexHooks(projectRoot) {
|
|
74
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
75
|
+
if (!fs.existsSync(file)) return {};
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
78
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeCodexHooks(projectRoot, config) {
|
|
85
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reconcile the git guardrail into .codex/hooks.json — the only dev-workflow hook
|
|
92
|
+
* Codex gets: session-cost parses Claude Code transcripts and Codex has no
|
|
93
|
+
* statusLine command. Same marker-based contract as the Claude side.
|
|
94
|
+
* Returns 'added' | 'updated' | 'unchanged' | 'skipped-malformed'.
|
|
95
|
+
*/
|
|
96
|
+
function installCodexHooks(projectRoot) {
|
|
97
|
+
const config = readCodexHooks(projectRoot);
|
|
98
|
+
if (config === null) return 'skipped-malformed';
|
|
99
|
+
const before = JSON.stringify(config);
|
|
100
|
+
const result = reconcilePreToolUseGitGuardrails(config);
|
|
101
|
+
if (JSON.stringify(config) !== before) writeCodexHooks(projectRoot, config);
|
|
102
|
+
return result;
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
|
@@ -135,30 +194,14 @@ function reconcileStatusLine(settings) {
|
|
|
135
194
|
}
|
|
136
195
|
|
|
137
196
|
/**
|
|
138
|
-
* Remove ONLY dev-workflow-owned hook + statusLine entries
|
|
139
|
-
* Returns the number of entries removed.
|
|
197
|
+
* Remove ONLY dev-workflow-owned hook + statusLine entries from .claude/settings.json
|
|
198
|
+
* and .codex/hooks.json (used by uninstall). Returns the number of entries removed.
|
|
140
199
|
*/
|
|
141
200
|
function removeHooks(projectRoot) {
|
|
142
201
|
const settings = readSettings(projectRoot);
|
|
143
|
-
let removed = 0;
|
|
144
|
-
|
|
145
202
|
// Remove OUR hook entries from every event (PreToolUse, SessionEnd, PostToolUse, …),
|
|
146
203
|
// identified by the .dw/scripts/hooks/ marker; never touch user-added hooks.
|
|
147
|
-
|
|
148
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
149
|
-
const groups = settings.hooks[event];
|
|
150
|
-
if (!Array.isArray(groups)) continue;
|
|
151
|
-
for (const group of groups) {
|
|
152
|
-
if (!group || !Array.isArray(group.hooks)) continue;
|
|
153
|
-
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
154
|
-
removed += group.hooks.length - kept.length;
|
|
155
|
-
group.hooks = kept;
|
|
156
|
-
}
|
|
157
|
-
settings.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
158
|
-
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
159
|
-
}
|
|
160
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
161
|
-
}
|
|
204
|
+
let removed = removeOurHookEntries(settings);
|
|
162
205
|
|
|
163
206
|
if (settings.statusLine && isOurs(settings.statusLine.command)) {
|
|
164
207
|
delete settings.statusLine;
|
|
@@ -166,6 +209,46 @@ function removeHooks(projectRoot) {
|
|
|
166
209
|
}
|
|
167
210
|
|
|
168
211
|
if (removed > 0) writeSettings(projectRoot, settings);
|
|
212
|
+
return removed + removeCodexHooks(projectRoot);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function removeOurHookEntries(config) {
|
|
216
|
+
let removed = 0;
|
|
217
|
+
if (!config.hooks || typeof config.hooks !== 'object') return 0;
|
|
218
|
+
for (const event of Object.keys(config.hooks)) {
|
|
219
|
+
const groups = config.hooks[event];
|
|
220
|
+
if (!Array.isArray(groups)) continue;
|
|
221
|
+
for (const group of groups) {
|
|
222
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
223
|
+
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
224
|
+
removed += group.hooks.length - kept.length;
|
|
225
|
+
group.hooks = kept;
|
|
226
|
+
}
|
|
227
|
+
config.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
228
|
+
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
231
|
+
return removed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Remove OUR entries from .codex/hooks.json; delete the file only when nothing
|
|
235
|
+
// of the user's is left in it.
|
|
236
|
+
function removeCodexHooks(projectRoot) {
|
|
237
|
+
const config = readCodexHooks(projectRoot);
|
|
238
|
+
if (!config) return 0;
|
|
239
|
+
const removed = removeOurHookEntries(config);
|
|
240
|
+
if (removed === 0) return 0;
|
|
241
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
242
|
+
if (Object.keys(config).length === 0) {
|
|
243
|
+
fs.rmSync(file);
|
|
244
|
+
try {
|
|
245
|
+
fs.rmdirSync(path.dirname(file));
|
|
246
|
+
} catch {
|
|
247
|
+
/* .codex/ still holds other files */
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
writeCodexHooks(projectRoot, config);
|
|
251
|
+
}
|
|
169
252
|
return removed;
|
|
170
253
|
}
|
|
171
254
|
|
|
@@ -173,6 +256,8 @@ module.exports = {
|
|
|
173
256
|
installHooks,
|
|
174
257
|
removeHooks,
|
|
175
258
|
HOOKS_DIR_MARKER,
|
|
259
|
+
CODEX_HOOKS_FILE,
|
|
260
|
+
GIT_GUARDRAILS_SCRIPT,
|
|
176
261
|
GIT_GUARDRAILS_CMD,
|
|
177
262
|
STATUSLINE_CMD,
|
|
178
263
|
SESSION_COST_CMD,
|
package/lib/init.js
CHANGED
|
@@ -4,7 +4,7 @@ const { ensureDir, copyDir, writeFile, upsertDelimitedBlock, log, listFilesRecur
|
|
|
4
4
|
const { selectLanguage } = require('./prompts');
|
|
5
5
|
const { generateWrappers } = require('./wrappers');
|
|
6
6
|
const { installMCPs } = require('./mcp');
|
|
7
|
-
const { installHooks } = require('./hooks');
|
|
7
|
+
const { installHooks, STATUSLINE_CMD } = require('./hooks');
|
|
8
8
|
const { generateAgents } = require('./agents');
|
|
9
9
|
const { migrate: migrateGsd } = require('./migrate-gsd');
|
|
10
10
|
const { migrate: migrateSkills } = require('./migrate-skills');
|
|
@@ -408,13 +408,19 @@ async function run({ force = false, lang = null, mode = 'init', profile = null,
|
|
|
408
408
|
const mcpResults = installMCPs(projectRoot);
|
|
409
409
|
console.log(` ${mcpResults.added} configured, ${mcpResults.skipped} already present\n`);
|
|
410
410
|
|
|
411
|
-
// 9.2. Reconcile dev-workflow hooks + statusline
|
|
412
|
-
//
|
|
411
|
+
// 9.2. Reconcile dev-workflow hooks + statusline into .claude/settings.json and the
|
|
412
|
+
// git guardrail into .codex/hooks.json (marker-based, merge-aware: refreshes our
|
|
413
|
+
// entries, never touches user-defined hooks/statusLine).
|
|
413
414
|
console.log(' Hooks & statusline:');
|
|
414
415
|
const hookResults = installHooks(projectRoot);
|
|
415
|
-
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}\n`);
|
|
416
|
+
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}, codex git-guardrails: ${hookResults.codex}\n`);
|
|
417
|
+
if (hookResults.codex === 'skipped-malformed') {
|
|
418
|
+
console.log(' Note: .codex/hooks.json is not valid JSON; the Codex git guardrail was not installed.\n');
|
|
419
|
+
} else if (hookResults.codex !== 'unchanged') {
|
|
420
|
+
console.log(' Note: Codex runs .codex/hooks.json only after you trust the project and review the hook in Codex.\n');
|
|
421
|
+
}
|
|
416
422
|
if (hookResults.statusLine === 'skipped-user') {
|
|
417
|
-
console.log(
|
|
423
|
+
console.log(` Note: a custom statusLine is set; to show dev-workflow state add \`${STATUSLINE_CMD}\`.\n`);
|
|
418
424
|
}
|
|
419
425
|
|
|
420
426
|
// 9.5. Persist install state for diagnostics and repair.
|
package/lib/install-deps.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const openDesign = require('./install-open-design');
|
|
3
|
+
const qualityTools = require('./install-quality-tools');
|
|
3
4
|
|
|
4
5
|
function run() {
|
|
5
6
|
console.log('\n dev-workflow install-deps');
|
|
@@ -72,6 +73,7 @@ function run() {
|
|
|
72
73
|
' Windows: scoop install syft',
|
|
73
74
|
],
|
|
74
75
|
},
|
|
76
|
+
...qualityTools.deps,
|
|
75
77
|
{
|
|
76
78
|
name: 'Docker + Docker Compose',
|
|
77
79
|
check: 'docker --version && (docker compose version || docker-compose --version)',
|
|
@@ -154,6 +156,7 @@ function run() {
|
|
|
154
156
|
if (failed) parts.push(`${failed} failed`);
|
|
155
157
|
console.log(` Done! ${parts.join(', ')}`);
|
|
156
158
|
console.log();
|
|
159
|
+
for (const line of qualityTools.pathHint()) console.log(` ${line}`);
|
|
157
160
|
|
|
158
161
|
checkPlaywrightVersion();
|
|
159
162
|
printBrowserGuidance();
|