@foldspace_npm/harness 0.1.27 → 0.1.29

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 (119) hide show
  1. package/CLAUDE.md +301 -188
  2. package/README.md +54 -36
  3. package/bin/ask.mjs +9 -7
  4. package/bin/attach.mjs +80 -51
  5. package/bin/badge.mjs +28 -8
  6. package/bin/build.ts +19 -6
  7. package/bin/cli.mjs +23 -6
  8. package/bin/inject.mjs +3 -4
  9. package/bin/observe.mjs +22 -6
  10. package/bin/published.mjs +1 -1
  11. package/package.json +6 -2
  12. package/recipes/INDEX.md +13 -8
  13. package/recipes/README.md +30 -7
  14. package/recipes/account-overview/README.md +6 -6
  15. package/recipes/account-overview/agent/actions/show_account_overview.ts +11 -16
  16. package/recipes/account-overview/agent/views/brand.ts +23 -14
  17. package/recipes/account-overview/agent/views/overview.ts +13 -17
  18. package/recipes/bottom-bar/README.md +12 -9
  19. package/recipes/bottom-bar/agent/bottomBar.ts +14 -12
  20. package/recipes/create-a-record/README.md +97 -0
  21. package/recipes/create-a-record/agent/actions/create_project.ts +26 -0
  22. package/recipes/create-a-record/agent/api/projects.ts +62 -0
  23. package/recipes/create-a-record/agent/projects.ts +145 -0
  24. package/recipes/create-a-record/agent/views/approval.ts +181 -0
  25. package/recipes/create-a-record/agent/views/brand.ts +35 -0
  26. package/recipes/create-a-record/agent/views/fields.ts +252 -0
  27. package/recipes/create-a-record/agent/views/project-card.ts +253 -0
  28. package/recipes/create-a-record/agent/views/steps.ts +92 -0
  29. package/recipes/create-a-record/fixtures/owners.ok.json +9 -0
  30. package/recipes/create-a-record/fixtures/project.created.json +1 -0
  31. package/recipes/create-a-record/fixtures/project.rejected.json +1 -0
  32. package/recipes/create-a-record/recipe.json +13 -0
  33. package/recipes/create-a-record/variants/actions/create_project.steps.ts +160 -0
  34. package/recipes/find-by-name/README.md +3 -1
  35. package/recipes/find-by-name/agent/actions/find_project_id.ts +1 -1
  36. package/recipes/lookup-then-view/README.md +73 -0
  37. package/recipes/lookup-then-view/agent/accounts.ts +174 -0
  38. package/recipes/lookup-then-view/agent/actions/find_account.ts +108 -0
  39. package/recipes/lookup-then-view/agent/actions/show_account_overview.ts +42 -0
  40. package/recipes/lookup-then-view/agent/api/accounts.ts +68 -0
  41. package/recipes/lookup-then-view/agent/views/account.ts +183 -0
  42. package/recipes/lookup-then-view/agent/views/brand.ts +35 -0
  43. package/recipes/lookup-then-view/agent/views/format.ts +73 -0
  44. package/recipes/lookup-then-view/agent/views/pick-one.ts +240 -0
  45. package/recipes/lookup-then-view/fixtures/accounts.json +319 -0
  46. package/recipes/lookup-then-view/fixtures/overview.ok.json +122 -0
  47. package/recipes/lookup-then-view/fixtures/overview.unsigned.json +14 -0
  48. package/recipes/lookup-then-view/recipe.json +11 -0
  49. package/recipes/lookup-then-view/studio/find_account.json +10 -0
  50. package/recipes/lookup-then-view/studio/show_account_overview.json +15 -0
  51. package/recipes/opportunities-at-risk/README.md +5 -5
  52. package/recipes/opportunities-at-risk/agent/actions/show_opportunities_at_risk.ts +9 -7
  53. package/recipes/opportunities-at-risk/agent/opportunities.ts +11 -10
  54. package/recipes/opportunities-at-risk/agent/views/at-risk.ts +1 -1
  55. package/recipes/opportunities-at-risk/agent/views/brand.ts +26 -15
  56. package/recipes/pick-from-a-list/README.md +7 -6
  57. package/recipes/pick-from-a-list/agent/actions/choose_project.ts +27 -12
  58. package/recipes/pick-from-a-list/agent/views/brand.ts +30 -9
  59. package/recipes/pick-from-a-list/agent/views/picker.ts +3 -3
  60. package/recipes/prepare-for-a-meeting/README.md +4 -4
  61. package/recipes/prepare-for-a-meeting/agent/actions/prepare_for_meeting.ts +3 -3
  62. package/recipes/records-list/README.md +6 -6
  63. package/recipes/records-list/agent/accounts.ts +3 -1
  64. package/recipes/records-list/agent/actions/list_accounts.ts +7 -7
  65. package/recipes/records-list/agent/views/brand.ts +26 -14
  66. package/recipes/server-rendered-forms/README.md +35 -0
  67. package/recipes/server-rendered-forms/agent/actions/create_record_via_form.ts +62 -0
  68. package/recipes/server-rendered-forms/agent/api/pages.ts +57 -0
  69. package/recipes/server-rendered-forms/agent/forms.ts +192 -0
  70. package/recipes/server-rendered-forms/fixtures/form.errors.html +9 -0
  71. package/recipes/server-rendered-forms/fixtures/form.html +17 -0
  72. package/recipes/server-rendered-forms/fixtures/list.html +9 -0
  73. package/recipes/server-rendered-forms/fixtures/record.html +4 -0
  74. package/recipes/server-rendered-forms/recipe.json +10 -0
  75. package/recipes/several-hosts/README.md +10 -13
  76. package/recipes/several-hosts/agent/hosts.ts +19 -136
  77. package/recipes/several-hosts/fixtures/wrong-page.json +6 -0
  78. package/recipes/several-hosts/recipe.json +3 -3
  79. package/recipes/swap-the-login-method/README.md +16 -5
  80. package/recipes/swap-the-login-method/agent/utils.ts +52 -3
  81. package/recipes/swap-the-login-method/variants/utils.cookies.ts +59 -6
  82. package/recipes/update-meeting-notes/README.md +10 -4
  83. package/recipes/update-meeting-notes/agent/actions/update_meeting_notes.ts +16 -5
  84. package/recipes/upload-contacts/README.md +5 -5
  85. package/recipes/upload-contacts/agent/actions/upload_contacts.ts +7 -7
  86. package/recipes/upload-contacts/agent/views/brand.ts +24 -14
  87. package/recipes/upload-contacts/agent/views/uploader.ts +28 -17
  88. package/recipes/who-is-the-user/README.md +7 -3
  89. package/src/ask-core.mjs +3 -3
  90. package/src/attach-daemon.mjs +9 -2
  91. package/src/attach-helpers.mjs +105 -0
  92. package/src/attach-preflight.mjs +34 -11
  93. package/src/badge-core.mjs +1 -1
  94. package/src/cdp-client.mjs +14 -19
  95. package/src/cdp-ownership.mjs +41 -20
  96. package/src/cli-help.mjs +7 -5
  97. package/src/cli-registry.mjs +65 -46
  98. package/src/diagnostics.mjs +1 -1
  99. package/src/init.mjs +5 -3
  100. package/src/lint/index.mjs +10 -1
  101. package/src/lint/rules.mjs +371 -35
  102. package/src/observe-core.mjs +163 -34
  103. package/src/region.mjs +1 -1
  104. package/src/runtime/agent.ts +16 -12
  105. package/src/runtime/http.ts +25 -15
  106. package/src/runtime/match.ts +1 -1
  107. package/src/runtime/render.ts +4 -4
  108. package/src/transports/README.md +2 -2
  109. package/src/upgrade.mjs +33 -10
  110. package/templates/agent-starter/CLAUDE.md +8 -5
  111. package/templates/agent-starter/README.md +32 -33
  112. package/templates/agent-starter/agent/actions/_example.ts +4 -4
  113. package/templates/agent-starter/agent/actions/index.ts +2 -2
  114. package/templates/agent-starter/agent/constants.ts +4 -3
  115. package/templates/agent-starter/agent/utils.ts +15 -18
  116. package/templates/agent-starter/claude-settings.json +1 -1
  117. package/templates/agent-starter/docs/app-profile.md +19 -14
  118. package/templates/agent-starter/foldspace.dev.json +1 -1
  119. package/recipes/several-hosts/fixtures/nav-record.json +0 -5
package/README.md CHANGED
@@ -12,12 +12,12 @@ else about how a builder runs differs between them; this does not.
12
12
 
13
13
  | | **A — local** *(today)* | **B — remote** *(designed, unbuilt)* |
14
14
  |---|---|---|
15
- | Who runs the agent | Claude Code on the builder's desktop | Claude Agent SDK in GCP |
16
- | Whose browser | an isolated Chrome the harness launches | the user's **own** Chrome |
15
+ | Where the build runs | Claude Code on the human's computer | Claude Agent SDK in GCP |
16
+ | Whose browser | an isolated Chrome the harness launches | the human's **own** Chrome |
17
17
  | How code reaches the page | harness CDP preparation | builder extension loads the cloud artifact |
18
18
  | Prerequisites | Claude Code, a client repo, Node, an isolated Chrome profile | the extension installed |
19
- | Session and cookies | in a profile the harness creates | the user's own, untouched |
20
- | Code and history | a git repo on the builder's machine | a workspace in the hosted environment |
19
+ | Session and cookies | in a profile the harness creates | the human's own, untouched |
20
+ | Code and history | a git repo on the human's computer | a workspace in the hosted environment |
21
21
 
22
22
  "Export to repo" moves a build from B to A — **and it is only a copy, not a
23
23
  rewrite, while `agent/` is identical in both.** Keeping it identical is the
@@ -29,7 +29,7 @@ constraint this package exists to protect.
29
29
  nothing more. Actions built in one track run unmodified in the other.
30
30
  - `agent/utils.ts` — configures and re-exports `@foldspace_npm/harness/runtime`
31
31
  - `foldspace build` — esbuild → `dist/index.js`
32
- - `foldspace deploy` — publish to `agent/actions/<env>/<productId>/<agentApiName>`
32
+ - `foldspace deploy` (Foldspace-internal) - uploads `dist/index.js` to `agent/actions/<env>/<productId>/<agentApiName>`
33
33
  - fixtures and tests
34
34
  - **the verb interface** below
35
35
 
@@ -95,7 +95,7 @@ Non-interactive / CI form:
95
95
 
96
96
  ```bash
97
97
  npx --yes @foldspace_npm/harness init foldspace-agent \
98
- --product-id FR8JUQZAQRZB \
98
+ --product-id <product-id> \
99
99
  --agent-key my-agent \
100
100
  --app-domain app.example.com \
101
101
  --name "My Agent"
@@ -105,7 +105,7 @@ When running directly from a harness checkout during development:
105
105
 
106
106
  ```bash
107
107
  node bin/cli.mjs init ../foldspace-agent \
108
- --product-id FR8JUQZAQRZB \
108
+ --product-id <product-id> \
109
109
  --agent-key my-agent \
110
110
  --app-domain app.example.com
111
111
  ```
@@ -115,8 +115,9 @@ not the sidecar directory name. `--agent-api-name` remains a deprecated alias
115
115
  for `--agent-key`. `--domain` remains a deprecated alias for `--app-domain`.
116
116
  `--name` is optional and defaults to the target directory
117
117
  name (`foldspace-agent` unless you pass a directory). The product ID must be
118
- the bare ID, not the `EU-…-1-1` SDK loader key. The app domain may be a hostname
119
- or an HTTP(S) URL without a port or path.
118
+ the bare ID, not the SDK key (the one that starts with `EU-`, in every
119
+ region). The app domain may be a hostname or an HTTP(S) URL without a port or
120
+ path.
120
121
 
121
122
  `--region us|eu` (default `us`) is the Foldspace region the account lives in.
122
123
  It is written to `foldspace.dev.json` as `region`, and picks the SDK the
@@ -131,9 +132,11 @@ initialize Git or overwrite files. On a TTY it can offer
131
132
  ```bash
132
133
  cd foldspace-agent
133
134
  npm install --ignore-scripts # if you skipped the install prompt
135
+ # before the first build, CLAUDE.md step 5: a code project's agent/ replaces the scaffold's
134
136
  npm run build
135
- npm run inject
136
- npm run attach
137
+ npm run inject # the human signs in in that window
138
+ npx foldspace observe wait-login
139
+ npx foldspace attach --daemon
137
140
  ```
138
141
 
139
142
  The generated npm scripts intentionally remain the normal project interface;
@@ -146,21 +149,21 @@ to that import with `foldspace upgrade --refresh-instructions`.
146
149
  ### Keep the harness current
147
150
 
148
151
  Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
149
- newer package does not move them. Check, ask the user, then bump:
152
+ newer package does not move them. Check, ask the human, then bump:
150
153
 
151
154
  ```bash
152
155
  npx --yes @foldspace_npm/harness@latest upgrade --check
153
- foldspace upgrade --yes # only after the user agrees
156
+ foldspace upgrade --yes # only after the human agrees
154
157
  ```
155
158
 
156
159
  `--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
157
- `--yes` rewrites the exact pin and runs `npm install --ignore-scripts`. Rebuild
158
- afterward; that is still not a deploy.
160
+ `--yes` writes the new exact version and runs `npm install --ignore-scripts`.
161
+ Rebuild afterward; that publishes nothing.
159
162
 
160
163
  `foldspace build` and `foldspace attach` run the same check before they start
161
164
  (`attach --status` and `--stop` do not). A project that is behind gets one line
162
165
  on stderr and carries on. A project on a **withdrawn** version (below) refuses
163
- to run and prints the reason. To build on it anyway, once the user has declined
166
+ to run and prints the reason. To build on it anyway, once the human has declined
164
167
  the upgrade:
165
168
 
166
169
  ```bash
@@ -200,13 +203,14 @@ To undo a withdrawal, deprecate with an empty message:
200
203
  npm deprecate @foldspace_npm/harness@0.1.14 ""
201
204
  ```
202
205
 
203
- **It is not instant.** Each project caches the registry's answer for 12 hours
204
- in `.foldspace-dev/harness-update.json`. A project that checked recently keeps
205
- its "fine" answer until that expires, so a withdrawal reaches every project
206
- within 12 hours. It only reaches projects on a harness version that has this
207
- check.
206
+ **It is not instant for `build` and `attach`.** They use the registry's answer
207
+ cached for 12 hours in `.foldspace-dev/harness-update.json`. `foldspace help
208
+ --json` (which the manual reads first) and `foldspace upgrade` ask the registry
209
+ each time and refresh that cache, so a withdrawal reaches a project at its next
210
+ session start, and within 12 hours otherwise. It only reaches projects on a
211
+ harness version that has this check.
208
212
 
209
- ### Lint handlers before they ship
213
+ ### Lint agent code before it ships
210
214
 
211
215
  `foldspace build` runs `foldspace lint` first. Errors skip bundling; warnings
212
216
  print and still produce `dist/index.js`.
@@ -218,12 +222,16 @@ foldspace lint --json
218
222
 
219
223
  v1 flags:
220
224
 
221
- - **error** `no-static-agent-prompt` — `execute` / chatterblock `callback` returning copilot instructions (`directive`, “tell the user”, …)
225
+ - **error** `no-static-agent-prompt` - `execute`, a card's `callback` or its `readyState` handing the agent instructions (`directive`, “tell the user”, …)
222
226
  - **error** `no-runtask-prompt` — `runTask({ data: { prompt } })` and the same keys; a customer API field named `prompt` is allowed
227
+ - **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
228
+ - **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`
229
+ - **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)
223
230
  - **warning** `no-emoji` — emoji in handler or `agent/views` string literals
224
231
  - **warning** `registry-integrity` — duplicate keys, `{ execute }` not registered, registry value missing `execute`
225
232
 
226
- Put action instructions in Agent Studio or MCP. `foldspace lint` cannot see that copy.
233
+ Put action instructions in Agent Studio or with the Foldspace tools. `foldspace
234
+ lint` cannot see that copy.
227
235
 
228
236
  ### Runtime helpers
229
237
 
@@ -232,7 +240,10 @@ Tenant actions import from `agent/utils.ts`, which configures and re-exports
232
240
  runs in the customer's page — it is not a CLI command.
233
241
 
234
242
  `API_BASE` and `AUTH_SOURCE` stamp empty. Fill them from a captured XHR, not a
235
- guess. Custom auth headers stay a tenant override of one function in `utils.ts`.
243
+ guess. For auth that is not Bearer, follow
244
+ `recipes/swap-the-login-method/README.md`: for a custom header, copy its
245
+ `agent/utils.ts` over `agent/utils.ts`; for cookies with no token, its
246
+ `variants/utils.cookies.ts`.
236
247
 
237
248
  ### Choose an attach mode
238
249
 
@@ -242,21 +253,25 @@ guess. Custom auth headers stay a tenant override of one function in `utils.ts`.
242
253
  has an SDK without the configured agent.
243
254
 
244
255
  Use `foldspace help attach` for mode requirements, effects, and safety options.
245
- Coding agents should run `foldspace attach --daemon` so the invoking tool
256
+ Coding agents run `foldspace attach --daemon` so the invoking tool
246
257
  returns after `[lifecycle] inspect_registration:…`. Foreground attach remains
247
- the default for humans watching the terminal. `attach --status` and
258
+ the default for someone watching the terminal. `attach --status` and
248
259
  `attach --stop` inspect and tear down a daemon. Look at the customer's app with
249
- `foldspace observe`: it reads the same window, read-only, while attach runs.
260
+ `foldspace observe`: it reads the test window, read-only, while attach runs.
250
261
 
251
262
  ### Verify actions through the agent
252
263
 
253
264
  `foldspace attach` does not invoke action handlers directly. It loads the local
254
265
  artifact, verifies that the captured local registry matches the configured SDK
255
- agent — including an empty registry — and then observes action callbacks while
266
+ agent - including an empty registry - and then observes action calls while
256
267
  you exercise the normal agent experience. Named actions are not required to
257
268
  attach.
258
269
 
259
- Action output identifies SDK callbacks and local `execute`/`render` phases:
270
+ `npx foldspace ask "<question>" --retry 2` types the question into the agent's
271
+ panel and reports the reply, the card text and a screenshot, and - while attach
272
+ runs - whether execute and render ran.
273
+
274
+ Action output shows the SDK's action calls and local `execute`/`render` phases:
260
275
 
261
276
  ```text
262
277
  [actions] sdk:action show_account_360 executed
@@ -311,8 +326,7 @@ npm i -D @foldspace_npm/harness
311
326
  "build": "foldspace build",
312
327
  "inject": "foldspace inject",
313
328
  "attach": "foldspace attach",
314
- "attach:daemon": "foldspace attach --daemon",
315
- "deploy": "foldspace deploy"
329
+ "attach:daemon": "foldspace attach --daemon"
316
330
  } }
317
331
  ```
318
332
 
@@ -320,9 +334,13 @@ Every command resolves the **consuming** repo — `process.cwd()`, or
320
334
  `FOLDSPACE_PROJECT_DIR` so a hosted builder can point it at a workspace it
321
335
  controls. Config comes from the consumer's `foldspace.dev.json`.
322
336
 
323
- `foldspace deploy` is an explicit remote publication step that reads product
324
- and agent identity from the selected target in `foldspace.dev.json`. It requires
325
- local cloud credentials and is separate from local verification.
337
+ `foldspace deploy` is Foldspace-internal: it writes to Foldspace's own storage,
338
+ which no customer can write to. A coding agent never runs or offers it. With a
339
+ code project, a build session publishes the agent's code with the Foldspace
340
+ tools instead: `repo_write` saves it, and the `deploy` tool publishes it - to
341
+ dev after saying so, to production only on the human's yes - as the manual's
342
+ "Done, and what you say at the end" says. Without a code project, the
343
+ Foldspace team does it with the human.
326
344
 
327
345
  ---
328
346
 
@@ -336,7 +354,7 @@ and **neither had all five**:
336
354
  |---|---|
337
355
  | SDK load guard + retry | On an app with no `document.body` at document-start the append throws; the old guard tested for the stub, so nothing retried and the agent silently never loaded |
338
356
  | Worker release on auto-attach | Auto-attach pauses every worker until the attaching client releases it — the harness froze the app's own workers for the whole session |
339
- | `agentIds` enumeration | `agent({apiName})` returns the OVERLAY handle; arming it on an embedded copilot leaves real conversations untagged |
357
+ | `agentIds` enumeration | `agent({apiName})` returns the OVERLAY handle; arming it on an embedded agent leaves real conversations untagged |
340
358
  | `--test-mode` (off by default) | Test mode was armed unconditionally, hiding exactly what initial setup needs to see; now it is opt-in |
341
359
  | Honest badge | It asserted `TEST MODE` whether or not test mode was on |
342
360
 
package/bin/ask.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // foldspace ask — put a question to the visible assistant through its panel,
2
+ // foldspace ask - put a question to the visible agent through its panel,
3
3
  // exactly as a person would, and report what came back: the reply, any card,
4
4
  // what the handlers did, and a picture.
5
5
  //
6
- // It exists so the coding agent stops asking a non-technical customer to
7
- // "reload, try these two sentences and tell me what you saw".
6
+ // It exists so the coding agent stops asking the human to "reload, try these
7
+ // two sentences and tell me what you saw".
8
8
  //
9
- // node bin/ask.mjs "<question>" [--retry <n>] [--every <s>] [--timeout <s>] [--out <dir>]
9
+ // npx foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>]
10
10
  //
11
11
  // Nothing here reaches into the SDK, patches fetch or injects into the panel:
12
12
  // it opens the panel, types into the composer, presses Enter and reads the
@@ -272,7 +272,7 @@ async function main() {
272
272
  {
273
273
  ok: false,
274
274
  reason:
275
- 'No question. Usage: foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>] [--out <dir>]',
275
+ 'No question. Usage: foldspace ask "<question>" [--retry <n>] [--every <s>] [--timeout <s>]',
276
276
  },
277
277
  1,
278
278
  );
@@ -298,8 +298,10 @@ async function main() {
298
298
  ok: false,
299
299
  question,
300
300
  reason: state.frame
301
- ? "The assistant panel is on the page but never showed a composer."
302
- : "No assistant panel on this page. Run `foldspace attach --daemon` (add --bootstrap when the page has no Foldspace SDK), then ask again.",
301
+ ? "The agent panel is on the page but never showed a composer. Run `npx foldspace observe screenshot` and look at the page."
302
+ : tracked
303
+ ? "No agent panel on this page while attach runs. Read the lines under the last [lifecycle] line in .foldspace-dev/attach-daemon.log, then ask again."
304
+ : "No agent panel on this page. Run `npx foldspace attach --daemon`, follow what it says about the page, then ask again.",
303
305
  url,
304
306
  },
305
307
  1,
package/bin/attach.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * --replace The app embeds a different Foldspace product or agent.
17
17
  * Rebind the page bootstrap to the configured product and
18
- * make the configured agent the sole visible widget owner.
18
+ * make the configured agent the only visible one.
19
19
  *
20
20
  * Chrome 136+ removed --load-extension and does not run content scripts for
21
21
  * CDP-loaded extensions, so neither path uses the extension at runtime.
@@ -25,11 +25,13 @@ import path from "path";
25
25
  import { fileURLToPath } from "url";
26
26
  import {
27
27
  ATTACH_MODES,
28
+ attachAgainFlags,
28
29
  attachModeFromArgs,
29
30
  characterizePageAgents,
30
31
  hostMatches,
31
32
  hostPatternsFromTarget,
32
33
  parseAgentId,
34
+ registrationMismatchReport,
33
35
  registrationVerified,
34
36
  resolveAttachLaunch,
35
37
  } from "../src/attach-helpers.mjs";
@@ -101,8 +103,8 @@ const { useSavedLaunch, port } = resolveAttachLaunch({
101
103
  });
102
104
  if (!port) {
103
105
  console.error(
104
- `attach: no CDP port. Run "foldspace inject" first, or pass --port / CDP_PORT.\n` +
105
- ` Refusing to guess 9222 — that port is often someone else's Chrome.`,
106
+ `attach: no CDP port recorded here. Run \`npm run inject\` first; it opens the test window and records its port.\n` +
107
+ ` Refusing to guess 9222 - that port is often someone else's Chrome.`,
106
108
  );
107
109
  process.exit(1);
108
110
  }
@@ -113,7 +115,7 @@ const claimed = verifyLaunchSentinel({
113
115
  });
114
116
  if (!claimed.ok) {
115
117
  console.error(
116
- `attach: ${ownershipErrorMessage(claimed.reason, { port, profileDir, root })}`,
118
+ `attach: ${ownershipErrorMessage(claimed.reason, { port, command: "attach" })}`,
117
119
  );
118
120
  process.exit(1);
119
121
  }
@@ -124,7 +126,7 @@ const owned = await assertOwnedCdp({
124
126
  });
125
127
  if (!owned.ok) {
126
128
  console.error(
127
- `attach: ${ownershipErrorMessage(owned.reason, { port, profileDir, root })}`,
129
+ `attach: ${ownershipErrorMessage(owned.reason, { port, command: "attach" })}`,
128
130
  );
129
131
  process.exit(1);
130
132
  }
@@ -148,6 +150,8 @@ const noTestMode = !process.argv.includes("--test-mode");
148
150
  // string that asserts you are safe while you are not. It now states what is
149
151
  // actually true. --no-badge drops it entirely, for recording a demo.
150
152
  const noBadge = process.argv.includes("--no-badge");
153
+ // What a restart step names, so it keeps this attach's mode, test mode and tag.
154
+ const againFlags = attachAgainFlags({ mode: attachMode, testMode: !noTestMode, noBadge });
151
155
  // Agent api name for setTestMode — from the same config inject.mjs used.
152
156
  const cfg = JSON.parse(fs.readFileSync(path.join(root, "foldspace.dev.json"), "utf8"));
153
157
  const targetName =
@@ -189,7 +193,7 @@ const unresolved = PLACEHOLDERS.filter((ph) =>
189
193
  if (unresolved.length) {
190
194
  console.error(
191
195
  `attach: foldspace.dev.json still contains placeholders: ${unresolved.join(", ")}\n` +
192
- ` Run "foldspace init" first, or pass --agent <apiName>.\n` +
196
+ ` Run "foldspace init" first, or pass --agent <agent-key>.\n` +
193
197
  ` Refusing to run — injecting a placeholder breaks the app's agent.`
194
198
  );
195
199
  process.exit(1);
@@ -203,7 +207,7 @@ if (bootstrap && (!sdkUrl || typeof sdkUrl !== "string")) {
203
207
  process.exit(1);
204
208
  }
205
209
  if (!fs.existsSync(bundlePath)) {
206
- console.error(`attach: ${path.relative(root, bundlePath)} is missing — run "npm run build" first.`);
210
+ console.error(`attach: ${path.relative(root, bundlePath)} is missing - run \`npm run build\` first.`);
207
211
  process.exit(1);
208
212
  }
209
213
  const hostPatterns = hostPatternsFromTarget(cfgTarget);
@@ -224,10 +228,10 @@ const SDK_PATTERNS = [
224
228
 
225
229
  // The badge has three states, set from outside with \`foldspace badge\`:
226
230
  // label - the small corner tag (default): who is attached, which mode
227
- // working - a pill at the foot of the window: the agent is building in this
228
- // window, please leave it alone
231
+ // working - a pill at the foot of the window: the build session is working
232
+ // in this window, please leave it alone
229
233
  // ready - the pill goes away and the small tag comes back: their turn.
230
- // No bar for that - they may type whatever, whenever
234
+ // No pill for that - they may type whatever, whenever
231
235
  // The pill sits bottom-left, never across the top: a bar there covered the
232
236
  // app's own header and menu (seen on a CRM, 2026-09-22). The agent's panel
233
237
  // is on the right, so bottom-left is the corner nothing else uses.
@@ -260,11 +264,11 @@ const badgeSrc = `(() => {
260
264
  '#foldspace-dev-badge .fs-dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:#FFD314;margin-right:10px;vertical-align:middle;animation:fsPulse 1s ease-in-out infinite}' +
261
265
  '#foldspace-dev-badge .fs-line{position:absolute;left:0;right:0;bottom:0;height:3px;background:rgba(255,255,255,.15);overflow:hidden}' +
262
266
  '#foldspace-dev-badge .fs-line::after{content:"";position:absolute;top:0;width:30%;height:100%;background:#FFD314;animation:fsSweep 1.4s linear infinite}</style>' +
263
- '<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#FFD314">Please leave this window alone</b> until the agent says it is ready.' +
267
+ '<span class="fs-dot"></span>Foldspace is building your agent on this page. <b style="color:#FFD314">Please leave this window alone</b> until you are told in the chat that it is ready.' +
264
268
  (s.text ? " " + s.text.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c]) : "") +
265
269
  '<span class="fs-line"></span>';
266
270
  } else {
267
- // "ready" and "label" both draw the small tag: their turn needs no bar -
271
+ // "ready" and "label" both draw the small tag: their turn needs no pill -
268
272
  // they may type whatever, whenever. Only the wait ever gets a pill.
269
273
  b.style.cssText = base + "background:#3247F2;padding:5px 10px;border-bottom-right-radius:4px;";
270
274
  b.textContent = LABEL;
@@ -292,7 +296,7 @@ const badgeSrc = `(() => {
292
296
  // https://docs.foldspace.ai/reference/test-mode.md
293
297
  //
294
298
  // A handle is scoped by mode: window.foldspace.agent({apiName}) alone returns
295
- // the OVERLAY instance. On an app that embeds the copilot that is NOT the
299
+ // the OVERLAY instance. On an app that embeds the agent in-page that is NOT the
296
300
  // instance serving the chat, so arming it succeeds, logs success, and leaves
297
301
  // the real conversations untagged — the failure that actually leaks. Enumerate
298
302
  // foldspace.agentIds() — each id is shaped "<mode>-<apiName>" — and arm every
@@ -349,7 +353,7 @@ const testModeScript = (enable) => `(() => {
349
353
  try { window.foldspace("when", "ready", armAll); } catch (e) {}
350
354
  armAll();
351
355
 
352
- // The instance serving the chat is built when the copilot panel opens, long
356
+ // The instance serving the chat is built when the agent panel opens, long
353
357
  // after "ready" — arming once is too early. Keep arming.
354
358
  setInterval(armAll, 1000);
355
359
 
@@ -406,6 +410,9 @@ function logLifecycle(result) {
406
410
  if (!result.ok && result.error?.message) {
407
411
  console.log(` ${result.error.message}`);
408
412
  }
413
+ if (!result.ok && Array.isArray(result.details?.findings)) {
414
+ for (const line of result.details.findings) console.log(` ${line}`);
415
+ }
409
416
  if (!result.ok && result.details?.suggestion) {
410
417
  console.log(` Next: ${result.details.suggestion}`);
411
418
  }
@@ -703,6 +710,7 @@ async function prepare(ws, sessionId, url) {
703
710
  mode: agentMode,
704
711
  namespace: pageState?.namespace || "foldspace",
705
712
  aliases,
713
+ testMode: !noTestMode,
706
714
  }),
707
715
  hostPatterns,
708
716
  ),
@@ -718,7 +726,8 @@ async function prepare(ws, sessionId, url) {
718
726
  // Test mode is armed when --test-mode is set, and actively CLEARED otherwise:
719
727
  // it is page-session state that only a reload or setTestMode(false) undoes,
720
728
  // so a page left in test mode by an earlier attach would keep the striped
721
- // banner under a LIVE badge until reload (seen on RoofSnap, 2026-09-30).
729
+ // test-mode banner under a LIVE corner tag until reload (a customer build,
730
+ // 2026-09-30).
722
731
  scripts.push(testModeScript(!noTestMode));
723
732
  if (bootstrap) {
724
733
  scripts.push(
@@ -762,6 +771,45 @@ async function prepare(ws, sessionId, url) {
762
771
  const verification = await verifyPreparedPage(ws, sessionId);
763
772
  // For scoring a session: the moment the customer could first see their agent.
764
773
  if (verification.ok) recordEvent(root, "agent_visible", { mode: attachMode });
774
+ const registrationDetails = {
775
+ mode: attachMode,
776
+ testMode: !noTestMode,
777
+ noBadge,
778
+ expectedAgentId: verification.characterization.expectedAgentId,
779
+ detectedAgentIds: verification.state?.agentIds || [],
780
+ expectedProductId: productId,
781
+ detectedProductIds: verification.products,
782
+ widget: verification.widget,
783
+ registeredActionNames:
784
+ verification.registration?.actionNames || [],
785
+ expectedActionNames:
786
+ verification.observation?.expectedActionNames || [],
787
+ expectedActionCount: Array.isArray(
788
+ verification.observation?.expectedActionNames,
789
+ )
790
+ ? verification.observation.expectedActionNames.length
791
+ : 0,
792
+ missingActionNames:
793
+ verification.registration?.missingActionNames || [],
794
+ unexpectedActionNames:
795
+ verification.registration?.unexpectedActionNames || [],
796
+ actionObserverSubscribed:
797
+ verification.observation?.subscribed === true,
798
+ // null when attach never got as far as the action checks.
799
+ actionObserverInstalled: verification.observation
800
+ ? verification.observation.installed === true
801
+ : null,
802
+ actionCaptureCount: verification.observation
803
+ ? Number(verification.observation.captureCount) || 0
804
+ : null,
805
+ agentFound: verification.registration
806
+ ? verification.registration.agentFound === true
807
+ : null,
808
+ actionNameLimitExceeded:
809
+ verification.observation?.actionNameLimitExceeded === true,
810
+ diagnosticError: verification.diagnosticError,
811
+ };
812
+ const mismatch = verification.ok ? null : registrationMismatchReport(registrationDetails);
765
813
  logLifecycle(
766
814
  createLifecycleResult({
767
815
  operation: "inspect_registration",
@@ -774,32 +822,9 @@ async function prepare(ws, sessionId, url) {
774
822
  message:
775
823
  "The local action registry did not match the configured page agent.",
776
824
  },
777
- details: {
778
- mode: attachMode,
779
- expectedAgentId: verification.characterization.expectedAgentId,
780
- detectedAgentIds: verification.state?.agentIds || [],
781
- expectedProductId: productId,
782
- detectedProductIds: verification.products,
783
- widget: verification.widget,
784
- registeredActionNames:
785
- verification.registration?.actionNames || [],
786
- expectedActionNames:
787
- verification.observation?.expectedActionNames || [],
788
- expectedActionCount: Array.isArray(
789
- verification.observation?.expectedActionNames,
790
- )
791
- ? verification.observation.expectedActionNames.length
792
- : 0,
793
- missingActionNames:
794
- verification.registration?.missingActionNames || [],
795
- unexpectedActionNames:
796
- verification.registration?.unexpectedActionNames || [],
797
- actionObserverSubscribed:
798
- verification.observation?.subscribed === true,
799
- actionNameLimitExceeded:
800
- verification.observation?.actionNameLimitExceeded === true,
801
- diagnosticError: verification.diagnosticError,
802
- },
825
+ details: mismatch
826
+ ? { ...registrationDetails, findings: mismatch.lines, suggestion: mismatch.next }
827
+ : registrationDetails,
803
828
  }),
804
829
  );
805
830
  if (!verification.ok) {
@@ -825,7 +850,7 @@ function noteUncoveredHost(url) {
825
850
  if (coverage.covered || !coverage.pattern || uncoveredHostsSeen.has(coverage.host)) return;
826
851
  uncoveredHostsSeen.add(coverage.host);
827
852
  console.log(` [hosts] the window is on ${coverage.host}, which this project does not cover - the agent is not there.`);
828
- console.log(` Next: add "${coverage.pattern}" to "hosts" in foldspace.dev.json (with the human's yes), then inject and attach again.`);
853
+ console.log(` Next: if it is part of the product, add "${coverage.pattern}" to "hosts" in foldspace.dev.json (with the human's yes), then \`npx foldspace attach --stop\`, \`npm run inject\` and \`npx foldspace attach${againFlags} --daemon\`. If it is not, ask the human to go back to the product in the test window.`);
829
854
  }
830
855
 
831
856
  async function onEvent(ws, msg) {
@@ -1026,11 +1051,11 @@ const ver = await connect(owned.version);
1026
1051
  console.log(`Attached to ${ver.Browser} on :${port}`);
1027
1052
  console.log(`Mode: ${attachMode}`);
1028
1053
  console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
1029
- console.log(`Test: ${noTestMode ? "off — conversations appear in the dashboard (pass --test-mode to hide them)" : "on"}`);
1054
+ console.log(`Test: ${noTestMode ? "off - conversations appear in their Foldspace dashboard (--test-mode is only for an agent that already has real users)" : "on"}`);
1030
1055
  console.log(`Hosts: ${hostPatterns.join(", ")}`);
1031
1056
  console.log(`Serving: ${path.relative(root, bundlePath)}`);
1032
1057
  console.log(
1033
- `CDP: attached. \`foldspace observe\` reads this same window while attach runs; stop attach only to load rebuilt actions.\n`,
1058
+ `CDP: attached. \`npx foldspace observe\` and \`npx foldspace ask\` use this same window while attach runs; stop attach only to load rebuilt actions or changed hosts, or to check published code on dev.\n`,
1034
1059
  );
1035
1060
 
1036
1061
  function printSessionSummary() {
@@ -1136,9 +1161,9 @@ process.on("SIGTERM", requestDetach);
1136
1161
 
1137
1162
  // A closed socket does not mean the browser closed. Chrome drops the CDP
1138
1163
  // connection on target churn and transient errors too, and exiting on those
1139
- // silently ends the swap while the user keeps browsing — the page then loads
1140
- // the deployed bundle with no signal that anything changed. Reconnect, and
1141
- // only give up once the debugging port is genuinely gone.
1164
+ // silently ends the swap while the human keeps browsing - the page then runs
1165
+ // only what the site loads by itself, with no signal that anything changed.
1166
+ // Reconnect, and only give up once the debugging port is genuinely gone.
1142
1167
  async function browserIsUp() {
1143
1168
  try {
1144
1169
  const r = await fetch(`http://localhost:${port}/json/version`);
@@ -1162,12 +1187,16 @@ async function onDisconnect(closedSocket) {
1162
1187
  if (!(await browserIsUp())) {
1163
1188
  clearInterval(keepAlive);
1164
1189
  printSessionSummary();
1165
- console.log("\nBrowser closed. Detaching.");
1190
+ console.log("\nThe test window was closed. Detaching.");
1166
1191
  process.exit(0);
1167
1192
  }
1168
1193
  try {
1169
1194
  console.log(` connection lost — reconnecting (${attempt}/10)…`);
1170
- await connect();
1195
+ // connect() needs the browser's version endpoint; re-check ownership
1196
+ // so attach never adopts another Chrome that took the port meanwhile.
1197
+ const again = await assertOwnedCdp({ profileDir, port, token: devState.sentinel });
1198
+ if (!again.ok) throw new Error(again.reason);
1199
+ await connect(again.version);
1171
1200
  console.log(` reconnected. ${attachMode} mode is live again.`);
1172
1201
  return;
1173
1202
  } catch {
@@ -1176,8 +1205,8 @@ async function onDisconnect(closedSocket) {
1176
1205
  }
1177
1206
  clearInterval(keepAlive);
1178
1207
  printSessionSummary();
1179
- console.error("\nCould not reconnect after 10 attempts. Detaching — "
1180
- + "the page is now loading the DEPLOYED bundle, not your local build.");
1208
+ console.error("\nCould not reconnect after 10 attempts. Detaching - after a reload the page runs only what "
1209
+ + `the site loads by itself, not this build. Run \`npx foldspace attach${againFlags} --daemon\` again.`);
1181
1210
  process.exit(1);
1182
1211
  } finally {
1183
1212
  reconnecting = false;
package/bin/badge.mjs CHANGED
@@ -2,15 +2,16 @@
2
2
  /**
3
3
  * foldspace badge <working|ready|label|off> [--text "<what to show>"]
4
4
  *
5
- * Sets the pill the attach badge draws at the foot of the test window.
6
- * The agent and the human share that one Chrome window, so the pill says
7
- * when to wait:
5
+ * Sets what attach draws in the test window: the pill or the corner tag.
6
+ * You and the human share that one Chrome window, so the pill says when to
7
+ * wait:
8
8
  * working - "Foldspace is building your agent on this page. Please leave
9
- * this window alone until the agent says it is ready."
9
+ * this window alone until you are told in the chat that it is
10
+ * ready."
10
11
  * ready - the pill goes away, the small corner tag comes back: their
11
- * turn. No bar for that - they may type whatever, whenever.
12
+ * turn. No pill for that - they may type whatever, whenever.
12
13
  * label - the small corner tag
13
- * off - remove it
14
+ * off - draw nothing: no pill, no corner tag
14
15
  * The state is kept in the page's sessionStorage, so it survives navigation;
15
16
  * attach's badge script redraws within a second. Nothing else on the page is
16
17
  * touched.
@@ -36,12 +37,31 @@ try {
36
37
  let page;
37
38
  try {
38
39
  page = await CdpPage.open({ projectDir });
40
+ // attach's badge script redraws within a second (after `badge off`, or
41
+ // before the body exists, there is no element yet): give it that second.
39
42
  const shown = await page.evaluate(
40
43
  `sessionStorage.setItem("__foldspace_badge__", ${JSON.stringify(JSON.stringify(payload))});
41
- const b = document.getElementById("foldspace-dev-badge");
44
+ let b = document.getElementById("foldspace-dev-badge");
45
+ for (let i = 0; !b && ${payload.state !== "off"} && i < 8; i++) {
46
+ await new Promise((resolve) => setTimeout(resolve, 250));
47
+ b = document.getElementById("foldspace-dev-badge");
48
+ }
42
49
  return { url: location.href.split(/[?#]/)[0], hadBadge: Boolean(b) };`,
43
50
  );
44
- printJson({ ok: true, state: payload.state, ...(payload.text ? { text: payload.text } : {}), ...shown });
51
+ // Still no element: the state was stored but nothing will show it.
52
+ const note =
53
+ !shown.hadBadge && payload.state !== "off"
54
+ ? {
55
+ note:
56
+ "Nothing is drawn on this page. After `npx foldspace published` that is expected - attach is " +
57
+ "stopped - so say in chat whose turn it is instead. Otherwise run `npx foldspace attach --status`. " +
58
+ "With no live daemon, run `npx foldspace attach --daemon` with the flags it ran with. With one, " +
59
+ "read its log (`logPath`): a `registration_mismatch` there names what differs and the next step; " +
60
+ "without one, attach runs with --no-badge - `npx foldspace attach --stop`, then " +
61
+ "`npx foldspace attach --daemon` with the flags it ran with, minus --no-badge.",
62
+ }
63
+ : {};
64
+ printJson({ ok: true, state: payload.state, ...(payload.text ? { text: payload.text } : {}), ...shown, ...note });
45
65
  } catch (error) {
46
66
  printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
47
67
  process.exit(1);