modelroom 0.0.0-stage → 0.0.2
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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/dist/bin/modelroom.d.ts +3 -0
- package/dist/bin/modelroom.d.ts.map +1 -0
- package/dist/bin/modelroom.js +242 -0
- package/dist/bin/modelroom.js.map +1 -0
- package/dist/lib/agents/index.d.ts +22 -0
- package/dist/lib/agents/index.d.ts.map +1 -0
- package/dist/lib/agents/index.js +68 -0
- package/dist/lib/agents/index.js.map +1 -0
- package/dist/lib/artifact.d.ts +22 -0
- package/dist/lib/artifact.d.ts.map +1 -0
- package/dist/lib/artifact.js +62 -0
- package/dist/lib/artifact.js.map +1 -0
- package/dist/lib/config.d.ts +53 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +92 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/facets.d.ts +62 -0
- package/dist/lib/facets.d.ts.map +1 -0
- package/dist/lib/facets.js +181 -0
- package/dist/lib/facets.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +41 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +219 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/lint.d.ts +26 -0
- package/dist/lib/lint.d.ts.map +1 -0
- package/dist/lib/lint.js +139 -0
- package/dist/lib/lint.js.map +1 -0
- package/dist/lib/opencode.d.ts +42 -0
- package/dist/lib/opencode.d.ts.map +1 -0
- package/dist/lib/opencode.js +229 -0
- package/dist/lib/opencode.js.map +1 -0
- package/dist/lib/pkg.d.ts +2 -0
- package/dist/lib/pkg.d.ts.map +1 -0
- package/dist/lib/pkg.js +19 -0
- package/dist/lib/pkg.js.map +1 -0
- package/dist/lib/rooms.d.ts +63 -0
- package/dist/lib/rooms.d.ts.map +1 -0
- package/dist/lib/rooms.js +178 -0
- package/dist/lib/rooms.js.map +1 -0
- package/dist/lib/server.d.ts +53 -0
- package/dist/lib/server.d.ts.map +1 -0
- package/dist/lib/server.js +538 -0
- package/dist/lib/server.js.map +1 -0
- package/docs/01_concept.md +69 -0
- package/docs/02-convention.md +109 -0
- package/docs/03-cli.md +103 -0
- package/docs/04-agents.md +88 -0
- package/examples/rooms/sign-in/BEHAVIOUR.md +23 -0
- package/examples/rooms/sign-in/BRIEF.md +15 -0
- package/examples/rooms/sign-in/SPEC.md +14 -0
- package/examples/rooms/sign-in/index.html +56 -0
- package/package.json +83 -4
- package/templates/AGENTS.md +31 -0
- package/templates/modelroom.json +14 -0
- package/templates/room/BEHAVIOUR.md +19 -0
- package/templates/room/BRIEF.md +16 -0
- package/templates/room/SPEC.md +9 -0
- package/templates/room/index.html +53 -0
- package/ui/dashboard.html +332 -0
- package/ui/modelroom.css +2 -0
- package/ui/room.html +466 -0
- package/ui/settings.html +125 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# The convention
|
|
2
|
+
|
|
3
|
+
A room is one screen. Everything in a room is a file with one job, and every file has one
|
|
4
|
+
audience. That is the whole idea; the rest follows.
|
|
5
|
+
|
|
6
|
+
## Files
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
rooms/<room>/
|
|
10
|
+
├── BRIEF.md why this room exists → director, everyone
|
|
11
|
+
├── SPEC.md what the artifact cannot show → designer, agent, implementer
|
|
12
|
+
├── BEHAVIOUR.md what it promises → tests, implementer
|
|
13
|
+
└── index.html the artifact (the screen) → director, owner, implementer
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The artifact is the visible spec. `SPEC.md` is the invisible one. They must not overlap.
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
1. **The artifact owns what is visible.** If a statement can be shown in a screenshot, it
|
|
21
|
+
belongs to `index.html`, not to `SPEC.md`.
|
|
22
|
+
2. **A statement lives in one file.** To reach another file, reference it by id
|
|
23
|
+
(`@sign-in-1`) instead of copying it.
|
|
24
|
+
3. **The design layer never reads the application.** Rooms hold promises the room itself can
|
|
25
|
+
keep. Anything that needs the real system — records, timestamps, rounding, permissions —
|
|
26
|
+
belongs to the application and is handed over, not fetched.
|
|
27
|
+
4. **Implementation reads the design layer, never the other way round.**
|
|
28
|
+
|
|
29
|
+
This is enforceable rather than aspirational: an agent working in the design layer can be
|
|
30
|
+
denied access outside it (for example, OpenCode `permissions` with
|
|
31
|
+
`external_directory: deny` and write rules limited to `rooms/*`).
|
|
32
|
+
|
|
33
|
+
## The artifact
|
|
34
|
+
|
|
35
|
+
- Self-contained: no CDN, no web fonts, no framework, no build step. It is going to be handed
|
|
36
|
+
to implementation, so it has to stand on its own (and render with no network).
|
|
37
|
+
- One file per state (`index-error.html` and so on). Steps in a flow are separate rooms.
|
|
38
|
+
- Anything shared (a component, a token) is named in `SPEC.md`, not reinvented per room.
|
|
39
|
+
|
|
40
|
+
## Frontmatter is the record
|
|
41
|
+
|
|
42
|
+
The body is the content. The frontmatter is the record the dashboard reads: the title,
|
|
43
|
+
the one-line summary, the tags, the state. Nothing else stores it, so nothing has to be
|
|
44
|
+
kept in sync.
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
---
|
|
48
|
+
title: Sign in
|
|
49
|
+
status: draft
|
|
50
|
+
version: 1
|
|
51
|
+
summary: A boring front door — the person is signed in, or told plainly why not.
|
|
52
|
+
tags: [auth, form]
|
|
53
|
+
---
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Reserved keys — shape only, never a fixed vocabulary:
|
|
57
|
+
|
|
58
|
+
| Key | Shape | Shown as |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `title` | string | the card's heading |
|
|
61
|
+
| `summary` | string | one line under the heading |
|
|
62
|
+
| `key_points` | string[] | up to three bullets |
|
|
63
|
+
| `tags` | string[] | chips, and a checkbox facet |
|
|
64
|
+
| `status` | string | a badge, and a facet |
|
|
65
|
+
| `version` | number or string | a `v3` badge |
|
|
66
|
+
| `updated_at` | date string | sort order (falls back to file mtime) |
|
|
67
|
+
| `deprecated` | boolean | hidden unless archived is on |
|
|
68
|
+
| `superseded_by` | string | a pointer to the room that replaced this one |
|
|
69
|
+
|
|
70
|
+
**Everything else is a facet.** Add `category: kiosk`, `platform: tablet`, or anything
|
|
71
|
+
that helps you narrow the list; a string becomes a radio group or a select (built from
|
|
72
|
+
the values that exist, with counts), a list becomes checkboxes. The dashboard never
|
|
73
|
+
validates values — it shows what is there.
|
|
74
|
+
|
|
75
|
+
`adopted` (or `accepted`, `approved`) is a human decision. An agent must never set it.
|
|
76
|
+
|
|
77
|
+
## What an agent may do
|
|
78
|
+
|
|
79
|
+
An agent adds. It does not delete.
|
|
80
|
+
|
|
81
|
+
- new room, new key, new tag, new doc → fine
|
|
82
|
+
- merge two spellings → add an alias, leave both values in place
|
|
83
|
+
- retire a room → `deprecated: true` and `superseded_by: <room>`
|
|
84
|
+
- remove a file or a key → a human does it
|
|
85
|
+
|
|
86
|
+
That is what makes "the agent keeps the record tidy" safe: every move is additive and
|
|
87
|
+
reversible in git.
|
|
88
|
+
|
|
89
|
+
## The check
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
modelroom check
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It reads the frontmatter — head of the file only — and reports what would stop the
|
|
96
|
+
dashboard from deriving the record: malformed or nested frontmatter, a reserved key with
|
|
97
|
+
the wrong shape, two spellings of the same value, a `title` that disagrees with the H1,
|
|
98
|
+
a `superseded_by` that points nowhere. Errors are fixable by an agent; warnings are for a
|
|
99
|
+
human. It never writes.
|
|
100
|
+
|
|
101
|
+
## Naming
|
|
102
|
+
|
|
103
|
+
- Files are named for **what they are**, not for who writes them and not for the tool:
|
|
104
|
+
`BRIEF`, `SPEC`, `BEHAVIOUR`, `DESIGN`, `principles`, `components`, `tokens`.
|
|
105
|
+
- Avoid words that shift meaning with the reader (`context`, `system`, `ui`, `ux`) and words
|
|
106
|
+
that name a medium (`screen`, `page`) when the thing is not a medium.
|
|
107
|
+
- **Scope is expressed by the directory, not by the filename.** One `BRIEF.md` per level —
|
|
108
|
+
product, flow, room — read top-down, written only at your own level.
|
|
109
|
+
- Prefer one word that survives translation (see the list above) over a compound.
|
package/docs/03-cli.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
modelroom [serve] [--port 4321] [--host 127.0.0.1] [--open]
|
|
5
|
+
modelroom init [<room>] [--dir rooms] [--force]
|
|
6
|
+
modelroom check [--json] [--strict]
|
|
7
|
+
modelroom doctor
|
|
8
|
+
modelroom --version | --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`modelroom` with no command is `serve`. `serve` needs Node 20+ and nothing else. `init` only
|
|
12
|
+
copies templates. `check` reads the frontmatter of every room and reports convention problems.
|
|
13
|
+
`doctor` checks the setup and exits non-zero when something is broken (optionally missing things
|
|
14
|
+
do not count).
|
|
15
|
+
|
|
16
|
+
## serve
|
|
17
|
+
|
|
18
|
+
Starts the workspace on `127.0.0.1:4321` by default. `modelroom` alone does the same.
|
|
19
|
+
|
|
20
|
+
- `/` — every room, side by side
|
|
21
|
+
- `/room/<name>` — chat, the room's docs, the artifact
|
|
22
|
+
- `/settings` — which agent to run
|
|
23
|
+
- `/files/<path>` — your repository, served read-only and **unmodified** (no injection)
|
|
24
|
+
|
|
25
|
+
`--open` tries to open a browser; on a headless machine it is ignored.
|
|
26
|
+
|
|
27
|
+
## init
|
|
28
|
+
|
|
29
|
+
Copies the room templates and, if missing, `modelroom.json`, `AGENTS.md` and a
|
|
30
|
+
`.modelroom/` line in `.gitignore`.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
modelroom init checkout # creates rooms/checkout/
|
|
34
|
+
modelroom init checkout --dir design/screens
|
|
35
|
+
modelroom init checkout --force # overwrite existing files
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## check
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
modelroom check # errors and warnings
|
|
42
|
+
modelroom check --json # machine-readable
|
|
43
|
+
modelroom check --strict # warnings fail too
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
It only reads the head of each doc (frontmatter lives there) and never writes:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
FAIL sign-in/BRIEF.md reserved-type tags must be array, got string
|
|
50
|
+
warn checkout/BRIEF.md h1-title H1 "Checkout" differs from title "check out"
|
|
51
|
+
warn sign-in/BRIEF.md variant status: draft / Draft — same value, different spelling
|
|
52
|
+
|
|
53
|
+
2 room(s), 1 error(s), 2 warning(s)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exit code is 1 on errors (or on warnings with `--strict`).
|
|
57
|
+
|
|
58
|
+
## doctor
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
modelroom doctor
|
|
62
|
+
ok node 22.14.0 (needs >= 20)
|
|
63
|
+
ok root exists: /path/to/repo
|
|
64
|
+
ok rooms dir exists: rooms
|
|
65
|
+
ok rooms found: checkout, sign-in
|
|
66
|
+
ok config: modelroom.json
|
|
67
|
+
ok agent command on PATH: opencode
|
|
68
|
+
ok png rendering (playwright): not installed (optional)
|
|
69
|
+
ok room checkout: docs ok
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Config: `modelroom.json`
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{
|
|
76
|
+
"root": ".", // where your repository is
|
|
77
|
+
"rooms": "rooms", // rooms live here
|
|
78
|
+
"artifact": "index.html", // the file that is the screen
|
|
79
|
+
"docs": ["BRIEF.md", "SPEC.md", "BEHAVIOUR.md"], // the only files the UI may write
|
|
80
|
+
"port": 4321,
|
|
81
|
+
"agent": {
|
|
82
|
+
"adapter": "opencode", // "opencode" | "command"
|
|
83
|
+
"command": "opencode",
|
|
84
|
+
"agent": "build",
|
|
85
|
+
"model": null, // set this when your agent's default fails
|
|
86
|
+
"standalone": true,
|
|
87
|
+
"args": [] // adapter "command": e.g. ["-p", "{prompt}"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Anything you change from `/settings` is kept locally in `.modelroom/state.json`
|
|
93
|
+
(git-ignore it) — committed configuration stays in `modelroom.json`.
|
|
94
|
+
|
|
95
|
+
## Thumbnails
|
|
96
|
+
|
|
97
|
+
Rendering the artifact to PNG is optional and needs Playwright:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
npm i -D playwright && npx playwright install chromium
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Without it, rooms simply show no thumbnail and the **make png** button reports why.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
modelroom does not bundle a model and does not own a chat product. It runs the agent you
|
|
4
|
+
configure, one session per room, and shows you what changed.
|
|
5
|
+
|
|
6
|
+
## Built-in adapters
|
|
7
|
+
|
|
8
|
+
| Adapter | What it runs | Notes |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `opencode` | [OpenCode](https://opencode.ai) CLI | sessions, model and agent selection |
|
|
11
|
+
| `command` | any CLI | you provide `agent.args`; `{prompt}` and `{session}` are substituted |
|
|
12
|
+
|
|
13
|
+
### opencode
|
|
14
|
+
|
|
15
|
+
```jsonc
|
|
16
|
+
{
|
|
17
|
+
"agent": {
|
|
18
|
+
"adapter": "opencode",
|
|
19
|
+
"command": "opencode",
|
|
20
|
+
"agent": "build", // an agent defined in .opencode/agents/
|
|
21
|
+
"model": "openrouter/deepseek/deepseek-v4.1-flash",
|
|
22
|
+
"standalone": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
modelroom runs, roughly:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
opencode run --standalone --agent <agent> --model <model> --session <id> --auto "<prompt>"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Two things are worth knowing, both learned the hard way:
|
|
34
|
+
|
|
35
|
+
- **Set `model`.** Many agents resolve to a free tier by default, and the CLI then refuses to
|
|
36
|
+
run outside its own app (`OpenCode's free tier can only be used from within OpenCode`).
|
|
37
|
+
- **`standalone: true`** runs the agent against a private server that inherits this process's
|
|
38
|
+
environment, which is what makes provider keys visible to it. If your shared service already
|
|
39
|
+
has keys, either setting works.
|
|
40
|
+
|
|
41
|
+
Agents that need to *write* only the room's files should be scoped on their side, for example
|
|
42
|
+
with OpenCode `permissions`:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
# .opencode/agents/design.md
|
|
46
|
+
permissions:
|
|
47
|
+
- { action: "*", resource: "*", effect: deny }
|
|
48
|
+
- { action: read, resource: "rooms/*", effect: allow }
|
|
49
|
+
- { action: edit, resource: "rooms/*", effect: allow }
|
|
50
|
+
- { action: glob, resource: "*", effect: allow }
|
|
51
|
+
- { action: grep, resource: "*", effect: allow }
|
|
52
|
+
- { action: external_directory, resource: "*", effect: deny }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### command
|
|
56
|
+
|
|
57
|
+
For anything else — Claude Code, Codex, a local model, a shell script:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"agent": { "adapter": "command", "command": "claude", "args": ["-p", "{prompt}"] }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`{session}` is substituted with the room's session id. Sessions are stored per room in
|
|
66
|
+
`.modelroom/state.json`; **new session** in the room UI starts a fresh one.
|
|
67
|
+
|
|
68
|
+
## Writing an adapter
|
|
69
|
+
|
|
70
|
+
An adapter is one object:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
export const mine = {
|
|
74
|
+
name: 'mine',
|
|
75
|
+
newSessionId: () => `ses_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`,
|
|
76
|
+
run({ cfg, cwd, prompt, sessionId, onChunk, onExit }) {
|
|
77
|
+
// spawn your CLI; stream output with onChunk('stdout'|'stderr', text)
|
|
78
|
+
// call onExit(code, errorMessage?) once it finishes
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Add it to `lib/agents/index.mjs`. Two rules keep adapters honest:
|
|
84
|
+
|
|
85
|
+
1. **Close stdin** (`stdio: ['ignore', 'pipe', 'pipe']`). A CLI that reads stdin will hang
|
|
86
|
+
forever if the pipe stays open.
|
|
87
|
+
2. **Do not parse the output into meaning.** modelroom shows the agent's words as they are.
|
|
88
|
+
Interpretation belongs to the person reading the room.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# sign-in — behaviour
|
|
2
|
+
|
|
3
|
+
What this room promises.
|
|
4
|
+
|
|
5
|
+
```gherkin
|
|
6
|
+
Feature: sign in
|
|
7
|
+
|
|
8
|
+
@sign-in-1
|
|
9
|
+
Scenario: correct credentials
|
|
10
|
+
Given the sign-in screen is open
|
|
11
|
+
When the person submits a valid email and password
|
|
12
|
+
Then the screen reports success and leaves for the next room
|
|
13
|
+
|
|
14
|
+
@sign-in-2
|
|
15
|
+
Scenario: wrong credentials
|
|
16
|
+
Given the sign-in screen is open
|
|
17
|
+
When the person submits a password that does not match
|
|
18
|
+
Then the screen stays, and explains in one sentence that the details did not match
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Notes
|
|
22
|
+
- The room does not decide how credentials are checked — that belongs to the application.
|
|
23
|
+
- `@sign-in-2` must not reveal whether the email exists.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sign in
|
|
3
|
+
status: draft
|
|
4
|
+
version: 1
|
|
5
|
+
summary: A boring front door — the person is signed in, or told plainly why not.
|
|
6
|
+
tags: [auth, form]
|
|
7
|
+
---
|
|
8
|
+
# Sign in
|
|
9
|
+
|
|
10
|
+
Why does this room exist?
|
|
11
|
+
|
|
12
|
+
- **For:** someone who has an account and needs to get in, once, on a device they may not own
|
|
13
|
+
- **Because:** we want a front door that is boring, quick and impossible to get wrong
|
|
14
|
+
- **Matters most:** the person is signed in, or told plainly why not
|
|
15
|
+
- **Not doing:** sign-up, password reset, "remember me", social login
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# sign-in — spec
|
|
2
|
+
|
|
3
|
+
Only what the artifact cannot show.
|
|
4
|
+
|
|
5
|
+
- **Where it is used:** desktop and tablet browsers; keyboard and touch; works offline
|
|
6
|
+
(nothing on this screen fetches until submit)
|
|
7
|
+
- **States:**
|
|
8
|
+
- default — `index.html`
|
|
9
|
+
- rejected credentials — `index-error.html` (same layout, one alert above the button)
|
|
10
|
+
- **Constraints:**
|
|
11
|
+
- one purpose per screen: sign in, nothing else
|
|
12
|
+
- the submit button is reachable without scrolling at 1024×768
|
|
13
|
+
- errors are stated in one sentence, without blame
|
|
14
|
+
- **Uses:** `components/field`, `components/button-primary`, tokens `--surface`, `--primary`
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- sign-in — example room artifact. Self-contained: no CDN, no framework, no build. -->
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>sign in</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #faf9f7; --surface: #ffffff; --text: #1c1917; --muted: #78716c;
|
|
11
|
+
--line: #e7e5e4; --primary: #1c1917; --primary-text: #fafaf9;
|
|
12
|
+
--danger-bg: #fef2f2; --danger-line: #fecaca; --danger-text: #991b1b;
|
|
13
|
+
--radius: 14px;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
html, body { height: 100%; }
|
|
17
|
+
body {
|
|
18
|
+
margin: 0; display: grid; place-items: center; padding: 32px 20px; background: var(--bg); color: var(--text);
|
|
19
|
+
font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Noto Sans JP", system-ui, sans-serif; line-height: 1.7;
|
|
20
|
+
}
|
|
21
|
+
.card {
|
|
22
|
+
width: min(440px, 100%); background: var(--surface); border: 1px solid var(--line);
|
|
23
|
+
border-radius: var(--radius); padding: 32px; display: flex; flex-direction: column; gap: 20px;
|
|
24
|
+
}
|
|
25
|
+
.brand { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
|
|
26
|
+
h1 { font-size: 22px; margin: 0; }
|
|
27
|
+
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
28
|
+
label { font-size: 13px; color: var(--muted); }
|
|
29
|
+
input {
|
|
30
|
+
font: inherit; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--text);
|
|
31
|
+
}
|
|
32
|
+
input:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
|
|
33
|
+
button {
|
|
34
|
+
font: inherit; padding: 14px 20px; border-radius: 10px; border: 1px solid var(--primary);
|
|
35
|
+
background: var(--primary); color: var(--primary-text); cursor: pointer; min-height: 48px;
|
|
36
|
+
}
|
|
37
|
+
.hint { font-size: 13px; color: var(--muted); }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<main class="card">
|
|
42
|
+
<div class="brand">modelroom · example</div>
|
|
43
|
+
<h1>Sign in</h1>
|
|
44
|
+
<div class="field">
|
|
45
|
+
<label for="email">Email</label>
|
|
46
|
+
<input id="email" type="email" autocomplete="username" placeholder="you@example.com">
|
|
47
|
+
</div>
|
|
48
|
+
<div class="field">
|
|
49
|
+
<label for="password">Password</label>
|
|
50
|
+
<input id="password" type="password" autocomplete="current-password" placeholder="••••••••">
|
|
51
|
+
</div>
|
|
52
|
+
<button>Sign in</button>
|
|
53
|
+
<p class="hint">This is a model room — nothing here is connected to anything.</p>
|
|
54
|
+
</main>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelroom",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "A local workspace to design UI screens with any coding agent — one room per screen: BRIEF, SPEC, BEHAVIOUR and the artifact.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-agnostic",
|
|
7
|
+
"ai-agents",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"codex",
|
|
10
|
+
"coding-agents",
|
|
11
|
+
"design",
|
|
12
|
+
"design-system",
|
|
13
|
+
"design-workflow",
|
|
14
|
+
"live-preview",
|
|
15
|
+
"opencode",
|
|
16
|
+
"spec-driven",
|
|
17
|
+
"ui-design"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/modelroom/modelroom#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/modelroom/modelroom/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/modelroom/modelroom.git"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"modelroom": "dist/bin/modelroom.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"ui",
|
|
34
|
+
"templates",
|
|
35
|
+
"examples",
|
|
36
|
+
"docs",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"type": "module",
|
|
42
|
+
"types": "./dist/lib/server.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/lib/server.d.ts",
|
|
46
|
+
"default": "./dist/lib/server.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "node --watch bin/modelroom.ts",
|
|
51
|
+
"modelroom": "node bin/modelroom.ts",
|
|
52
|
+
"start": "node bin/modelroom.ts",
|
|
53
|
+
"build": "tsc -p tsconfig.build.json",
|
|
54
|
+
"build:css": "tailwindcss -i src/modelroom.css -o ui/modelroom.css --minify",
|
|
55
|
+
"dev:css": "tailwindcss -i src/modelroom.css -o ui/modelroom.css --watch",
|
|
56
|
+
"lint": "oxlint",
|
|
57
|
+
"lint:types": "oxlint --type-aware",
|
|
58
|
+
"format": "oxfmt",
|
|
59
|
+
"format:check": "oxfmt --check",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run --coverage",
|
|
62
|
+
"test:watch": "vitest",
|
|
63
|
+
"verify": "npm run lint && npm run lint:types && npm run format:check && npm run typecheck && npm test && npm run build",
|
|
64
|
+
"prepublishOnly": "npm run build"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
68
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
69
|
+
"@types/node": "^26.6.5",
|
|
70
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
71
|
+
"daisyui": "^5.7.50",
|
|
72
|
+
"lefthook": "^2.2.2",
|
|
73
|
+
"oxfmt": "^0.73.0",
|
|
74
|
+
"oxlint": "^1.88.0",
|
|
75
|
+
"oxlint-tsgolint": "^7.0.2004",
|
|
76
|
+
"playwright": "^1.64.1",
|
|
77
|
+
"publint": "^0.3.25",
|
|
78
|
+
"tailwindcss": "^4.3.3",
|
|
79
|
+
"typescript": "^7.0.2",
|
|
80
|
+
"vitest": "^5.0.5"
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=22.18"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# AGENTS.md — design layer
|
|
2
|
+
|
|
3
|
+
This repository uses [modelroom](https://github.com/modelroom/modelroom): one room per screen,
|
|
4
|
+
with the reason, the invisible spec, the promises and the artifact kept apart.
|
|
5
|
+
|
|
6
|
+
## Where you may write
|
|
7
|
+
|
|
8
|
+
- `rooms/<room>/BRIEF.md` — why this room exists
|
|
9
|
+
- `rooms/<room>/SPEC.md` — what the artifact cannot show (devices, states, constraints, what it may use)
|
|
10
|
+
- `rooms/<room>/BEHAVIOUR.md` — what it promises (operations and results)
|
|
11
|
+
- `rooms/<room>/index.html` — the artifact itself
|
|
12
|
+
|
|
13
|
+
Nothing else. Do not touch the application code from here; the design layer never reads it.
|
|
14
|
+
|
|
15
|
+
## How to work
|
|
16
|
+
|
|
17
|
+
1. Read the room's `BRIEF.md` first, then `SPEC.md` and `BEHAVIOUR.md`.
|
|
18
|
+
2. Change the artifact to match them. If they are wrong, say so — do not quietly reword them.
|
|
19
|
+
3. Keep the artifact self-contained: no CDN, no web fonts, no framework, no build step.
|
|
20
|
+
It is going to be handed to implementation, so it must stand on its own.
|
|
21
|
+
4. Never restate in `SPEC.md` what can be seen in the artifact. Never copy a promise into
|
|
22
|
+
two files; reference it by id (`@verb-1`) instead.
|
|
23
|
+
5. Promises that need the real system (records, timestamps, permissions) do not belong in
|
|
24
|
+
this layer.
|
|
25
|
+
|
|
26
|
+
## Conventions
|
|
27
|
+
|
|
28
|
+
- Add `Status: draft|review|adopted` to `BRIEF.md` when you want the dashboard to show a state.
|
|
29
|
+
Changing it to `adopted` is a human decision, not yours.
|
|
30
|
+
|
|
31
|
+
See `docs/02-convention.md` in the modelroom package for the reasoning.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": ".",
|
|
3
|
+
"rooms": "rooms",
|
|
4
|
+
"artifact": "index.html",
|
|
5
|
+
"docs": ["BRIEF.md", "SPEC.md", "BEHAVIOUR.md"],
|
|
6
|
+
"port": 4321,
|
|
7
|
+
"agent": {
|
|
8
|
+
"adapter": "opencode",
|
|
9
|
+
"command": "opencode",
|
|
10
|
+
"agent": "build",
|
|
11
|
+
"model": null,
|
|
12
|
+
"standalone": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# {{room}} — behaviour
|
|
2
|
+
|
|
3
|
+
What this room promises: operations and their results. Keep the wording in the language of
|
|
4
|
+
the work, not of the code.
|
|
5
|
+
|
|
6
|
+
```gherkin
|
|
7
|
+
Feature: {{room}}
|
|
8
|
+
|
|
9
|
+
@first-1
|
|
10
|
+
Scenario: a first promise
|
|
11
|
+
Given some starting point
|
|
12
|
+
When the person does something
|
|
13
|
+
Then something observable happens
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Notes
|
|
17
|
+
- One id per promise (`@<verb>-<n>`), and one promise per scenario.
|
|
18
|
+
- Promises that need the real system (records, timestamps, permissions) belong to the
|
|
19
|
+
application, not to this file.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: {{room}}
|
|
3
|
+
status: draft
|
|
4
|
+
version: 1
|
|
5
|
+
summary: One sentence the next person can decide from.
|
|
6
|
+
tags: []
|
|
7
|
+
---
|
|
8
|
+
# {{room}}
|
|
9
|
+
|
|
10
|
+
Why does this room exist? Who is it for, and what has to be true for it to be worth
|
|
11
|
+
building? Keep the priorities and the things you are deliberately not doing.
|
|
12
|
+
|
|
13
|
+
- **For:**
|
|
14
|
+
- **Because:**
|
|
15
|
+
- **Matters most:**
|
|
16
|
+
- **Not doing:**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# {{room}} — spec
|
|
2
|
+
|
|
3
|
+
Only what the artifact cannot show. If it can be seen in a screenshot, it belongs to the
|
|
4
|
+
artifact, not here.
|
|
5
|
+
|
|
6
|
+
- **Where it is used:** (device, input, environment)
|
|
7
|
+
- **States:** (e.g. default, empty, error — and the file each one lives in)
|
|
8
|
+
- **Constraints:** (what must stay true regardless of the layout)
|
|
9
|
+
- **Uses:** (shared components and tokens this room is allowed to use)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- {{room}} — artifact (the thing you hand to implementation)
|
|
3
|
+
Self-contained on purpose: no CDN, no framework, no build. -->
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>{{room}}</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #faf9f7;
|
|
12
|
+
--surface: #ffffff;
|
|
13
|
+
--text: #1c1917;
|
|
14
|
+
--muted: #78716c;
|
|
15
|
+
--line: #e7e5e4;
|
|
16
|
+
--primary: #1c1917;
|
|
17
|
+
--primary-text: #fafaf9;
|
|
18
|
+
--radius: 16px;
|
|
19
|
+
--space: 24px;
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
html, body { height: 100%; }
|
|
23
|
+
body {
|
|
24
|
+
margin: 0; display: grid; place-items: center; padding: 40px 20px; background: var(--bg); color: var(--text);
|
|
25
|
+
font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Noto Sans JP", system-ui, sans-serif;
|
|
26
|
+
line-height: 1.7;
|
|
27
|
+
}
|
|
28
|
+
.card {
|
|
29
|
+
width: min(520px, 100%); background: var(--surface); border: 1px solid var(--line);
|
|
30
|
+
border-radius: var(--radius); padding: var(--space); display: flex; flex-direction: column; gap: var(--space);
|
|
31
|
+
}
|
|
32
|
+
h1 { font-size: 22px; margin: 0; letter-spacing: .01em; }
|
|
33
|
+
p { margin: 0; color: var(--muted); font-size: 15px; }
|
|
34
|
+
.actions { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
35
|
+
button {
|
|
36
|
+
font: inherit; padding: 14px 22px; border-radius: var(--radius); border: 1px solid var(--line);
|
|
37
|
+
background: var(--surface); color: var(--text); cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
button.primary { background: var(--primary); color: var(--primary-text); border-color: var(--primary); }
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<main class="card">
|
|
44
|
+
<h1>{{room}}</h1>
|
|
45
|
+
<p>This is the artifact for the room. Replace it from the chat, or write it by hand.
|
|
46
|
+
The spec and the promises live next to it in SPEC.md and BEHAVIOUR.md.</p>
|
|
47
|
+
<div class="actions">
|
|
48
|
+
<button class="primary">Primary action</button>
|
|
49
|
+
<button>Secondary</button>
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|