@brunosps00/dev-workflow 1.6.0 → 1.7.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.
@@ -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": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "author": {
6
6
  "name": "Bruno Santos"
7
7
  },
package/README.md CHANGED
@@ -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 v1.6.0 ships **37 commands** organized into four tiers. Most users only invoke Tier 1 + Tier 2.
45
+ dev-workflow v1.7.0 ships **38 commands** organized into four tiers. Most users only invoke Tier 1 + Tier 2.
46
46
 
47
47
  ### Tier 1 — Gateway (3)
48
48
 
package/lib/constants.js CHANGED
@@ -22,6 +22,7 @@ const COMMANDS = {
22
22
  { name: 'dw-intel', description: 'Codebase intelligence: query mode (default) answers questions citing .dw/intel/ + .dw/rules/; --build mode (re)builds the index.' },
23
23
  { name: 'dw-learn', description: 'Synthesizes durable decisions, bugfixes, deviations, and git history into atomic confidence-weighted instincts you approve; can propose constitution principles. On-demand and human-in-the-loop — no always-on observer.' },
24
24
  { name: 'dw-new-project', description: 'Interviews you about stack and infra, then scaffolds a working monorepo with docker-compose for dev, .env, scripts, CI, and seeded rules.' },
25
+ { name: 'dw-open-design', userInvoked: true, description: 'Trigger when user asks to generate, iterate, or refactor/redesign headless Open Design HTML prototypes via the od CLI. Imports a target folder with isolated OD_DATA_DIR, refines briefs before dispatch, supports --refactor screenshots via --url/--viewports or --screenshot, runs web-prototype briefs with explicit --agent codex|claude, verifies files, captures JSONL audit logs, and stops for a dual >=9 visual gate.' },
25
26
  { name: 'dw-opportunities', description: 'Scans the installed project for product, UX, automation, refactor, and security opportunities, then routes each idea to the right next dw-* command.' },
26
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.' },
27
28
  { name: 'dw-plan', description: 'Trigger when user has a feature idea and needs spec + architecture + tasks. Runs PRD → TechSpec → Tasks sequentially. Stages: prd / techspec / tasks; --from techspec; --council.' },
@@ -61,6 +62,7 @@ const COMMANDS = {
61
62
  { name: 'dw-intel', description: 'Inteligencia do codebase: modo query (default) responde citando .dw/intel/ + .dw/rules/; modo --build (re)constroi indice.' },
62
63
  { name: 'dw-learn', description: 'Sintetiza decisoes duraveis, bugfixes, deviations e historico git em instincts atomicos ponderados por confianca que voce aprova; pode propor principios de constitution. On-demand e human-in-the-loop — sem observador sempre-ligado.' },
63
64
  { name: 'dw-new-project', description: 'Entrevista voce sobre stack e infra, depois faz scaffold de um monorepo com docker-compose para dev, .env, scripts, CI e rules seed.' },
65
+ { name: 'dw-open-design', userInvoked: true, description: 'Trigger quando usuario pede para gerar, iterar ou refatorar/redesenhar prototipos HTML headless do Open Design via od CLI. Importa pasta alvo com OD_DATA_DIR isolado, refina briefs antes de despachar, suporta prints --refactor via --url/--viewports ou --screenshot, roda briefs web-prototype com --agent codex|claude explicito, verifica arquivos, captura logs JSONL e para para gate visual dual >=9.' },
64
66
  { name: 'dw-opportunities', description: 'Escaneia o projeto instalado por oportunidades de produto, UX, automacao, refactor e seguranca, e roteia cada ideia para o proximo comando dw-* correto.' },
65
67
  { 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.' },
66
68
  { name: 'dw-plan', description: 'Trigger quando usuario tem ideia de feature e precisa spec + arquitetura + tasks. Roda PRD → TechSpec → Tasks sequencial. Stages: prd / techspec / tasks; --from techspec; --council.' },
@@ -1,4 +1,5 @@
1
1
  const { execSync } = require('child_process');
2
+ const openDesign = require('./install-open-design');
2
3
 
3
4
  function run() {
4
5
  console.log('\n dev-workflow install-deps');
@@ -84,6 +85,12 @@ function run() {
84
85
  'Verify after install: docker --version && docker compose version',
85
86
  ],
86
87
  },
88
+ {
89
+ name: 'Open Design CLI (od)',
90
+ check: openDesign.check,
91
+ install: openDesign.install,
92
+ instructions: openDesign.instructions(),
93
+ },
87
94
  ];
88
95
 
89
96
  let installed = 0;
@@ -113,12 +120,26 @@ function run() {
113
120
 
114
121
  process.stdout.write(` Installing ${dep.name}...`);
115
122
  try {
116
- execSync(dep.install, { stdio: 'pipe', timeout: 300000 });
123
+ if (typeof dep.check === 'function' && dep.check()) {
124
+ console.log(' \x1b[32m✓ already installed\x1b[0m');
125
+ installed++;
126
+ continue;
127
+ }
128
+ if (typeof dep.install === 'function') {
129
+ dep.install();
130
+ } else {
131
+ execSync(dep.install, { stdio: 'pipe', timeout: 300000 });
132
+ }
117
133
  console.log(' \x1b[32m✓\x1b[0m');
118
134
  installed++;
119
135
  } catch (err) {
120
136
  console.log(' \x1b[31m✗\x1b[0m');
121
137
  console.log(` Error: ${err.message.split('\n')[0]}`);
138
+ if (Array.isArray(dep.instructions)) {
139
+ for (const line of dep.instructions) {
140
+ console.log(` ${line}`);
141
+ }
142
+ }
122
143
  failed++;
123
144
  }
124
145
  }
@@ -0,0 +1,89 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { execFileSync } = require('child_process');
5
+
6
+ const OPEN_DESIGN_REPO = 'https://github.com/nexu-io/open-design';
7
+ const OPEN_DESIGN_REF = '1cb7eae';
8
+ const OPEN_DESIGN_VENDOR_DIR = path.join(os.homedir(), '.dw', 'vendor', 'open-design');
9
+
10
+ function odBin(dir = OPEN_DESIGN_VENDOR_DIR) {
11
+ return path.join(dir, 'apps', 'daemon', 'bin', 'od.mjs');
12
+ }
13
+
14
+ function check() {
15
+ return fs.existsSync(odBin());
16
+ }
17
+
18
+ function install() {
19
+ checkPrerequisites();
20
+
21
+ fs.mkdirSync(path.dirname(OPEN_DESIGN_VENDOR_DIR), { recursive: true });
22
+ if (!fs.existsSync(path.join(OPEN_DESIGN_VENDOR_DIR, '.git'))) {
23
+ if (fs.existsSync(OPEN_DESIGN_VENDOR_DIR)) {
24
+ throw new Error(`${OPEN_DESIGN_VENDOR_DIR} exists but is not a git checkout; move it aside and retry.`);
25
+ }
26
+ execFileSync('git', ['clone', OPEN_DESIGN_REPO, OPEN_DESIGN_VENDOR_DIR], { stdio: 'pipe', timeout: 600000 });
27
+ }
28
+
29
+ execFileSync('git', ['fetch', '--tags', '--force', 'origin'], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 600000 });
30
+ execFileSync('git', ['checkout', OPEN_DESIGN_REF], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 120000 });
31
+ execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 1200000 });
32
+
33
+ if (!check()) {
34
+ throw new Error(`Open Design install finished but ${odBin()} was not found.`);
35
+ }
36
+ }
37
+
38
+ function instructions() {
39
+ return [
40
+ 'Open Design CLI is provisioned by dev-workflow into a managed checkout:',
41
+ ` ${OPEN_DESIGN_VENDOR_DIR}`,
42
+ ` repo: ${OPEN_DESIGN_REPO}`,
43
+ ` pinned ref: ${OPEN_DESIGN_REF}`,
44
+ '',
45
+ 'Prerequisites:',
46
+ ' Node.js ~24: install with fnm/nvm/asdf, then verify: node -v',
47
+ ' pnpm >=10.33: corepack enable && corepack prepare pnpm@10.33.2 --activate',
48
+ ' git: required to clone the upstream monorepo',
49
+ '',
50
+ 'Then run: dev-workflow install-deps',
51
+ 'To use your own checkout instead of the managed one, set OD_CLI_DIR=/path/to/open-design.',
52
+ ];
53
+ }
54
+
55
+ function checkPrerequisites() {
56
+ const nodeVersion = process.versions.node;
57
+ const nodeMajor = Number(nodeVersion.split('.')[0]);
58
+ if (nodeMajor !== 24) {
59
+ throw new Error(`Open Design requires Node.js ~24; detected ${nodeVersion}.`);
60
+ }
61
+
62
+ const pnpmRaw = execFileSync('pnpm', ['-v'], { stdio: 'pipe', timeout: 10000 }).toString().trim();
63
+ if (!isAtLeast(pnpmRaw, '10.33.0')) {
64
+ throw new Error(`Open Design requires pnpm >=10.33; detected ${pnpmRaw}.`);
65
+ }
66
+
67
+ execFileSync('git', ['--version'], { stdio: 'pipe', timeout: 10000 });
68
+ }
69
+
70
+ function isAtLeast(actual, minimum) {
71
+ const a = actual.split('.').map((part) => Number(part));
72
+ const b = minimum.split('.').map((part) => Number(part));
73
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
74
+ const av = a[index] || 0;
75
+ const bv = b[index] || 0;
76
+ if (av > bv) return true;
77
+ if (av < bv) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ module.exports = {
83
+ OPEN_DESIGN_REF,
84
+ OPEN_DESIGN_VENDOR_DIR,
85
+ check,
86
+ install,
87
+ instructions,
88
+ odBin,
89
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunosps00/dev-workflow",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "AI-driven development workflow commands for any project. Scaffolds a complete PRD-to-PR pipeline with multi-platform AI assistant support.",
5
5
  "bin": {
6
6
  "dev-workflow": "./bin/dev-workflow.js"
@@ -38,12 +38,13 @@ Use `/dw-autopilot "wish"` as the gateway for most feature work. The granular co
38
38
  | `/dw-commit` | Atomic Conventional Commits for pending work. |
39
39
  | `/dw-generate-pr [target]` | Push branch, draft PR body, open browser. |
40
40
 
41
- ## Tier 3 — Specialty (11)
41
+ ## Tier 3 — Specialty (12)
42
42
 
43
43
  | Command | What |
44
44
  |---------|------|
45
45
  | `/dw-analyze-project` | Scan the repo, write `.dw/rules/`, offer `.dw/constitution.md`, generate `.dw/rules/concerns.md`, and synthesize frontend `DESIGN.md` when tokens exist. |
46
46
  | `/dw-redesign-ui "target"` | Audit, propose 2-3 design directions, ship. Enforces UI grounding + WCAG; can run the deterministic impeccable slop detector. |
47
+ | `/dw-open-design --brief <file>` | Generate, iterate, or `--refactor` headless Open Design HTML prototypes via `od`; refines briefs first, can capture current screenshots with `--url --viewports` or `--screenshot`, then verifies file + visual gate. |
47
48
  | `/dw-refactor "target"` | Audit code-health and tech debt with Fowler smells, deep-modules analysis, and behavior-preserving test gates. |
48
49
  | `/dw-functional-doc` | Map screens + flows into a functional doc validated with Playwright; uses WSL-resilient browser resolution. |
49
50
  | `/dw-context-budget` | Audit context overhead from commands, skills, agents, instructions, and MCPs. |
@@ -112,6 +113,7 @@ Match the keyword and suggest:
112
113
  | `research`, `compare`, `state of the art` | `/dw-brainstorm --research` |
113
114
  | `refactor`, `smell`, `code health`, `tech debt` | `/dw-refactor` |
114
115
  | `ui`, `design`, `redesign` | `/dw-redesign-ui` |
116
+ | `open design`, `prototype`, `prototipo`, `od cli`, `html prototype` | `/dw-open-design --brief <file>`, `/dw-open-design --brief-glob "<glob>" --agent claude`, or `/dw-open-design --refactor --url <url> --viewports "1440x900,375x812"` |
115
117
  | `intel`, `where is`, `what uses` | `/dw-intel` (or `--build` to (re)create the index) |
116
118
  | `context`, `tokens`, `slow agent` | `/dw-context-budget` |
117
119
  | `harness`, `install`, `wrappers`, `agents` | `/dw-harness-audit` |
@@ -0,0 +1,324 @@
1
+ <system_instructions>
2
+ You are the **headless Open Design (`od`) runner** for generating and iterating HTML prototypes in a project
3
+ folder, without Electron and without depending on the user's GUI state. Start the `nexu-io/open-design` daemon in
4
+ `--headless`, import the target folder once, run single briefs or serial batches, verify the output file, run the
5
+ Playwright/Firefox visual gate, and **STOP for the owner's gate** before any commit.
6
+
7
+ <critical>NEVER use the user's GUI state. Always run with an isolated per-project `OD_DATA_DIR`, for example `.dw/.open-design/data/`.</critical>
8
+ <critical>NEVER commit a rejected prototype. The gate is dual >=9: automated gate + your own visual score >=9, then owner gate.</critical>
9
+ <critical>The agent (`codex` or `claude`) must always be explicit in `od run start --agent <agent>`. If the user does not pass one, resolve from project config/state; fallback `codex`.</critical>
10
+ <critical>NEVER dispatch the user's raw request to `od`. This command's value is refining the prompt first: ground it in real code, structure the brief, qualify states/themes/a11y/deep-link, persist the reviewable brief, and only then append the headless appendix.</critical>
11
+
12
+ ## Inputs
13
+
14
+ | Flag/argument | Default | Description |
15
+ |---|---:|---|
16
+ | `--target <dir>` | `open-design/` | Folder imported as the external project and where HTML files are written. |
17
+ | `--brief <file>` | none | One markdown brief for a single run. |
18
+ | `--brief-glob <glob>` | none | Serial batch. Expand to a stable list and run one prompt at a time. |
19
+ | `--output <file>` | required in the brief/appendix | Expected HTML file; verify it on disk after the run. |
20
+ | `--agent <codex|claude>` | state/config -> `codex` | Agent forwarded to `od run start --agent`. |
21
+ | `--platform <text>` | `Responsive web, desktop-first and mobile-safe` | Pre-filled discovery answer for the `web-prototype` skill. |
22
+ | `--deep-link-id <id>` | none | Value used by the behavioral gate for `?aberto=<id>` + Esc. |
23
+ | `--refactor` | false | Existing-screen redesign/refactor mode; captures current screenshots and grounds the brief in visible state + real code. |
24
+ | `--url <url>` | none | Running app URL for current screenshot capture via Playwright Firefox. |
25
+ | `--viewports <list>` | `1440x900,375x812` | Comma-separated viewports for `--refactor --url`; expand as the pain requires (for example `1920x1080` for a wide table). |
26
+ | `--screenshot <path>` | repeatable | Already-captured current screenshot when there is no `--url`; copy it into `_refs/<slug>/`. |
27
+
28
+ ## 0. Refine The Brief
29
+
30
+ Before any daemon pre-flight or `od` call, turn the user's intent into a qualified brief. Do not send the user's raw
31
+ sentence to the runner.
32
+
33
+ Required contract:
34
+
35
+ | Step | Requirement |
36
+ |---|---|
37
+ | Ground in reality | If the screen exists, read its real code: columns, enums, actions, states, permissions, contracts, and domain. Also read the project's design system, sibling prototypes, and design guides. Inventing a field, status, or action is forbidden. |
38
+ | Structure | Rewrite the request into the standard skeleton below, filling real paths and the exact output file. |
39
+ | Qualify | Cover loading/skeleton, empty, error/failure, light + dark, a11y, local design-system limits (for example <=6 cards/filters), responsiveness, and deep-link. |
40
+ | Persist | Save the refined brief inside the project, for example `<target>/PROMPT-<slug>.md`. It is a reviewable owner deliverable, not a disposable scratch file. |
41
+ | Review if already ready | If the user already provides a qualified brief, do a quick checklist review, patch only gaps, and persist the final version. |
42
+
43
+ Standard refined brief skeleton:
44
+
45
+ ```markdown
46
+ # <screen/prototype title>
47
+
48
+ Paste this brief into Open Design. Exact output file: `<OUTPUT_FILE>`.
49
+
50
+ ## 1. Why
51
+ <real user/product pain; observable problem this prototype must solve>
52
+
53
+ ## 2. Interaction Decision
54
+ <navigation, opening, selection, filters, sorting, editing, and confirmation pattern>
55
+
56
+ ## 3. Layout/List
57
+ Table/list based only on REAL columns:
58
+
59
+ | Real column | Source/contract | Presentation | State/limit |
60
+ |---|---|---|---|
61
+ | <name> | <file/API/schema> | <text, badge, action> | <truncate, empty, error> |
62
+
63
+ ## 4. Detail/Actions
64
+ <drawer/modal/detail page; contextual actions allowed by real state/permission>
65
+
66
+ ## 5. Domain Fidelity
67
+ So the mock does not lie: use real enums/statuses, realistic sample data in the project's language, and include rare/broken states that exist in the domain.
68
+
69
+ ## 6. Visual Direction
70
+ <tokens, components, density, inherited references, sibling prototypes, and design-system limits>
71
+
72
+ ## Deliver
73
+ - `<OUTPUT_FILE>`
74
+ - `<OUTPUT_FILE>.artifact.json` when there is a sidecar
75
+ ```
76
+
77
+ Only after persisting this file should you append the headless appendix under `.dw/.open-design/runs/` and dispatch
78
+ the run.
79
+
80
+ ### 0R. Refactor/Redesign Mode
81
+
82
+ When `--refactor` is present, phase 0 remains mandatory. Screenshots complement grounding in code; they do not
83
+ replace reading contracts, enums, permissions, states, the design system, and sibling prototypes.
84
+
85
+ Capture or copy current screenshots into the imported folder, under `_refs/<slug>/`, before persisting the brief:
86
+
87
+ ```bash
88
+ node .dw/scripts/open-design/capture-current.mjs \
89
+ --target "$TARGET_DIR" \
90
+ --slug "<slug>" \
91
+ --url "<url>" \
92
+ --viewports "1440x900,375x812"
93
+ ```
94
+
95
+ Without a running app URL, accept existing screenshots by repeating `--screenshot`:
96
+
97
+ ```bash
98
+ node .dw/scripts/open-design/capture-current.mjs \
99
+ --target "$TARGET_DIR" \
100
+ --slug "<slug>" \
101
+ --screenshot "./before-1440-light.png" \
102
+ --screenshot "./before-375-dark.png"
103
+ ```
104
+
105
+ Capture rules:
106
+
107
+ | Case | Requirement |
108
+ |---|---|
109
+ | Viewports | Sensible default: desktop `1440x900` + mobile `375x812`. The agent chooses/expands as the pain requires: mobile bug includes `375x812`; wide table includes `1920x1080`; tablet/console includes `768x1024`. |
110
+ | Themes | Capture light and dark when applicable. The helper uses `light,dark` by default and writes names like `_refs/<slug>/atual-1440-light.png` and `_refs/<slug>/atual-375-dark.png`. |
111
+ | Robust path | The `od` agent reads images from the project filesystem. Reference relative paths inside the target; do not depend on an image flag in `run start`. |
112
+ | Hygiene | `_refs/` and reference material must not become prototypes. If the target project tracks final prototypes, document cleanup or add `<target>/_refs/` to `.gitignore` according to local convention. |
113
+
114
+ The refined refactor brief must add this block:
115
+
116
+ ```markdown
117
+ ## Current Screen References
118
+ Open and ANALYZE the screenshots before writing:
119
+ - `_refs/<slug>/atual-1440-light.png`
120
+ - `_refs/<slug>/atual-375-dark.png`
121
+
122
+ ## What Is Wrong Today
123
+ <pain pointing to visible elements in the screenshots: density, hierarchy, table, filters, states, contrast, mobile, etc.>
124
+
125
+ ## The Change Idea
126
+ <what changes and why; interaction/visual decision proposed by the user or by the agent after reading code and screenshots>
127
+
128
+ ## Preserve
129
+ <flows, fields, permissions, states, copy, affordances, and integrations that must not change>
130
+ ```
131
+
132
+ ## Pre-flight
133
+
134
+ | Check | Action |
135
+ |---|---|
136
+ | Resolve `od` | `OD_CLI_DIR` wins. Otherwise use `~/.dw/vendor/open-design`. If `apps/daemon/bin/od.mjs` is missing, stop and tell the user to run `dev-workflow install-deps`. |
137
+ | Prerequisites | The Open Design checkout requires Node `~24` and pnpm `>=10.33`. If missing, stop with concrete instructions. |
138
+ | Isolated state | Create `.dw/.open-design/`, `.dw/.open-design/data/`, `.dw/.open-design/runs/`, and `.dw/.open-design/state.json`. |
139
+ | Port | Pick a free port on `127.0.0.1`; persist `daemonUrl` and `port` in state. |
140
+ | Live daemon | If `od daemon status --daemon-url <url> --json` returns `ok: true`, reuse it. Otherwise start a new daemon. |
141
+
142
+ Daemon command shape:
143
+
144
+ ```bash
145
+ OD_DATA_DIR="$PWD/.dw/.open-design/data" \
146
+ OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000 \
147
+ node "$OD_DIR/apps/daemon/bin/od.mjs" daemon start \
148
+ --headless \
149
+ --host 127.0.0.1 \
150
+ --port "$PORT"
151
+ ```
152
+
153
+ Known noise: `codex_core::shell_snapshot` may log a syntax `ERROR`. Treat it as benign when the run continues and
154
+ the target file is created.
155
+
156
+ ## Idempotent Import
157
+
158
+ Use `project import-folder` on the resolved absolute target folder. Default: `<repo>/open-design/`.
159
+
160
+ Persist in `.dw/.open-design/state.json`:
161
+
162
+ ```json
163
+ {
164
+ "schema_version": "1.0",
165
+ "targetDir": "/abs/repo/open-design",
166
+ "projectId": "<project.id>",
167
+ "conversationId": "<conversationId>",
168
+ "daemonUrl": "http://127.0.0.1:<port>",
169
+ "port": 17556,
170
+ "agent": "codex"
171
+ }
172
+ ```
173
+
174
+ If `projectId` exists in state and `od project info <id> --daemon-url <url> --json` works for the same `targetDir`,
175
+ reuse it. Otherwise, reimport:
176
+
177
+ ```bash
178
+ node "$OD_DIR/apps/daemon/bin/od.mjs" project import-folder "$TARGET_DIR" \
179
+ --daemon-url "$OD_URL" \
180
+ --name "$(basename "$TARGET_DIR")" \
181
+ --skill web-prototype \
182
+ --design-system default \
183
+ --json
184
+ ```
185
+
186
+ Capture `project.id` and `conversationId`; do not depend on global app state.
187
+
188
+ ## Required Headless Appendix
189
+
190
+ Append this block to every brief in a temporary file under `.dw/.open-design/runs/`.
191
+ Fill `<PLATFORM>`, `<OUTPUT_FILE>`, and sibling references when applicable.
192
+
193
+ ```markdown
194
+ ---
195
+
196
+ ## Headless appendix for Open Design
197
+
198
+ Target platform: <PLATFORM>.
199
+
200
+ Do not run discovery and do not ask questions. Do not emit `<question-form>`. Follow this brief with the answers
201
+ above and deliver file-first.
202
+
203
+ Required output file: `<OUTPUT_FILE>`.
204
+
205
+ File-first contract:
206
+ - Write the HTML directly to the project filesystem, exactly at `<OUTPUT_FILE>`.
207
+ - Do not alter existing files, except when this prompt is explicitly a surgical iteration on a target file.
208
+ - If you need to create a sidecar, keep `<OUTPUT_FILE>.artifact.json` coherent.
209
+ - Do not answer only with a textual `<artifact>` block; run `succeeded` without the file is a failure.
210
+
211
+ To avoid headless timeout:
212
+ - Write large files in parts, with edits of about 150 lines per call.
213
+ - Keep visible progress until the HTML is complete.
214
+
215
+ Consistency context:
216
+ - Read sibling prototypes in `prototipos/` or the folder named by the brief before defining visual patterns.
217
+ - Preserve the local visual system when a nearby reference exists.
218
+
219
+ Required deep link:
220
+ - Implement `?aberto=<id>` to open the correct item's drawer/modal.
221
+ - On Esc or close action, clear the parameter with `history.replaceState`.
222
+ - The drawer/modal must use `role="dialog"` and appropriate ARIA attributes.
223
+ - Include skeleton/loading, empty, and error states.
224
+ ```
225
+
226
+ ## Single Run
227
+
228
+ Always pass `--agent` explicitly:
229
+
230
+ ```bash
231
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
232
+ --daemon-url "$OD_URL" \
233
+ --project "$PROJECT_ID" \
234
+ --conversation "$CONVERSATION_ID" \
235
+ --agent "$AGENT" \
236
+ --skill web-prototype \
237
+ --design-system default \
238
+ --prompt-file "$PROMPT_WITH_APPENDIX" \
239
+ --follow \
240
+ --json | tee ".dw/.open-design/runs/<slug>.jsonl"
241
+ ```
242
+
243
+ After the run, check the file. **Do not accept only `succeeded`:**
244
+
245
+ ```bash
246
+ test -s "$TARGET_DIR/<OUTPUT_FILE>"
247
+ ```
248
+
249
+ Record in JSONL or an audit sidecar: brief, prompt with appendix, agent, daemonUrl, projectId, conversationId,
250
+ expected output, final status, and file verification result.
251
+
252
+ ## Batch
253
+
254
+ For `--brief-glob`, expand the briefs, sort by name, and run serially. Stop at the first case where:
255
+
256
+ | Condition | Result |
257
+ |---|---|
258
+ | run fails | `BLOCKED` with JSONL log |
259
+ | run says success but file is missing or empty | `FINDINGS` and surgical follow-up or `BLOCKED` |
260
+ | visual gate <9 | `FINDINGS`; iterate before continuing |
261
+
262
+ Do not run prompts in parallel in the same `od` project; preserve auditability and context.
263
+
264
+ ## Surgical Iteration
265
+
266
+ Use a follow-up run on the same `projectId`/`conversationId`, with a short prompt and explicit target file:
267
+
268
+ ```bash
269
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
270
+ --daemon-url "$OD_URL" \
271
+ --project "$PROJECT_ID" \
272
+ --conversation "$CONVERSATION_ID" \
273
+ --agent "$AGENT" \
274
+ --skill web-prototype \
275
+ --design-system default \
276
+ --prompt-file ".dw/.open-design/runs/<slug>.followup.md" \
277
+ --follow \
278
+ --json | tee -a ".dw/.open-design/runs/<slug>.jsonl"
279
+ ```
280
+
281
+ The follow-up must say: single file, exact change, do not touch sibling prototypes, preserve deep-link and states.
282
+
283
+ ## Required Visual Gate
284
+
285
+ Run the automated gate:
286
+
287
+ ```bash
288
+ node .dw/scripts/open-design/gate-prototype.mjs \
289
+ --file "$TARGET_DIR/<OUTPUT_FILE>" \
290
+ --deep-link-id "<real-id>" \
291
+ --expected-text "<item-text>" \
292
+ --out ".dw/.open-design/gate/<slug>"
293
+ ```
294
+
295
+ The gate covers:
296
+
297
+ | Layer | Requirement |
298
+ |---|---|
299
+ | Structural | `<title>`, `aberto`, `replaceState`, `role=dialog`/ARIA, skeleton/loading, empty, error. |
300
+ | Screenshot | Headless Firefox in light and dark, with `colorScheme` and `data-theme`. |
301
+ | Behavioral | `?aberto=<id>` opens the correct item; Esc closes and clears the parameter. |
302
+ | Evaluation | Automated PASS + your visual score >=9. If below 9, run a surgical follow-up. |
303
+
304
+ After it passes, **stop for the owner's gate**. Do not commit until the owner approves.
305
+
306
+ ## Validated Pitfalls
307
+
308
+ | Pitfall | Symptom | Required workaround |
309
+ |---|---|---|
310
+ | `web-prototype` discovery | Opens with `<question-form>` and ends the turn; headless does not create GenUI (`od ui list` empty). | Append the headless appendix answering platform and forbidding questions/discovery. |
311
+ | Inactivity timeout | Large HTML goes silent and the run dies near 10 minutes. | `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000` on the daemon + instruct ~150-line chunked writes. |
312
+ | `codex_core::shell_snapshot` | Logs a syntax `ERROR` in the stream. | Treat as noise if the run continues and the final file exists. |
313
+ | GUI state | App config/agent/projects leak from the user's GUI. | Isolated `OD_DATA_DIR` always; explicit `--agent`, `--skill`, and `--design-system`. |
314
+
315
+ ## Structured Return
316
+
317
+ **Status:** `PASS` | `FINDINGS` | `BLOCKED` | `NOT_APPLICABLE`
318
+ **Scope:** target folder, briefs run, agent used, projectId/conversationId.
319
+ **Evidence:** JSONL logs, verified HTML file(s), gate JSON, light/dark screenshots.
320
+ **Artifacts:** prototype and sidecar paths.
321
+ **Decisions:** defaults/overrides applied (`agent`, `target`, platform, port).
322
+ **Risks:** any gate below 9, missing file despite run succeeded, missing dependency.
323
+ **Next Step:** owner gate, required surgical follow-up, or exact resume command.
324
+ </system_instructions>
@@ -36,12 +36,13 @@ Use `/dw-autopilot "desejo"` como gateway pra maior parte do trabalho. Comandos
36
36
  | `/dw-commit` | Commits atômicos Conventional pra trabalho pendente. |
37
37
  | `/dw-generate-pr [target]` | Push branch, draft do PR body, abre browser. |
38
38
 
39
- ## Tier 3 — Especialidade (11)
39
+ ## Tier 3 — Especialidade (12)
40
40
 
41
41
  | Comando | O que |
42
42
  |---------|-------|
43
43
  | `/dw-analyze-project` | Scan do repo, escreve `.dw/rules/`, oferece `.dw/constitution.md`, gera `.dw/rules/concerns.md` e sintetiza `DESIGN.md` em frontend quando há tokens. |
44
44
  | `/dw-redesign-ui "target"` | Audit, propõe 2-3 direções, entrega. Enforça UI grounding + WCAG; pode rodar o detector determinístico do impeccable. |
45
+ | `/dw-open-design --brief <file>` | Gera, itera ou faz `--refactor` de prototipos HTML headless via Open Design `od`; refina briefs primeiro, pode capturar prints atuais com `--url --viewports` ou `--screenshot`, depois verifica arquivo + gate visual. |
45
46
  | `/dw-refactor "target"` | Audita code health e tech debt com smells Fowler, deep-modules e gates de teste preservando comportamento. |
46
47
  | `/dw-functional-doc` | Mapeia screens + flows em doc funcional validado com Playwright; usa resolução de browser resiliente no WSL. |
47
48
  | `/dw-context-budget` | Audita contexto de commands, skills, agentes, instrucoes e MCPs. |
@@ -108,6 +109,7 @@ Auto-invocados por comandos primários mas disponíveis standalone.
108
109
  | `research`, `compara`, `estado da arte` | `/dw-brainstorm --research` |
109
110
  | `refactor`, `smell`, `code health`, `tech debt` | `/dw-refactor` |
110
111
  | `ui`, `design`, `redesign` | `/dw-redesign-ui` |
112
+ | `open design`, `prototype`, `prototipo`, `od cli`, `html prototype` | `/dw-open-design --brief <file>`, `/dw-open-design --brief-glob "<glob>" --agent claude` ou `/dw-open-design --refactor --url <url> --viewports "1440x900,375x812"` |
111
113
  | `intel`, `onde está`, `o que usa` | `/dw-intel` (ou `--build`) |
112
114
  | `contexto`, `tokens`, `agente lento` | `/dw-context-budget` |
113
115
  | `harness`, `install`, `wrappers`, `agentes` | `/dw-harness-audit` |
@@ -0,0 +1,323 @@
1
+ <system_instructions>
2
+ Voce e o **runner headless do Open Design (`od`)** para gerar e iterar prototipos HTML em uma pasta de projeto,
3
+ sem abrir Electron e sem depender do estado da GUI do usuario. Use o daemon do `nexu-io/open-design` em
4
+ `--headless`, importe a pasta alvo uma vez, rode briefs single ou batch em serie, verifique o arquivo produzido,
5
+ rode gate visual Playwright/Firefox e **PARE para o gate do dono** antes de qualquer commit.
6
+
7
+ <critical>NUNCA use o estado da GUI do usuario. Sempre rode com `OD_DATA_DIR` isolado por projeto, por exemplo `.dw/.open-design/data/`.</critical>
8
+ <critical>NUNCA commite prototipo reprovado. O gate e dual >=9: gate automatico + avaliacao visual propria >=9, depois gate do dono.</critical>
9
+ <critical>O agente (`codex` ou `claude`) deve ser sempre explicito em `od run start --agent <agent>`. Se o usuario nao passar, resolva de config/estado do projeto; fallback `codex`.</critical>
10
+ <critical>NUNCA despache o pedido cru do usuario para o `od`. O valor deste comando e refinar o prompt primeiro: ancore no codigo real, estruture o brief, qualifique estados/temas/a11y/deep-link, persista o brief revisavel e so entao anexe o apendice headless.</critical>
11
+
12
+ ## Inputs
13
+
14
+ | Flag/argumento | Default | Descricao |
15
+ |---|---:|---|
16
+ | `--target <dir>` | `open-design/` | Pasta importada como projeto externo e onde os HTMLs serao escritos. |
17
+ | `--brief <file>` | nenhum | Um brief markdown para uma run single. |
18
+ | `--brief-glob <glob>` | nenhum | Batch em serie. Expanda para lista estavel e rode um prompt por vez. |
19
+ | `--output <file>` | obrigatorio no brief/apendice | HTML esperado; verifique no filesystem depois da run. |
20
+ | `--agent <codex|claude>` | estado/config -> `codex` | Agente repassado ao `od run start --agent`. |
21
+ | `--platform <text>` | `Web responsivo desktop-first e mobile-safe` | Resposta pre-preenchida para o discovery da skill `web-prototype`. |
22
+ | `--deep-link-id <id>` | nenhum | Valor usado no gate comportamental `?aberto=<id>` + Esc. |
23
+ | `--refactor` | falso | Modo redesign/refactor de tela existente; captura prints atuais e ancora o brief no estado visivel + codigo real. |
24
+ | `--url <url>` | nenhum | URL de app rodando para capturar prints atuais via Playwright Firefox. |
25
+ | `--viewports <lista>` | `1440x900,375x812` | Viewports separados por virgula para `--refactor --url`; expanda conforme a dor (ex.: `1920x1080` para tabela larga). |
26
+ | `--screenshot <path>` | repetivel | Print atual ja capturado quando nao houver `--url`; copie para `_refs/<slug>/`. |
27
+
28
+ ## 0. Refinar o brief
29
+
30
+ Antes de qualquer pre-flight de daemon ou chamada ao `od`, transforme a intencao do usuario em um brief qualificado.
31
+ Nao envie a frase crua do usuario para o runner.
32
+
33
+ Contrato obrigatorio:
34
+
35
+ | Etapa | Exigencia |
36
+ |---|---|
37
+ | Ancorar na realidade | Se a tela existe, leia o codigo real dela: colunas, enums, acoes, estados, permissoes, contracts e dominio. Leia tambem design system, prototipos irmaos e guias de design do projeto. E proibido inventar campo, status ou acao. |
38
+ | Estruturar | Reescreva o pedido no esqueleto padrao abaixo, preenchendo paths reais e arquivo de saida exato. |
39
+ | Qualificar | Cubra loading/skeleton, empty/vazio, erro/falha, light + dark, a11y, limites do design system local (ex.: <=6 cards/filtros), responsividade e deep-link. |
40
+ | Persistir | Salve o brief refinado dentro do projeto, por exemplo `<target>/PROMPT-<slug>.md`. Ele e entregavel revisavel pelo dono, nao arquivo descartavel. |
41
+ | Revisar se ja vier pronto | Se o usuario trouxer um brief ja qualificado, faca uma revisao rapida por checklist, ajuste apenas lacunas e persista a versao final. |
42
+
43
+ Esqueleto padrao do brief refinado:
44
+
45
+ ```markdown
46
+ # <titulo da tela/prototipo>
47
+
48
+ Cole este brief no Open Design. Arquivo de saida exato: `<OUTPUT_FILE>`.
49
+
50
+ ## 1. Por que
51
+ <dor real do usuario/produto; problema observavel que este prototipo precisa resolver>
52
+
53
+ ## 2. Decisao de interacao
54
+ <padrao de navegacao, abertura, selecao, filtros, ordenacao, edicao e confirmacoes>
55
+
56
+ ## 3. Layout/lista
57
+ Tabela/lista baseada apenas em colunas REAIS:
58
+
59
+ | Coluna real | Origem/contract | Como aparece | Estado/limite |
60
+ |---|---|---|---|
61
+ | <nome> | <arquivo/API/schema> | <texto, badge, acao> | <truncate, vazio, erro> |
62
+
63
+ ## 4. Detalhe/acoes
64
+ <drawer/modal/pagina de detalhe; acoes contextuais permitidas por estado/permissao real>
65
+
66
+ ## 5. Fidelidade de dominio
67
+ Para o mock nao mentir: usar enums/status reais, dados de exemplo realistas no idioma do projeto e incluir estados raros/quebrados que existem no dominio.
68
+
69
+ ## 6. Direcao visual
70
+ <tokens, componentes, densidade, referencias herdadas, prototipos irmaos e limites do design system>
71
+
72
+ ## Entregar
73
+ - `<OUTPUT_FILE>`
74
+ - `<OUTPUT_FILE>.artifact.json` se houver sidecar
75
+ ```
76
+
77
+ So depois de persistir esse arquivo anexe o apendice headless em `.dw/.open-design/runs/` e despache a run.
78
+
79
+ ### 0R. Modo refactor/redesign
80
+
81
+ Quando `--refactor` estiver presente, a fase 0 continua obrigatoria. Prints complementam a ancoragem no codigo; eles
82
+ nao substituem leitura de contracts, enums, permissoes, estados, design system e prototipos irmaos.
83
+
84
+ Capture ou copie os prints atuais para dentro da pasta importada, em `_refs/<slug>/`, antes de persistir o brief:
85
+
86
+ ```bash
87
+ node .dw/scripts/open-design/capture-current.mjs \
88
+ --target "$TARGET_DIR" \
89
+ --slug "<slug>" \
90
+ --url "<url>" \
91
+ --viewports "1440x900,375x812"
92
+ ```
93
+
94
+ Sem URL de app rodando, aceite prints prontos repetindo `--screenshot`:
95
+
96
+ ```bash
97
+ node .dw/scripts/open-design/capture-current.mjs \
98
+ --target "$TARGET_DIR" \
99
+ --slug "<slug>" \
100
+ --screenshot "./antes-1440-light.png" \
101
+ --screenshot "./antes-375-dark.png"
102
+ ```
103
+
104
+ Regras de captura:
105
+
106
+ | Caso | Exigencia |
107
+ |---|---|
108
+ | Viewports | Default sensato: desktop `1440x900` + mobile `375x812`. O agente escolhe/expande conforme a dor: bug mobile inclui `375x812`; tabela larga inclui `1920x1080`; tablet/console inclui `768x1024`. |
109
+ | Temas | Capture light e dark quando aplicavel. O helper usa `light,dark` por default e grava nomes como `_refs/<slug>/atual-1440-light.png` e `_refs/<slug>/atual-375-dark.png`. |
110
+ | Caminho robusto | O agente do `od` le imagens do filesystem do projeto. Referencie caminhos relativos dentro do target; nao dependa de flag de imagem no `run start`. |
111
+ | Higiene | `_refs/` e material de referencia nao devem virar prototipo. Se o projeto-alvo versiona prototipos finais, documente limpeza ou adicione `<target>/_refs/` ao `.gitignore` conforme a convencao local. |
112
+
113
+ O brief refinado do modo refactor deve adicionar este bloco:
114
+
115
+ ```markdown
116
+ ## Referencias da tela atual
117
+ Abra e ANALISE os prints antes de escrever:
118
+ - `_refs/<slug>/atual-1440-light.png`
119
+ - `_refs/<slug>/atual-375-dark.png`
120
+
121
+ ## O que esta errado hoje
122
+ <dor apontando elementos visiveis nos prints: densidade, hierarquia, tabela, filtros, estados, contraste, mobile, etc.>
123
+
124
+ ## A ideia da mudanca
125
+ <o que muda e por que; decisao de interacao/visual proposta pelo usuario ou pelo agente apos ler codigo e prints>
126
+
127
+ ## Preservar
128
+ <fluxos, campos, permissoes, estados, copys, affordances e integracoes que nao podem mudar>
129
+ ```
130
+
131
+ ## Pre-flight
132
+
133
+ | Checagem | Acao |
134
+ |---|---|
135
+ | Resolver `od` | `OD_CLI_DIR` vence. Senao use `~/.dw/vendor/open-design`. Se `apps/daemon/bin/od.mjs` nao existir, pare e mande rodar `dev-workflow install-deps`. |
136
+ | Prerequisitos | O checkout do Open Design exige Node `~24` e pnpm `>=10.33`. Se faltarem, pare com instrucoes objetivas. |
137
+ | Estado isolado | Crie `.dw/.open-design/`, `.dw/.open-design/data/`, `.dw/.open-design/runs/` e `.dw/.open-design/state.json`. |
138
+ | Porta | Escolha porta livre em `127.0.0.1`; persista `daemonUrl` e `port` no state. |
139
+ | Daemon vivo | Se `od daemon status --daemon-url <url> --json` retornar `ok: true`, reuse. Senao inicie novo daemon. |
140
+
141
+ Comando base do daemon:
142
+
143
+ ```bash
144
+ OD_DATA_DIR="$PWD/.dw/.open-design/data" \
145
+ OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000 \
146
+ node "$OD_DIR/apps/daemon/bin/od.mjs" daemon start \
147
+ --headless \
148
+ --host 127.0.0.1 \
149
+ --port "$PORT"
150
+ ```
151
+
152
+ Ruido conhecido: `codex_core::shell_snapshot` pode logar `ERROR` de sintaxe. Isso e benigno; nao aborte se a run
153
+ continua e o arquivo alvo e criado.
154
+
155
+ ## Import idempotente
156
+
157
+ Use `project import-folder` na pasta alvo resolvida absoluta. Default: `<repo>/open-design/`.
158
+
159
+ Persistir em `.dw/.open-design/state.json`:
160
+
161
+ ```json
162
+ {
163
+ "schema_version": "1.0",
164
+ "targetDir": "/abs/repo/open-design",
165
+ "projectId": "<project.id>",
166
+ "conversationId": "<conversationId>",
167
+ "daemonUrl": "http://127.0.0.1:<port>",
168
+ "port": 17556,
169
+ "agent": "codex"
170
+ }
171
+ ```
172
+
173
+ Se `projectId` existe no state e `od project info <id> --daemon-url <url> --json` funciona para o mesmo
174
+ `targetDir`, reuse. Caso contrario, reimporte:
175
+
176
+ ```bash
177
+ node "$OD_DIR/apps/daemon/bin/od.mjs" project import-folder "$TARGET_DIR" \
178
+ --daemon-url "$OD_URL" \
179
+ --name "$(basename "$TARGET_DIR")" \
180
+ --skill web-prototype \
181
+ --design-system default \
182
+ --json
183
+ ```
184
+
185
+ Capture `project.id` e `conversationId`; nao dependa de estado global do app.
186
+
187
+ ## Apendice headless obrigatorio
188
+
189
+ Anexe este bloco ao final de cada brief em um arquivo temporario dentro de `.dw/.open-design/runs/`.
190
+ Preencha `<PLATFORM>`, `<OUTPUT_FILE>` e referencias irma se houver.
191
+
192
+ ```markdown
193
+ ---
194
+
195
+ ## Apendice headless para Open Design
196
+
197
+ Plataforma-alvo: <PLATFORM>.
198
+
199
+ Nao faca discovery e nao faca perguntas. Nao emita `<question-form>`. Siga este brief com as respostas acima e
200
+ entregue arquivo primeiro.
201
+
202
+ Arquivo de saida obrigatorio: `<OUTPUT_FILE>`.
203
+
204
+ Contrato file-first:
205
+ - Escreva o HTML diretamente no filesystem do projeto, exatamente em `<OUTPUT_FILE>`.
206
+ - Nao altere arquivos existentes, exceto quando este prompt for explicitamente uma iteracao cirurgica sobre um arquivo alvo.
207
+ - Se precisar criar sidecar, mantenha `<OUTPUT_FILE>.artifact.json` coerente.
208
+ - Nao responda apenas com bloco `<artifact>` textual; run `succeeded` sem arquivo e falha.
209
+
210
+ Para evitar timeout headless:
211
+ - Escreva arquivos grandes em partes, com edicoes de aproximadamente 150 linhas por chamada.
212
+ - Mantenha progresso visivel ate finalizar o HTML.
213
+
214
+ Contexto de consistencia:
215
+ - Leia prototipos irmaos em `prototipos/` ou na pasta indicada no brief antes de definir padroes visuais.
216
+ - Preserve o sistema visual local quando houver referencia proxima.
217
+
218
+ Deep-link obrigatorio:
219
+ - Implemente `?aberto=<id>` para abrir o drawer/modal do item correto.
220
+ - Ao fechar com Esc ou acao de fechar, limpe o parametro com `history.replaceState`.
221
+ - O drawer/modal deve usar `role="dialog"` e atributos ARIA adequados.
222
+ - Inclua estados skeleton/loading, empty/vazio e erro/falha.
223
+ ```
224
+
225
+ ## Run single
226
+
227
+ Sempre passe `--agent` explicitamente:
228
+
229
+ ```bash
230
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
231
+ --daemon-url "$OD_URL" \
232
+ --project "$PROJECT_ID" \
233
+ --conversation "$CONVERSATION_ID" \
234
+ --agent "$AGENT" \
235
+ --skill web-prototype \
236
+ --design-system default \
237
+ --prompt-file "$PROMPT_WITH_APPENDIX" \
238
+ --follow \
239
+ --json | tee ".dw/.open-design/runs/<slug>.jsonl"
240
+ ```
241
+
242
+ Depois da run, confira o arquivo. **Nao aceite apenas status `succeeded`:**
243
+
244
+ ```bash
245
+ test -s "$TARGET_DIR/<OUTPUT_FILE>"
246
+ ```
247
+
248
+ Registre no JSONL ou em sidecar de auditoria: brief, prompt com apendice, agente, daemonUrl, projectId,
249
+ conversationId, output esperado, status final e resultado da verificacao do arquivo.
250
+
251
+ ## Batch
252
+
253
+ Para `--brief-glob`, expanda os briefs, ordene por nome e rode em serie. Pare no primeiro caso em que:
254
+
255
+ | Condicao | Resultado |
256
+ |---|---|
257
+ | run falha | `BLOCKED` com log JSONL |
258
+ | run diz sucesso mas arquivo nao existe ou esta vazio | `FINDINGS` e follow-up cirurgico ou `BLOCKED` |
259
+ | gate visual <9 | `FINDINGS`; iterar antes de continuar |
260
+
261
+ Nao rode prompts em paralelo no mesmo projeto `od`; preserve auditabilidade e contexto.
262
+
263
+ ## Iteracao cirurgica
264
+
265
+ Use follow-up run no mesmo `projectId`/`conversationId`, com prompt curto e arquivo alvo explicito:
266
+
267
+ ```bash
268
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
269
+ --daemon-url "$OD_URL" \
270
+ --project "$PROJECT_ID" \
271
+ --conversation "$CONVERSATION_ID" \
272
+ --agent "$AGENT" \
273
+ --skill web-prototype \
274
+ --design-system default \
275
+ --prompt-file ".dw/.open-design/runs/<slug>.followup.md" \
276
+ --follow \
277
+ --json | tee -a ".dw/.open-design/runs/<slug>.jsonl"
278
+ ```
279
+
280
+ O follow-up deve dizer: arquivo unico, mudanca exata, nao tocar prototipos irmaos, manter deep-link e estados.
281
+
282
+ ## Gate visual obrigatorio
283
+
284
+ Execute o gate automatico:
285
+
286
+ ```bash
287
+ node .dw/scripts/open-design/gate-prototype.mjs \
288
+ --file "$TARGET_DIR/<OUTPUT_FILE>" \
289
+ --deep-link-id "<id-real>" \
290
+ --expected-text "<texto-do-item>" \
291
+ --out ".dw/.open-design/gate/<slug>"
292
+ ```
293
+
294
+ O gate cobre:
295
+
296
+ | Camada | Exigencia |
297
+ |---|---|
298
+ | Estrutural | `<title>`, `aberto`, `replaceState`, `role=dialog`/ARIA, skeleton/loading, empty/vazio, erro/falha. |
299
+ | Screenshot | Firefox headless em light e dark, com `colorScheme` e `data-theme`. |
300
+ | Comportamental | `?aberto=<id>` abre o item certo; Esc fecha e limpa o parametro. |
301
+ | Avaliacao | Automatico PASS + sua nota visual >=9. Se abaixo de 9, faca follow-up cirurgico. |
302
+
303
+ Depois de passar, **pare para o gate do dono**. Nao commite ate o dono aprovar.
304
+
305
+ ## Armadilhas validadas
306
+
307
+ | Armadilha | Sintoma | Contorno obrigatorio |
308
+ |---|---|---|
309
+ | Discovery da `web-prototype` | Abre com `<question-form>` e encerra o turno; headless nao gera GenUI (`od ui list` vazio). | Anexar o apendice headless respondendo plataforma e proibindo perguntas/discovery. |
310
+ | Timeout de inatividade | HTML grande fica silencioso e o run morre perto de 10min. | `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000` no daemon + instruir escrita em partes de ~150 linhas. |
311
+ | `codex_core::shell_snapshot` | Loga `ERROR` de sintaxe no stream. | Tratar como ruido se a run prossegue e o arquivo final existe. |
312
+ | Estado da GUI | Config/agente/projetos vazam do app do usuario. | `OD_DATA_DIR` isolado sempre; `--agent`, `--skill` e `--design-system` explicitos. |
313
+
314
+ ## Structured Return
315
+
316
+ **Status:** `PASS` | `FINDINGS` | `BLOCKED` | `NOT_APPLICABLE`
317
+ **Scope:** pasta alvo, briefs executados, agente usado, projectId/conversationId.
318
+ **Evidence:** logs JSONL, arquivo(s) HTML verificados, gate JSON, screenshots light/dark.
319
+ **Artifacts:** paths dos prototipos e sidecars.
320
+ **Decisions:** defaults/overrides aplicados (`agent`, `target`, plataforma, porta).
321
+ **Risks:** qualquer gate abaixo de 9, arquivo nao gerado apesar de run succeeded, dependencia ausente.
322
+ **Next Step:** gate do dono, follow-up cirurgico necessario, ou comando de resume exato.
323
+ </system_instructions>
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ function parseArgs(argv) {
7
+ const result = { screenshot: [] };
8
+ for (let index = 0; index < argv.length; index += 1) {
9
+ const token = argv[index];
10
+ if (!token.startsWith("--")) continue;
11
+ const [rawKey, inlineValue] = token.slice(2).split("=", 2);
12
+ const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
13
+ if (inlineValue !== undefined) {
14
+ if (key === "screenshot") result.screenshot.push(inlineValue);
15
+ else result[key] = inlineValue;
16
+ continue;
17
+ }
18
+ const next = argv[index + 1];
19
+ if (!next || next.startsWith("--")) {
20
+ if (key === "screenshot") {
21
+ throw new Error("Missing value for --screenshot <path>.");
22
+ }
23
+ result[key] = true;
24
+ continue;
25
+ }
26
+ if (key === "screenshot") result.screenshot.push(next);
27
+ else result[key] = next;
28
+ index += 1;
29
+ }
30
+ return result;
31
+ }
32
+
33
+ async function importFirefox() {
34
+ try {
35
+ return (await import("@playwright/test")).firefox;
36
+ } catch {
37
+ try {
38
+ return (await import("playwright")).firefox;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ }
44
+
45
+ function slugify(value) {
46
+ return (
47
+ String(value)
48
+ .toLowerCase()
49
+ .replace(/^https?:\/\//, "")
50
+ .replace(/[^a-z0-9]+/g, "-")
51
+ .replace(/^-+|-+$/g, "")
52
+ .slice(0, 70) || "screen"
53
+ );
54
+ }
55
+
56
+ function parseViewports(value) {
57
+ return String(value || "1440x900,375x812")
58
+ .split(",")
59
+ .map((entry) => {
60
+ const match = entry.trim().match(/^(\d{2,5})x(\d{2,5})$/);
61
+ if (!match) return null;
62
+ return { width: Number(match[1]), height: Number(match[2]) };
63
+ })
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function parseThemes(value) {
68
+ return String(value || "light,dark")
69
+ .split(",")
70
+ .map((entry) => entry.trim().toLowerCase())
71
+ .filter((entry) => entry === "light" || entry === "dark");
72
+ }
73
+
74
+ function inferProvidedName(source, index) {
75
+ const base = path.basename(source, path.extname(source)).toLowerCase();
76
+ const width = base.match(/(?:^|[^0-9])(\d{3,5})(?:x\d{3,5})?(?:[^0-9]|$)/)?.[1];
77
+ const theme = base.includes("dark") ? "dark" : base.includes("light") ? "light" : "";
78
+ if (width && theme) return `atual-${width}-${theme}.png`;
79
+ if (width) return `atual-${width}-${String(index + 1).padStart(2, "0")}.png`;
80
+ return `atual-ref-${String(index + 1).padStart(2, "0")}.png`;
81
+ }
82
+
83
+ async function captureFromUrl(args, outDir) {
84
+ if (!args.url) return [];
85
+ const firefox = await importFirefox();
86
+ if (!firefox) {
87
+ throw new Error("Playwright is not installed. Run: npm i -D @playwright/test && npx playwright install firefox");
88
+ }
89
+
90
+ const viewports = parseViewports(args.viewports);
91
+ if (viewports.length === 0) {
92
+ throw new Error("No valid --viewports entries. Use comma-separated WIDTHxHEIGHT values, for example 1440x900,375x812.");
93
+ }
94
+ const themes = parseThemes(args.themes);
95
+ if (themes.length === 0) {
96
+ throw new Error("No valid --themes entries. Use light,dark or one of them.");
97
+ }
98
+
99
+ const browser = await firefox.launch({ headless: true });
100
+ const shots = [];
101
+ try {
102
+ for (const viewport of viewports) {
103
+ for (const theme of themes) {
104
+ const context = await browser.newContext({
105
+ colorScheme: theme,
106
+ viewport,
107
+ });
108
+ const page = await context.newPage();
109
+ await page.goto(String(args.url), { waitUntil: "networkidle" }).catch(() => page.goto(String(args.url)));
110
+ await page.evaluate((value) => {
111
+ document.documentElement.setAttribute("data-theme", value);
112
+ }, theme);
113
+ await page.waitForTimeout(300);
114
+ const target = path.join(outDir, `atual-${viewport.width}-${theme}.png`);
115
+ await page.screenshot({ path: target, fullPage: true });
116
+ shots.push({ source: String(args.url), viewport, theme, path: target });
117
+ await context.close();
118
+ }
119
+ }
120
+ } finally {
121
+ await browser.close();
122
+ }
123
+ return shots;
124
+ }
125
+
126
+ function copyProvidedScreenshots(args, outDir) {
127
+ return args.screenshot.map((source, index) => {
128
+ const resolved = path.resolve(String(source));
129
+ if (!fs.existsSync(resolved)) {
130
+ throw new Error(`Screenshot not found: ${resolved}`);
131
+ }
132
+ const target = path.join(outDir, inferProvidedName(resolved, index));
133
+ fs.copyFileSync(resolved, target);
134
+ return { source: resolved, path: target };
135
+ });
136
+ }
137
+
138
+ async function main() {
139
+ const args = parseArgs(process.argv.slice(2));
140
+ if (!args.url && args.screenshot.length === 0) {
141
+ throw new Error("Pass --url <app-url> or one or more --screenshot <path> values.");
142
+ }
143
+
144
+ const targetDir = path.resolve(String(args.target || "open-design"));
145
+ const slug = slugify(args.slug || args.url || "screen");
146
+ const outDir = path.resolve(String(args.out || path.join(targetDir, "_refs", slug)));
147
+ fs.mkdirSync(outDir, { recursive: true });
148
+
149
+ const copied = copyProvidedScreenshots(args, outDir);
150
+ const captured = await captureFromUrl(args, outDir);
151
+ const shots = [...copied, ...captured];
152
+
153
+ process.stdout.write(
154
+ `${JSON.stringify(
155
+ {
156
+ targetDir,
157
+ refsDir: outDir,
158
+ relativeRefs: shots.map((shot) => path.relative(targetDir, shot.path).split(path.sep).join("/")),
159
+ shots,
160
+ },
161
+ null,
162
+ 2,
163
+ )}\n`,
164
+ );
165
+ }
166
+
167
+ main().catch((error) => {
168
+ process.stderr.write(`capture-current: ${error.message}\n`);
169
+ process.exit(1);
170
+ });
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ function parseArgs(argv) {
8
+ const result = {};
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const token = argv[index];
11
+ if (!token.startsWith("--")) continue;
12
+ const [rawKey, inlineValue] = token.slice(2).split("=", 2);
13
+ const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
14
+ if (inlineValue !== undefined) {
15
+ result[key] = inlineValue;
16
+ continue;
17
+ }
18
+ const next = argv[index + 1];
19
+ if (!next || next.startsWith("--")) {
20
+ result[key] = true;
21
+ continue;
22
+ }
23
+ result[key] = next;
24
+ index += 1;
25
+ }
26
+ return result;
27
+ }
28
+
29
+ async function importFirefox() {
30
+ try {
31
+ return (await import("@playwright/test")).firefox;
32
+ } catch {
33
+ try {
34
+ return (await import("playwright")).firefox;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ }
40
+
41
+ function slugify(value) {
42
+ return (
43
+ String(value)
44
+ .toLowerCase()
45
+ .replace(/\.html?$/, "")
46
+ .replace(/[^a-z0-9]+/g, "-")
47
+ .replace(/^-+|-+$/g, "")
48
+ .slice(0, 70) || "prototype"
49
+ );
50
+ }
51
+
52
+ function addResult(results, name, pass, details = "") {
53
+ results.push({ name, pass: Boolean(pass), details });
54
+ }
55
+
56
+ async function structuralChecks(page, html, results) {
57
+ const data = await page.evaluate(() => {
58
+ const visibleText = document.body ? document.body.innerText : "";
59
+ const hasDialogRole = Boolean(document.querySelector('[role="dialog"], [aria-modal="true"], [aria-labelledby]'));
60
+ const title = document.title.trim();
61
+ return { visibleText, hasDialogRole, title };
62
+ });
63
+
64
+ addResult(results, "title", data.title.length > 0, data.title || "missing document title");
65
+ addResult(results, "aberto-param", /\baberto\b/.test(html), "source contains aberto");
66
+ addResult(results, "replace-state", /replaceState/.test(html), "source contains replaceState");
67
+ addResult(results, "dialog-aria", data.hasDialogRole, "role=dialog, aria-modal, or aria-labelledby present");
68
+ addResult(results, "skeleton-state", /skeleton|carregando|loading/i.test(html + "\n" + data.visibleText));
69
+ addResult(results, "empty-state", /empty|vazio|sem\s+(resultado|dados|itens|registros)/i.test(html + "\n" + data.visibleText));
70
+ addResult(results, "error-state", /error|erro|falha|failed|retry|tentar novamente/i.test(html + "\n" + data.visibleText));
71
+ }
72
+
73
+ async function visibleDialog(page, selector) {
74
+ const selected = selector ? page.locator(selector).first() : page.locator('[role="dialog"], [aria-modal="true"]').first();
75
+ try {
76
+ return (await selected.count()) > 0 && (await selected.isVisible());
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ async function clickFirstOpenable(page, rowSelector) {
83
+ const selector =
84
+ rowSelector ||
85
+ '[data-open], [data-id], tbody tr, [role="row"], [role="button"], article, li button, li, button';
86
+ const candidates = page.locator(selector);
87
+ const count = Math.min(await candidates.count(), 20);
88
+ for (let index = 0; index < count; index += 1) {
89
+ const candidate = candidates.nth(index);
90
+ try {
91
+ if (!(await candidate.isVisible())) continue;
92
+ await candidate.click({ timeout: 1500 });
93
+ return true;
94
+ } catch {
95
+ // Try the next plausible opener.
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+
101
+ async function behavioralChecks(page, baseUrl, args, results) {
102
+ const dialogSelector = args.dialogSelector ? String(args.dialogSelector) : "";
103
+ if (!(await visibleDialog(page, dialogSelector))) {
104
+ const clicked = await clickFirstOpenable(page, args.rowSelector ? String(args.rowSelector) : "");
105
+ await page.waitForTimeout(300);
106
+ addResult(results, "open-drawer-click", clicked && (await visibleDialog(page, dialogSelector)), "first row/openable opens dialog or drawer");
107
+ } else {
108
+ addResult(results, "open-drawer-click", true, "dialog already visible");
109
+ }
110
+
111
+ const deepLinkId = args.deepLinkId ? String(args.deepLinkId) : "";
112
+ if (!deepLinkId) {
113
+ addResult(results, "deep-link-opens", false, "missing --deep-link-id");
114
+ addResult(results, "esc-clears-param", false, "missing --deep-link-id");
115
+ return;
116
+ }
117
+
118
+ const url = new URL(baseUrl);
119
+ url.searchParams.set("aberto", deepLinkId);
120
+ await page.goto(url.toString(), { waitUntil: "load" });
121
+ await page.waitForTimeout(500);
122
+
123
+ const expectedText = String(args.expectedText || deepLinkId);
124
+ const deepLinkData = await page.evaluate((text) => {
125
+ const bodyText = document.body ? document.body.innerText : "";
126
+ const hasDialog = Boolean(document.querySelector('[role="dialog"], [aria-modal="true"]'));
127
+ return {
128
+ hasDialog,
129
+ hasText: bodyText.toLowerCase().includes(text.toLowerCase()),
130
+ search: window.location.search,
131
+ };
132
+ }, expectedText);
133
+ addResult(
134
+ results,
135
+ "deep-link-opens",
136
+ deepLinkData.search.includes(`aberto=${encodeURIComponent(deepLinkId)}`) && deepLinkData.hasDialog && deepLinkData.hasText,
137
+ `expected visible text: ${expectedText}`,
138
+ );
139
+
140
+ await page.keyboard.press("Escape");
141
+ await page.waitForTimeout(500);
142
+ const searchAfterEsc = await page.evaluate(() => window.location.search);
143
+ addResult(results, "esc-clears-param", !new URLSearchParams(searchAfterEsc).has("aberto"), searchAfterEsc || "empty search");
144
+ }
145
+
146
+ async function captureScreenshots(browser, fileUrl, outDir, slug) {
147
+ const shots = [];
148
+ for (const scheme of ["light", "dark"]) {
149
+ const context = await browser.newContext({
150
+ colorScheme: scheme,
151
+ viewport: { width: 1440, height: 1000 },
152
+ });
153
+ const page = await context.newPage();
154
+ await page.goto(fileUrl, { waitUntil: "load" });
155
+ await page.evaluate((theme) => {
156
+ document.documentElement.setAttribute("data-theme", theme);
157
+ }, scheme);
158
+ await page.waitForTimeout(300);
159
+ const shotPath = path.join(outDir, `${slug}-${scheme}.png`);
160
+ await page.screenshot({ path: shotPath, fullPage: true });
161
+ shots.push({ colorScheme: scheme, path: shotPath });
162
+ await context.close();
163
+ }
164
+ return shots;
165
+ }
166
+
167
+ async function main() {
168
+ const args = parseArgs(process.argv.slice(2));
169
+ if (!args.file) {
170
+ throw new Error("Missing required argument: --file <prototype.html>");
171
+ }
172
+
173
+ const filePath = path.resolve(String(args.file));
174
+ if (!fs.existsSync(filePath)) {
175
+ throw new Error(`Prototype file not found: ${filePath}`);
176
+ }
177
+
178
+ const firefox = await importFirefox();
179
+ if (!firefox) {
180
+ throw new Error("Playwright is not installed. Run: npm i -D @playwright/test && npx playwright install firefox");
181
+ }
182
+
183
+ const html = fs.readFileSync(filePath, "utf8");
184
+ const outDir = path.resolve(String(args.out || path.join(".dw", ".open-design", "gate", slugify(path.basename(filePath)))));
185
+ fs.mkdirSync(outDir, { recursive: true });
186
+ const baseUrl = pathToFileURL(filePath).toString();
187
+ const slug = slugify(path.basename(filePath));
188
+
189
+ const browser = await firefox.launch({ headless: true });
190
+ const results = [];
191
+ let screenshots = [];
192
+ try {
193
+ const context = await browser.newContext({ colorScheme: "light", viewport: { width: 1440, height: 1000 } });
194
+ const page = await context.newPage();
195
+ await page.goto(baseUrl, { waitUntil: "load" });
196
+ await structuralChecks(page, html, results);
197
+ await behavioralChecks(page, baseUrl, args, results);
198
+ await context.close();
199
+ screenshots = await captureScreenshots(browser, baseUrl, outDir, slug);
200
+ } finally {
201
+ await browser.close();
202
+ }
203
+
204
+ const failed = results.filter((result) => !result.pass);
205
+ const summary = {
206
+ file: filePath,
207
+ score: Number(((results.length - failed.length) / results.length * 10).toFixed(1)),
208
+ pass: failed.length === 0,
209
+ results,
210
+ screenshots,
211
+ };
212
+ fs.writeFileSync(path.join(outDir, `${slug}-gate.json`), JSON.stringify(summary, null, 2) + "\n", "utf8");
213
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
214
+
215
+ if (failed.length > 0) {
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ main().catch((error) => {
221
+ process.stderr.write(`gate-prototype: ${error.message}\n`);
222
+ process.exit(1);
223
+ });