mockpit 1.2.0 → 2.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 (59) hide show
  1. package/README.md +69 -48
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +426 -51
  4. package/dist/mcp/server.js +45 -7
  5. package/dist/server/apiViews.d.ts +407 -21
  6. package/dist/server/apiViews.js +110 -11
  7. package/dist/server/app.d.ts +5 -1
  8. package/dist/server/app.js +1454 -192
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +23 -0
  11. package/dist/server/decisions.js +100 -0
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +96 -27
  14. package/dist/server/embeds.d.ts +26 -0
  15. package/dist/server/embeds.js +144 -0
  16. package/dist/server/eventLog.js +2 -1
  17. package/dist/server/feedbackBatch.d.ts +15 -4
  18. package/dist/server/feedbackBatch.js +31 -7
  19. package/dist/server/fills.d.ts +41 -0
  20. package/dist/server/fills.js +328 -0
  21. package/dist/server/guidance.d.ts +23 -0
  22. package/dist/server/guidance.js +71 -0
  23. package/dist/server/index.js +25 -2
  24. package/dist/server/knobs.d.ts +6 -2
  25. package/dist/server/knobs.js +11 -1
  26. package/dist/server/mcpHttp.d.ts +6 -0
  27. package/dist/server/mcpHttp.js +35 -0
  28. package/dist/server/mcpSpec.d.ts +14 -13
  29. package/dist/server/mcpSpec.js +133 -18
  30. package/dist/server/postSurfaces.js +6 -12
  31. package/dist/server/richRender.js +3 -1
  32. package/dist/server/run.js +10 -1
  33. package/dist/server/runApi.d.ts +3 -3
  34. package/dist/server/runApi.js +18 -13
  35. package/dist/server/sqlStore.d.ts +97 -5
  36. package/dist/server/sqlStore.js +367 -27
  37. package/dist/server/surfacePage.d.ts +6 -2
  38. package/dist/server/surfacePage.js +142 -10
  39. package/dist/server/themes.d.ts +2 -0
  40. package/dist/server/themes.js +23 -3
  41. package/dist/server/types.d.ts +107 -16
  42. package/dist/server/types.js +73 -10
  43. package/dist/server/viewerRequest.d.ts +2 -0
  44. package/dist/server/viewerRequest.js +14 -0
  45. package/extensions/mockpit.js +179 -16
  46. package/guide/packs/design-craft.md +35 -0
  47. package/guide/topics/asks.md +82 -31
  48. package/guide/topics/feedback.md +151 -43
  49. package/guide/topics/html.md +8 -10
  50. package/guide/topics/http.md +8 -4
  51. package/guide/topics/knobs.md +4 -3
  52. package/guide/topics/scripts.md +2 -2
  53. package/guide/topics/surfaces.md +45 -11
  54. package/package.json +3 -15
  55. package/skills/launch-video/SKILL.md +2 -2
  56. package/skills/mockpit/SKILL.md +23 -7
  57. package/viewer/dist/index.html +9 -8
  58. package/dist/server/slots.d.ts +0 -14
  59. package/dist/server/slots.js +0 -71
package/bin/mockpit.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  writeFileSync,
10
10
  writeSync,
11
11
  } from "node:fs";
12
- import { tmpdir, userInfo } from "node:os";
12
+ 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";
@@ -29,23 +29,23 @@ const HEADERS = {
29
29
  const HELP = `mockpit — a live visual surface for terminal coding agents
30
30
 
31
31
  vocabulary: project › mock › state › variant › version. A project is a repo; a
32
- mock is the page or component on stage; a state is one moment of it ("Lab
32
+ mock is a sketch, page or component, by slug; a state is one thing the user sees in it ("Lab
33
33
  open"); a variant is a parallel design of a state; a version is its history.
34
- Mark the parts you want feedback on with data-part="name" in the html.
34
+ Declare the parts you want feedback on with data-part="name" in the html.
35
35
 
36
36
  the loop (never blocks on the user):
37
37
  1. mockpit init once per repo; read the brief (mockpit guide).
38
- 2. mockpit publish once per variant; mark parts with data-part.
39
- 3. A choice is several variants plus one ask that binds them: mockpit ask
38
+ 2. mockpit publish once per variant; declare parts with data-part.
39
+ 3. A choice is several variants plus one question that binds them: mockpit ask
40
40
  with options bound to variants. A variant decides everything it differs
41
41
  on; don't re-ask it. Visual options carry a variant or knob set. One
42
42
  render plus a control: --knobs.
43
43
  4. Tell the user in one line where to look, then end your turn. The question
44
- itself lives in the mock (ask), never in chat. The user answers in the
44
+ itself lives in the mock, never in chat. The user answers in the
45
45
  browser at their own pace. Never poll.
46
46
  5. When the user says they answered (or a mockpit watch line wakes you), run
47
47
  mockpit feedback. Every write also returns feedback; read it.
48
- 6. mockpit publish the revision; mockpit export once a variant is accepted.
48
+ 6. mockpit publish the revision; mockpit export once a variant is in use.
49
49
  In Claude Code, arm mockpit watch under Monitor after asking, then end the
50
50
  turn; elsewhere the user's next message is the wake-up.
51
51
 
@@ -56,32 +56,45 @@ verbs:
56
56
  mockpit publish --mock <slug> --html <file> [options]
57
57
  create or version one variant of one state
58
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")
59
60
  --variant <name> variant label (default "default")
60
61
  --html <file|-> the html body (or a positional file)
61
62
  --surfaces <json|file> instead: the full ordered surface list; {"id"}
62
63
  alone keeps a surface, a missing id removes it
63
64
  --parts <name=file|-> instead: replace just that data-part's element in
64
65
  the latest version (repeatable; name#key targets one instance)
66
+ --fills <part#fill=file|-> instead: replace or add one fill of a part in
67
+ the latest version (repeatable)
65
68
  --title <t> mock title
66
- --kind <k> component|page (default component)
69
+ --kind <k> page|component, only for an app's screens (default sketch)
70
+ --area <name> where in the product it belongs ("Shell"); Home groups by it
71
+ --source <path> the code path that implements it
67
72
  --knobs <json|file> knobs in tunekit usePane shape, keyed by path
68
73
  --from <N> branch from version N
69
74
  --prompt <text> what prompted this version
75
+ --note <text> one line on what this variant is, shown beside it
76
+ --widths <list> device widths to show it at, e.g. 390,1280
77
+ --flow <name> [--step <N>] make this state step N of a flow (0 takes
78
+ it out); with no html it only places the state
70
79
  --project <name> project (default: git remote, else directory name)
71
80
  --kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
81
+ --poster <file> a still of the page (png, jpg); Studio and Home show it
82
+ instead of running the page. Send one with every version
83
+ that changes the page, or it goes
72
84
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
73
85
  add a surface of that kind after the html (repeatable;
74
86
  --data is a JSON tree)
75
87
  mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
76
88
  ask the user; bind options to variants
77
89
  --option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
78
- "=knob:path=value" sets one knob
90
+ "=variant@3" to its version 3, "=knob:path=value" sets one knob
91
+ --recommend <l> the option you would pick, by label; a quiet mark on it
79
92
  --scope <s> mock|state|part (default mock)
80
- --state <label> the state a state-scoped ask is about
81
- --part <name> the part a part-scoped ask is about
93
+ --state <label> the state a state-scoped question is about
94
+ --part <name> the part a part-scoped question is about
82
95
  --multi allow several answers
83
- --id <id> stable id; reusing one replaces that ask
84
- --asks <json|file> several asks at once, full shape
96
+ --id <id> stable id; reusing one replaces that question
97
+ --asks <json|file> several questions at once, full shape
85
98
  mockpit read [slug] [--body] [--history]
86
99
  no slug: every mock plus what the user
87
100
  is doing now; a slug: that mock
@@ -89,12 +102,20 @@ verbs:
89
102
  plus pending; returns at once
90
103
  mockpit say "<text>" --mock <slug> [--state s] [--variant v]
91
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)
107
+ mockpit edit --project <name> --rename <new>
108
+ rename a project
109
+ mockpit edit --project <name> --area-order "Shell,Food"
110
+ the order Home lists areas in
92
111
  mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
93
- write the accepted html + history per state
112
+ write the html in use + history per state
94
113
  to .mockpit/accepted/<mock>/<state>/
95
114
  mockpit upload <file> [--kind image|file]
96
115
  upload an asset, print its id and URL
97
116
  mockpit guide [--topic <id>] the project-aware brief, or one reference topic
117
+ mockpit rules [--mine] send .mockpit/rules.md (or, with --mine, your
118
+ ~/.mockpit/rules.md) to the server for the brief
98
119
  mockpit run <file|-> run a script against the mockpit API on the
99
120
  server (mockpit guide --topic scripts)
100
121
  mockpit watch [--session <id>] stream user feedback, one line per Send
@@ -142,6 +163,15 @@ const COMMAND_HELP = {
142
163
  (gitignored). --kit makes a bundled or project kit the default for every html
143
164
  surface. --kit-url defines the project's own kit (id from --kit, else
144
165
  "project") and makes it the default; the doc is the cheat sheet the brief prints.`,
166
+ rules: `mockpit rules [--project <name>]
167
+ mockpit rules --mine
168
+ Guidance on top of the core brief, highest first: the project's
169
+ .mockpit/rules.md (committed with the repo), your ~/.mockpit/rules.md, then
170
+ the packs either names (\`packs: [design-craft]\` in its frontmatter). A file
171
+ has "## Rules" (always followed) and "## Suggestions" (aimed for).
172
+ This sends the file as it is on disk; a missing file clears the stored copy.
173
+ init sends the project's too. A local server reads yours live, so --mine is
174
+ for a deployed one.`,
145
175
  kit: `mockpit kit add <id> --url <https url> --doc <file.md|text> [--script <url>]
146
176
  mockpit kit remove <id>
147
177
  A project kit is a stylesheet (and optional script) on the CDN allowlist plus a
@@ -156,42 +186,84 @@ mockpit icons remove <set> [<set>...]
156
186
  publish: `mockpit publish --mock <slug> --html <file> [options]
157
187
  mockpit publish --mock <slug> --surfaces <json|file> [options]
158
188
  mockpit publish --mock <slug> --parts <name=file|-> ... [options]
189
+ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
159
190
  Creates the mock, state or variant, or the next version of an existing one.
160
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
161
194
  --variant <name> variant label (default "default")
162
195
  --surfaces <json|file> the full ordered list: {"id"} alone keeps a surface,
163
196
  a missing id removes it, order is the list order
164
197
  --parts <name=file|-> replace only the element carrying data-part="name"
165
198
  (name#key for one instance) in the latest version; repeatable
199
+ --fills <part#fill=file|-> replace the element carrying data-part="part"
200
+ data-fill="fill" in the latest version, or add it after
201
+ that part's other fills; the marks are added if missing
166
202
  --title <t> mock title
167
- --kind <k> component|page (default component)
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)
206
+ --area <name> where in the product it belongs, in the project's words
207
+ ("Shell", "Food"); Home groups each tab by it. "" clears
208
+ --source <path> the code path that implements it (apps/web/src/shell). "" clears
168
209
  --knobs <json|file> knobs in tunekit usePane shape
169
210
  --from <N> branch from version N
211
+ --fork <v[@N]> start this new variant from sibling v (its version N, else
212
+ its latest); alone it copies v, with --parts/--fills it
213
+ edits the copy
170
214
  --prompt <text> what prompted this version
215
+ --note <text> one line on what this variant is, shown beside it (never
216
+ inside the design)
217
+ --widths <list> device widths the mock is judged at, side by side, e.g.
218
+ 390,1280 (the ones the user ships to)
219
+ --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
221
+ places the existing state
222
+ --step <N> its 1-based place in the flow (moves it if it is there);
223
+ 0 takes it out; omitted, it joins at the end
171
224
  --project <name> project (default: git remote, else directory name)
225
+ --poster <file> a still of the page (png, jpg) that Studio and Home show
226
+ instead of running it; with --parts or --fills it is the
227
+ still of the spliced page. A version that changes the page
228
+ without one drops the old still
172
229
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
173
230
  --json / --quiet
174
231
  Prints the parts found per state, parts that vanished or were renamed,
175
- nudges, a ready-to-run "mockpit ask" when variants have no ask, and feedback.`,
232
+ nudges, a ready-to-run "mockpit ask" when variants have no question, and feedback.`,
176
233
  ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
177
- [--state s] [--part p] [--multi] [--id id]
234
+ [--state s] [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
178
235
  mockpit ask --mock <slug> --asks <json|file>
179
- Ask the user. A choice is several variants plus one ask that binds them.
236
+ Ask the user. A choice is several variants plus one question that binds them.
180
237
  Then tell the user where to look and end your turn.`,
181
- read: `mockpit read [slug] [--project <name>] [--body] [--history]
182
- No slug: one line per mock (states, variants, open asks) plus pending, what
183
- the user is doing now. A slug: the mock's states, variants, asks with
184
- answers, parts, knobs and last tuned values, plus its pending, as JSON.
185
- --body includes surfaces, --history version rows. Never consumes feedback.`,
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.
242
+ --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.`,
186
245
  feedback: `mockpit feedback [--project <name>] [--session <id>]
187
246
  Returns at once with {feedback, pending} as JSON: one batch per mock the user
188
- sent since you last heard ({mock, reply: {asks, mix, tuned, comments},
189
- comments, accepted, archived}), delivered once across feedback, watch and
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
190
250
  every write; and pending ({mock, viewerOpen, draft}) so you can tell the
191
251
  user "take your time" or "2 of 3 answered". Run it when the user says they
192
252
  answered.`,
193
253
  say: `mockpit say "<text>" --mock <slug> [--state s] [--variant v]
194
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]
256
+ [--area <name>|""] [--source <path>|""] [--project <name>]
257
+ mockpit edit --project <name> --rename <new name>
258
+ mockpit edit --project <name> --area-order "Shell,Food,Chat"
259
+ Change a mock without publishing a new version; "" clears area or source. A new
260
+ 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
264
+ MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
265
+ --area-order lists the project's areas in the order Home shows them; areas it
266
+ leaves out follow alphabetically, "" goes back to alphabetical.`,
195
267
  run: `mockpit run <file|-> [--session <id>] [--project <name>] [--json]
196
268
  Run a script (the body of an async function, plain JavaScript) in the server's
197
269
  sandbox against the mockpit API; see mockpit guide --topic scripts. Prints each
@@ -203,11 +275,12 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
203
275
  Claude Code, arm it under Monitor after asking, then end the turn).
204
276
  Delivery is shared with feedback and every write: each Send arrives once.`,
205
277
  export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
206
- Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/.`,
278
+ Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
279
+ with fills, index.html is the chosen state, plus all-fills.html and decisions.json.`,
207
280
  upload: `mockpit upload <file> [--kind image|file] [--session <id>]
208
281
  Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
209
282
  guide: `mockpit guide [--topic <id>] [--project <name>]
210
- No flag prints the brief: the loop, parts, asks and knobs, feedback, and
283
+ No flag prints the brief: the loop, parts, questions and knobs, feedback, and
211
284
  this project's palette, kit and icons. --topic prints one reference topic
212
285
  (knobs, asks, surfaces, html, feedback, http, scripts).`,
213
286
  };
@@ -396,6 +469,19 @@ async function resolveSession(flags, { create = false } = {}) {
396
469
  return session.id;
397
470
  }
398
471
 
472
+ // The harness's conversation id names exactly one session, where the cwd can
473
+ // match several agents working in one repo; the watcher would then take a
474
+ // sibling's replies. Never creates one: most conversations never use mockpit.
475
+ async function resolveSessionByKey(key) {
476
+ try {
477
+ const res = await fetch(`${BASE}/api/sessions${query({ key })}`, { headers: HEADERS });
478
+ if (!res.ok) return null;
479
+ return (await res.json())[0]?.id ?? null;
480
+ } catch {
481
+ return null;
482
+ }
483
+ }
484
+
399
485
  // A monitor process (e.g. the Claude Code plugin) may not share the local
400
486
  // state file written by the agent's CLI calls — different spawn tree, so
401
487
  // `agentPid()` can hash to a different key. Fall back to asking the server for
@@ -810,6 +896,7 @@ function printPublished(result, flags) {
810
896
  console.log(`parts${state ? ` (${state})` : ""}: ${parts.map((p) => p.name).join(", ")}`);
811
897
  }
812
898
  if (result.applied?.length) console.log(`applied: ${result.applied.join(", ")}`);
899
+ if (result.flow) console.log(flowLine(result.flow));
813
900
  const changes = result.partChanges;
814
901
  if (changes?.renamed?.length) {
815
902
  console.log(`renamed: ${changes.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
@@ -822,6 +909,11 @@ function printPublished(result, flags) {
822
909
  printFeedback(result.feedback);
823
910
  }
824
911
 
912
+ function flowLine(flow) {
913
+ const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
914
+ return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
915
+ }
916
+
825
917
  // Only speak when there IS feedback: an empty line per write is pure token
826
918
  // cost, and the cursor guarantees anything pending arrives on some write.
827
919
  function printFeedback(feedback) {
@@ -847,14 +939,24 @@ function parsePublishFlags() {
847
939
  allowPositionals: true,
848
940
  options: {
849
941
  ...MOCK_FLAGS,
942
+ situation: { type: "string" },
850
943
  title: { type: "string" },
851
944
  kind: { type: "string" },
945
+ area: { type: "string" },
946
+ source: { type: "string" },
852
947
  html: { type: "string" },
948
+ poster: { type: "string" },
853
949
  surfaces: { type: "string" },
854
950
  parts: { type: "string", multiple: true },
951
+ fills: { type: "string", multiple: true },
855
952
  knobs: { type: "string" },
856
953
  from: { type: "string" },
954
+ fork: { type: "string" },
857
955
  prompt: { type: "string" },
956
+ note: { type: "string" },
957
+ widths: { type: "string" },
958
+ flow: { type: "string" },
959
+ step: { type: "string" },
858
960
  kit: { type: "string", multiple: true },
859
961
  layout: { type: "string" },
860
962
  md: { type: "string", multiple: true },
@@ -873,18 +975,27 @@ function parsePublishFlags() {
873
975
 
874
976
  // `--parts name=file` (repeatable; `name#key` targets one instance): the new
875
977
  // outer html of one part, spliced server-side into the latest version.
876
- function readPartFlags(values, slug) {
978
+ // `--fills part#fill=file` is the same for one fill of a part.
979
+ function readPartFlags(values, slug, flag = "parts") {
877
980
  if (!values?.length) return undefined;
878
- const example = `mockpit publish --mock ${slug} --parts body=body.html`;
981
+ const example =
982
+ flag === "parts"
983
+ ? `mockpit publish --mock ${slug} --parts body=body.html`
984
+ : `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
879
985
  const parts = {};
880
986
  let stdin = false;
881
987
  for (const raw of values) {
882
988
  const at = raw.indexOf("=");
883
- if (at <= 0 || at === raw.length - 1) die(`--parts needs name=file (got "${raw}")`, example);
989
+ if (at <= 0 || at === raw.length - 1) {
990
+ die(
991
+ `--${flag} needs ${flag === "parts" ? "name" : "part#fill"}=file (got "${raw}")`,
992
+ example,
993
+ );
994
+ }
884
995
  const name = raw.slice(0, at);
885
996
  const file = raw.slice(at + 1);
886
997
  if (file === "-") {
887
- if (stdin) die("only one --parts can read stdin", example);
998
+ if (stdin) die(`only one --${flag} can read stdin`, example);
888
999
  stdin = true;
889
1000
  } else if (!existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
890
1001
  parts[name] = readContent(file);
@@ -900,6 +1011,13 @@ async function publishMock() {
900
1011
  if (from !== undefined && !Number.isInteger(from)) {
901
1012
  die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
902
1013
  }
1014
+ const step = flags.step === undefined ? undefined : Number(flags.step);
1015
+ if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
1016
+ die(
1017
+ `--step must be a place in the flow, 1 or more, or 0 (got "${flags.step}")`,
1018
+ `mockpit read`,
1019
+ );
1020
+ }
903
1021
  const file = flags.html ?? positionals[0];
904
1022
  const composed =
905
1023
  file !== undefined || tokens.some((t) => t.kind === "option" && SURFACE_FLAGS.has(t.name));
@@ -907,16 +1025,21 @@ async function publishMock() {
907
1025
  composed && "--html",
908
1026
  flags.surfaces !== undefined && "--surfaces",
909
1027
  flags.parts?.length && "--parts",
1028
+ flags.fills?.length && "--fills",
910
1029
  ].filter(Boolean);
911
1030
  if (given.length > 1) {
912
1031
  die(
913
- `pass one of --html, --surfaces or --parts, not ${given.join(" and ")}`,
1032
+ `pass one of --html, --surfaces, --parts or --fills, not ${given.join(" and ")}`,
914
1033
  `mockpit publish --help`,
915
1034
  );
916
1035
  }
917
1036
  // Checked before the session exists so a typo'd path leaves nothing behind on the server.
918
1037
  if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
1038
+ if (flags.poster !== undefined && !existsSync(flags.poster)) {
1039
+ die(`cannot read ${flags.poster}`, `ls ${flags.poster}`);
1040
+ }
919
1041
  const parts = readPartFlags(flags.parts, slug);
1042
+ const fills = readPartFlags(flags.fills, slug, "fills");
920
1043
  let surfaces =
921
1044
  flags.surfaces === undefined ? undefined : readJsonFlag("surfaces", flags.surfaces);
922
1045
  if (surfaces !== undefined && !Array.isArray(surfaces)) {
@@ -936,21 +1059,38 @@ async function publishMock() {
936
1059
  }
937
1060
  surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
938
1061
  }
939
- if (!surfaces && !parts)
1062
+ if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
940
1063
  die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
1064
+ const poster =
1065
+ flags.poster === undefined ? undefined : (await uploadFile(flags.poster, { session })).id;
941
1066
  const body = {
942
1067
  session,
943
1068
  project,
944
1069
  mock: slug,
945
1070
  state: flags.state,
1071
+ situation: flags.situation,
946
1072
  variant: flags.variant,
947
1073
  title: flags.title,
948
1074
  kind: flags.kind,
1075
+ area: flags.area,
1076
+ source: flags.source,
949
1077
  ...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
950
1078
  ...(from !== undefined && { from }),
1079
+ ...(flags.fork !== undefined && { fork: flags.fork }),
951
1080
  prompt: flags.prompt,
1081
+ note: flags.note,
1082
+ ...(flags.widths !== undefined && {
1083
+ widths: flags.widths
1084
+ .split(",")
1085
+ .filter((w) => w.trim())
1086
+ .map((w) => Number(w)),
1087
+ }),
1088
+ ...(flags.flow !== undefined && { flow: flags.flow }),
1089
+ ...(step !== undefined && { step }),
952
1090
  ...(surfaces && { surfaces }),
953
1091
  ...(parts && { parts }),
1092
+ ...(fills && { fills }),
1093
+ ...(poster && { poster }),
954
1094
  };
955
1095
  const result = await api(
956
1096
  "/api/mocks",
@@ -962,6 +1102,7 @@ async function publishMock() {
962
1102
  }
963
1103
 
964
1104
  // "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant;
1105
+ // "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
965
1106
  // "Above=knob:trim.position=top" sets one knob. The value is JSON when it
966
1107
  // parses (17, true), else a string, so slider and toggle values keep their type.
967
1108
  function parseOption(raw) {
@@ -980,7 +1121,11 @@ function parseOption(raw) {
980
1121
  }
981
1122
  const at = raw.lastIndexOf("=");
982
1123
  if (at <= 0) return { label: raw };
983
- return { label: raw.slice(0, at), variant: raw.slice(at + 1) };
1124
+ const label = raw.slice(0, at);
1125
+ const bound = raw.slice(at + 1);
1126
+ const pin = /^(.*)@(\d+)$/.exec(bound);
1127
+ if (!pin) return { label, variant: bound };
1128
+ return { label, ...(pin[1] ? { variant: pin[1] } : {}), version: Number(pin[2]) };
984
1129
  }
985
1130
 
986
1131
  // One feedback batch, one line — for a background monitor's notifications.
@@ -1005,6 +1150,20 @@ function watchLines(batch) {
1005
1150
  const lines = [];
1006
1151
  const name = batch.mock ?? "a mock";
1007
1152
  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
+ if (batch.edited) {
1160
+ const e = batch.edited;
1161
+ const bits = [
1162
+ e.kind ? `now a ${e.kind}` : "",
1163
+ e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
1164
+ ].filter(Boolean);
1165
+ lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
1166
+ }
1008
1167
  if (batch.reply) {
1009
1168
  const r = batch.reply;
1010
1169
  const bits = [
@@ -1017,21 +1176,30 @@ function watchLines(batch) {
1017
1176
  .join("; ")
1018
1177
  : "",
1019
1178
  Object.keys(r.tuned ?? {}).length ? `${Object.keys(r.tuned).length} tuned` : "",
1020
- Object.keys(r.mix ?? {}).length
1021
- ? `mix ${Object.entries(r.mix)
1179
+ Object.keys(r.borrowed ?? {}).length
1180
+ ? `borrowed ${Object.entries(r.borrowed)
1022
1181
  .map(([p, v]) => `${p}←${v}`)
1023
1182
  .join(", ")}`
1024
1183
  : "",
1025
- r.comments?.length ? `${r.comments.length} comment${r.comments.length === 1 ? "" : "s"}` : "",
1184
+ ...(r.comments ?? [])
1185
+ .filter((c) => c.borrow)
1186
+ .map(
1187
+ (c) =>
1188
+ `borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? ` (“${c.text}”)` : ""}`,
1189
+ ),
1190
+ (() => {
1191
+ const n = (r.comments ?? []).filter((c) => !c.borrow).length;
1192
+ return n ? `${n} comment${n === 1 ? "" : "s"}` : "";
1193
+ })(),
1026
1194
  r.reactions?.length ? r.reactions.join(", ") : "",
1027
- r.decision ? r.decision.kind : "",
1195
+ r.verdict ? `${r.verdict.kind} ${r.verdict.variant}` : "",
1028
1196
  r.text ?? "",
1029
1197
  ].filter(Boolean);
1030
1198
  lines.push(`mockpit reply on ${name}: ${bits.join(" · ").replace(/\s+/g, " ")}`);
1031
1199
  }
1032
- for (const c of batch.comments ?? []) {
1200
+ for (const c of batch.messages ?? []) {
1033
1201
  lines.push(
1034
- `mockpit comment on ${name}: “${String(c.text ?? "")
1202
+ `mockpit message on ${name}: “${String(c.text ?? "")
1035
1203
  .replace(/\s+/g, " ")
1036
1204
  .trim()}”`,
1037
1205
  );
@@ -1054,8 +1222,13 @@ async function apiSoft(path) {
1054
1222
  return res.json().catch(() => null);
1055
1223
  }
1056
1224
 
1057
- // Keep the scratch directory out of the repo. Returns true when .gitignore was
1058
- // touched, so init can report it.
1225
+ // Keep the scratch directory out of the repo, all but the project's rules
1226
+ // file, which is meant to be committed. Git can't re-include a file under an
1227
+ // ignored directory, so the ignore has to name the directory's contents, and
1228
+ // the `.mockpit/` line older versions wrote is upgraded in place. Returns true
1229
+ // when .gitignore was touched, so init can report it.
1230
+ const MOCKPIT_IGNORE = ".mockpit/*\n!.mockpit/rules.md";
1231
+
1059
1232
  function ignoreMockpitDir() {
1060
1233
  const file = join(process.cwd(), ".gitignore");
1061
1234
  let current = "";
@@ -1064,14 +1237,56 @@ function ignoreMockpitDir() {
1064
1237
  } catch {
1065
1238
  // no .gitignore yet — create one
1066
1239
  }
1067
- if (/^\.mockpit\/?$/m.test(current)) return false;
1240
+ if (/^\.mockpit\/\*$/m.test(current) && /^!\.mockpit\/rules\.md$/m.test(current)) return false;
1241
+ if (/^\.mockpit\/?$/m.test(current)) {
1242
+ writeFileSync(file, current.replace(/^\.mockpit\/?$/m, MOCKPIT_IGNORE));
1243
+ return true;
1244
+ }
1245
+ const lines = /^\.mockpit\/\*$/m.test(current) ? "!.mockpit/rules.md" : MOCKPIT_IGNORE;
1068
1246
  writeFileSync(
1069
1247
  file,
1070
- current && !current.endsWith("\n") ? `${current}\n.mockpit/\n` : `${current}.mockpit/\n`,
1248
+ current && !current.endsWith("\n") ? `${current}\n${lines}\n` : `${current}${lines}\n`,
1071
1249
  );
1072
1250
  return true;
1073
1251
  }
1074
1252
 
1253
+ function repoRoot() {
1254
+ try {
1255
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
1256
+ encoding: "utf8",
1257
+ stdio: ["ignore", "pipe", "ignore"],
1258
+ }).trim();
1259
+ if (top) return top;
1260
+ } catch {}
1261
+ return process.cwd();
1262
+ }
1263
+
1264
+ // Where `mockpit serve` reads the user's own rules: next to its database.
1265
+ const myRulesFile = () =>
1266
+ join(dirname(process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db")), "rules.md");
1267
+
1268
+ // Sends a rules file as it is on disk: a missing file clears the stored copy,
1269
+ // so deleting the file and re-running is how rules are switched off.
1270
+ async function sendRules(file, path) {
1271
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
1272
+ const result = await api(
1273
+ path,
1274
+ { method: "PUT", body: JSON.stringify({ text }) },
1275
+ { report: "die", fix: "mockpit rules --help" },
1276
+ );
1277
+ return { ...result, missing: !text };
1278
+ }
1279
+
1280
+ function rulesSummary(r) {
1281
+ if (r.cleared) return "none (cleared)";
1282
+ const bits = [
1283
+ r.packs?.length ? `packs ${r.packs.join(", ")}` : null,
1284
+ r.rules ? "own rules" : null,
1285
+ ].filter(Boolean);
1286
+ const unknown = r.unknown?.length ? ` · unknown pack ${r.unknown.join(", ")}` : "";
1287
+ return `${bits.join(" · ") || "empty"}${unknown}`;
1288
+ }
1289
+
1075
1290
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1076
1291
 
1077
1292
  // Semver greater-than for plain x.y.z (mirrors server/app.ts versionGt).
@@ -1318,7 +1533,11 @@ const commands = {
1318
1533
  mkdirSync(dirname(starter), { recursive: true });
1319
1534
  writeFileSync(starter, renderStarter({ ...design, kit }, iconSets));
1320
1535
  say("wrote:", ".mockpit/starter.html");
1321
- if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/)");
1536
+ if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/, keeping .mockpit/rules.md)");
1537
+ const rulesFile = join(repoRoot(), ".mockpit", "rules.md");
1538
+ if (existsSync(rulesFile)) {
1539
+ say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
1540
+ }
1322
1541
  say("next:", "mockpit guide");
1323
1542
  if (flags.json) out({ project, design: stored, starter });
1324
1543
  },
@@ -1335,15 +1554,20 @@ const commands = {
1335
1554
  project: { type: "string" },
1336
1555
  session: { type: "string" },
1337
1556
  option: { type: "string", multiple: true },
1557
+ recommend: { type: "string" },
1338
1558
  scope: { type: "string" },
1339
1559
  state: { type: "string" },
1340
1560
  part: { type: "string" },
1341
1561
  multi: { type: "boolean" },
1342
1562
  id: { type: "string" },
1563
+ after: { type: "string" },
1343
1564
  asks: { type: "string" },
1344
1565
  },
1345
1566
  });
1346
- const slug = requireMock(flags, 'mockpit ask --mock writer "Which look?" --option Quiet=quiet');
1567
+ const slug = requireMock(
1568
+ flags,
1569
+ 'mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet',
1570
+ );
1347
1571
  let asks;
1348
1572
  if (flags.asks !== undefined) {
1349
1573
  if (positionals.length > 0) {
@@ -1359,7 +1583,7 @@ const commands = {
1359
1583
  if (!text)
1360
1584
  die(
1361
1585
  "ask needs a question",
1362
- `mockpit ask --mock ${slug} "Which look?" --option Quiet=quiet`,
1586
+ `mockpit ask --mock ${slug} "Quiet or dark?" --option Quiet=quiet`,
1363
1587
  );
1364
1588
  const options = (flags.option ?? []).map(parseOption);
1365
1589
  if (options.length === 0) {
@@ -1368,6 +1592,17 @@ const commands = {
1368
1592
  `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
1369
1593
  );
1370
1594
  }
1595
+ if (flags.recommend !== undefined) {
1596
+ const pick = options.find(
1597
+ (o) => o.label.toLowerCase() === flags.recommend.trim().toLowerCase(),
1598
+ );
1599
+ if (!pick)
1600
+ die(
1601
+ `--recommend "${flags.recommend}" names no option`,
1602
+ `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
1603
+ );
1604
+ pick.recommended = true;
1605
+ }
1371
1606
  asks = [
1372
1607
  {
1373
1608
  text,
@@ -1377,6 +1612,7 @@ const commands = {
1377
1612
  ...(flags.state && { state: flags.state }),
1378
1613
  ...(flags.part && { part: flags.part }),
1379
1614
  ...(flags.multi && { multi: true }),
1615
+ ...(flags.after && { after: flags.after }),
1380
1616
  },
1381
1617
  ];
1382
1618
  }
@@ -1414,10 +1650,20 @@ const commands = {
1414
1650
  project: { type: "string" },
1415
1651
  body: { type: "boolean" },
1416
1652
  history: { type: "boolean" },
1653
+ tree: { type: "boolean" },
1417
1654
  },
1418
1655
  });
1419
1656
  const project = resolveProject(flags).name;
1420
1657
  if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
1658
+ if (flags.tree) {
1659
+ return out(
1660
+ await api(
1661
+ `/api/projects/${encodeURIComponent(project)}/tree`,
1662
+ {},
1663
+ { report: "die", fix: `mockpit read --project ${project}` },
1664
+ ),
1665
+ );
1666
+ }
1421
1667
  if (positionals.length === 1) {
1422
1668
  const slug = slugify(positionals[0]);
1423
1669
  return out(
@@ -1439,15 +1685,16 @@ const commands = {
1439
1685
  console.log(
1440
1686
  `${project} · ${mocks.length} mock${mocks.length === 1 ? "" : "s"}` +
1441
1687
  (list.open
1442
- ? ` · ${list.open} open ask${list.open === 1 ? "" : "s"}`
1688
+ ? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
1443
1689
  : " · nothing waiting on the user"),
1444
1690
  );
1445
1691
  for (const m of mocks) {
1446
1692
  const states = m.states.length ? m.states.join(" / ") : "single state";
1447
1693
  console.log(
1448
- ` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.open ? ` · ${m.open} open` : ""}`,
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` : ""}`,
1449
1695
  );
1450
1696
  }
1697
+ for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
1451
1698
  for (const line of pendingLines(list.pending)) console.log(line);
1452
1699
  },
1453
1700
  async export() {
@@ -1473,6 +1720,15 @@ const commands = {
1473
1720
  const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
1474
1721
  mkdirSync(dir, { recursive: true });
1475
1722
  writeFileSync(join(dir, "index.html"), entry.html ?? "");
1723
+ // The state with every fill, when index.html is the composed choice.
1724
+ if (entry.allFills) writeFileSync(join(dir, "all-fills.html"), entry.allFills);
1725
+ const decided = data.decisions?.states?.[entry.state ?? ""];
1726
+ if (decided) {
1727
+ writeFileSync(
1728
+ join(dir, "decisions.json"),
1729
+ JSON.stringify({ ...decided, knobs: data.decisions.knobs }, null, 2) + "\n",
1730
+ );
1731
+ }
1476
1732
  writeFileSync(
1477
1733
  join(dir, "history.json"),
1478
1734
  JSON.stringify(
@@ -1560,9 +1816,13 @@ const commands = {
1560
1816
  // feedback is delivered exactly once across watch, feedback, and piggyback.
1561
1817
  let firstAfter = flags.after;
1562
1818
  for (;;) {
1563
- const session = (await resolveSession(flags)) ?? (await resolveSessionByCwd());
1819
+ const key = harnessKey();
1820
+ const session =
1821
+ (await resolveSession(flags)) ??
1822
+ (key ? await resolveSessionByKey(key) : await resolveSessionByCwd());
1564
1823
  if (!session) {
1565
- await sleep(2000);
1824
+ // A conversation that hasn't used mockpit yet may never; ask rarely.
1825
+ await sleep(key ? 10_000 : 2000);
1566
1826
  continue;
1567
1827
  }
1568
1828
  let result;
@@ -1612,6 +1872,102 @@ const commands = {
1612
1872
  console.log(`said on ${slug}`);
1613
1873
  printFeedback(result.feedback);
1614
1874
  },
1875
+ async edit() {
1876
+ const { values: flags } = parse({
1877
+ options: {
1878
+ mock: { type: "string" },
1879
+ project: { type: "string" },
1880
+ session: { type: "string" },
1881
+ slug: { type: "string" },
1882
+ title: { type: "string" },
1883
+ kind: { type: "string" },
1884
+ area: { type: "string" },
1885
+ source: { type: "string" },
1886
+ rename: { type: "string" },
1887
+ "area-order": { type: "string" },
1888
+ },
1889
+ });
1890
+ const project = resolveProject(flags).name;
1891
+ const session = (await resolveSession(flags)) ?? undefined;
1892
+ const areaOrder = flags["area-order"];
1893
+ if (flags.rename !== undefined || areaOrder !== undefined) {
1894
+ if (flags.mock) {
1895
+ die(
1896
+ "--rename and --area-order edit the project; edit the mock without them",
1897
+ "mockpit edit --help",
1898
+ );
1899
+ }
1900
+ if (flags.rename !== undefined && !flags.rename.trim()) {
1901
+ die("--rename needs the new name", `mockpit edit --rename <name>`);
1902
+ }
1903
+ const result = await api(
1904
+ projectPath(project),
1905
+ {
1906
+ method: "PATCH",
1907
+ body: JSON.stringify({
1908
+ name: flags.rename,
1909
+ ...(areaOrder === undefined ? {} : { areaOrder: areaOrder.split(",") }),
1910
+ session,
1911
+ }),
1912
+ },
1913
+ { report: "die", fix: "mockpit read" },
1914
+ );
1915
+ if (flags.json) return out(result);
1916
+ if (flags.quiet) return;
1917
+ if (result.areaOrder) {
1918
+ console.log(
1919
+ `area order ${result.areaOrder.length ? result.areaOrder.join(", ") : "alphabetical"} · ${result.url}`,
1920
+ );
1921
+ }
1922
+ for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1923
+ if (!result.renamed) return printFeedback(result.feedback);
1924
+ console.log(
1925
+ `renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.mocks} mock${result.mocks === 1 ? "" : "s"} · ${result.url}`,
1926
+ );
1927
+ if (!flags.project && !process.env.MOCKPIT_PROJECT) {
1928
+ console.log(
1929
+ `note: this repo still resolves to "${project}"; pass --project ${shq(result.project)} or set MOCKPIT_PROJECT`,
1930
+ );
1931
+ }
1932
+ printFeedback(result.feedback);
1933
+ return;
1934
+ }
1935
+ const slug = requireMock(flags, "mockpit edit --mock writer --area Shell");
1936
+ const fields = ["slug", "title", "kind", "area", "source"].filter(
1937
+ (k) => flags[k] !== undefined,
1938
+ );
1939
+ if (!fields.length) {
1940
+ die("edit needs --slug, --title, --kind, --area or --source", "mockpit edit --help");
1941
+ }
1942
+ const result = await api(
1943
+ mockPath(slug),
1944
+ {
1945
+ method: "PATCH",
1946
+ body: JSON.stringify({
1947
+ project,
1948
+ session,
1949
+ ...Object.fromEntries(fields.map((k) => [k, flags[k]])),
1950
+ }),
1951
+ },
1952
+ { report: "die", fix: `mockpit read ${slug}` },
1953
+ );
1954
+ if (flags.json) return out(result);
1955
+ if (flags.quiet) return;
1956
+ const m = result.mock;
1957
+ console.log(
1958
+ [
1959
+ result.renamed ? `${result.renamed.from} → ${m.slug}` : m.slug,
1960
+ m.kind,
1961
+ m.area && `area ${m.area}`,
1962
+ m.source && `source ${m.source}`,
1963
+ result.url,
1964
+ ]
1965
+ .filter(Boolean)
1966
+ .join(" · "),
1967
+ );
1968
+ for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1969
+ printFeedback(result.feedback);
1970
+ },
1615
1971
  async demo() {
1616
1972
  parse();
1617
1973
  const { DEMO } = await import("./demoData.js");
@@ -1629,6 +1985,7 @@ const commands = {
1629
1985
  session: session.id,
1630
1986
  project: DEMO.project,
1631
1987
  mock: DEMO.slug,
1988
+ kind: "page",
1632
1989
  title: DEMO.title,
1633
1990
  state: state.label,
1634
1991
  variant: variant.name,
@@ -1760,6 +2117,24 @@ const commands = {
1760
2117
  return printBrief(flags);
1761
2118
  },
1762
2119
 
2120
+ // A rules file is the user's or the project's own guidance on top of the
2121
+ // core brief; the brief prints it, so this only has to put it on the server.
2122
+ async rules() {
2123
+ const { values: flags } = parse({
2124
+ options: { mine: { type: "boolean" }, project: { type: "string" } },
2125
+ });
2126
+ const mine = !!flags.mine;
2127
+ const file = mine ? myRulesFile() : join(repoRoot(), ".mockpit", "rules.md");
2128
+ const project = mine ? null : resolveProject(flags).name;
2129
+ const result = await sendRules(file, mine ? "/api/rules" : `${projectPath(project)}/rules`);
2130
+ if (flags.json) return out(result);
2131
+ if (flags.quiet) return;
2132
+ const where = mine ? "your rules" : `project ${project}`;
2133
+ const from = result.missing ? `no ${file}` : file;
2134
+ console.log(`${where}: ${rulesSummary(result)} (from ${from})`);
2135
+ if (!result.missing) console.log("next:", "mockpit guide");
2136
+ },
2137
+
1763
2138
  async setup() {
1764
2139
  parse();
1765
2140
  console.log(await fetchTextWithFallback("/setup", join(ROOT, "guide", "AGENT_SETUP.md")));