@foldspace_npm/harness 0.1.32 → 0.1.34

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 (36) hide show
  1. package/CLAUDE.md +169 -49
  2. package/README.md +30 -2
  3. package/bin/inject.mjs +2 -18
  4. package/bin/observe.mjs +18 -0
  5. package/bin/record.mjs +2 -0
  6. package/bin/share.mjs +63 -0
  7. package/bin/sync.mjs +48 -0
  8. package/package.json +1 -1
  9. package/recipes/INDEX.md +1 -1
  10. package/recipes/bottom-bar/README.md +24 -11
  11. package/recipes/bottom-bar/agent/bottomBar.ts +76 -38
  12. package/recipes/bottom-bar/fixtures/configuration.sent.json +13 -12
  13. package/recipes/swap-the-login-method/README.md +1 -1
  14. package/recipes/swap-the-login-method/agent/utils.ts +8 -7
  15. package/recipes/swap-the-login-method/variants/utils.cookies.ts +8 -7
  16. package/src/chrome-binary.mjs +33 -0
  17. package/src/cli-registry.mjs +96 -5
  18. package/src/customer-subdomain.mjs +149 -0
  19. package/src/lint/index.mjs +23 -3
  20. package/src/lint/rules.mjs +46 -2
  21. package/src/lint/safety.mjs +785 -0
  22. package/src/observe-core.mjs +1 -1
  23. package/src/runtime/VERSION +1 -1
  24. package/src/runtime/config.ts +15 -2
  25. package/src/runtime/http.ts +40 -3
  26. package/src/runtime/index.ts +1 -0
  27. package/src/sync-core.mjs +374 -0
  28. package/src/upgrade.mjs +36 -7
  29. package/src/video/compose.mjs +12 -3
  30. package/src/video/recorder.mjs +15 -7
  31. package/src/video/share.mjs +284 -0
  32. package/templates/agent-starter/agent/constants.ts +15 -1
  33. package/templates/agent-starter/agent/utils.ts +3 -0
  34. package/templates/agent-starter/claude-settings.json +3 -1
  35. package/templates/agent-starter/skills/review-experience/SKILL.md +135 -0
  36. package/templates/agent-starter/skills/share-a-video/SKILL.md +47 -4
package/CLAUDE.md CHANGED
@@ -11,15 +11,23 @@ session ends at something they can **see on their own page** - not at
11
11
  ## Every session starts the same way
12
12
 
13
13
  1. **Harness version.** Run `npx foldspace help --json` (from an older install,
14
- `npx --yes @foldspace_npm/harness@latest upgrade --check`). If
15
- `update.outdated` is true, say which version the project is on and which is
16
- newest, and **ask before upgrading**; then `npx foldspace upgrade --yes` and
17
- `npm run build`. Never upgrade as a side effect of build, attach or
18
- implementing an action. `build` and `attach` check too: on an older version
19
- they print one line and carry on; on a version Foldspace **withdrew** they
20
- refuse to run and say why. Tell the human the reason and ask to upgrade.
21
- Only if they decide not to, rerun with `FOLDSPACE_SKIP_UPDATE_CHECK=1` -
22
- never set it on your own. If the project `CLAUDE.md` does not contain
14
+ `npx --yes @foldspace_npm/harness@latest upgrade --check`). Several people
15
+ can build one agent, so every session runs the newest harness:
16
+ - **`update.autoUpgrade` is true** - a newer version in the same line, such
17
+ as 0.1.30 to 0.1.34. Run `npx foldspace upgrade --yes` and
18
+ `npm run build` without asking, and tell the human in one line: *"I've
19
+ updated the Foldspace tools to the newest version."*
20
+ - **`update.outdated` is true, `autoUpgrade` is not** - the newest is a new
21
+ line (0.1 to 0.2). Say which version the project is on and which is
22
+ newest, and **ask before upgrading**; then the same two commands.
23
+
24
+ `build` and `attach` never upgrade by themselves. They check too: on an
25
+ older version they print one line and carry on - upgrade then, the same
26
+ way. On a version Foldspace **withdrew** they refuse to run and say why:
27
+ within the line, upgrade without asking; across a line, tell the human the
28
+ reason and ask. Only if they decide not to, rerun with
29
+ `FOLDSPACE_SKIP_UPDATE_CHECK=1` - never set it on your own. If the project
30
+ `CLAUDE.md` does not contain
23
31
  `@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
24
32
  `npx foldspace upgrade --refresh-instructions`.
25
33
  2. **Is Foldspace connected? Check — never assume.** `region` in
@@ -49,22 +57,18 @@ session ends at something they can **see on their own page** - not at
49
57
  shows them.
50
58
  - **A code project.** Call `repo_read` with command `instructions` and
51
59
  follow what it returns. The project's `agent/` is the copy Foldspace
52
- builds and publishes, and it may hold work someone else saved. Before you
53
- build, replace this folder's `agent/` with it (`repo_read` commands `tree`
54
- and `file`, with the `ref` the instructions name - the default can be
55
- older than what someone saved): delete what is there rather than copying
56
- over the top, or their page shows the template instead of their agent.
57
- This folder's `agent/` may hold work a session never saved, so copy it to
58
- `.foldspace-dev/agent-kept/` first. Put back the action files the project
59
- lacks; where a file differs, the project's wins. If any differed, tell
60
+ builds and publishes, and it may hold work someone else saved. Before
61
+ you build, pull it into this folder (**Several builders, one agent**):
62
+ `npx foldspace sync` replaces the template with their agent, keeps a copy of this folder's
63
+ `agent/` in `.foldspace-dev/agent-kept/`, and puts back unsaved action
64
+ files the project lacks. Where a file differs, the project's wins: tell
60
65
  the human in one line which actions had changes on this computer that
61
66
  were never saved.
62
67
  - **`NO_REPO`.** Nothing is saved yet. Follow the next step `status` gives:
63
68
  it creates the code project in about a minute and changes nothing anyone
64
69
  sees - say you are setting up their project, no need to ask. Get the agent
65
70
  live in this folder meanwhile (L0 needs no code). Once `status` shows the
66
- project, replace this folder's `agent/` with its `agent/` as above, before
67
- you write the first action.
71
+ project, pull its `agent/` as above, before you write the first action.
68
72
  - **A refusal that names `foldspace-ai-access`.** This agent has saved
69
73
  code that Foldspace has not opened to these tools; that is final. Tell
70
74
  the human that Foldspace manages their agent's saved code, so this build
@@ -145,19 +149,29 @@ marks their turn: they may type whatever, whenever; the pill only ever asks them
145
149
  to wait. `npx foldspace badge label` puts the small corner tag back;
146
150
  `npx foldspace badge off` removes it, for a screenshot they will share.
147
151
 
148
- **After they have tried it, the second wave: tell Foldspace who the user is
149
- (item 3 - no question, one line in chat), and offer the rest as one choice,
150
- recommended first.** Once you have handed over (`badge ready`), the human has
151
- asked the agent the question and - with a code project - you have taken the
152
- steps in **Done, and what you say at the end**:
152
+ **After they have tried it, the second wave: write the page starters (item 2)
153
+ and tell Foldspace who the user is (item 3) - no question, one line in chat
154
+ each - and offer the rest as one choice, recommended first.** Once you have
155
+ handed over (`badge ready`), the human has asked the agent the question and -
156
+ with a code project - you have taken the steps in **Done, and what you say at
157
+ the end**:
153
158
 
154
159
  1. **Starters.** The sign-up starters point at actions the agent does not have
155
160
  (clicking one answers "that action isn't available"): replace them with the
156
161
  question this experience answers. `set_conversation_starters` replaces the
157
- whole set - send the full list. Their account, so their yes.
158
- 2. **The bottom bar** - `recipes/bottom-bar`: the agent's resting place at the
159
- foot of the page, with starters that change per screen. Ships in this
160
- project's start-up code; nothing in their source changes.
162
+ whole set - send the full list, **at most 3**, each in the right group
163
+ (**How the agent looks and opens is configuration**). Their account, so
164
+ their yes.
165
+ 2. **Page starters** - `recipes/bottom-bar`. Foldspace already shows the bar
166
+ at the foot of the page; this picks what it offers on each screen. It
167
+ ships in this project's start-up code; nothing in their source changes.
168
+ 1. `npx foldspace observe menu` lists the main-menu pages.
169
+ 2. For each page, write at most 3 starters the agent can answer today.
170
+ Early on most pages have none: leave them out, and they keep Agent
171
+ Studio's starters.
172
+ 3. `ACTION` when clicking it runs an action, `KNOWLEDGE` when the answer
173
+ comes from the knowledge base.
174
+ 4. `npm run build`, attach, and look at one page that has starters.
161
175
  3. **Tell Foldspace who the user is** - `recipes/who-is-the-user`: from then on
162
176
  their conversations and analytics stop being anonymous, and the agent has
163
177
  real-time context about who it is talking to.
@@ -184,7 +198,7 @@ Every question to the human is an **enumerated choice with the recommended
184
198
  option first** (in Claude Code, `AskUserQuestion`; elsewhere, the editor's own question tool), asked once - not repeated in your message. A first session asks only
185
199
  this: which agent (only if several), the app's hostname, which experience
186
200
  first - and a yes wherever this manual asks for one, such as agreeing the
187
- experience (below), upgrading, publishing an action, task agent or
201
+ experience (below), upgrading to a new line (0.1 to 0.2), publishing an action, task agent or
188
202
  navigation, publishing their code for their users, replacing their starters or
189
203
  adding hosts. The hostname is the only free-text question. Everything else -
190
204
  attach mode, folder, name, level - you **state and move on**.
@@ -229,6 +243,42 @@ command refuses the window.
229
243
  with both, the user sees an extra message and the agent takes a second
230
244
  turn.
231
245
 
246
+ ## Each customer on their own subdomain
247
+
248
+ Some products give each of their customers their own address -
249
+ `acme.example.com`, `globex.example.com` - and often a backend that carries
250
+ it too (`acme.api.example.com`). You build signed in to **one** of those
251
+ customers, and the code goes to **all** of them: an address you copy as you
252
+ saw it sends every user's agent to that one customer's data.
253
+
254
+ - **Find out before you write `API_BASE`.** `npx foldspace observe menu` and
255
+ `observe screen` report `customerSubdomain` when the page's address starts
256
+ with what looks like a customer's name, and `screen` names the backend
257
+ calls that carry it too. Ask the human once, as a choice: *"Does each of
258
+ your customers get their own address, like acme.example.com?"* Never decide
259
+ it from the address alone - `reports.` or `eu1.` is a part of the product,
260
+ and only they know.
261
+ - **Write their answer** in `agent/constants.ts`:
262
+ `PER_CUSTOMER_SUBDOMAIN = true` or `false`. Until it is set, observe keeps
263
+ asking and the build refuses an address that may be one customer's.
264
+ - **With `true`, `API_BASE` is built from the page's address**, at run time,
265
+ in the user's own browser:
266
+ - the backend is on the page's own host - `API_BASE = "/api"`;
267
+ - the backend's address carries the customer - write it with
268
+ `{subdomain}` where their name was:
269
+ `API_BASE = "https://{subdomain}.api.example.com"`;
270
+ - one backend for every customer (`api.example.com`, the customer known
271
+ from the sign-in) - the address as it is.
272
+
273
+ The runtime puts each user's own subdomain in (`apiBaseUrl()`), and
274
+ `apiFetch` uses it. A page whose address has no subdomain gets `reason:
275
+ "config"`, never another customer's backend.
276
+ - **Nothing else names one customer either:** a link is a path (`"/invoices/42"`),
277
+ never `https://acme.example.com/invoices/42`. The build refuses one
278
+ customer's address anywhere in `agent/` (`no-fixed-customer-address`).
279
+ - **Record it** in `docs/app-profile.md`: what the human said, and the
280
+ address you observed, with the customer's name marked as theirs.
281
+
232
282
  ## Find the call yourself
233
283
 
234
284
  Never guess `API_BASE`, `AUTH_SOURCE`, an endpoint or a schema. But guessing
@@ -280,7 +330,8 @@ GET only, never submit a form, never click anything that creates, sends or
280
330
  deletes.
281
331
 
282
332
  Capture at least one real HTTP 200 for the data the action needs. If there is
283
- none, pivot; do not create Foldspace resources. Write what you establish into
333
+ none, pivot; do not create Foldspace resources. Before that address goes into
334
+ `API_BASE`, settle **Each customer on their own subdomain**. Write what you establish into
284
335
  `docs/app-profile.md` **with how you know it**, at that moment. Do not promote
285
336
  an assumption by quoting that file later.
286
337
 
@@ -428,6 +479,33 @@ of the human's, read the attach log for the `[actions]` SDK callback and local
428
479
  execute/render lines. Those record names, statuses, durations and parameter
429
480
  keys only - never results or error bodies.
430
481
 
482
+ ## Several builders, one agent
483
+
484
+ With a code project, other people - or other machines - may save to the same
485
+ agent while you work. So this folder pulls the saved code again whenever
486
+ someone saved since it last did. Saving and publishing stay exactly as
487
+ `repo_read` command `instructions` says; this only keeps `agent/` current.
488
+
489
+ **Pull at session start (step 5), before each new experience, and before each
490
+ save or publish:**
491
+
492
+ 1. `repo_read` `status` gives `develop_head`. Then `repo_read` `tree` with
493
+ `ref` `foldspace/<Agent Key>` - the line of work the instructions save to -
494
+ gives its head (the sha after the ref). If that line does not exist yet,
495
+ nothing has been saved to it: use `develop` only.
496
+ 2. `npx foldspace sync check --develop <develop_head>`, plus
497
+ `--line <its head>` when the line exists. Its `baseSha` is the `base_sha`
498
+ for your next save, as the instructions say.
499
+ 3. If `pull` lists anything: read `agent/` at each `ref` (`tree` and `file`)
500
+ into its `into` folder, same paths (`<into>/agent/actions/x.ts`). Then
501
+ `npx foldspace sync apply`, and do what its `next` says. Files under
502
+ `conflicts` changed both here and in the saved code: they now hold the
503
+ saved version - put your change back on top from the kept copy. Then build.
504
+
505
+ **After each save**, `npx foldspace sync saved --head <commit_sha> --paths
506
+ <committed_paths, comma-separated>`, so your own save is not taken for
507
+ someone else's.
508
+
431
509
  ## Done, and what you say at the end
432
510
 
433
511
  A first session is done when the customer has seen one experience answer from
@@ -438,9 +516,16 @@ gates pass, and not when there is nothing left to improve.
438
516
  their users is yours to do. Once they have tried it in the test window, before
439
517
  you offer the second wave:
440
518
 
441
- 1. Say *"I'm saving your changes and publishing them to dev"* - no need to
442
- ask - then save with `repo_write` and publish with the `deploy` tool, the
443
- way `repo_read` command `instructions` says. One exception: a dev preview
519
+ 1. Before **every** save, review the experience with the
520
+ `review-experience` skill (`.claude/skills/review-experience/SKILL.md`;
521
+ any other editor: read
522
+ `node_modules/@foldspace_npm/harness/templates/agent-starter/skills/review-experience/SKILL.md`):
523
+ only the data the answer needs, a yes from the user before every write,
524
+ nothing the user cannot reach in the product. A serious finding stops the
525
+ save until it is fixed. Then say *"I'm saving your changes and publishing
526
+ them to dev"* - no need to
527
+ ask - then pull (**Several builders, one agent**), and save and publish
528
+ the way `repo_read` command `instructions` says. One exception: a dev preview
444
529
  that comes back with no warnings means this agent loads dev for its users -
445
530
  then ask first, as for production. In chat it is "saving your changes",
446
531
  "publishing to dev" and "publishing for your users" - never GitHub,
@@ -466,8 +551,8 @@ with `--bootstrap` or `--replace`, it does not yet: do step 1, keep attach
466
551
  running, and give the answer below for the rest - their site loading the agent
467
552
  is the step the Foldspace team does with them.
468
553
 
469
- A save refused because someone saved after you read is not final: the
470
- instructions say how to redo it. Any other refusal from the Foldspace tools
554
+ A save refused because someone saved after you read is not final: pull again
555
+ (**Several builders, one agent**), put your change on top and save. Any other refusal from the Foldspace tools
471
556
  here is final: then the answer below.
472
557
 
473
558
  **Without a code project**, the answer to *"how do I put this in front of my
@@ -489,7 +574,9 @@ Do not report success without all six:
489
574
 
490
575
  1. `npm run typecheck` compiles (`tsc --noEmit -p tsconfig.json`). Do not run
491
576
  `npx tsc` — it can install the wrong package.
492
- 2. `foldspace lint` reports no errors (`foldspace build` runs it first).
577
+ 2. `foldspace lint` reports no errors (`foldspace build` runs it first). It
578
+ also refuses what is unsafe for the product's users - **What the build
579
+ refuses**, below.
493
580
  3. The handler appears in `dist/index.js`.
494
581
  4. The browser reports `inspect_registration:registration_ok`, and for a named
495
582
  action the captured registry includes it.
@@ -508,12 +595,27 @@ Do not report success without all six:
508
595
  (**Agentic UI components**).
509
596
  6. Browser evidence came from the live target, not hand-authored examples.
510
597
 
598
+ ### What the build refuses
599
+
600
+ The first three are errors: the build stops, and the message says what to do
601
+ instead. Fix the code; never work round the rule. `known-hosts-only` is a
602
+ warning: the build goes on; mention it to the human in one line. Cards may draw
603
+ any markup and code they need - that is what they are for.
604
+
605
+ | Rule | Refuses | Do instead |
606
+ |---|---|---|
607
+ | `no-fixed-customer-address` | One customer's address in `API_BASE` or anywhere in `agent/`, on a product with a subdomain per customer - or one that may be, while `PER_CUSTOMER_SUBDOMAIN` is undecided | `"/api"` or `"https://{subdomain}.…"`; a path for a link (**Each customer on their own subdomain**) |
608
+ | `token-stays-home` | The user's sign-in token returned from `execute` or a card, handed to Foldspace, logged, stored again (storage, a cookie, `window`) or sent to a host that is not the product's backend | Let `apiFetch` add it; log `redact(token)` |
609
+ | `no-secrets` | API keys, JWTs, bearer tokens, cookie values, `.har` captures, storage dumps and `.env` files anywhere in the project | Names and where things live go in `docs/app-profile.md`; values never |
610
+ | `known-hosts-only` | A call (`fetch`, `sendBeacon`, `WebSocket`, `XMLHttpRequest`, `publicFetch`) to a host that is neither the product's own, Foldspace's (`*.foldspace.ai`), nor named in `agent/constants.ts` | The product's backend through `apiFetch`; another host only with the human's yes, as a constant in `agent/constants.ts` |
611
+
511
612
  ## Rules that are not negotiable
512
613
 
513
614
  1. **Handlers return data only.** Never `directive`, `instructions`, or a
514
615
  paragraph telling the agent what to say - those live in Agent Studio.
515
616
  Never return from `execute` a token, a cookie, any other secret or
516
- `ApiResult.detail` (it is for the console): the agent reads what
617
+ `ApiResult.detail` (it is for the console) - the build refuses the
618
+ user's token there (`token-stays-home`): the agent reads what
517
619
  `execute` returns for an action with no card or a view card (rule 2).
518
620
  Validate parameters and return sanitised errors.
519
621
  **What goes back to the agent states what happened - it never says what to
@@ -612,14 +714,29 @@ wins**. <https://docs.foldspace.ai/customize/configuration/>
612
714
  list. It changes their Foldspace account, so it needs their yes. Offer it in
613
715
  the second wave, replacing the sign-up starters that point at actions the
614
716
  agent does not have.
615
- - **Page-specific starters and the bottom bar are code** -
616
- `setConversationStarters` (`null` restores Agent Studio's) and
617
- `setConfiguration({ bottomBarSettings })` then `openBottomBar()`. Send one
618
- whole settings group per call, as the docs do. That code lives in this
619
- project's start-up (`recipes/bottom-bar`, kind `page-setup`) and ships with
620
- the actions bundle, so nothing in the customer's source changes; their own
621
- mount code is the other place it can live, their engineers' call. Offer it in
622
- the second wave.
717
+ 1. **At most 3, both groups together.** They are also the agent panel's
718
+ opening screen, and a long list buries the one that matters. When the
719
+ list already holds more, say which 3 you would keep and ask.
720
+ 2. **Each in its group.** `conversation_starters` takes
721
+ `{"KNOWLEDGE": [{"title": ...}], "ACTION": [{"title": ...}]}`, both keys
722
+ always. `ACTION` when clicking it runs one of the agent's actions (it
723
+ shows the action icon); `KNOWLEDGE` when the answer comes from the
724
+ knowledge base. Starters for the experiences you built go in `ACTION`.
725
+ 3. **`default_type`** picks the group shown first: `ACTION` when only that
726
+ group has starters.
727
+ - **Page starters are code** - `setConversationStarters` (`null` restores
728
+ Agent Studio's), the same 3-at-most and the same two groups;
729
+ `npx foldspace lint` refuses more than 3. That code lives in this project's
730
+ start-up (`recipes/bottom-bar`, kind `page-setup`) and ships with the
731
+ actions bundle, so nothing in the customer's source changes; their own mount
732
+ code is the other place it can live, their engineers' call. The first
733
+ session writes them for the main-menu pages (second wave, item 2).
734
+ - **The bottom bar is on by default** - Foldspace turns it on from its own
735
+ settings, so code never sends `enabled`. Send `bottomBarSettings` only for a
736
+ setting the human asks to change, one whole settings group per call, as the
737
+ docs do, with `enabled: true` in it (⚠️ whether a partial group merges with
738
+ Foldspace's own settings is not verified). `maxVisibleStarters` caps the
739
+ bar only, never the panel.
623
740
  - **An action's `execute` never calls `setConfiguration`**: a partial call from
624
741
  one can collapse the agent's panel to nothing.
625
742
 
@@ -712,7 +829,8 @@ Each of these cost a real build.
712
829
  a mismatch does not error, the action simply never fires.
713
830
  - `agent/api/` — one HTTP helper per endpoint
714
831
  - `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
715
- `AUTH_SOURCE` and `LOAD_MODE`
832
+ `AUTH_SOURCE`, undecided `PER_CUSTOMER_SUBDOMAIN` and `LOAD_MODE`. Any
833
+ host outside the product the human agreed to is named here too
716
834
  - `agent/utils.ts` — configures the harness runtime and re-exports it. **The
717
835
  only import surface for actions.**
718
836
  - `agent/runtime/` — a code project only: this harness's runtime, which every
@@ -760,6 +878,7 @@ not import it — fix the import when you implement.
760
878
  | Helper | Use when | Do not use when |
761
879
  |---|---|---|
762
880
  | `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
881
+ | `apiBaseUrl` | Your own `apiFetch` (custom header, cookies) builds its URLs from it - `"/api"` and `{subdomain}` resolved for this page | Reading `API_BASE` or `getConfig().apiBase` directly: on a product with a subdomain per customer that is the wrong backend |
763
882
  | `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
764
883
  | `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
765
884
  | `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, industry classification codes) |
@@ -790,8 +909,8 @@ Import the helpers the way the project's own `agent/utils.ts` does - from
790
909
  `./runtime` - in a recipe file you copy too. `agent/runtime/` is this harness's
791
910
  runtime, copied in for that reason. A build that changes files there names
792
911
  them: save exactly those files in a save of their own as soon as the agent is
793
- on their page. Say you are bringing their project up to date - no need to ask,
794
- it changes nothing for their users. If their account cannot save, carry on.
912
+ on their page; a save of only those files needs no `review-experience`. Say you are bringing their project up to date - no
913
+ need to ask, it changes nothing for their users. If their account cannot save, carry on.
795
914
  Never edit a file there: the next build puts the harness's copy back. To change
796
915
  how a helper behaves, define it in `agent/utils.ts` and export it from there in
797
916
  place of the runtime's. A project with an `agent/utils/` folder has its own
@@ -830,7 +949,8 @@ import is right.
830
949
  goes. **Never widen your own access**: if something is refused for a site or
831
950
  a permission, say so and ask; do not try others.
832
951
  - Do not commit secrets, cookies, HAR files, browser storage, or
833
- `.foldspace-dev/`. Nothing transient goes in `docs/`.
952
+ `.foldspace-dev/` - the build refuses them in the project (`no-secrets`).
953
+ Nothing transient goes in `docs/`.
834
954
  - Local handler changes are not cloud publication.
835
955
  - Creating or publishing Foldspace resources needs the human's yes - except
836
956
  the agent's code: creating its code project, saving to it and publishing it
package/README.md CHANGED
@@ -157,11 +157,14 @@ to that import with `foldspace upgrade --refresh-instructions`.
157
157
  ### Keep the harness current
158
158
 
159
159
  Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
160
- newer package does not move them. Check, ask the human, then bump:
160
+ newer package does not move them: each session checks and bumps. Several
161
+ builders can work on one agent, so within one line (0.1.x to a newer 0.1.y,
162
+ `update.autoUpgrade`) the coding agent upgrades without asking and says so in
163
+ one line; across a line (0.1 to 0.2) it asks the human first.
161
164
 
162
165
  ```bash
163
166
  npx --yes @foldspace_npm/harness@latest upgrade --check
164
- foldspace upgrade --yes # only after the human agrees
167
+ foldspace upgrade --yes # within the line: no question; across a line: after the human agrees
165
168
  ```
166
169
 
167
170
  `--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
@@ -180,6 +183,27 @@ FOLDSPACE_SKIP_UPDATE_CHECK=1 npm run build
180
183
 
181
184
  That turns the check off entirely; it does not fix the withdrawn version.
182
185
 
186
+ ### Keep up with the saved code
187
+
188
+ With a code project, several builders can save to one agent. `foldspace sync`
189
+ keeps a folder's `agent/` up to date: it never reaches Foldspace itself - the
190
+ coding agent reads the saved code with the Foldspace tools and passes the
191
+ heads it read.
192
+
193
+ ```bash
194
+ npx foldspace sync check --develop <sha> [--line <sha>] # what to pull, and where to write it
195
+ npx foldspace sync apply # merge it into agent/
196
+ npx foldspace sync saved --head <commit_sha> --paths <a,b> # after a save
197
+ ```
198
+
199
+ `--line` is the head of `foldspace/<Agent Key>`, the line of work the code
200
+ project's instructions save to, once it exists. `apply` keeps a copy of
201
+ `agent/` in `.foldspace-dev/agent-kept/<time>/` and merges file by file
202
+ against what the folder last pulled (kept in `.foldspace-dev/saved-code/`): a
203
+ file only one side changed takes that side; a file both changed takes the
204
+ saved version and is listed under `conflicts`. Saving and publishing are
205
+ unchanged: they follow the code project's instructions.
206
+
183
207
  ### Withdrawing a release (maintainers)
184
208
 
185
209
  When a published version is broken badly enough that nobody should build on
@@ -234,6 +258,10 @@ v1 flags:
234
258
  - **error** `no-runtask-prompt` — `runTask({ data: { prompt } })` and the same keys; a customer API field named `prompt` is allowed
235
259
  - **error** `card-contract` - `awaitUserInput: true` on a view card (its `render` uses no `callback`, `cancel` or `readyState`): the chat panel ignores it, but in headless mode the agent waits for a callback that never comes
236
260
  - **error** `no-test-mode-in-bundle` - `armAllInstances` used anywhere in `agent/` (called, or handed on to be called later), or `setTestMode(...)` with anything but a literal `false`. A code project's own copy of the runtime in `agent/runtime/` is not refused for the `setTestMode` inside its `armAllInstances`
261
+ - **error** `no-fixed-customer-address` - on a product with a subdomain per customer (`PER_CUSTOMER_SUBDOMAIN` in `agent/constants.ts`), one customer's address in `API_BASE` or anywhere in `agent/`; while that is undecided, an address whose first part looks like a customer's name (`acme.` - not `app.`, `eu1.`, `reports.`)
262
+ - **error** `token-stays-home` - the user's sign-in token (`getAuthToken`, a session-named storage key, `document.cookie`, followed through variables and helper functions) returned from `execute` or a card, handed to Foldspace, logged, stored again, or sent to a host that is not the product's backend
263
+ - **error** `no-secrets` - API keys, JWTs, bearer tokens, cookie values, secret-named strings, `.har` captures, storage dumps and `.env` files anywhere in the project (not `node_modules/`, `dist/`, `.foldspace-dev/`, `agent/runtime/`)
264
+ - **warning** `known-hosts-only` - `fetch`, `sendBeacon`, `WebSocket`, `EventSource`, `XMLHttpRequest`, `publicFetch` or `import()` to an address written out whose host is neither Foldspace's (`*.foldspace.ai`), the product's (`foldspace.dev.json` hosts, `API_BASE`, the app's own domain) nor written in `agent/constants.ts`
237
265
  - **warning** `card-contract` - any other `awaitUserInput` on a view card, `awaitUserInput` on an input card (not needed), or a `render` that declares `callback`, `cancel` or `readyState` and uses none of them (the SDK treats it as a view card)
238
266
  - **warning** `no-emoji` — emoji in handler or `agent/views` string literals
239
267
  - **warning** `registry-integrity` — duplicate keys, `{ execute }` not registered, registry value missing `execute`
package/bin/inject.mjs CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  waitForOwnedCdp,
21
21
  writeLaunchSentinel,
22
22
  } from "../src/cdp-ownership.mjs";
23
- import { findChrome } from "../src/chrome-binary.mjs";
23
+ import { findChrome, testWindowArgs } from "../src/chrome-binary.mjs";
24
24
 
25
25
  // Resolve the CONSUMING repo, not this package. FOLDSPACE_PROJECT_DIR lets a
26
26
  // hosted builder point the harness at a workspace it controls.
@@ -205,23 +205,7 @@ function nameProfile() {
205
205
  }
206
206
  nameProfile();
207
207
 
208
- const chromeArgs = [
209
- `--user-data-dir=${profileDir}`,
210
- `--remote-debugging-port=${debugPort}`,
211
- // The theme only paints the isolated window Foldspace blue. SDK and action
212
- // code are loaded later by attach over CDP.
213
- `--load-extension=${themeDir}`,
214
- `--disable-extensions-except=${themeDir}`,
215
- // Chrome 136+ dropped --load-extension. Keep the legacy switch for older
216
- // builds, and enable the CDP Extensions domain so we can load it at runtime.
217
- "--disable-features=DisableLoadExtensionCommandLineSwitch",
218
- "--enable-unsafe-extension-debugging",
219
- "--no-first-run",
220
- "--no-default-browser-check",
221
- "--test-type",
222
- "--remote-allow-origins=*",
223
- startUrl,
224
- ];
208
+ const chromeArgs = testWindowArgs({ profileDir, debugPort, themeDir, startUrl });
225
209
 
226
210
  console.log(`\nOpening the test window - profile ${path.relative(root, profileDir)}, debugging port :${debugPort}.`);
227
211
  console.log("The human signs in there; the profile keeps what the site remembers between runs.\n");
package/bin/observe.mjs CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  summarizeJson,
51
51
  toHex,
52
52
  } from "../src/observe-core.mjs";
53
+ import { customerSubdomainHint, readCustomerSubdomainSetting } from "../src/customer-subdomain.mjs";
53
54
  import { recordEvent } from "../src/session-events.mjs";
54
55
  import { resolveProjectDir } from "../src/upgrade.mjs";
55
56
 
@@ -326,6 +327,21 @@ async function runPages() {
326
327
  };
327
328
  }
328
329
 
330
+ // Whether the project has settled PER_CUSTOMER_SUBDOMAIN in agent/constants.ts:
331
+ // once it has, observe stops asking.
332
+ function customerSubdomainSetting() {
333
+ try {
334
+ return readCustomerSubdomainSetting(fs.readFileSync(path.join(projectDir, "agent", "constants.ts"), "utf8"));
335
+ } catch {
336
+ return undefined;
337
+ }
338
+ }
339
+
340
+ function customerSubdomainFields(pageUrl, requestUrls) {
341
+ const found = customerSubdomainHint(pageUrl, requestUrls, customerSubdomainSetting());
342
+ return found ? { customerSubdomain: found } : {};
343
+ }
344
+
329
345
  async function runMenu(page) {
330
346
  const perFrame = await evaluateInFrames(page, navigationScript(null));
331
347
  const seen = new Set();
@@ -351,6 +367,7 @@ async function runMenu(page) {
351
367
  count: items.length,
352
368
  items,
353
369
  ...(siblings ? { otherHosts: siblings.hosts, hostsHint: siblings.hint } : {}),
370
+ ...customerSubdomainFields(url, []),
354
371
  ...(items.length ? {} : { note: "No visible navigation found. The app may still be loading, or showing a login page." }),
355
372
  };
356
373
  }
@@ -440,6 +457,7 @@ async function runScreen(page, flags) {
440
457
  requestsSeen: all.filter(isApiCall).length,
441
458
  candidates: ranked,
442
459
  writesSeen: [...writes.values()].slice(0, 20),
460
+ ...customerSubdomainFields(url, all.filter(isApiCall).map((request) => request.url)),
443
461
  ...(ranked.length
444
462
  ? {}
445
463
  : { note: "No successful JSON GET was seen. The data may load after another click, or arrive inside the HTML." }),
package/bin/record.mjs CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  checkRecordingMatches,
38
38
  closeAllHeadless,
39
39
  loadRecording,
40
+ wideWindowNote,
40
41
  needsChrome,
41
42
  probeEncoder,
42
43
  renderInChrome,
@@ -337,6 +338,7 @@ async function compose(config, recordDir, chromePath) {
337
338
  note: `${Math.round(plan.fit.totalS)} s, longer than the ${TIMING.maxTotalS} s it should be at most, with the waits already shortened: drop a scene, or pick questions with shorter answers.`,
338
339
  }
339
340
  : {}),
341
+ ...(wideWindowNote(recording) ? { wideWindow: wideWindowNote(recording) } : {}),
340
342
  sceneSeconds: plan.clips.filter((clip) => clip.kind === "scene").map((clip) => Math.round(clip.durationS * 10) / 10),
341
343
  lastFrames: lastFrames(recording),
342
344
  };
package/bin/share.mjs ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // foldspace share <inspect|upload> [<video.mp4>] [--poster <file.jpg>] [--targets '<json>']
3
+ //
4
+ // inspect - sizes and checks the MP4 and its poster here; uploads nothing.
5
+ // Its video.bytes and poster.bytes go into the MCP tool
6
+ // create_video_share.
7
+ // upload - sends both files to Foldspace's video storage with the one-time
8
+ // targets create_video_share returned (its upload command), then
9
+ // the agent calls publish_video_share for the link.
10
+ //
11
+ // The CLI holds no Foldspace sign-in: creating and publishing a share are MCP
12
+ // tools, only the bytes go through here. The targets are never printed.
13
+ // Sharing is always the human's call: run upload only after they said yes.
14
+
15
+ import path from "node:path";
16
+
17
+ import { parseArgs, printJson } from "../src/cdp-client.mjs";
18
+ import { recordEvent } from "../src/session-events.mjs";
19
+ import { resolveProjectDir } from "../src/upgrade.mjs";
20
+ import { ShareError, defaultVideo, inspectForShare, parseTargets, posterFor, resumableUpload } from "../src/video/share.mjs";
21
+
22
+ const { flags, rest } = parseArgs(process.argv.slice(2), ["poster", "targets"]);
23
+ const [action, videoArg] = rest;
24
+ const projectDir = path.resolve(resolveProjectDir());
25
+
26
+ try {
27
+ if (action !== "inspect" && action !== "upload") {
28
+ throw new ShareError("USAGE", "Usage: foldspace share <inspect|upload> [<video.mp4>] [--poster <file.jpg>] [--targets '<json>']");
29
+ }
30
+ const videoPath = videoArg ? path.resolve(projectDir, videoArg) : defaultVideo(projectDir);
31
+ const posterPath = flags.poster ? path.resolve(projectDir, String(flags.poster)) : posterFor(videoPath);
32
+ const files = inspectForShare({ videoPath, posterPath });
33
+
34
+ if (action === "inspect") {
35
+ printJson({
36
+ ok: true,
37
+ video: { path: path.relative(projectDir, files.video.path), bytes: files.video.bytes, durationS: round(files.video.durationS) },
38
+ poster: { path: path.relative(projectDir, files.poster.path), bytes: files.poster.bytes },
39
+ next: "Only if the human agreed to share it: call create_video_share with a title, video_bytes and poster_bytes, then run the upload command it returns.",
40
+ });
41
+ } else {
42
+ if (!flags.targets) throw new ShareError("BAD_TARGETS", "--targets is required: run the upload command create_video_share returned.");
43
+ const targets = parseTargets(flags.targets);
44
+ await resumableUpload(targets.video, files.video.path, { label: "video" });
45
+ await resumableUpload(targets.poster, files.poster.path, { label: "poster" });
46
+ recordEvent(projectDir, "share-upload", { videoBytes: files.video.bytes });
47
+ printJson({
48
+ ok: true,
49
+ status: "uploaded",
50
+ videoBytes: files.video.bytes,
51
+ posterBytes: files.poster.bytes,
52
+ next: "Call publish_video_share with the share_id from create_video_share; it returns the public link.",
53
+ });
54
+ }
55
+ } catch (error) {
56
+ const code = error instanceof ShareError ? error.code : "UNEXPECTED";
57
+ printJson({ ok: false, code, message: error instanceof Error ? error.message : String(error) });
58
+ process.exitCode = 1;
59
+ }
60
+
61
+ function round(seconds) {
62
+ return Math.round(seconds * 10) / 10;
63
+ }
package/bin/sync.mjs ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * foldspace sync <check|apply|saved> [options]
4
+ *
5
+ * Keeps this folder's agent/ up to date with the agent's saved code, which
6
+ * several builders can work on. The coding agent reads the saved code with the
7
+ * Foldspace tools and passes the heads it read; this command remembers what
8
+ * this folder last pulled (.foldspace-dev/saved-code/) and merges.
9
+ *
10
+ * check --develop <sha> [--line <sha>]
11
+ * apply
12
+ * saved --head <commit_sha> --paths <a,b,...>
13
+ *
14
+ * Saving and publishing follow the code project's own instructions.
15
+ */
16
+ import { resolveProjectDir } from "../src/upgrade.mjs";
17
+ import { syncApply, syncCheck, syncSaved } from "../src/sync-core.mjs";
18
+
19
+ const VALUE_FLAGS = new Set(["--develop", "--line", "--head", "--paths"]);
20
+
21
+ function parse(argv) {
22
+ const flags = {};
23
+ const rest = [];
24
+ for (let index = 0; index < argv.length; index++) {
25
+ const arg = argv[index];
26
+ if (VALUE_FLAGS.has(arg)) flags[arg.slice(2)] = argv[++index];
27
+ else if (arg.startsWith("--")) throw new Error(`unknown option '${arg}'`);
28
+ else rest.push(arg);
29
+ }
30
+ return { flags, rest };
31
+ }
32
+
33
+ try {
34
+ const projectDir = resolveProjectDir();
35
+ const { flags, rest } = parse(process.argv.slice(2));
36
+ const [verb, extra] = rest;
37
+ if (extra) throw new Error(`unexpected argument '${extra}'`);
38
+ const allowed = { check: ["develop", "line"], apply: [], saved: ["head", "paths"] }[verb];
39
+ if (!allowed) throw new Error("usage: foldspace sync <check|apply|saved> - see foldspace help sync");
40
+ const stray = Object.keys(flags).find((name) => !allowed.includes(name));
41
+ if (stray) throw new Error(`option '--${stray}' does not go with sync ${verb}`);
42
+ const report =
43
+ verb === "check" ? syncCheck(projectDir, flags) : verb === "apply" ? syncApply(projectDir) : syncSaved(projectDir, flags);
44
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
45
+ } catch (error) {
46
+ console.error(`sync: ${error instanceof Error ? error.message : String(error)}`);
47
+ process.exitCode = 1;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldspace_npm/harness",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Build and verify portable Foldspace action artifacts against a live app.",
5
5
  "type": "module",
6
6
  "bin": {
package/recipes/INDEX.md CHANGED
@@ -8,7 +8,7 @@ Read this whole table, then open the recipe closest to the outcome.
8
8
  | [`find-by-name`](find-by-name/) | **L2** | The agent turns a name the user said into the id the next action needs; a count when no name is given; every tie when the name is ambiguous; near-misses when nothing matches | A data action. **No card** | 3 production builds |
9
9
  | [`lookup-then-view`](lookup-then-view/) | **L2** | Show one account the user names - **start here**. One clear match: its card, no question. Several or none: the **pick-one** list - search, pages, one click - then the card: who it is, the money, open deals, people, open cases. The card action's **required** `accountId`, described as coming from the lookup, makes the agent call the lookup first. The app already hands over the id → `account-overview` | Two actions - an input card (`find_account`) and a view card (`show_account_overview`) - with their Agent Studio definitions word for word | 1 production build |
10
10
  | [`pick-from-a-list`](pick-from-a-list/) | **L2** | A clickable list, shown **only** when the user has to choose — with "none of these" and cancel | An input card: the agent waits for the user's answer | 1 production build |
11
- | [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page whose starters change with the screen the user is on - configured from code, the way customers do it | Page set-up, runs once at start-up | 2 production builds |
11
+ | [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page (Foldspace turns it on) whose starters - at most 3 - change with the screen the user is on, set from code | Page set-up, runs once at start-up | 2 production builds |
12
12
  | [`swap-the-login-method`](swap-the-login-method/) | any | `apiFetch` for an app that authenticates with a custom header, or with cookies, instead of a bearer token | A replacement `agent/utils.ts` | 2 production builds (a third uses the default bearer token) |
13
13
  | [`account-overview`](account-overview/) | **L2** | One account on a card, when the app already hands over its id: identity, pipeline total, contacts, cases, meetings. The agent gets what the card shows. An account the user names → `lookup-then-view` | A view card: no `callback`, no `awaitUserInput` | 1 production build |
14
14
  | [`upload-contacts`](upload-contacts/) | **L4** | A CSV is previewed, then written only after the user chooses Import | An input card: the agent waits for the user's answer | 1 production build |