mockpit 2.0.0 → 3.0.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.
Files changed (73) hide show
  1. package/README.md +60 -212
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +407 -187
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +80 -24
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +289 -52
  10. package/dist/server/apiViews.js +189 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +2845 -832
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +638 -0
  18. package/dist/server/codeWorker.js +1 -1
  19. package/dist/server/decisionList.d.ts +45 -0
  20. package/dist/server/decisionList.js +302 -0
  21. package/dist/server/decisions.d.ts +4 -4
  22. package/dist/server/decisions.js +9 -9
  23. package/dist/server/designGuide.d.ts +5 -5
  24. package/dist/server/designGuide.js +128 -67
  25. package/dist/server/embeds.d.ts +3 -1
  26. package/dist/server/embeds.js +31 -6
  27. package/dist/server/eventLog.d.ts +2 -2
  28. package/dist/server/eventLog.js +11 -11
  29. package/dist/server/events.d.ts +9 -6
  30. package/dist/server/feedbackBatch.d.ts +35 -13
  31. package/dist/server/feedbackBatch.js +111 -47
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/handoff.d.ts +4 -1
  35. package/dist/server/handoff.js +8 -2
  36. package/dist/server/icons.js +1 -1
  37. package/dist/server/index.js +39 -10
  38. package/dist/server/jump.d.ts +21 -0
  39. package/dist/server/jump.js +175 -0
  40. package/dist/server/knobs.d.ts +3 -3
  41. package/dist/server/knobs.js +6 -6
  42. package/dist/server/mcpHttp.d.ts +5 -1
  43. package/dist/server/mcpHttp.js +16 -11
  44. package/dist/server/mcpSpec.js +118 -46
  45. package/dist/server/parts.js +9 -1
  46. package/dist/server/run.d.ts +1 -1
  47. package/dist/server/run.js +25 -16
  48. package/dist/server/runApi.d.ts +3 -3
  49. package/dist/server/runApi.js +25 -19
  50. package/dist/server/sqlStore.d.ts +112 -96
  51. package/dist/server/sqlStore.js +906 -172
  52. package/dist/server/standings.d.ts +25 -0
  53. package/dist/server/standings.js +113 -0
  54. package/dist/server/surfacePage.d.ts +6 -9
  55. package/dist/server/surfacePage.js +361 -86
  56. package/dist/server/types.d.ts +226 -72
  57. package/dist/server/types.js +73 -78
  58. package/extensions/mockpit.js +89 -72
  59. package/guide/AGENT_SETUP.md +13 -1
  60. package/guide/packs/design-craft.md +2 -3
  61. package/guide/topics/asks.md +106 -62
  62. package/guide/topics/feedback.md +126 -119
  63. package/guide/topics/html.md +17 -24
  64. package/guide/topics/http.md +41 -41
  65. package/guide/topics/knobs.md +7 -10
  66. package/guide/topics/scripts.md +22 -33
  67. package/guide/topics/surfaces.md +39 -48
  68. package/opencode/mockpit.js +120 -0
  69. package/opencode/package.json +8 -0
  70. package/package.json +4 -3
  71. package/skills/launch-video/SKILL.md +11 -11
  72. package/skills/mockpit/SKILL.md +26 -22
  73. package/viewer/dist/index.html +9 -9
package/bin/mockpit.js CHANGED
@@ -13,6 +13,7 @@ import { homedir, tmpdir, userInfo } from "node:os";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { parseArgs } from "node:util";
16
+ import { findBrowser, queuePoster } from "./poster.js";
16
17
  import { serveUrl } from "./serveUrl.js";
17
18
 
18
19
  const BASE = (process.env.MOCKPIT_URL ?? "http://localhost:8228").replace(/\/$/, "");
@@ -28,9 +29,9 @@ const HEADERS = {
28
29
 
29
30
  const HELP = `mockpit — a live visual surface for terminal coding agents
30
31
 
31
- vocabulary: project › mock › state › variant › version. A project is a repo; a
32
- mock is a sketch, page or component, by slug; a state is one thing the user sees in it ("Lab
33
- open"); a variant is a parallel design of a state; a version is its history.
32
+ vocabulary: project › design › state › variant › version. A project is a repo; a
33
+ design is an idea, screen or component, by slug; a state is one thing the user sees in it ("Lab
34
+ open"); a variant is a parallel take on a state; a version is its history.
34
35
  Declare the parts you want feedback on with data-part="name" in the html.
35
36
 
36
37
  the loop (never blocks on the user):
@@ -41,22 +42,22 @@ the loop (never blocks on the user):
41
42
  on; don't re-ask it. Visual options carry a variant or knob set. One
42
43
  render plus a control: --knobs.
43
44
  4. Tell the user in one line where to look, then end your turn. The question
44
- itself lives in the mock, never in chat. The user answers in the
45
+ itself lives in the design, never in chat. The user answers in the
45
46
  browser at their own pace. Never poll.
46
47
  5. When the user says they answered (or a mockpit watch line wakes you), run
47
48
  mockpit feedback. Every write also returns feedback; read it.
48
49
  6. mockpit publish the revision; mockpit export once a variant is in use.
49
50
  In Claude Code, arm mockpit watch under Monitor after asking, then end the
50
- turn; elsewhere the user's next message is the wake-up.
51
+ turn, unless you have the mockpit plugin, which wakes you itself; elsewhere
52
+ the user's next message is the wake-up.
51
53
 
52
54
  verbs:
53
55
  mockpit init [--project name] [--kit <id>] [--kit-url <url> --kit-doc <file|text>]
54
56
  detect the repo's design system, store
55
57
  palette/kit/icon sets, write .mockpit/starter.html
56
- mockpit publish --mock <slug> --html <file> [options]
58
+ mockpit publish --design <slug> --html <file> [options]
57
59
  create or version one variant of one state
58
- --state <label> state, in the user's words (omit for a single-state mock)
59
- --situation <s> another render of that state ("Couldn't save", "Loading")
60
+ --state <label> state, in the user's words (omit for a single-state design)
60
61
  --variant <name> variant label (default "default")
61
62
  --html <file|-> the html body (or a positional file)
62
63
  --surfaces <json|file> instead: the full ordered surface list; {"id"}
@@ -65,14 +66,18 @@ verbs:
65
66
  the latest version (repeatable; name#key targets one instance)
66
67
  --fills <part#fill=file|-> instead: replace or add one fill of a part in
67
68
  the latest version (repeatable)
68
- --title <t> mock title
69
- --kind <k> page|component, only for an app's screens (default sketch)
69
+ --title <t> design title
70
+ --kind <k> screen|component, only for an app's screens (default idea);
71
+ with no html it plans one, drawn by a later publish
70
72
  --area <name> where in the product it belongs ("Shell"); Home groups by it
71
73
  --source <path> the code path that implements it
72
74
  --knobs <json|file> knobs in tunekit usePane shape, keyed by path
73
75
  --from <N> branch from version N
76
+ --amend replace the latest version in place: a fix before the
77
+ user has acted on it (one request, one version)
78
+ --withdraw take the latest version back, under the same rule
74
79
  --prompt <text> what prompted this version
75
- --note <text> one line on what this variant is, shown beside it
80
+ --description <text> one line on what this variant is, shown beside it
76
81
  --widths <list> device widths to show it at, e.g. 390,1280
77
82
  --flow <name> [--step <N>] make this state step N of a flow (0 takes
78
83
  it out); with no html it only places the state
@@ -84,33 +89,38 @@ verbs:
84
89
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
85
90
  add a surface of that kind after the html (repeatable;
86
91
  --data is a JSON tree)
87
- mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
92
+ mockpit ask --design <slug> "<question>" --option <label[=variant]> ...
88
93
  ask the user; bind options to variants
89
94
  --option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
90
95
  "=variant@3" to its version 3, "=knob:path=value" sets one knob
91
96
  --recommend <l> the option you would pick, by label; a quiet mark on it
92
- --scope <s> mock|state|part (default mock)
97
+ --scope <s> design|state|part (default design)
93
98
  --state <label> the state a state-scoped question is about
99
+ --states <label> a design-wide question: a state its answers can be seen
100
+ on (repeatable, best first)
94
101
  --part <name> the part a part-scoped question is about
95
102
  --multi allow several answers
96
103
  --id <id> stable id; reusing one replaces that question
97
104
  --asks <json|file> several questions at once, full shape
98
- mockpit read [slug] [--body] [--history]
99
- no slug: every mock plus what the user
100
- is doing now; a slug: that mock
105
+ mockpit read [slug] [--body] [--history] [--decisions]
106
+ what the user already decided, then
107
+ no slug: every design plus what the user
108
+ is doing now; a slug: that design
101
109
  mockpit feedback what the user sent since you last heard,
102
110
  plus pending; returns at once
103
- mockpit say "<text>" --mock <slug> [--state s] [--variant v]
104
- a plain-text message in a mock's thread
105
- mockpit edit --mock <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
106
- change a mock without a new version ("" clears)
111
+ mockpit say "<text>" --design <slug> [--state s] [--variant v]
112
+ a plain-text message in a design's thread
113
+ mockpit move --design <slug> --variant <v>... (--to <design> [--to-state s] | --archive | --restore)
114
+ move variants elsewhere, archive or restore them
115
+ mockpit edit --design <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
116
+ change a design without a new version ("" clears)
107
117
  mockpit edit --project <name> --rename <new>
108
118
  rename a project
109
119
  mockpit edit --project <name> --area-order "Shell,Food"
110
120
  the order Home lists areas in
111
- mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
121
+ mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
112
122
  write the html in use + history per state
113
- to .mockpit/accepted/<mock>/<state>/
123
+ to .mockpit/accepted/<design>/<state>/
114
124
  mockpit upload <file> [--kind image|file]
115
125
  upload an asset, print its id and URL
116
126
  mockpit guide [--topic <id>] the project-aware brief, or one reference topic
@@ -124,7 +134,7 @@ verbs:
124
134
  other commands:
125
135
  mockpit serve [--port N] [--host H] [--open]
126
136
  start the server (API + viewer)
127
- mockpit demo seed the Writer mock to explore the viewer
137
+ mockpit demo seed the Writer design to explore the viewer
128
138
  mockpit icons [add|remove <set>...] list the icon sets html surfaces can name
129
139
  (icon="prefix:name"); add or remove an
130
140
  Iconify set for this project
@@ -147,6 +157,7 @@ environment:
147
157
  MOCKPIT_PROJECT project name; overrides the git-remote/directory default
148
158
  MOCKPIT_URL server base URL (default http://localhost:8228)
149
159
  MOCKPIT_TOKEN bearer token for a deployed instance
160
+ CHROME_PATH the browser for automatic posters; pointing nowhere turns them off
150
161
  MOCKPIT_HOST address serve binds to (default: every interface)
151
162
  MOCKPIT_LOG serve's event log file, or "off" (default ~/.mockpit/events.jsonl)
152
163
  MOCKPIT_SESSION fixed session id (overrides auto-detection)
@@ -183,14 +194,21 @@ mockpit icons remove <set> [<set>...]
183
194
  html surfaces write <i icon="lucide:check"></i>; the server inlines the svg.
184
195
  lucide and mage are bundled. add finds @iconify-json/<set> in node_modules,
185
196
  the global npm root or bun's cache, else fetches it from jsDelivr.`,
186
- publish: `mockpit publish --mock <slug> --html <file> [options]
187
- mockpit publish --mock <slug> --surfaces <json|file> [options]
188
- mockpit publish --mock <slug> --parts <name=file|-> ... [options]
189
- mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
190
- Creates the mock, state or variant, or the next version of an existing one.
191
- --state <label> state in the user's words (omit for a single-state mock)
192
- --situation <s> another render of that state ("Couldn't save", "Loading"),
193
- after the state itself is published
197
+ publish: `mockpit publish --design <slug> --html <file> [options]
198
+ mockpit publish --design <slug> --surfaces <json|file> [options]
199
+ mockpit publish --design <slug> --parts <name=file|-> ... [options]
200
+ mockpit publish --design <slug> --fills <part#fill=file|-> ... [options]
201
+ mockpit publish --design <slug> [--state s] [--variant v] --withdraw
202
+ Creates the design, state or variant, or the next version of an existing one.
203
+ One request gets one version: fixes to it before the user acts use --amend.
204
+ --amend replace the variant's latest version in place (same number)
205
+ instead of adding one. Only from the session that published
206
+ it, and only while the user hasn't acted on it (no Send,
207
+ comment, draft answer, not in use)
208
+ --withdraw take the variant's latest version back under the same
209
+ rule: the version before it is current again, and a
210
+ variant with no other version goes
211
+ --state <label> state in the user's words (omit for a single-state design)
194
212
  --variant <name> variant label (default "default")
195
213
  --surfaces <json|file> the full ordered list: {"id"} alone keeps a surface,
196
214
  a missing id removes it, order is the list order
@@ -199,10 +217,10 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
199
217
  --fills <part#fill=file|-> replace the element carrying data-part="part"
200
218
  data-fill="fill" in the latest version, or add it after
201
219
  that part's other fills; the marks are added if missing
202
- --title <t> mock title
203
- --kind <k> page|component|sketch: page for a screen of the product,
204
- component for a piece pages embed (embedding makes one);
205
- omit for sketches, diagrams, reviews (default sketch)
220
+ --title <t> design title
221
+ --kind <k> screen|component|idea: screen for a screen of the product,
222
+ component for a reusable design screens embed (embedding makes one);
223
+ omit for ideas, diagrams, reviews (default idea)
206
224
  --area <name> where in the product it belongs, in the project's words
207
225
  ("Shell", "Food"); Home groups each tab by it. "" clears
208
226
  --source <path> the code path that implements it (apps/web/src/shell). "" clears
@@ -212,17 +230,18 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
212
230
  its latest); alone it copies v, with --parts/--fills it
213
231
  edits the copy
214
232
  --prompt <text> what prompted this version
215
- --note <text> one line on what this variant is, shown beside it (never
233
+ --description <text> one line on what this variant is, shown beside it (never
216
234
  inside the design)
217
- --widths <list> device widths the mock is judged at, side by side, e.g.
235
+ --widths <list> device widths the design is judged at, side by side, e.g.
218
236
  390,1280 (the ones the user ships to)
219
237
  --flow <name> a flow this state is a step of: the states the user moves
220
- between, in order, across mocks; without html it only
238
+ between, in order, across designs; without html it only
221
239
  places the existing state
222
240
  --step <N> its 1-based place in the flow (moves it if it is there);
223
241
  0 takes it out; omitted, it joins at the end
224
242
  --project <name> project (default: git remote, else directory name)
225
- --poster <file> a still of the page (png, jpg) that Studio and Home show
243
+ --poster <file> a still of the page (png, jpg) that Studio and Home show; without
244
+ one, a local Chrome or Chromium takes it just after the publish
226
245
  instead of running it; with --parts or --fills it is the
227
246
  still of the spliced page. A version that changes the page
228
247
  without one drops the old still
@@ -230,37 +249,56 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
230
249
  --json / --quiet
231
250
  Prints the parts found per state, parts that vanished or were renamed,
232
251
  nudges, a ready-to-run "mockpit ask" when variants have no question, and feedback.`,
233
- ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
234
- [--state s] [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
235
- mockpit ask --mock <slug> --asks <json|file>
252
+ ask: `mockpit ask --design <slug> "<question>" --option <label[=variant]> ... [--scope design|state|part]
253
+ [--state s] [--states s]... [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
254
+ mockpit ask --design <slug> --asks <json|file>
236
255
  Ask the user. A choice is several variants plus one question that binds them.
237
256
  Then tell the user where to look and end your turn.`,
238
- read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
239
- No slug: one line per mock (states, variants, open questions), one per flow, plus pending, what
240
- the user is doing now. A slug: the mock's states, variants, questions with
241
- answers, parts, knobs and settled values, plus its pending, as JSON.
257
+ read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree] [--decisions]
258
+ Read this first. Both forms carry the project's decision list, what the user
259
+ already settled, from their replies: treat each line as settled unless they say
260
+ otherwise. No slug: one line per design (states, variants, open questions), one per
261
+ flow, plus pending, what the user is doing now, then the decisions. A slug: the
262
+ design's states, variants, questions with answers, parts, knobs and settled values,
263
+ plus its pending and decisionList (its own lines marked here), as JSON.
242
264
  --body includes surfaces, --history version rows. --tree (no slug): the
243
- project as a tree, each mock's variants with status and forkedFrom, and the
244
- mocks it embeds and is embedded in, as JSON. Never consumes feedback.`,
265
+ project as a tree, each design's variants with status and forkedFrom, and the
266
+ designs it embeds and is embedded in, as JSON. --decisions: the whole list, when
267
+ a read says lines were left out. Never consumes feedback.`,
245
268
  feedback: `mockpit feedback [--project <name>] [--session <id>]
246
- Returns at once with {feedback, pending} as JSON: one batch per mock the user
247
- sent since you last heard ({mock, reply: {asks, borrowed, tuned, comments,
248
- verdict}, messages, inUse, archived}, or {mock, closed: true, dismissed} when the
249
- user closed it unanswered), delivered once across feedback, watch and
250
- every write; and pending ({mock, viewerOpen, draft}) so you can tell the
269
+ Returns at once with {feedback, pending} as JSON: one batch per design the user
270
+ sent since you last heard ({design, reply: {asks, borrowed, tuned, comments,
271
+ verdict}, messages, inUse, archived}), delivered once across feedback, watch and
272
+ every write; and pending ({design, viewerOpen, draft}) so you can tell the
251
273
  user "take your time" or "2 of 3 answered". Run it when the user says they
252
274
  answered.`,
253
- say: `mockpit say "<text>" --mock <slug> [--state s] [--variant v]
254
- A plain-text message in the mock's thread, as your session's agent.`,
255
- edit: `mockpit edit --mock <slug> [--slug <new>] [--title <t>] [--kind page|component|sketch]
275
+ say: `mockpit say "<text>" --design <slug> [--state s] [--variant v]
276
+ A plain-text message in the design's thread, as your session's agent.`,
277
+ move: `mockpit move --design <slug> [--state s] --variant <v> [--variant <v>...]
278
+ --to <design> [--to-state <s>] | --to-state <s> | --archive | --restore
279
+ mockpit move --design <slug> [--state s] [--variant <v>...] --hide-older | --delete-older
280
+ Tidy up. --to or --to-state moves them to another design or tab (a new state is
281
+ created) and takes them out of here; --archive takes them out of sight; --restore
282
+ brings them back. Their group goes along; one in use stays and is reported. The
283
+ thread says what happened, so the user can undo it.
284
+ Only when the user asks: --hide-older hides all but each variant's newest visible
285
+ version (no --variant: every variant), and --delete-older deletes them for good,
286
+ skipping and listing any that a reply, comment, question, embed, copy or fork still
287
+ points at, or that is in use. It can't be undone: confirm first.`,
288
+ edit: `mockpit edit --design <slug> [--slug <new>] [--title <t>] [--kind screen|component|idea]
289
+ [--first-state <name>] [--notes <text>|""]
256
290
  [--area <name>|""] [--source <path>|""] [--project <name>]
257
291
  mockpit edit --project <name> --rename <new name>
258
292
  mockpit edit --project <name> --area-order "Shell,Food,Chat"
259
- Change a mock without publishing a new version; "" clears area or source. A new
293
+ Change a design without publishing a new version; "" clears area or source. A new
260
294
  slug rewrites every embed, data-goto and flow step that names the old one, in
261
- every version. A mock still embedded somewhere stays a component, not a sketch.
262
- --rename moves the project's mocks, sessions, flows and design settings to the
263
- new name (refused if that project has mocks); pass --project <new> or set
295
+ every version. A design still embedded somewhere stays a component, not an idea.
296
+ --notes drafts the design's short plain-text spec, which read returns; the user's own
297
+ edit of it stands, so the draft is then not applied and the result says so.
298
+ --first-state names a design's one unnamed state, and every variant it holds goes
299
+ there, so the design can then have more states.
300
+ --rename moves the project's designs, sessions, flows and design settings to the
301
+ new name (refused if that project has designs); pass --project <new> or set
264
302
  MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
265
303
  --area-order lists the project's areas in the order Home shows them; areas it
266
304
  leaves out follow alphabetically, "" goes back to alphabetical.`,
@@ -270,12 +308,15 @@ mockpit edit --project <name> --area-order "Shell,Food,Chat"
270
308
  host call in order, the script's prints, its value or error, and any user
271
309
  feedback the run received. --json prints the whole result. Exits 2 when the
272
310
  script failed.`,
273
- watch: `mockpit watch [--session <id>] [--after <seq>]
274
- Stream user feedback, one line per Send, for a background monitor (in
275
- Claude Code, arm it under Monitor after asking, then end the turn).
276
- Delivery is shared with feedback and every write: each Send arrives once.`,
277
- export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
278
- Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
311
+ watch: `mockpit watch [--session <id>] [--after <seq>] [--once] [--exit-code <n>]
312
+ Print one line per Send, for a background monitor (in Claude Code, arm it
313
+ under Monitor after asking, then end the turn; with the mockpit plugin, don't,
314
+ since it wakes you itself). A line says a reply landed;
315
+ feedback or the next write delivers it, once.
316
+ --once stops after the first Send, for a harness whose wake is a process
317
+ exiting; --exit-code sets the code it stops with (default 0).`,
318
+ export: `mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
319
+ Write index.html + history.json per state to .mockpit/accepted/<design>/<state>/;
279
320
  with fills, index.html is the chosen state, plus all-fills.html and decisions.json.`,
280
321
  upload: `mockpit upload <file> [--kind image|file] [--session <id>]
281
322
  Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
@@ -314,7 +355,7 @@ function die(what, fix) {
314
355
  }
315
356
 
316
357
  // `report` decides how a failure reads: plumbing verbs keep the one-line
317
- // `mockpit: …` (exit 1); the mock verbs pass `die` so the error / fix / exit 2
358
+ // `mockpit: …` (exit 1); the design verbs pass `die` so the error / fix / exit 2
318
359
  // format is what an agent parses.
319
360
  async function api(path, init = {}, { report, fix } = {}) {
320
361
  const bail = (what, hint) => (report === "die" ? die(what, hint ?? fix) : fail(what));
@@ -343,7 +384,11 @@ async function api(path, init = {}, { report, fix } = {}) {
343
384
  (body.error ?? `${res.status} ${res.statusText}`) +
344
385
  (Array.isArray(choices) ? ` (${choices.join(", ")})` : "");
345
386
  const extra = issue?.message ?? issue?.code;
346
- bail(extra && !main.includes(extra) ? `${main}: ${extra}` : main);
387
+ // The server's own fix, when it names one, beats the caller's generic hint.
388
+ bail(
389
+ extra && !main.includes(extra) ? `${main}: ${extra}` : main,
390
+ typeof body.fix === "string" ? body.fix : undefined,
391
+ );
347
392
  }
348
393
  return body;
349
394
  }
@@ -437,8 +482,11 @@ function agentName(flags) {
437
482
  // doesn't keep; Claude Code's session id does, so the server can hand back the
438
483
  // same mockpit session (and its feedback cursor).
439
484
  function harnessKey() {
440
- const id = process.env.CLAUDE_CODE_SESSION_ID;
441
- return id ? `claude-code:${id}` : undefined;
485
+ if (process.env.MOCKPIT_SESSION_KEY) return process.env.MOCKPIT_SESSION_KEY;
486
+ if (process.env.CLAUDE_CODE_SESSION_ID)
487
+ return `claude-code:${process.env.CLAUDE_CODE_SESSION_ID}`;
488
+ if (process.env.CODEX_THREAD_ID) return `codex:${process.env.CODEX_THREAD_ID}`;
489
+ return undefined;
442
490
  }
443
491
 
444
492
  async function resolveSession(flags, { create = false } = {}) {
@@ -680,17 +728,17 @@ async function installIconSets(project, prefixes) {
680
728
  errors.push({ prefix, message: err instanceof Error ? err.message : String(err) });
681
729
  }
682
730
  }
683
- if (added.length === 0) return { added: [], errors, design: null };
684
- const stored = (await api(`${projectPath(project)}/design`)) ?? {};
731
+ if (added.length === 0) return { added: [], errors, designSystem: null };
732
+ const stored = (await api(`${projectPath(project)}/design-system`)) ?? {};
685
733
  const keep = (stored.iconSets ?? []).filter((s) => !added.some((a) => a.prefix === s.prefix));
686
- const design = await api(`${projectPath(project)}/design`, {
734
+ const designSystem = await api(`${projectPath(project)}/design-system`, {
687
735
  method: "PUT",
688
736
  body: JSON.stringify({ ...stored, iconSets: [...keep, ...added] }),
689
737
  });
690
738
  return {
691
- added: design.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
739
+ added: designSystem.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
692
740
  errors,
693
- design,
741
+ designSystem,
694
742
  };
695
743
  }
696
744
 
@@ -834,7 +882,7 @@ function slugify(text) {
834
882
 
835
883
  const projectPath = (project) => `/api/projects/${encodeURIComponent(project)}`;
836
884
 
837
- const mockPath = (slug) => `/api/mocks/${encodeURIComponent(slug)}`;
885
+ const designPath = (slug) => `/api/designs/${encodeURIComponent(slug)}`;
838
886
 
839
887
  const query = (params) => {
840
888
  const q = new URLSearchParams();
@@ -845,18 +893,18 @@ const query = (params) => {
845
893
  return s ? `?${s}` : "";
846
894
  };
847
895
 
848
- // The flags every mock verb shares: which mock, which state, which variant.
849
- const MOCK_FLAGS = {
850
- mock: { type: "string" },
896
+ // The flags every design verb shares: which design, which state, which variant.
897
+ const DESIGN_FLAGS = {
898
+ design: { type: "string" },
851
899
  state: { type: "string" },
852
900
  variant: { type: "string" },
853
901
  project: { type: "string" },
854
902
  session: { type: "string" },
855
903
  };
856
904
 
857
- function requireMock(flags, example) {
858
- const slug = flags.mock ? slugify(flags.mock) : "";
859
- if (!slug) die("--mock needs a slug", example);
905
+ function requireDesign(flags, example) {
906
+ const slug = flags.design ? slugify(flags.design) : "";
907
+ if (!slug) die("--design needs a slug", example);
860
908
  return slug;
861
909
  }
862
910
 
@@ -878,7 +926,7 @@ const shq = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${String(s).replace(/'/g,
878
926
  // The server's suggested ask as the command that sends it: an agent that was
879
927
  // nudged can run it as is. --asks carries ids and scope, which flags can't.
880
928
  function suggestedAskLine(slug, ask) {
881
- return `mockpit ask --mock ${shq(slug)} --asks ${shq(JSON.stringify([ask]))}`;
929
+ return `mockpit ask --design ${shq(slug)} --asks ${shq(JSON.stringify([ask]))}`;
882
930
  }
883
931
 
884
932
  // One line per write: what was published, its URL, the parts found per state,
@@ -886,10 +934,22 @@ function suggestedAskLine(slug, ask) {
886
934
  function printPublished(result, flags) {
887
935
  if (flags.json) return out(result);
888
936
  if (flags.quiet) return;
889
- const { mock, post } = result;
937
+ const { design, post } = result;
938
+ if (result.planned) {
939
+ console.log(`${design.slug}: planned, nothing drawn yet · ${result.url}`);
940
+ if (result.flow) console.log(flowLine(result.flow));
941
+ for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
942
+ printFeedback(result.feedback);
943
+ return;
944
+ }
890
945
  const branch = post.from !== undefined && post.from !== post.version - 1;
946
+ const how = result.withdrawn
947
+ ? ` (v${result.withdrawn.version} withdrawn)`
948
+ : flags.amend
949
+ ? " (amended)"
950
+ : "";
891
951
  console.log(
892
- `${mock.slug}/${stateLabel(post.state)}${post.variant} v${post.version}${branch && post.from ? ` (from v${post.from})` : ""} · ${result.url}`,
952
+ `${design.slug}/${stateLabel(post.state)}${post.variant} v${post.version}${branch && post.from ? ` (from v${post.from})` : ""}${how} · ${result.url}`,
893
953
  );
894
954
  for (const { state, parts } of result.parts ?? []) {
895
955
  if (parts.length === 0) continue;
@@ -904,13 +964,41 @@ function printPublished(result, flags) {
904
964
  if (changes?.vanished?.length) console.log(`vanished: ${changes.vanished.join(", ")}`);
905
965
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
906
966
  if (result.suggestedAsk)
907
- console.log(`suggested: ${suggestedAskLine(mock.slug, result.suggestedAsk)}`);
967
+ console.log(`suggested: ${suggestedAskLine(design.slug, result.suggestedAsk)}`);
908
968
  for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
909
969
  printFeedback(result.feedback);
910
970
  }
911
971
 
972
+ async function withdrawVersion(flags, slug, project) {
973
+ const session = await resolveSession(flags);
974
+ if (!session) die(`no session has published ${slug} from here`, `mockpit read ${slug}`);
975
+ const result = await api(
976
+ "/api/designs",
977
+ {
978
+ method: "POST",
979
+ body: JSON.stringify({
980
+ session,
981
+ project,
982
+ design: slug,
983
+ state: flags.state,
984
+ variant: flags.variant,
985
+ withdraw: true,
986
+ }),
987
+ },
988
+ { report: "die", fix: `mockpit read ${slug} --history` },
989
+ );
990
+ if (!result.removed) return printPublished(result, flags);
991
+ if (flags.json) return out(result);
992
+ if (flags.quiet) return;
993
+ const w = result.withdrawn;
994
+ console.log(
995
+ `${slug}/${stateLabel(w.state)}${w.variant} v${w.version} withdrawn, variant removed · ${result.url}`,
996
+ );
997
+ printFeedback(result.feedback);
998
+ }
999
+
912
1000
  function flowLine(flow) {
913
- const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
1001
+ const steps = flow.steps.map((x) => (x.state === null ? x.design : `${x.design}/${x.state}`));
914
1002
  return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
915
1003
  }
916
1004
 
@@ -920,15 +1008,15 @@ function printFeedback(feedback) {
920
1008
  if (feedback?.length) console.log(`feedback: ${JSON.stringify(feedback)}`);
921
1009
  }
922
1010
 
923
- // What the user is doing right now, per mock.
1011
+ // What the user is doing right now, per design.
924
1012
  function pendingLines(pending) {
925
1013
  return (pending ?? []).flatMap((p) =>
926
1014
  p.draft
927
1015
  ? [
928
- `pending: ${p.mock} — the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comment${p.draft.comments === 1 ? "" : "s"})`,
1016
+ `pending: ${p.design} — the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comment${p.draft.comments === 1 ? "" : "s"})`,
929
1017
  ]
930
1018
  : p.viewerOpen
931
- ? [`pending: ${p.mock} — open in the viewer`]
1019
+ ? [`pending: ${p.design} — open in the viewer`]
932
1020
  : [],
933
1021
  );
934
1022
  }
@@ -938,8 +1026,7 @@ function parsePublishFlags() {
938
1026
  tokens: true,
939
1027
  allowPositionals: true,
940
1028
  options: {
941
- ...MOCK_FLAGS,
942
- situation: { type: "string" },
1029
+ ...DESIGN_FLAGS,
943
1030
  title: { type: "string" },
944
1031
  kind: { type: "string" },
945
1032
  area: { type: "string" },
@@ -953,7 +1040,7 @@ function parsePublishFlags() {
953
1040
  from: { type: "string" },
954
1041
  fork: { type: "string" },
955
1042
  prompt: { type: "string" },
956
- note: { type: "string" },
1043
+ description: { type: "string" },
957
1044
  widths: { type: "string" },
958
1045
  flow: { type: "string" },
959
1046
  step: { type: "string" },
@@ -969,6 +1056,8 @@ function parsePublishFlags() {
969
1056
  "session-title": { type: "string" },
970
1057
  agent: { type: "string" },
971
1058
  "new-session": { type: "boolean" },
1059
+ amend: { type: "boolean" },
1060
+ withdraw: { type: "boolean" },
972
1061
  },
973
1062
  });
974
1063
  }
@@ -980,8 +1069,8 @@ function readPartFlags(values, slug, flag = "parts") {
980
1069
  if (!values?.length) return undefined;
981
1070
  const example =
982
1071
  flag === "parts"
983
- ? `mockpit publish --mock ${slug} --parts body=body.html`
984
- : `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
1072
+ ? `mockpit publish --design ${slug} --parts body=body.html`
1073
+ : `mockpit publish --design ${slug} --fills entry#skeleton=skeleton.html`;
985
1074
  const parts = {};
986
1075
  let stdin = false;
987
1076
  for (const raw of values) {
@@ -1003,14 +1092,15 @@ function readPartFlags(values, slug, flag = "parts") {
1003
1092
  return parts;
1004
1093
  }
1005
1094
 
1006
- async function publishMock() {
1095
+ async function publishDesign() {
1007
1096
  const { values: flags, positionals, tokens } = parsePublishFlags();
1008
- const slug = requireMock(flags, "mockpit publish --mock writer --html writer.html");
1097
+ const slug = requireDesign(flags, "mockpit publish --design writer --html writer.html");
1009
1098
  const project = resolveProject(flags).name;
1010
1099
  const from = flags.from === undefined ? undefined : Number(flags.from);
1011
1100
  if (from !== undefined && !Number.isInteger(from)) {
1012
1101
  die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
1013
1102
  }
1103
+ if (flags.withdraw) return withdrawVersion(flags, slug, project);
1014
1104
  const step = flags.step === undefined ? undefined : Number(flags.step);
1015
1105
  if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
1016
1106
  die(
@@ -1059,16 +1149,16 @@ async function publishMock() {
1059
1149
  }
1060
1150
  surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
1061
1151
  }
1062
- if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
1063
- die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
1152
+ const declaring = flags.kind === "screen" || flags.kind === "component";
1153
+ if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined && !declaring)
1154
+ die(`no html for ${slug}`, `mockpit publish --design ${slug} --html <file>`);
1064
1155
  const poster =
1065
1156
  flags.poster === undefined ? undefined : (await uploadFile(flags.poster, { session })).id;
1066
1157
  const body = {
1067
1158
  session,
1068
1159
  project,
1069
- mock: slug,
1160
+ design: slug,
1070
1161
  state: flags.state,
1071
- situation: flags.situation,
1072
1162
  variant: flags.variant,
1073
1163
  title: flags.title,
1074
1164
  kind: flags.kind,
@@ -1078,7 +1168,7 @@ async function publishMock() {
1078
1168
  ...(from !== undefined && { from }),
1079
1169
  ...(flags.fork !== undefined && { fork: flags.fork }),
1080
1170
  prompt: flags.prompt,
1081
- note: flags.note,
1171
+ description: flags.description,
1082
1172
  ...(flags.widths !== undefined && {
1083
1173
  widths: flags.widths
1084
1174
  .split(",")
@@ -1091,13 +1181,26 @@ async function publishMock() {
1091
1181
  ...(parts && { parts }),
1092
1182
  ...(fills && { fills }),
1093
1183
  ...(poster && { poster }),
1184
+ ...(!poster && findBrowser() && { posterComing: true }),
1185
+ ...(flags.amend && { amend: true }),
1094
1186
  };
1095
1187
  const result = await api(
1096
- "/api/mocks",
1188
+ "/api/designs",
1097
1189
  { method: "POST", body: JSON.stringify(body) },
1098
1190
  { report: "die", fix: `mockpit read ${slug}` },
1099
1191
  );
1100
1192
  printPublished(result, flags);
1193
+ if (!poster && result.post?.surfaces?.some((s) => s.kind === "html") && !result.withdrawn) {
1194
+ queuePoster({
1195
+ base: BASE,
1196
+ token: TOKEN,
1197
+ client: HEADERS["x-mockpit-client"],
1198
+ session,
1199
+ design: result.design.id,
1200
+ postId: result.post.id,
1201
+ version: result.post.version,
1202
+ });
1203
+ }
1101
1204
  return result;
1102
1205
  }
1103
1206
 
@@ -1148,21 +1251,22 @@ function printRun(r) {
1148
1251
 
1149
1252
  function watchLines(batch) {
1150
1253
  const lines = [];
1151
- const name = batch.mock ?? "a mock";
1254
+ const name = batch.design ?? "a design";
1152
1255
  if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
1153
- if (batch.closed) {
1154
- const n = batch.dismissed?.length ?? 0;
1155
- lines.push(
1156
- `mockpit closed ${name}: ${n ? `${n} question${n === 1 ? "" : "s"} dismissed unanswered (${batch.dismissed.map((d) => d.text || d.ask).join("; ")}); don't ask again` : "closed by the user"}`,
1157
- );
1158
- }
1159
1256
  if (batch.edited) {
1160
1257
  const e = batch.edited;
1161
1258
  const bits = [
1162
1259
  e.kind ? `now a ${e.kind}` : "",
1163
1260
  e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
1164
1261
  ].filter(Boolean);
1165
- lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
1262
+ if (bits.length) lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
1263
+ if (e.notes !== undefined) {
1264
+ lines.push(
1265
+ e.notes === null
1266
+ ? `mockpit edited ${name}: the user cleared its notes`
1267
+ : `mockpit edited ${name}: the user edited its notes: ${e.notes}`,
1268
+ );
1269
+ }
1166
1270
  }
1167
1271
  if (batch.reply) {
1168
1272
  const r = batch.reply;
@@ -1171,7 +1275,7 @@ function watchLines(batch) {
1171
1275
  ? r.asks
1172
1276
  .map(
1173
1277
  (a) =>
1174
- `${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.note ? ` (“${a.note}”)` : ""}`,
1278
+ `${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.comment ? ` (“${a.comment}”)` : ""}`,
1175
1279
  )
1176
1280
  .join("; ")
1177
1281
  : "",
@@ -1207,7 +1311,7 @@ function watchLines(batch) {
1207
1311
  return lines;
1208
1312
  }
1209
1313
 
1210
- // GET where "not there yet" is an answer, not an exit — a project or mock that
1314
+ // GET where "not there yet" is an answer, not an exit — a project or design that
1211
1315
  // doesn't exist is the normal state before the first publish.
1212
1316
  async function apiSoft(path) {
1213
1317
  let res;
@@ -1443,28 +1547,32 @@ const commands = {
1443
1547
  const say = (label, text) => {
1444
1548
  if (!flags.quiet && !flags.json) console.log(`${label.padEnd(8)} ${text}`);
1445
1549
  };
1446
- const { detectDesign, detectIconSets, renderStarter } = await import("./initDesign.js");
1550
+ const { detectDesignSystem, detectIconSets, renderStarter } = await import("./initDesign.js");
1447
1551
  say("project:", `${project} (from ${source})`);
1448
1552
 
1449
- const design = await detectDesign(process.cwd());
1450
- const d = design.detected;
1553
+ const designSystem = await detectDesignSystem(process.cwd());
1554
+ const d = designSystem.detected;
1451
1555
  // `detected` is always an object, so emptiness has to be read off the
1452
1556
  // fields — otherwise a repo with no design system printed a blank line.
1453
- const stripped = design.strippedImports?.length
1454
- ? ` (stripped: ${design.strippedImports.join(", ")})`
1557
+ const stripped = designSystem.strippedImports?.length
1558
+ ? ` (stripped: ${designSystem.strippedImports.join(", ")})`
1455
1559
  : "";
1456
1560
  const found = [
1457
1561
  d?.tailwind &&
1458
- (design.tailwindSource ? `tailwind: ${design.tailwindSource}${stripped}` : "tailwind"),
1562
+ (designSystem.tailwindSource
1563
+ ? `tailwind: ${designSystem.tailwindSource}${stripped}`
1564
+ : "tailwind"),
1459
1565
  d?.shadcn && "shadcn",
1460
- d?.cssVars ? `${d.cssVars} css vars${design.source ? ` from ${design.source}` : ""}` : null,
1566
+ d?.cssVars
1567
+ ? `${d.cssVars} css vars${designSystem.source ? ` from ${designSystem.source}` : ""}`
1568
+ : null,
1461
1569
  d?.fonts?.length ? `fonts ${d.fonts.join(", ")}` : null,
1462
1570
  ].filter(Boolean);
1463
1571
  say(
1464
1572
  "design:",
1465
1573
  found.length ? found.join(" · ") : "nothing detected — using the built-in palette",
1466
1574
  );
1467
- let kit = flags.kit ?? design.kit ?? "builtin";
1575
+ let kit = flags.kit ?? designSystem.kit ?? "builtin";
1468
1576
  // The project kit is stored first so the design PUT can name it as default.
1469
1577
  if (flags["kit-url"] !== undefined) {
1470
1578
  kit = flags.kit ?? "project";
@@ -1475,7 +1583,7 @@ const commands = {
1475
1583
  });
1476
1584
  }
1477
1585
  say("kit:", kit);
1478
- const files = design.designFiles;
1586
+ const files = designSystem.designFiles;
1479
1587
  if (files) {
1480
1588
  const sh = files.shadcn;
1481
1589
  say(
@@ -1490,16 +1598,16 @@ const commands = {
1490
1598
  );
1491
1599
  }
1492
1600
 
1493
- let stored = await api(`${projectPath(project)}/design`, {
1601
+ let stored = await api(`${projectPath(project)}/design-system`, {
1494
1602
  method: "PUT",
1495
1603
  body: JSON.stringify({
1496
- detected: design.detected ?? null,
1497
- palette: design.palette ?? null,
1498
- cssVars: design.cssVars ?? "",
1499
- tailwindCss: design.tailwindCss ?? "",
1500
- strippedImports: design.strippedImports ?? [],
1604
+ detected: designSystem.detected ?? null,
1605
+ palette: designSystem.palette ?? null,
1606
+ cssVars: designSystem.cssVars ?? "",
1607
+ tailwindCss: designSystem.tailwindCss ?? "",
1608
+ strippedImports: designSystem.strippedImports ?? [],
1501
1609
  kit,
1502
- designFiles: design.designFiles ?? null,
1610
+ designFiles: designSystem.designFiles ?? null,
1503
1611
  }),
1504
1612
  });
1505
1613
 
@@ -1522,7 +1630,7 @@ const commands = {
1522
1630
  if (added) say("icons:", `${f.prefix} (${added.count} icons, from ${f.from})`);
1523
1631
  else say("icons:", `${f.prefix} not added: ${error?.message ?? "unknown error"}`);
1524
1632
  }
1525
- if (result.design) stored = result.design;
1633
+ if (result.designSystem) stored = result.designSystem;
1526
1634
  }
1527
1635
  const iconSets = [
1528
1636
  ...new Set([...(stored?.iconSets ?? []).map((s) => s.prefix), ...BUNDLED_ICON_SETS]),
@@ -1531,7 +1639,7 @@ const commands = {
1531
1639
 
1532
1640
  const starter = join(process.cwd(), ".mockpit", "starter.html");
1533
1641
  mkdirSync(dirname(starter), { recursive: true });
1534
- writeFileSync(starter, renderStarter({ ...design, kit }, iconSets));
1642
+ writeFileSync(starter, renderStarter({ ...designSystem, kit }, iconSets));
1535
1643
  say("wrote:", ".mockpit/starter.html");
1536
1644
  if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/, keeping .mockpit/rules.md)");
1537
1645
  const rulesFile = join(repoRoot(), ".mockpit", "rules.md");
@@ -1539,24 +1647,25 @@ const commands = {
1539
1647
  say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
1540
1648
  }
1541
1649
  say("next:", "mockpit guide");
1542
- if (flags.json) out({ project, design: stored, starter });
1650
+ if (flags.json) out({ project, designSystem: stored, starter });
1543
1651
  },
1544
1652
 
1545
1653
  async publish() {
1546
- await publishMock();
1654
+ await publishDesign();
1547
1655
  },
1548
1656
 
1549
1657
  async ask() {
1550
1658
  const { values: flags, positionals } = parse({
1551
1659
  allowPositionals: true,
1552
1660
  options: {
1553
- mock: { type: "string" },
1661
+ design: { type: "string" },
1554
1662
  project: { type: "string" },
1555
1663
  session: { type: "string" },
1556
1664
  option: { type: "string", multiple: true },
1557
1665
  recommend: { type: "string" },
1558
1666
  scope: { type: "string" },
1559
1667
  state: { type: "string" },
1668
+ states: { type: "string", multiple: true },
1560
1669
  part: { type: "string" },
1561
1670
  multi: { type: "boolean" },
1562
1671
  id: { type: "string" },
@@ -1564,16 +1673,16 @@ const commands = {
1564
1673
  asks: { type: "string" },
1565
1674
  },
1566
1675
  });
1567
- const slug = requireMock(
1676
+ const slug = requireDesign(
1568
1677
  flags,
1569
- 'mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet',
1678
+ 'mockpit ask --design writer "Quiet or dark?" --option Quiet=quiet',
1570
1679
  );
1571
1680
  let asks;
1572
1681
  if (flags.asks !== undefined) {
1573
1682
  if (positionals.length > 0) {
1574
1683
  die(
1575
1684
  "use the question or --asks, not both",
1576
- `mockpit ask --mock ${slug} --asks <json|file>`,
1685
+ `mockpit ask --design ${slug} --asks <json|file>`,
1577
1686
  );
1578
1687
  }
1579
1688
  asks = readJsonFlag("asks", flags.asks);
@@ -1583,13 +1692,13 @@ const commands = {
1583
1692
  if (!text)
1584
1693
  die(
1585
1694
  "ask needs a question",
1586
- `mockpit ask --mock ${slug} "Quiet or dark?" --option Quiet=quiet`,
1695
+ `mockpit ask --design ${slug} "Quiet or dark?" --option Quiet=quiet`,
1587
1696
  );
1588
1697
  const options = (flags.option ?? []).map(parseOption);
1589
1698
  if (options.length === 0) {
1590
1699
  die(
1591
1700
  "ask needs options",
1592
- `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
1701
+ `mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
1593
1702
  );
1594
1703
  }
1595
1704
  if (flags.recommend !== undefined) {
@@ -1599,7 +1708,7 @@ const commands = {
1599
1708
  if (!pick)
1600
1709
  die(
1601
1710
  `--recommend "${flags.recommend}" names no option`,
1602
- `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
1711
+ `mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
1603
1712
  );
1604
1713
  pick.recommended = true;
1605
1714
  }
@@ -1610,6 +1719,7 @@ const commands = {
1610
1719
  ...(flags.id && { id: flags.id }),
1611
1720
  ...(flags.scope && { scope: flags.scope }),
1612
1721
  ...(flags.state && { state: flags.state }),
1722
+ ...(flags.states && { states: flags.states }),
1613
1723
  ...(flags.part && { part: flags.part }),
1614
1724
  ...(flags.multi && { multi: true }),
1615
1725
  ...(flags.after && { after: flags.after }),
@@ -1618,7 +1728,7 @@ const commands = {
1618
1728
  }
1619
1729
  const session = await resolveSession(flags);
1620
1730
  const result = await api(
1621
- `${mockPath(slug)}/asks`,
1731
+ `${designPath(slug)}/asks`,
1622
1732
  {
1623
1733
  method: "POST",
1624
1734
  body: JSON.stringify({
@@ -1633,7 +1743,7 @@ const commands = {
1633
1743
  if (flags.quiet) return;
1634
1744
  for (const ask of result.asks) {
1635
1745
  console.log(
1636
- `asked on ${result.mock}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
1746
+ `asked on ${result.design}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
1637
1747
  );
1638
1748
  }
1639
1749
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
@@ -1651,10 +1761,20 @@ const commands = {
1651
1761
  body: { type: "boolean" },
1652
1762
  history: { type: "boolean" },
1653
1763
  tree: { type: "boolean" },
1764
+ decisions: { type: "boolean" },
1654
1765
  },
1655
1766
  });
1656
1767
  const project = resolveProject(flags).name;
1657
- if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
1768
+ if (positionals.length > 1) die("read takes one design slug", "mockpit read writer");
1769
+ if (flags.decisions) {
1770
+ return out(
1771
+ await api(
1772
+ `/api/projects/${encodeURIComponent(project)}/decisions`,
1773
+ {},
1774
+ { report: "die", fix: `mockpit read --project ${project}` },
1775
+ ),
1776
+ );
1777
+ }
1658
1778
  if (flags.tree) {
1659
1779
  return out(
1660
1780
  await api(
@@ -1668,49 +1788,56 @@ const commands = {
1668
1788
  const slug = slugify(positionals[0]);
1669
1789
  return out(
1670
1790
  await api(
1671
- `${mockPath(slug)}${query({ project, body: flags.body, history: flags.history })}`,
1791
+ `${designPath(slug)}${query({ project, body: flags.body, history: flags.history })}`,
1672
1792
  {},
1673
1793
  { report: "die", fix: `mockpit read --project ${project}` },
1674
1794
  ),
1675
1795
  );
1676
1796
  }
1677
- const list = (await apiSoft(`/api/mocks${query({ project })}`)) ?? {
1678
- mocks: [],
1797
+ const list = (await apiSoft(`/api/designs${query({ project })}`)) ?? {
1798
+ designs: [],
1679
1799
  open: 0,
1680
1800
  pending: [],
1681
1801
  };
1682
1802
  if (flags.json) return out(list);
1683
1803
  if (flags.quiet) return;
1684
- const mocks = list.mocks ?? [];
1804
+ const designs = list.designs ?? [];
1685
1805
  console.log(
1686
- `${project} · ${mocks.length} mock${mocks.length === 1 ? "" : "s"}` +
1806
+ `${project} · ${designs.length} design${designs.length === 1 ? "" : "s"}` +
1687
1807
  (list.open
1688
1808
  ? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
1689
1809
  : " · nothing waiting on the user"),
1690
1810
  );
1691
- for (const m of mocks) {
1811
+ for (const m of designs) {
1692
1812
  const states = m.states.length ? m.states.join(" / ") : "single state";
1693
1813
  console.log(
1694
- ` ${m.slug} · ${m.kind}${m.area ? ` · ${m.area}` : ""} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.closedAt ? " · closed" : m.open ? ` · ${m.open} open` : ""}`,
1814
+ ` ${m.slug} · ${m.kind}${m.area ? ` · ${m.area}` : ""} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.planned ? " · planned" : ""}${m.archivedAt ? " · archived" : m.open ? ` · ${m.open} open` : ""}`,
1695
1815
  );
1696
1816
  }
1697
1817
  for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
1698
1818
  for (const line of pendingLines(list.pending)) console.log(line);
1819
+ if (list.decisionList) {
1820
+ console.log(
1821
+ `Decided, ${list.decisionList.total}. Treat as settled unless the user says otherwise:`,
1822
+ );
1823
+ for (const d of list.decisionList.lines) console.log(` ${d.design}: ${d.text}`);
1824
+ if (list.decisionList.more) console.log(` ${list.decisionList.more} (--decisions)`);
1825
+ }
1699
1826
  },
1700
1827
  async export() {
1701
1828
  const { values: flags } = parse({
1702
1829
  options: {
1703
- mock: { type: "string" },
1830
+ design: { type: "string" },
1704
1831
  state: { type: "string" },
1705
1832
  variant: { type: "string" },
1706
1833
  project: { type: "string" },
1707
1834
  out: { type: "string" },
1708
1835
  },
1709
1836
  });
1710
- const slug = requireMock(flags, "mockpit export --mock writer");
1837
+ const slug = requireDesign(flags, "mockpit export --design writer");
1711
1838
  const project = resolveProject(flags).name;
1712
1839
  const data = await api(
1713
- `${mockPath(slug)}/export${query({ project, state: flags.state, variant: flags.variant })}`,
1840
+ `${designPath(slug)}/export${query({ project, state: flags.state, variant: flags.variant })}`,
1714
1841
  {},
1715
1842
  { report: "die", fix: `mockpit read ${slug}` },
1716
1843
  );
@@ -1734,7 +1861,7 @@ const commands = {
1734
1861
  JSON.stringify(
1735
1862
  {
1736
1863
  project,
1737
- mock: slug,
1864
+ design: slug,
1738
1865
  state: entry.state,
1739
1866
  variant: entry.variant,
1740
1867
  version: entry.version,
@@ -1802,24 +1929,44 @@ const commands = {
1802
1929
  },
1803
1930
  async watch() {
1804
1931
  const { values: flags } = parse({
1805
- options: { session: { type: "string" }, after: { type: "string" } },
1932
+ options: {
1933
+ session: { type: "string" },
1934
+ after: { type: "string" },
1935
+ once: { type: "boolean" },
1936
+ "exit-code": { type: "string" },
1937
+ },
1806
1938
  });
1807
1939
  if (flags.after !== undefined && !/^\d+$/.test(flags.after)) {
1808
1940
  fail(`--after must be a number (got "${flags.after}")`);
1809
1941
  }
1942
+ const exitCode = flags["exit-code"] ?? "0";
1943
+ if (!/^\d+$/.test(exitCode)) fail(`--exit-code must be a number (got "${exitCode}")`);
1944
+ // A waiter outliving its agent would take a Send and tell no one, and each
1945
+ // Send is delivered only once.
1946
+ if (flags.once) {
1947
+ const parent = process.ppid;
1948
+ setInterval(() => {
1949
+ if (process.ppid !== parent || process.ppid === 1) process.exit(0);
1950
+ }, 5000).unref();
1951
+ }
1810
1952
  // A continuous long-poll that streams each piece of feedback as one line —
1811
1953
  // one line is one monitor notification. It re-arms forever; a transient
1812
1954
  // network error backs off and retries rather than exiting.
1813
1955
  //
1814
- // After the first poll it carries no client cursor: an author=user read
1815
- // resumes from the session's server-side cursor and advances it, so
1816
- // feedback is delivered exactly once across watch, feedback, and piggyback.
1956
+ // It only says a reply landed (peek): the reply stays on the session's cursor
1957
+ // until the agent's feedback or next write takes it, so a harness dropping or
1958
+ // cutting this output can't lose it, and each Send is still delivered once.
1817
1959
  let firstAfter = flags.after;
1818
1960
  for (;;) {
1819
1961
  const key = harnessKey();
1962
+ // The state file is per process and cwd, which several threads of one harness
1963
+ // can share, so a conversation's own key decides whenever there is one.
1820
1964
  const session =
1821
- (await resolveSession(flags)) ??
1822
- (key ? await resolveSessionByKey(key) : await resolveSessionByCwd());
1965
+ flags.session ??
1966
+ process.env.MOCKPIT_SESSION ??
1967
+ (key
1968
+ ? await resolveSessionByKey(key)
1969
+ : ((await resolveSession(flags)) ?? (await resolveSessionByCwd())));
1823
1970
  if (!session) {
1824
1971
  // A conversation that hasn't used mockpit yet may never; ask rarely.
1825
1972
  await sleep(key ? 10_000 : 2000);
@@ -1828,7 +1975,7 @@ const commands = {
1828
1975
  let result;
1829
1976
  try {
1830
1977
  const res = await fetch(
1831
- `${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60 })}`,
1978
+ `${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60, peek: 1 })}`,
1832
1979
  { headers: HEADERS },
1833
1980
  );
1834
1981
  if (!res.ok) {
@@ -1841,20 +1988,26 @@ const commands = {
1841
1988
  continue;
1842
1989
  }
1843
1990
  firstAfter = undefined;
1844
- for (const batch of result.feedback ?? []) {
1845
- for (const line of watchLines(batch)) console.log(line);
1991
+ const lines = (result.feedback ?? []).flatMap(watchLines);
1992
+ for (const line of lines) console.log(line);
1993
+ if (flags.once && lines.length) {
1994
+ // A harness may frame this output as a hook failure, so it says what it is.
1995
+ console.log(
1996
+ "This is the user's reply from the mockpit viewer, not an error. Call mockpit's feedback for all of it.",
1997
+ );
1998
+ process.exit(Number(exitCode));
1846
1999
  }
1847
2000
  }
1848
2001
  },
1849
2002
 
1850
2003
  async say() {
1851
- const { values: flags, positionals } = parse({ allowPositionals: true, options: MOCK_FLAGS });
2004
+ const { values: flags, positionals } = parse({ allowPositionals: true, options: DESIGN_FLAGS });
1852
2005
  const text = positionals.join(" ").trim();
1853
- if (!text) die("say needs text", 'mockpit say "…" --mock writer');
1854
- const slug = requireMock(flags, `mockpit say "${text}" --mock writer`);
2006
+ if (!text) die("say needs text", 'mockpit say "…" --design writer');
2007
+ const slug = requireDesign(flags, `mockpit say "${text}" --design writer`);
1855
2008
  const session = await resolveSession(flags);
1856
2009
  const result = await api(
1857
- `${mockPath(slug)}/say`,
2010
+ `${designPath(slug)}/say`,
1858
2011
  {
1859
2012
  method: "POST",
1860
2013
  body: JSON.stringify({
@@ -1872,10 +2025,69 @@ const commands = {
1872
2025
  console.log(`said on ${slug}`);
1873
2026
  printFeedback(result.feedback);
1874
2027
  },
2028
+ async move() {
2029
+ const { values: flags } = parse({
2030
+ options: {
2031
+ ...DESIGN_FLAGS,
2032
+ variant: { type: "string", multiple: true },
2033
+ to: { type: "string" },
2034
+ "to-state": { type: "string" },
2035
+ archive: { type: "boolean" },
2036
+ restore: { type: "boolean" },
2037
+ "hide-older": { type: "boolean" },
2038
+ "delete-older": { type: "boolean" },
2039
+ },
2040
+ });
2041
+ const example = "mockpit move --design writer --variant quiet --archive";
2042
+ const slug = requireDesign(flags, example);
2043
+ const tidying = flags["hide-older"] || flags["delete-older"];
2044
+ if (!flags.variant?.length && !tidying) die("move needs --variant", example);
2045
+ const session = await resolveSession(flags);
2046
+ const result = await api(
2047
+ `${designPath(slug)}/move`,
2048
+ {
2049
+ method: "POST",
2050
+ body: JSON.stringify({
2051
+ state: flags.state,
2052
+ variants: flags.variant,
2053
+ to: flags.to,
2054
+ toState: flags["to-state"],
2055
+ archive: flags.archive,
2056
+ restore: flags.restore,
2057
+ hideOlder: flags["hide-older"],
2058
+ deleteOlder: flags["delete-older"],
2059
+ project: resolveProject(flags).name,
2060
+ session: session ?? undefined,
2061
+ }),
2062
+ },
2063
+ { report: "die", fix: `mockpit read ${slug}` },
2064
+ );
2065
+ if (flags.json) return out(result);
2066
+ if (flags.quiet) return;
2067
+ if (tidying) {
2068
+ const verb = flags["delete-older"] ? "deleted" : "hid";
2069
+ console.log(`${verb} ${result.versions.length} older versions`);
2070
+ for (const s of result.skipped) {
2071
+ const which = s.version ? ` v${s.version}` : "";
2072
+ console.log(`stayed: ${s.state ? `${s.state}/` : ""}${s.variant}${which} (${s.why})`);
2073
+ }
2074
+ printFeedback(result.feedback);
2075
+ return;
2076
+ }
2077
+ const names = result.variants.map((v) => v.variant).join(", ") || "nothing";
2078
+ const done = result.to
2079
+ ? `moved to ${result.to.design}`
2080
+ : flags.restore
2081
+ ? "restored"
2082
+ : "archived";
2083
+ console.log(`${done}: ${names}`);
2084
+ for (const s of result.skipped) console.log(`stayed: ${s.variant} (${s.why})`);
2085
+ printFeedback(result.feedback);
2086
+ },
1875
2087
  async edit() {
1876
2088
  const { values: flags } = parse({
1877
2089
  options: {
1878
- mock: { type: "string" },
2090
+ design: { type: "string" },
1879
2091
  project: { type: "string" },
1880
2092
  session: { type: "string" },
1881
2093
  slug: { type: "string" },
@@ -1883,6 +2095,8 @@ const commands = {
1883
2095
  kind: { type: "string" },
1884
2096
  area: { type: "string" },
1885
2097
  source: { type: "string" },
2098
+ "first-state": { type: "string" },
2099
+ notes: { type: "string" },
1886
2100
  rename: { type: "string" },
1887
2101
  "area-order": { type: "string" },
1888
2102
  },
@@ -1891,9 +2105,9 @@ const commands = {
1891
2105
  const session = (await resolveSession(flags)) ?? undefined;
1892
2106
  const areaOrder = flags["area-order"];
1893
2107
  if (flags.rename !== undefined || areaOrder !== undefined) {
1894
- if (flags.mock) {
2108
+ if (flags.design) {
1895
2109
  die(
1896
- "--rename and --area-order edit the project; edit the mock without them",
2110
+ "--rename and --area-order edit the project; edit the design without them",
1897
2111
  "mockpit edit --help",
1898
2112
  );
1899
2113
  }
@@ -1922,7 +2136,7 @@ const commands = {
1922
2136
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1923
2137
  if (!result.renamed) return printFeedback(result.feedback);
1924
2138
  console.log(
1925
- `renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.mocks} mock${result.mocks === 1 ? "" : "s"} · ${result.url}`,
2139
+ `renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.designs} design${result.designs === 1 ? "" : "s"} · ${result.url}`,
1926
2140
  );
1927
2141
  if (!flags.project && !process.env.MOCKPIT_PROJECT) {
1928
2142
  console.log(
@@ -1932,40 +2146,46 @@ const commands = {
1932
2146
  printFeedback(result.feedback);
1933
2147
  return;
1934
2148
  }
1935
- const slug = requireMock(flags, "mockpit edit --mock writer --area Shell");
1936
- const fields = ["slug", "title", "kind", "area", "source"].filter(
2149
+ const slug = requireDesign(flags, "mockpit edit --design writer --area Shell");
2150
+ const fields = ["slug", "title", "kind", "area", "source", "notes"].filter(
1937
2151
  (k) => flags[k] !== undefined,
1938
2152
  );
1939
- if (!fields.length) {
1940
- die("edit needs --slug, --title, --kind, --area or --source", "mockpit edit --help");
2153
+ if (!fields.length && flags["first-state"] === undefined) {
2154
+ die(
2155
+ "edit needs --slug, --title, --kind, --area, --source, --notes or --first-state",
2156
+ "mockpit edit --help",
2157
+ );
1941
2158
  }
1942
2159
  const result = await api(
1943
- mockPath(slug),
2160
+ designPath(slug),
1944
2161
  {
1945
2162
  method: "PATCH",
1946
2163
  body: JSON.stringify({
1947
2164
  project,
1948
2165
  session,
1949
2166
  ...Object.fromEntries(fields.map((k) => [k, flags[k]])),
2167
+ firstState: flags["first-state"],
1950
2168
  }),
1951
2169
  },
1952
2170
  { report: "die", fix: `mockpit read ${slug}` },
1953
2171
  );
1954
2172
  if (flags.json) return out(result);
1955
2173
  if (flags.quiet) return;
1956
- const m = result.mock;
2174
+ const m = result.design;
1957
2175
  console.log(
1958
2176
  [
1959
2177
  result.renamed ? `${result.renamed.from} → ${m.slug}` : m.slug,
1960
2178
  m.kind,
1961
2179
  m.area && `area ${m.area}`,
1962
2180
  m.source && `source ${m.source}`,
2181
+ result.notes && (result.notes.applied ? "notes saved" : "notes NOT applied"),
1963
2182
  result.url,
1964
2183
  ]
1965
2184
  .filter(Boolean)
1966
2185
  .join(" · "),
1967
2186
  );
1968
2187
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
2188
+ if (result.notes && !result.notes.applied) console.log(`notes: ${result.notes.text}`);
1969
2189
  printFeedback(result.feedback);
1970
2190
  },
1971
2191
  async demo() {
@@ -1979,13 +2199,13 @@ const commands = {
1979
2199
  });
1980
2200
  for (const state of DEMO.states) {
1981
2201
  for (const variant of DEMO.variants) {
1982
- await api("/api/mocks", {
2202
+ await api("/api/designs", {
1983
2203
  method: "POST",
1984
2204
  body: JSON.stringify({
1985
2205
  session: session.id,
1986
2206
  project: DEMO.project,
1987
- mock: DEMO.slug,
1988
- kind: "page",
2207
+ design: DEMO.slug,
2208
+ kind: "screen",
1989
2209
  title: DEMO.title,
1990
2210
  state: state.label,
1991
2211
  variant: variant.name,
@@ -1995,7 +2215,7 @@ const commands = {
1995
2215
  });
1996
2216
  }
1997
2217
  }
1998
- await api(`${mockPath(DEMO.slug)}/asks`, {
2218
+ await api(`${designPath(DEMO.slug)}/asks`, {
1999
2219
  method: "POST",
2000
2220
  body: JSON.stringify({ project: DEMO.project, session: session.id, asks: DEMO.asks }),
2001
2221
  });
@@ -2048,14 +2268,14 @@ const commands = {
2048
2268
  if (sub === "add") {
2049
2269
  const result = await installIconSets(project, sets);
2050
2270
  if (result.errors.length) fail(result.errors.map((e) => e.message).join("; "));
2051
- if (flags.json) return out(result.design);
2271
+ if (flags.json) return out(result.designSystem);
2052
2272
  for (const a of result.added) {
2053
2273
  if (!flags.quiet) console.log(`added ${a.prefix} (${a.count} icons)`);
2054
2274
  }
2055
2275
  return;
2056
2276
  }
2057
2277
  if (sub === "remove") {
2058
- const stored = await api(`${projectPath(project)}/design`);
2278
+ const stored = await api(`${projectPath(project)}/design-system`);
2059
2279
  const installed = stored?.iconSets ?? [];
2060
2280
  for (const set of sets) {
2061
2281
  if (installed.some((s) => s.prefix === set)) continue;
@@ -2065,14 +2285,14 @@ const commands = {
2065
2285
  : `${set} is not installed for ${project}`,
2066
2286
  );
2067
2287
  }
2068
- const design = await api(`${projectPath(project)}/design`, {
2288
+ const designSystem = await api(`${projectPath(project)}/design-system`, {
2069
2289
  method: "PUT",
2070
2290
  body: JSON.stringify({
2071
2291
  ...stored,
2072
2292
  iconSets: installed.filter((s) => !sets.includes(s.prefix)),
2073
2293
  }),
2074
2294
  });
2075
- if (flags.json) return out(design);
2295
+ if (flags.json) return out(designSystem);
2076
2296
  if (!flags.quiet) console.log(`removed ${sets.join(", ")}`);
2077
2297
  return;
2078
2298
  }
@@ -2099,13 +2319,13 @@ const commands = {
2099
2319
  const { name: project } = resolveProject(flags);
2100
2320
  if ((sub !== "add" && sub !== "remove") || !id)
2101
2321
  fail("usage: mockpit kit add <id> --url <url> --doc <file|text> | mockpit kit remove <id>");
2102
- const design =
2322
+ const designSystem =
2103
2323
  sub === "add"
2104
2324
  ? await putProjectKit(project, id, flags)
2105
2325
  : await api(`${projectPath(project)}/kits/${encodeURIComponent(id)}`, {
2106
2326
  method: "DELETE",
2107
2327
  });
2108
- if (flags.json) return out(design);
2328
+ if (flags.json) return out(designSystem);
2109
2329
  if (!flags.quiet) console.log(`${sub === "add" ? "added" : "removed"} kit ${id} (${project})`);
2110
2330
  },
2111
2331