@foldspace_npm/harness 0.1.26 → 0.1.28

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 (120) hide show
  1. package/CLAUDE.md +402 -204
  2. package/README.md +84 -36
  3. package/bin/ask.mjs +19 -9
  4. package/bin/attach.mjs +91 -56
  5. package/bin/badge.mjs +28 -8
  6. package/bin/cli.mjs +23 -6
  7. package/bin/inject.mjs +3 -4
  8. package/bin/observe.mjs +22 -6
  9. package/bin/published.mjs +141 -0
  10. package/package.json +6 -2
  11. package/recipes/INDEX.md +13 -8
  12. package/recipes/README.md +30 -7
  13. package/recipes/account-overview/README.md +6 -6
  14. package/recipes/account-overview/agent/actions/show_account_overview.ts +11 -16
  15. package/recipes/account-overview/agent/views/brand.ts +23 -14
  16. package/recipes/account-overview/agent/views/overview.ts +13 -17
  17. package/recipes/bottom-bar/README.md +12 -9
  18. package/recipes/bottom-bar/agent/bottomBar.ts +14 -12
  19. package/recipes/create-a-record/README.md +97 -0
  20. package/recipes/create-a-record/agent/actions/create_project.ts +26 -0
  21. package/recipes/create-a-record/agent/api/projects.ts +62 -0
  22. package/recipes/create-a-record/agent/projects.ts +145 -0
  23. package/recipes/create-a-record/agent/views/approval.ts +181 -0
  24. package/recipes/create-a-record/agent/views/brand.ts +35 -0
  25. package/recipes/create-a-record/agent/views/fields.ts +252 -0
  26. package/recipes/create-a-record/agent/views/project-card.ts +253 -0
  27. package/recipes/create-a-record/agent/views/steps.ts +92 -0
  28. package/recipes/create-a-record/fixtures/owners.ok.json +9 -0
  29. package/recipes/create-a-record/fixtures/project.created.json +1 -0
  30. package/recipes/create-a-record/fixtures/project.rejected.json +1 -0
  31. package/recipes/create-a-record/recipe.json +13 -0
  32. package/recipes/create-a-record/variants/actions/create_project.steps.ts +160 -0
  33. package/recipes/find-by-name/README.md +3 -1
  34. package/recipes/find-by-name/agent/actions/find_project_id.ts +1 -1
  35. package/recipes/lookup-then-view/README.md +73 -0
  36. package/recipes/lookup-then-view/agent/accounts.ts +174 -0
  37. package/recipes/lookup-then-view/agent/actions/find_account.ts +108 -0
  38. package/recipes/lookup-then-view/agent/actions/show_account_overview.ts +42 -0
  39. package/recipes/lookup-then-view/agent/api/accounts.ts +68 -0
  40. package/recipes/lookup-then-view/agent/views/account.ts +183 -0
  41. package/recipes/lookup-then-view/agent/views/brand.ts +35 -0
  42. package/recipes/lookup-then-view/agent/views/format.ts +73 -0
  43. package/recipes/lookup-then-view/agent/views/pick-one.ts +240 -0
  44. package/recipes/lookup-then-view/fixtures/accounts.json +319 -0
  45. package/recipes/lookup-then-view/fixtures/overview.ok.json +122 -0
  46. package/recipes/lookup-then-view/fixtures/overview.unsigned.json +14 -0
  47. package/recipes/lookup-then-view/recipe.json +11 -0
  48. package/recipes/lookup-then-view/studio/find_account.json +10 -0
  49. package/recipes/lookup-then-view/studio/show_account_overview.json +15 -0
  50. package/recipes/opportunities-at-risk/README.md +5 -5
  51. package/recipes/opportunities-at-risk/agent/actions/show_opportunities_at_risk.ts +9 -7
  52. package/recipes/opportunities-at-risk/agent/opportunities.ts +11 -10
  53. package/recipes/opportunities-at-risk/agent/views/at-risk.ts +1 -1
  54. package/recipes/opportunities-at-risk/agent/views/brand.ts +26 -15
  55. package/recipes/pick-from-a-list/README.md +7 -6
  56. package/recipes/pick-from-a-list/agent/actions/choose_project.ts +27 -12
  57. package/recipes/pick-from-a-list/agent/views/brand.ts +30 -9
  58. package/recipes/pick-from-a-list/agent/views/picker.ts +3 -3
  59. package/recipes/prepare-for-a-meeting/README.md +4 -4
  60. package/recipes/prepare-for-a-meeting/agent/actions/prepare_for_meeting.ts +3 -3
  61. package/recipes/records-list/README.md +6 -6
  62. package/recipes/records-list/agent/accounts.ts +3 -1
  63. package/recipes/records-list/agent/actions/list_accounts.ts +7 -7
  64. package/recipes/records-list/agent/views/brand.ts +26 -14
  65. package/recipes/server-rendered-forms/README.md +35 -0
  66. package/recipes/server-rendered-forms/agent/actions/create_record_via_form.ts +62 -0
  67. package/recipes/server-rendered-forms/agent/api/pages.ts +57 -0
  68. package/recipes/server-rendered-forms/agent/forms.ts +192 -0
  69. package/recipes/server-rendered-forms/fixtures/form.errors.html +9 -0
  70. package/recipes/server-rendered-forms/fixtures/form.html +17 -0
  71. package/recipes/server-rendered-forms/fixtures/list.html +9 -0
  72. package/recipes/server-rendered-forms/fixtures/record.html +4 -0
  73. package/recipes/server-rendered-forms/recipe.json +10 -0
  74. package/recipes/several-hosts/README.md +10 -13
  75. package/recipes/several-hosts/agent/hosts.ts +19 -136
  76. package/recipes/several-hosts/fixtures/wrong-page.json +6 -0
  77. package/recipes/several-hosts/recipe.json +3 -3
  78. package/recipes/swap-the-login-method/README.md +16 -5
  79. package/recipes/swap-the-login-method/agent/utils.ts +52 -3
  80. package/recipes/swap-the-login-method/variants/utils.cookies.ts +59 -6
  81. package/recipes/update-meeting-notes/README.md +10 -4
  82. package/recipes/update-meeting-notes/agent/actions/update_meeting_notes.ts +16 -5
  83. package/recipes/upload-contacts/README.md +5 -5
  84. package/recipes/upload-contacts/agent/actions/upload_contacts.ts +7 -7
  85. package/recipes/upload-contacts/agent/views/brand.ts +24 -14
  86. package/recipes/upload-contacts/agent/views/uploader.ts +28 -17
  87. package/recipes/who-is-the-user/README.md +7 -3
  88. package/src/ask-core.mjs +16 -4
  89. package/src/attach-daemon.mjs +9 -2
  90. package/src/attach-helpers.mjs +105 -0
  91. package/src/attach-preflight.mjs +34 -11
  92. package/src/badge-core.mjs +1 -1
  93. package/src/cdp-client.mjs +15 -19
  94. package/src/cdp-ownership.mjs +41 -20
  95. package/src/cli-help.mjs +7 -5
  96. package/src/cli-registry.mjs +102 -47
  97. package/src/diagnostics.mjs +1 -1
  98. package/src/init.mjs +5 -3
  99. package/src/keep-focus.mjs +17 -13
  100. package/src/lint/index.mjs +8 -1
  101. package/src/lint/rules.mjs +240 -43
  102. package/src/observe-core.mjs +163 -34
  103. package/src/published-core.mjs +392 -0
  104. package/src/region.mjs +1 -1
  105. package/src/runtime/agent.ts +16 -12
  106. package/src/runtime/http.ts +25 -15
  107. package/src/runtime/match.ts +1 -1
  108. package/src/runtime/render.ts +4 -4
  109. package/src/transports/README.md +2 -2
  110. package/src/upgrade.mjs +33 -10
  111. package/templates/agent-starter/CLAUDE.md +8 -5
  112. package/templates/agent-starter/README.md +32 -33
  113. package/templates/agent-starter/agent/actions/_example.ts +4 -4
  114. package/templates/agent-starter/agent/actions/index.ts +2 -2
  115. package/templates/agent-starter/agent/constants.ts +4 -3
  116. package/templates/agent-starter/agent/utils.ts +15 -18
  117. package/templates/agent-starter/claude-settings.json +2 -1
  118. package/templates/agent-starter/docs/app-profile.md +19 -14
  119. package/templates/agent-starter/foldspace.dev.json +1 -1
  120. package/recipes/several-hosts/fixtures/nav-record.json +0 -5
package/src/region.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  // points at that region's agent, recording and tracking hosts - the US SDK on
8
8
  // an EU page talks to a US backend that does not have the EU agents.
9
9
  //
10
- // `server`: the Product MCP's name in the editor, as the region's playbook
10
+ // `server`: the Foldspace tools' server name in the editor, as the region's playbook
11
11
  // installs it. Distinct per region, so both can sit side by side.
12
12
  export const REGIONS = Object.freeze({
13
13
  us: Object.freeze({
@@ -6,7 +6,7 @@ let cached: any | null = null;
6
6
  * Foldspace SDK handle for this agent's **overlay** instance.
7
7
  *
8
8
  * Takes no `mode` on purpose: `foldspace.agent({ apiName })` returns the overlay
9
- * handle. On an app that embeds the copilot in-page that is not the instance
9
+ * handle. On an app that embeds the agent in-page that is not the instance
10
10
  * serving the chat, so arming only it succeeds and leaves real conversations
11
11
  * untagged. For a specific instance, enumerate `window.foldspace.agentIds()`
12
12
  * (`"<mode>-<apiName>"`) and call `foldspace.agent({ apiName, mode })`.
@@ -25,20 +25,24 @@ export function getAgent(): any | null {
25
25
  }
26
26
 
27
27
  /**
28
- * Arm every Foldspace instance on the page for test mode and remote actions.
28
+ * Turns test mode on or off (`testMode`) on every Foldspace instance on the
29
+ * page (`window.foldspace.agentIds()`) and turns on their remote actions: SDK
30
+ * `setTestMode(testMode)` and `setConfiguration({ remoteActionsSettings })`.
29
31
  *
30
- * Calls SDK `setTestMode` and `setConfiguration({ remoteActionsSettings })`.
31
- * Local `foldspace attach` already injects test-mode arming without putting it
32
- * in `dist/index.js`. Prefer that. Importing this helper from an action can
33
- * ship those calls into the production CDN bundle.
32
+ * It does not belong in agent code: any file in `agent/` can ship to
33
+ * production, where test mode would stop real users' conversations being
34
+ * tracked. Leave it out even with `false`, which lint refuses too: it still
35
+ * calls `setConfiguration` on every instance, and a partial `setConfiguration`
36
+ * can shrink the agent's panel to 0×0. Test mode is only for an agent that
37
+ * already has real users, and only through `npx foldspace attach --test-mode`,
38
+ * which never touches the bundle. For an agent with no real users yet, leave
39
+ * test mode off.
34
40
  *
35
- * Do not call from `execute`. Partial `setConfiguration` has collapsed the
36
- * widget to 0×0 in a real build.
37
- *
38
- * @param testMode - When true, mark conversations as test traffic
39
- * @returns Instance ids that armed vs failed
41
+ * @param testMode - `true` marks new conversations on this page as test
42
+ * traffic; `false` (the default) turns that off.
43
+ * @returns The instance ids it set (`armed`) and the ones that threw (`failed`)
40
44
  */
41
- export function armAllInstances(testMode = true): {
45
+ export function armAllInstances(testMode = false): {
42
46
  armed: string[];
43
47
  failed: string[];
44
48
  } {
@@ -96,16 +96,17 @@ export interface ApiSuccess<T> {
96
96
  /**
97
97
  * Failed request. Errors are **returned**, never thrown.
98
98
  *
99
- * Show `error` to a person. Log `detail` to the console only — never put
100
- * `detail` in an action return value; Foldspace may quote that to the user.
99
+ * Show `error` to a person. Log `detail` to the console only - never put
100
+ * `detail` in an action return value; the agent may quote it to the end user.
101
101
  *
102
102
  * `unreachable` is true when the browser never got a response (DNS, CORS,
103
103
  * offline, connection refused). Timeouts set `unreachable: false` and
104
104
  * `error: "Request timed out."`. Empty `API_BASE` / `AUTH_SOURCE` also
105
- * return `{ ok: false }` with status 0 — they do not throw.
105
+ * return `{ ok: false }` with status 0 - they do not throw: `reason: "config"`,
106
+ * `error: "This is not set up yet."`, and which setting is missing in `detail`.
106
107
  *
107
108
  * Branch on `reason`, not on `status`: a search that finds nothing, an expired
108
- * session and a throttled call are three different widget states, and
109
+ * session and a throttled call are three different card states, and
109
110
  * `renderFailure` picks the right one for you.
110
111
  */
111
112
  export interface ApiFailure {
@@ -121,7 +122,7 @@ export interface ApiFailure {
121
122
  status: number;
122
123
  /** Safe, user-facing sentence. Pass this to `renderError`. */
123
124
  error: string;
124
- /** Truncated API body for the console. Never show this in the widget. */
125
+ /** For the console only: the API's body (truncated), or which setting is missing. Never show this in the card. */
125
126
  detail?: string;
126
127
  /**
127
128
  * True when the network itself failed (not HTTP 4xx/5xx, not abort).
@@ -131,7 +132,7 @@ export interface ApiFailure {
131
132
  }
132
133
 
133
134
  /**
134
- * Why a call failed, in terms a widget can act on.
135
+ * Why a call failed, in terms a card can act on.
135
136
  *
136
137
  * - `signed_out` — no token on the page, or the API answered 401. The session
137
138
  * observed at build time is not the session at run time; expect this.
@@ -155,6 +156,11 @@ export type FailureReason =
155
156
  /** Discriminated result of a customer-API call. Branch on `ok`. */
156
157
  export type ApiResult<T> = ApiSuccess<T> | ApiFailure;
157
158
 
159
+ // What reaches the agent and the card when the build is not configured yet:
160
+ // a state, never a developer instruction. The developer hint is in `detail`,
161
+ // for the console only: nothing shows it unless the handler logs it.
162
+ const NOT_SET_UP = "This is not set up yet.";
163
+
158
164
  function missingConfigResult(): ApiResult<never> | null {
159
165
  const config = getConfig();
160
166
  if (!config.apiBase) {
@@ -162,7 +168,8 @@ function missingConfigResult(): ApiResult<never> | null {
162
168
  ok: false,
163
169
  status: 0,
164
170
  reason: "config",
165
- error: "API_BASE is not set — capture the app's XHR first.",
171
+ error: NOT_SET_UP,
172
+ detail: "API_BASE is not set - capture the app's XHR first.",
166
173
  };
167
174
  }
168
175
  if (!config.authSource) {
@@ -170,7 +177,8 @@ function missingConfigResult(): ApiResult<never> | null {
170
177
  ok: false,
171
178
  status: 0,
172
179
  reason: "config",
173
- error: "AUTH_SOURCE is not set — capture the app's session first.",
180
+ error: NOT_SET_UP,
181
+ detail: "AUTH_SOURCE is not set - capture the app's session first.",
174
182
  };
175
183
  }
176
184
  return null;
@@ -196,7 +204,7 @@ function retryAfterMs(res: Response): number | undefined {
196
204
  }
197
205
 
198
206
  /**
199
- * One mapping from HTTP status to a failure a widget can act on.
207
+ * One mapping from HTTP status to a failure a card can act on.
200
208
  *
201
209
  * Exported for a tenant's own `apiFetch` — an app that authenticates with a
202
210
  * custom header or cookies replaces the transport, and should not also have to
@@ -206,7 +214,7 @@ export function httpFailure(res: Response, fallback: string, detail?: string): A
206
214
  const base = { ok: false as const, status: res.status, detail };
207
215
  switch (res.status) {
208
216
  case 401:
209
- return { ...base, reason: "signed_out", error: "Your session expired. Sign in again." };
217
+ return { ...base, reason: "signed_out", error: "Your session has expired." };
210
218
  case 403:
211
219
  return { ...base, reason: "forbidden", error: "You do not have access to that." };
212
220
  case 404:
@@ -216,7 +224,7 @@ export function httpFailure(res: Response, fallback: string, detail?: string): A
216
224
  ...base,
217
225
  reason: "rate_limited",
218
226
  retryAfterMs: retryAfterMs(res),
219
- error: "Too many requests. Try again in a moment.",
227
+ error: "Too many requests right now.",
220
228
  };
221
229
  default:
222
230
  return { ...base, reason: "server", error: fallback };
@@ -245,9 +253,10 @@ const SIGNED_OUT: ApiFailure = {
245
253
  /**
246
254
  * Authenticated JSON request against `apiBase` with `Authorization: Bearer`.
247
255
  *
248
- * Matches the common pattern (two of three early builds). If the live page
249
- * uses cookies, custom headers, or no Bearer, **stop re-exporting this** from
250
- * `agent/utils.ts` and write a local `apiFetch` instead.
256
+ * For an app whose own requests carry `Authorization: Bearer`. If the live
257
+ * page sends something else (a custom header, or cookies with no token),
258
+ * follow `recipes/swap-the-login-method/README.md`: it names the file to copy
259
+ * over `agent/utils.ts` for each case.
251
260
  *
252
261
  * Empty `API_BASE` / `AUTH_SOURCE` return `{ ok: false }` so a guessed host
253
262
  * cannot look like success. Errors never throw and never include the token.
@@ -401,7 +410,8 @@ export async function publicFetch<T = unknown>(
401
410
  ok: false,
402
411
  status: 0,
403
412
  reason: "config",
404
- error: "No public origin is configured for this app.",
413
+ error: NOT_SET_UP,
414
+ detail: "No public origin is set - pass one to publicFetch, or set publicOrigin in configure().",
405
415
  };
406
416
  }
407
417
  const controller = new AbortController();
@@ -41,7 +41,7 @@ const PLAUSIBLE = 0.45;
41
41
  * SDK method degrades the result rather than breaking the action.
42
42
  *
43
43
  * @param items - Records already loaded (e.g. from {@link apiFetch})
44
- * @param query - What the user typed
44
+ * @param query - What the end user typed
45
45
  * @param opts - Fields, identity, optional limit
46
46
  * @returns Up to `limit` items, substring matches first
47
47
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Unstyled chatterblock states. Tenant brand belongs in `agent/views/`.
2
+ * Unstyled card states. Tenant brand belongs in `agent/views/`.
3
3
  *
4
4
  * An action's `render` output cannot be typechecked. Look at a frame. Never
5
5
  * use `innerHTML` with API data.
@@ -38,7 +38,7 @@ function replace(host: ViewHost, node: HTMLElement): void {
38
38
  }
39
39
 
40
40
  /**
41
- * In-progress chatterblock. Unstyled scaffolding — replace with branded UI
41
+ * In-progress card. Unstyled scaffolding - replace with branded UI
42
42
  * when the card is the experience.
43
43
  *
44
44
  * @param host - Foldspace `render` host
@@ -60,8 +60,8 @@ export function renderEmpty(host: ViewHost, message: string): void {
60
60
  }
61
61
 
62
62
  /**
63
- * Recoverable error: the user can retry or narrow. Pass `ApiResult.error`,
64
- * never `detail` — `detail` is the API's own words and is for the console.
63
+ * Recoverable error: the end user can retry or narrow. Pass `ApiResult.error`,
64
+ * never `detail` - it is for the console.
65
65
  *
66
66
  * @param host - Foldspace `render` host
67
67
  * @param message - Safe, user-facing sentence
@@ -6,7 +6,7 @@ The harness does the same three jobs regardless of where the browser is:
6
6
 
7
7
  Track A (`local`) does them over CDP against a browser on this machine.
8
8
  Track B (`extension`) will do them over a websocket to a Web Store extension in
9
- the user's own Chrome.
9
+ the human's own Chrome.
10
10
 
11
11
  `agent/`, `fixtures/` and the build are identical in both — only this layer
12
12
  differs. Keep it that way: "export to repo" from a hosted build is a copy only
@@ -21,4 +21,4 @@ while the two tracks share an identical `agent/` directory.
21
21
  | `navigate` | `Page.navigate` | `tabs.update` |
22
22
 
23
23
  `local` is implemented today as `bin/inject.mjs` + `bin/attach.mjs`.
24
- `extension` is not built; see `architecture-hosted.md`.
24
+ `extension` is not built.
package/src/upgrade.mjs CHANGED
@@ -9,7 +9,7 @@ export const HARNESS_PACKAGE = "@foldspace_npm/harness";
9
9
  export const INSTRUCTION_IMPORT = `@node_modules/${HARNESS_PACKAGE}/CLAUDE.md`;
10
10
  export const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
11
11
  export const NPM_VIEW_TIMEOUT_MS = 2000;
12
- export const UPGRADE_NEXT = "Ask the user, then run foldspace upgrade --yes";
12
+ export const UPGRADE_NEXT = "Ask the human, then run npx foldspace upgrade --yes";
13
13
 
14
14
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
15
  const instructionTemplatePath = path.join(
@@ -267,11 +267,11 @@ function nextAction(report) {
267
267
  }
268
268
  if (report.status === "outdated") {
269
269
  return report.instructionsImport === false
270
- ? `${UPGRADE_NEXT}. If CLAUDE.md is still a snapshot, also pass --refresh-instructions`
270
+ ? "CLAUDE.md does not import the package instructions. Ask the human, then run npx foldspace upgrade --yes --refresh-instructions (the old CLAUDE.md is kept as CLAUDE.md.bak)."
271
271
  : UPGRADE_NEXT;
272
272
  }
273
273
  if (report.status === "upgraded") {
274
- return "Run npm run build. Deploy only if product users should receive the rebuilt runtime.";
274
+ return "Run `npm run build`. If attach is running, run `npx foldspace attach --stop`, then `npx foldspace attach --daemon` with the flags it ran with, to load the rebuild.";
275
275
  }
276
276
  return null;
277
277
  }
@@ -311,8 +311,14 @@ export function checkForUpdate(projectDir, options = {}) {
311
311
  // new answer down. `foldspace upgrade` is always fresh: someone asking to
312
312
  // upgrade minutes after a release was told the previous version was the
313
313
  // latest, for up to 12 hours (seen 2026-09-28).
314
+ // `preferFresh` (help --json, which the manual reads first) asks the
315
+ // registry too, but falls back to the remembered answer when the registry
316
+ // cannot be reached, so an offline session still hears something.
317
+ const preferFresh = options.preferFresh === true;
314
318
  const cached =
315
- options.cache === false || options.fresh === true ? null : readCache(projectDir, now);
319
+ options.cache === false || options.fresh === true || preferFresh
320
+ ? null
321
+ : readCache(projectDir, now);
316
322
  const cachedDeprecation =
317
323
  cached?.deprecation?.version === running ? cached.deprecation : null;
318
324
  if (cached?.latest && cachedDeprecation) {
@@ -329,12 +335,25 @@ export function checkForUpdate(projectDir, options = {}) {
329
335
  ? options.fetchDeprecation(running)
330
336
  : fetchDeprecationFromNpm(running, options);
331
337
  deprecated = message || null;
332
- if (latest && options.cache !== false) {
333
- const entry = { latest };
338
+ // Each of the two answers falls back on its own: the version can arrive
339
+ // while the withdrawn-version check times out, and that warning is the
340
+ // one step 1 must not lose.
341
+ const fetchedLatest = latest;
342
+ const remembered = preferFresh && options.cache !== false ? readCache(projectDir, now) : null;
343
+ const rememberedDeprecation =
344
+ remembered?.deprecation?.version === running ? remembered.deprecation : null;
345
+ if (!fetchedLatest && remembered?.latest) latest = remembered.latest;
346
+ if (typeof message !== "string" && rememberedDeprecation) {
347
+ deprecated = rememberedDeprecation.message || null;
348
+ }
349
+ if (fetchedLatest && options.cache !== false) {
350
+ const entry = { latest: fetchedLatest };
334
351
  // Only cache an answer the registry actually gave, so an offline
335
352
  // run is asked again next time instead of remembered as "fine".
336
353
  if (typeof message === "string") {
337
354
  entry.deprecation = { version: running, message };
355
+ } else if (rememberedDeprecation) {
356
+ entry.deprecation = rememberedDeprecation;
338
357
  }
339
358
  writeCache(projectDir, entry, now);
340
359
  }
@@ -367,7 +386,7 @@ export function checkForUpdate(projectDir, options = {}) {
367
386
  /**
368
387
  * What `build` and `attach` do with an update report before they run:
369
388
  * `block: true` stops the command, `message` (if any) is printed to stderr.
370
- * Never upgrades - that stays the user's decision (CLAUDE.md, step 1).
389
+ * Never upgrades - that stays the human's decision (CLAUDE.md, step 1).
371
390
  * The escape hatch is FOLDSPACE_SKIP_UPDATE_CHECK=1: the report comes back
372
391
  * check_skipped, so nothing here blocks.
373
392
  */
@@ -379,7 +398,7 @@ export function gateForCommand(report) {
379
398
  block: true,
380
399
  message:
381
400
  `${HARNESS_PACKAGE} ${report.installed || report.pinned} was withdrawn: ${report.deprecated}. ` +
382
- `${UPGRADE_NEXT}. If the user declines, rerun with FOLDSPACE_SKIP_UPDATE_CHECK=1 to build on this version anyway.`,
401
+ `${UPGRADE_NEXT}. If the human declines, rerun with FOLDSPACE_SKIP_UPDATE_CHECK=1 to run this version anyway.`,
383
402
  };
384
403
  }
385
404
  if (report.outdated) {
@@ -487,10 +506,14 @@ export async function runUpgrade(argv, options = {}) {
487
506
  log(
488
507
  `${HARNESS_PACKAGE} ${report.pinned || report.installed} -> ${report.latest} is available`,
489
508
  );
490
- const answer = await ask("Upgrade the exact pin in this project? [Y/n] ", options);
509
+ const answer = await ask("Upgrade the version this project uses? [Y/n] ", options);
491
510
  shouldInstall = isAffirmative(answer);
492
511
  if (!shouldInstall) {
493
- return { ...report, status: "declined", next: UPGRADE_NEXT };
512
+ // The answer was no: asking again is not the next step.
513
+ const next = report.broken
514
+ ? "The human declined: do not ask again this session. This version is withdrawn, so build and attach refuse it; run them with FOLDSPACE_SKIP_UPDATE_CHECK=1."
515
+ : "The human declined: carry on with this version, and do not ask again this session.";
516
+ return { ...report, status: "declined", next };
494
517
  }
495
518
  if (!report.instructionsImport && !shouldRefresh) {
496
519
  const refreshAnswer = await ask(
@@ -2,15 +2,18 @@
2
2
 
3
3
  @node_modules/@foldspace_npm/harness/CLAUDE.md
4
4
 
5
- If that import did not resolve, read `node_modules/@foldspace_npm/harness/CLAUDE.md`
6
- before building. What follows is only what is true of **this** product.
5
+ That import is the manual for this build. If it did not resolve, read
6
+ `node_modules/@foldspace_npm/harness/CLAUDE.md` before anything else; if that
7
+ file is missing too, run `npm install --ignore-scripts` first. What follows is
8
+ only what is true of **this** product.
7
9
 
8
10
  ## This product
9
11
 
10
12
  | | |
11
13
  |---|---|
12
- | Agent | `{{AGENT_API_NAME}}` |
13
- | Product | `{{PRODUCT_ID}}` |
14
+ | Agent Key | `{{AGENT_API_NAME}}` |
15
+ | Product ID | `{{PRODUCT_ID}}` |
14
16
  | App | `{{APP_DOMAIN}}` |
15
17
 
16
- Observed API, auth, and account facts for this app live in `docs/app-profile.md`.
18
+ What is known about this app, and how each fact was established, is in
19
+ `docs/app-profile.md`.
@@ -24,63 +24,62 @@ npm run build
24
24
  agent/
25
25
  actions/ one file per action; register actions in index.ts
26
26
  api/ one file per HTTP helper
27
+ views/ cards; brand.ts comes from `npx foldspace observe styles --write`
27
28
  constants.ts product, agent, and domain identifiers
28
- utils.ts Foldspace agent lookup
29
- docs/ optional notes for coding-agent learnings (create when needed)
29
+ utils.ts configures the harness runtime; the only import surface for actions
30
+ docs/
31
+ app-profile.md what is known about this app, and how each fact was established
30
32
  foldspace.dev.json
31
33
  CLAUDE.md imports platform instructions from the installed harness
32
34
  ```
33
35
 
34
- ## Agent learnings
36
+ ## What is known about this app
35
37
 
36
- Coding agents may keep durable, repository-specific lessons under `docs/` so
37
- later sessions do not repeat the same mistakes. Create the folder when the first
38
- note is useful; do not commit an empty `docs/` directory.
39
-
40
- Good notes cover verified gotchas, failed approaches, design rationale, and
41
- useful verification commands. Keep them concise and evidence-based. Before
42
- similar work, read any relevant notes in `docs/`. Do not store secrets, cookies,
43
- HAR files, browser storage, or other transient session data there.
38
+ `docs/app-profile.md` holds every fact established about this app, each with
39
+ how it was established, and every workaround. Never store secrets, cookies, HAR
40
+ files or browser storage there.
44
41
 
45
42
  ## Commands
46
43
 
47
44
  ```bash
48
- npx foldspace help # discover commands, risks, and next steps
49
- npm run build # create dist/index.js
50
- npm run dev # rebuild dist/index.js on changes
51
- npm run inject # launch the dedicated Chrome profile
52
- npm run attach # load local actions and observe the agent over CDP
53
- npm run attach:daemon # same, but return after inspect_registration
45
+ npx foldspace help # commands, risks, and next steps
46
+ npm run build # create dist/index.js
47
+ npm run dev # rebuild dist/index.js on changes
48
+ npm run inject # open the test window; the human signs in there
49
+ npx foldspace observe wait-login # returns when they are signed in
50
+ npx foldspace attach --daemon # load this build into the test window
54
51
  ```
55
52
 
56
- Run `inject` before `attach`. Sign in to the product in the Chrome window that
57
- `inject` opens. To see how the product loads its data, use `npx foldspace
58
- observe` - it reads that same window, read-only, and works while `attach` runs.
59
- `inject` does not generate an application extension; `attach` loads
60
- `dist/index.js` directly through CDP. An empty local registry is valid,
61
- so you can attach before implementing handlers. Coding agents should use
62
- `attach --daemon` (or `npm run attach:daemon`) so the command returns after
53
+ Run `inject` before `attach`. `npx foldspace observe` reads the test window,
54
+ read-only, and works while `attach` runs. `attach` loads `dist/index.js` into
55
+ the page over CDP; an empty registry is valid, so attach before any handler
56
+ exists. Run it as `npx foldspace attach --daemon`: it returns after
63
57
  registration instead of occupying the terminal.
64
58
 
65
- Choose the attach mode from the state of the target page:
59
+ The attach mode is read off the page:
66
60
 
67
61
  - Default swap: the page already has the configured product and agent.
68
- - `npm run attach -- --bootstrap`: the page has no Foldspace SDK.
69
- - `npm run attach -- --replace`: the page has a different product or agent, or
70
- an SDK without the configured agent.
62
+ - `npx foldspace attach --bootstrap --daemon`: the page has no Foldspace SDK.
63
+ - `npx foldspace attach --replace --daemon`: the page has a different product
64
+ or agent, or an SDK without the configured agent.
71
65
 
72
66
  Run `npx foldspace help attach` for full requirements and safety options. Coding
73
67
  agents can read the versioned contract with `npx foldspace help --json`.
74
68
 
75
69
  ## Add an action
76
70
 
77
- 1. Observe the real product workflow in the inject Chrome and capture a real
78
- 200 for the API the action will call.
79
- 2. Create the action as a draft in Agent Studio or through Product MCP.
71
+ 1. Once the human has chosen the experience, observe what it needs in the
72
+ test window (`npx foldspace observe screen`, then `read`) and capture a
73
+ real 200 for the API the action will call.
74
+ 2. Create the action as a draft with the Foldspace tools (`create_action`).
80
75
  3. Copy `agent/actions/_example.ts` to `agent/actions/<action_key>.ts`.
81
76
  4. Implement the handler from that observed request and response.
82
77
  5. Import and register the handler in `agent/actions/index.ts`.
83
- 6. Type-check, rebuild, `attach --daemon`, and verify it through the visible
84
- agent. Publish only when the copilot must call the action.
78
+ 6. Type-check and rebuild; if attach is running, `npx foldspace attach --stop`,
79
+ then `npx foldspace attach --daemon` with the flags it ran with. Ask the human
80
+ before publishing - the agent cannot call the action until it is published.
81
+ On a yes, publish it and prove it with
82
+ `npx foldspace ask "<question>" --retry 2`; on a not yet, it stays a draft:
83
+ say so in one line.
85
84
 
86
85
  Action keys and parameter names must exactly match the action schema.
@@ -17,9 +17,9 @@ export const example_action = {
17
17
 
18
18
  const result = await apiFetch<{ items?: Item[] }>("/__observe_me");
19
19
  if (!result.ok) {
20
- // Pass `reason` through so `render` can call renderFailure — an expired
21
- // session, a 404 and a throttled call are three different widget states.
22
- return { ok: false, error: result.error, reason: result.reason };
20
+ // Pass `reason` through so `render` can call renderFailure - an expired
21
+ // session, a 404 and a throttled call are three different card states.
22
+ return { success: false, error: result.error, reason: result.reason };
23
23
  }
24
24
 
25
25
  const items = result.data.items ?? [];
@@ -30,6 +30,6 @@ export const example_action = {
30
30
 
31
31
  // Zero matches is an answer, not an error. Return the query so the empty
32
32
  // state can name what was searched for.
33
- return { ok: true, query, matches };
33
+ return { success: true, query, matches };
34
34
  },
35
35
  };
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Registration here is the switch: the SDK transmits these to the server, and
4
4
  // the server excludes any active action it does not receive. Comment an entry
5
- // out and the copilot can no longer see it — no unpublishing required.
5
+ // out and the agent can no longer see it - no unpublishing required.
6
6
  //
7
- // LOAD_MODE "injected" publishes the registry for attach to swap. "embedded"
7
+ // LOAD_MODE "injected" puts the registry on window for attach to swap. "embedded"
8
8
  // skips that assignment because the host page already loads Foldspace.
9
9
 
10
10
  import { LOAD_MODE } from "../constants";
@@ -13,8 +13,9 @@ export const API_BASE = "";
13
13
  * `{ kind: "localStorage", name: "access_token" }` or
14
14
  * `{ kind: "cookie", name: "session" }`.
15
15
  *
16
- * Custom headers that are not Bearer belong in a local `apiFetch` override —
17
- * see CLAUDE.md. Do not guess this value.
16
+ * Auth that is not Bearer (a custom header, or cookies with no token) needs
17
+ * its own `apiFetch` - see "Custom auth" in `agent/utils.ts`. Do not guess
18
+ * this value.
18
19
  */
19
20
  export const AUTH_SOURCE:
20
21
  | { kind: "localStorage" | "cookie"; name: string }
@@ -22,7 +23,7 @@ export const AUTH_SOURCE:
22
23
 
23
24
  /**
24
25
  * "injected" assigns `window.__FOLDSPACE_REMOTE_ACTIONS__` so attach can swap
25
- * the bundle. "embedded" does not — the page already publishes handlers
26
+ * the bundle. "embedded" does not - the page already registers handlers
26
27
  * through its own SDK snippet.
27
28
  */
28
29
  export const LOAD_MODE: "injected" | "embedded" = "injected";
@@ -11,8 +11,8 @@
11
11
  * ## What `export *` gives you
12
12
  *
13
13
  * HTTP (customer API, from the signed-in page):
14
- * - `apiFetch` — JSON + `Authorization: Bearer`. Stop re-exporting this if
15
- * the live app uses cookies, custom headers, or no Bearer (see below).
14
+ * - `apiFetch` - JSON + `Authorization: Bearer`. For any other sign-in, see
15
+ * "Custom auth" below.
16
16
  * - `apiFetchBinary` — same auth, raw bytes (file download).
17
17
  * - `publicFetch` — no token; marketing / public origin only.
18
18
  * - `getAuthToken` / `parseJwt` / `redact` — session read and safe logging.
@@ -21,11 +21,11 @@
21
21
  * Foldspace SDK (thin wrappers; the SDK is the source of truth):
22
22
  * - `getAgent` — overlay handle for `searchInList` and similar.
23
23
  * - `rankBy` — substring first, then SDK fuzzy match. Not for domain ranking.
24
- * - `armAllInstances` — **attach/bootstrap only**, never from `execute`.
25
- * Local `foldspace attach` already arms test mode. Importing this from an
26
- * action can put those SDK calls in the production `dist` bundle.
24
+ * - `armAllInstances` - never in agent code: attach handles the page, and
25
+ * test mode is `npx foldspace attach --test-mode`, only for an agent that
26
+ * already has real users. Anything in `agent/` ships to production.
27
27
  *
28
- * Widget chrome (unstyled; replace in `agent/views/` when brand matters):
28
+ * Card states (unstyled; replace in `agent/views/` when brand matters):
29
29
  * - `renderLoading` / `renderEmpty` / `renderError` / `renderFatal`
30
30
  * - `renderFailure` — picks one of those from `ApiFailure.reason`, so an expired
31
31
  * session, a 404 and a throttled call each draw the right state.
@@ -36,20 +36,17 @@
36
36
  * Leave `API_BASE` and `AUTH_SOURCE` empty in `constants.ts` until you capture
37
37
  * a real XHR. Empty config makes `apiFetch` return `{ ok: false }` instead of
38
38
  * guessing a host. Errors are returned, never thrown; show `result.error` in
39
- * the widget and log `result.detail` only.
39
+ * the card and log `result.detail` only.
40
40
  *
41
- * ## Override example (custom auth — do not guess Bearer)
41
+ * ## Custom auth (not Bearer)
42
42
  *
43
- * ```ts
44
- * import { configure, apiFetch as defaultApiFetch, ... } from "@foldspace_npm/harness/runtime";
45
- * // After configure(...):
46
- * export async function apiFetch<T>(path: string, init: RequestInit = {}) {
47
- * // observe the live request first, then copy its headers here
48
- * return defaultApiFetch<T>(path, { ...init, headers: { ... } });
49
- * }
50
- * export { getAgent, rankBy, renderEmpty, renderError, renderFatal, renderLoading };
51
- * // Do not `export *` if that would re-export the default apiFetch.
52
- * ```
43
+ * Read `node_modules/@foldspace_npm/harness/recipes/swap-the-login-method/README.md`
44
+ * and copy over this file the variant it names for what the app's requests
45
+ * carry: `agent/utils.ts` for a custom header, `variants/utils.cookies.ts` for
46
+ * cookies with no token. Each defines its own `apiFetch`, keeps the harness's
47
+ * failure `reason`s through `httpFailure` / `networkFailure`, and re-exports
48
+ * the other helpers by name - keep it that way: an `export *` would also bring
49
+ * in helpers such as `apiFetchBinary` that still use the default sign-in.
53
50
  */
54
51
 
55
52
  import { configure } from "@foldspace_npm/harness/runtime";
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "Written by foldspace init. The commands a build runs, allowed up front so the coding agent is not stopped one denial at a time mid-build. Named one by one on purpose: init, upgrade and deploy are not here and still ask. Everything else still asks.",
2
+ "$comment": "Written by foldspace init: the commands a build runs, allowed up front so the coding agent is not stopped one denial at a time. init and upgrade are left out on purpose, so they ask, like anything not listed. foldspace deploy is Foldspace-internal and never runs in a build.",
3
3
  "permissions": {
4
4
  "allow": [
5
5
  "Bash(npx foldspace help:*)",
@@ -10,6 +10,7 @@
10
10
  "Bash(npx foldspace observe:*)",
11
11
  "Bash(npx foldspace ask:*)",
12
12
  "Bash(npx foldspace badge:*)",
13
+ "Bash(npx foldspace published:*)",
13
14
  "Bash(npm install:*)",
14
15
  "Bash(npm run build:*)",
15
16
  "Bash(npm run typecheck:*)",