@foldspace_npm/harness 0.1.10 → 0.1.12

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 (45) hide show
  1. package/CLAUDE.md +328 -0
  2. package/README.md +24 -5
  3. package/bin/cli.mjs +29 -1
  4. package/package.json +4 -2
  5. package/recipes/INDEX.md +15 -0
  6. package/recipes/README.md +46 -0
  7. package/recipes/find-by-name/README.md +47 -0
  8. package/recipes/find-by-name/agent/actions/find_project_id.ts +101 -0
  9. package/recipes/find-by-name/agent/api/projects.ts +36 -0
  10. package/recipes/find-by-name/agent/projects.ts +40 -0
  11. package/recipes/find-by-name/fixtures/projects.all.json +29 -0
  12. package/recipes/find-by-name/fixtures/projects.empty-account.json +4 -0
  13. package/recipes/find-by-name/fixtures/projects.none.json +4 -0
  14. package/recipes/find-by-name/recipe.json +10 -0
  15. package/recipes/pick-from-a-list/README.md +44 -0
  16. package/recipes/pick-from-a-list/agent/actions/choose_project.ts +161 -0
  17. package/recipes/pick-from-a-list/agent/api/projects.ts +36 -0
  18. package/recipes/pick-from-a-list/agent/projects.ts +40 -0
  19. package/recipes/pick-from-a-list/agent/views/brand.ts +14 -0
  20. package/recipes/pick-from-a-list/agent/views/picker.ts +119 -0
  21. package/recipes/pick-from-a-list/fixtures/projects.all.json +29 -0
  22. package/recipes/pick-from-a-list/fixtures/projects.empty-account.json +4 -0
  23. package/recipes/pick-from-a-list/recipe.json +10 -0
  24. package/recipes/swap-the-login-method/README.md +40 -0
  25. package/recipes/swap-the-login-method/agent/utils.ts +73 -0
  26. package/recipes/swap-the-login-method/fixtures/anything.ok.json +8 -0
  27. package/recipes/swap-the-login-method/recipe.json +12 -0
  28. package/recipes/swap-the-login-method/variants/utils.cookies.ts +64 -0
  29. package/recipes/who-is-the-user/README.md +56 -0
  30. package/recipes/who-is-the-user/agent/identify.ts +89 -0
  31. package/recipes/who-is-the-user/fixtures/profile.ok.json +6 -0
  32. package/recipes/who-is-the-user/recipe.json +9 -0
  33. package/src/cli-help.mjs +1 -0
  34. package/src/cli-registry.mjs +48 -3
  35. package/src/init.mjs +16 -8
  36. package/src/runtime/config.ts +1 -1
  37. package/src/runtime/http.ts +104 -53
  38. package/src/runtime/index.ts +4 -1
  39. package/src/runtime/match.ts +1 -1
  40. package/src/runtime/render.ts +42 -1
  41. package/src/upgrade.mjs +469 -0
  42. package/templates/agent-starter/CLAUDE.md +11 -220
  43. package/templates/agent-starter/README.md +1 -1
  44. package/templates/agent-starter/agent/actions/_example.ts +9 -2
  45. package/templates/agent-starter/agent/utils.ts +2 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,328 @@
1
+ # Foldspace Agent
2
+
3
+ You are adding an AI agent to a product that already exists. It answers
4
+ questions and *does things* inside that product, with the user's own session.
5
+
6
+ The person you are working with may not be a developer. **You run the
7
+ commands; they sign in and talk to the agent.** Their first session ends at
8
+ something they can **see on their own page** — not at "it builds".
9
+
10
+ ## Every session starts the same way
11
+
12
+ 1. **Harness version.** Run `npx foldspace help --json` (from an older install,
13
+ `npx --yes @foldspace_npm/harness@latest upgrade --check`). If
14
+ `update.outdated` is true, say the pinned and latest versions and **ask
15
+ before upgrading**; then `foldspace upgrade --yes` and `npm run build`. Never
16
+ upgrade as a side effect of build, attach or implementing an action. If the
17
+ project `CLAUDE.md` does not contain
18
+ `@node_modules/@foldspace_npm/harness/CLAUDE.md`, ask before
19
+ `foldspace upgrade --refresh-instructions`.
20
+ 2. **Is Foldspace connected? Check — never assume.** Call `list_agents`:
21
+
22
+ | Result | Do |
23
+ |---|---|
24
+ | The tool does not exist | The Foldspace MCP is not installed in this editor. Give the one install step for *this* editor — Claude Code: `claude plugin install foldspace-agent-builder@foldspace-plugins`; anything else: the MCP block on <https://foldspace.ai/agents.md> — then check again |
25
+ | It exists but fails on auth | Installed, not signed in. Only the human can approve sign-in. Tell them exactly where — Claude Code: `/mcp` → **foldspace** → finish in the browser — then check again |
26
+ | It returns agents | One agent: state its name, Agent Key and product in a line and carry on; the human objects if it is wrong. Several: ask which, as a choice |
27
+
28
+ 3. **Orient** — `get_agent_settings`, `list_actions`, `list_task_agents`,
29
+ `list_navigation_routes`. Record current versions before changing anything;
30
+ if something breaks, this is how you prove what moved. Conversation starters
31
+ seeded at sign-up are often a specification someone already wrote.
32
+ 4. **Read `docs/app-profile.md`.** It is what is already known about this app,
33
+ with how each fact was established.
34
+
35
+ Before an unfamiliar harness operation, `npx foldspace help --json` is the
36
+ current CLI contract — risk, prerequisites, effects, next step.
37
+
38
+ ## Aim at a level, then choose the cheapest experience that reaches it
39
+
40
+ | Level | Reached when |
41
+ |---|---|
42
+ | **L0 · agent live** | injected, visible on the page, `identify` fired |
43
+ | **L1 · read settings** | the user's own account, plan or settings in a widget, showing their real values |
44
+ | **L2 · look up entities** | a search from the user's own words, showing rows |
45
+
46
+ Navigation, uploads, extraction and anything that writes are a later session.
47
+ Say the target out loud, then offer three or four read-only experiences that
48
+ reach it, **cheapest first**: an endpoint the profile already verifies → the
49
+ user's own data with no parameter (L1) → a search with one parameter (L2).
50
+ `discover_actions` gives candidates, not an inventory.
51
+
52
+ **Check row counts before recommending.** An experience on a resource the
53
+ account has zero of is a demo of an empty state. On an empty account the useful
54
+ first action is one that creates data — say so instead.
55
+
56
+ ### Ask as a choice, and ask almost nothing
57
+
58
+ Every question to the human is an **enumerated choice with the recommended
59
+ option first** (in Claude Code, `AskUserQuestion`). A first session has four
60
+ decision points: which agent (only if several), the app's hostname, which
61
+ experience first, and whether to publish. The hostname is the only free-text
62
+ question. Everything else — attach mode, folder, name, level — you **state and
63
+ move on**.
64
+
65
+ Then agree the experience in the customer's words, before creating anything:
66
+ *"Show my plan and credits needs one thing the agent can call and one card to
67
+ show it in. Sound right?"*
68
+
69
+ ## Find the call yourself
70
+
71
+ Never guess `API_BASE`, `AUTH_SOURCE`, an endpoint or a schema. But guessing
72
+ and asking are not the only options — **observing is, and it comes first**:
73
+
74
+ 1. **Already known** — `docs/app-profile.md`. A verified endpoint needs no
75
+ browsing.
76
+ 2. **You drive.** `npm run inject`, the human signs in, and *you* go to the
77
+ screen: read the app's bundle for API paths and watch the requests it makes
78
+ (chrome-devtools MCP against the inject Chrome, or a page-context `fetch`,
79
+ **before** `attach` owns the debug port). Read the auth method off a request
80
+ the page already sent — bearer from `localStorage`, cookie, custom header.
81
+ Delegate the watching to a read-only subagent so the traffic does not flood
82
+ your context.
83
+ 3. **Ask the human to perform the workflow** — only for what you cannot reach:
84
+ an MFA step, a paywall, a screen that needs a record the account does not
85
+ have, or anything that would write.
86
+
87
+ Signing in is the human's step, because `inject` launches a fresh profile.
88
+ Navigating is yours. **While you drive a signed-in account you are read-only:**
89
+ GET only, never submit a form, never click anything that creates, sends or
90
+ deletes.
91
+
92
+ Capture at least one real HTTP 200 for the data the action needs. If there is
93
+ none, pivot; do not create Foldspace resources. Write what you establish into
94
+ `docs/app-profile.md` **with how you know it**, at that moment. Do not promote
95
+ an assumption by quoting that file later.
96
+
97
+ ## Build: objects first, code second
98
+
99
+ Four kinds of thing. Never make the customer learn these words — describe them
100
+ by what they do.
101
+
102
+ | Thing | Answers | Lives in | You write |
103
+ |---|---|---|---|
104
+ | **Action** | *when* should the agent invoke this? | Agent Studio | the contract |
105
+ | **Action handler** | what happens when it does | `agent/actions/<name>.ts` | the code |
106
+ | **Task agent** | *how* to do one reasoning job | Agent Studio | instructions + response schema |
107
+ | **Navigation route** | where the agent can send the user | Agent Studio | a URL template + what the page is for |
108
+
109
+ Knowledge search, navigation and sharing the current tab are platform
110
+ built-ins — no code, and a first experience can lean on them.
111
+
112
+ 1. `create_action` as a **draft**. `generate_action_handler` works from a draft
113
+ schema; do not publish yet. Generating the handler from the schema is what
114
+ keeps the two from drifting.
115
+ 2. Implement the handler from the observed request and response shapes. A
116
+ handler is `fetch` plus `runTask` and nothing more. Import helpers from
117
+ `agent/utils.ts` — inspect that file before writing another `fetch` or rank
118
+ helper.
119
+ 3. Register it in `agent/actions/index.ts` and build (`foldspace build` lints
120
+ first).
121
+
122
+ **Task agents** are for the one LLM subtask deterministic code cannot do —
123
+ extraction, summarisation, classification, normalisation. Not for API calls,
124
+ CRUD, routing or parsing a known shape. Created in Agent Studio, called with
125
+ `runTask({ taskKey, data })` — published, or it does not resolve. `data` carries
126
+ facts, never `prompt` or `instructions`. One owner per concern: extraction
127
+ reads, normalisation converts, nothing does both.
128
+ <https://docs.foldspace.ai/user-guides/task-agents/>
129
+
130
+ **Changing something already live is a production deploy** — immediate, no
131
+ review, no rollback. Never edit it to test. Stand up a `_gen2` beside it, point
132
+ the code at that, and roll back by pointing back. `_gen2`, never `_v2` —
133
+ Foldspace owns the `v` counter. The suffix is permanent.
134
+
135
+ ### The moment a handler exists, prove it
136
+
137
+ Do not batch verification to the end. As soon as a handler is registered:
138
+ build, confirm it reached `dist/index.js`, attach, confirm
139
+ `registration_ok`, execute it against the live endpoint, and check the result
140
+ is **not empty**. An empty widget that renders cleanly is not a working
141
+ experience.
142
+
143
+ Say what that proved and what it did not: executing a handler proves the
144
+ handler; only a real chat turn proves the experience (gate 5). Then record the
145
+ endpoint, auth source, response shape and row count in `docs/app-profile.md`,
146
+ with how you established each.
147
+
148
+ ## Reachability: everything must be published
149
+
150
+ | | Published to run? | Second gate |
151
+ |---|---|---|
152
+ | Action | **yes** | handler registered in `agent/actions/index.ts` |
153
+ | Task agent | **yes** | a `taskKey` in code names it |
154
+ | Navigation route | **yes** | `isActive: true` |
155
+
156
+ Publishing a **new** object is inert; publishing an **edit to a live one** is
157
+ the deploy above. Navigation has **no publish tool on the MCP** and
158
+ `get_navigation_route` returns only `isActive` — publish routes in Agent Studio
159
+ and say so. Route descriptions are a retrieval surface: write what the page is
160
+ *for*, in the user's words, and pair every parameterised route with the lookup
161
+ action that produces its parameter — or ship neither.
162
+
163
+ **Ask before publishing.** It is required only so the copilot can call the
164
+ action, and it is a live product change when the agent has real users.
165
+
166
+ ## Local loop
167
+
168
+ ```bash
169
+ npm run dev
170
+ npm run inject
171
+ npx foldspace attach --daemon
172
+ ```
173
+
174
+ `inject` launches an isolated Chrome profile and records its debug port; it
175
+ loads nothing. Observe after inject and **before** attach, while chrome-devtools
176
+ MCP can use the same Chrome. `attach` prepares the page and loads the local
177
+ `dist/index.js` over CDP. Coding agents use `--daemon` so the tool returns after
178
+ `[lifecycle] inspect_registration:…`. An empty local registry is valid; `npm run
179
+ build` is still required so `dist/index.js` exists.
180
+
181
+ Attach mode is read off the page, never asked: default swap when the page
182
+ already has the configured product and agent; `--bootstrap` when it has no
183
+ Foldspace SDK; `--replace` when it embeds a different product or agent, or an
184
+ SDK without the configured agent.
185
+
186
+ `inspect_registration:registration_ok` is the signal. On `registration_mismatch`
187
+ read `npx foldspace help attach --json` and map `missingActionNames`,
188
+ `unexpectedActionNames`, `diagnosticError`. While attach runs it owns the debug
189
+ port — `attach --stop` before using chrome-devtools MCP again.
190
+
191
+ Prove a named action through the visible agent: after the human talks to the
192
+ copilot, read the daemon log for `[actions]` SDK callback and local
193
+ execute/render lines. Those record names, statuses, durations and parameter keys
194
+ only — never results or error bodies.
195
+
196
+ ## Verification gates
197
+
198
+ Do not report success without all six:
199
+
200
+ 1. `npm run typecheck` compiles (`tsc --noEmit -p tsconfig.json`). Do not run
201
+ `npx tsc` — it can install the wrong package.
202
+ 2. `foldspace lint` reports no errors (`foldspace build` runs it first).
203
+ 3. The handler appears in `dist/index.js`.
204
+ 4. The browser reports `inspect_registration:registration_ok`, and for a named
205
+ action the captured registry includes it.
206
+ 5. The action behaves against the real workflow: `[actions]
207
+ local-handler:execute` in the attach log after the human exercises the
208
+ visible agent. A widget also needs `local-handler:render` from a real chat
209
+ turn — not a hand-built `render()` call. `runAction` refuses render actions
210
+ and driving `render()` yourself never runs `execute()`.
211
+ 6. Browser evidence came from the live target, not hand-authored examples.
212
+ Neighbour fixtures still pass when they exist.
213
+
214
+ ## Rules that are not negotiable
215
+
216
+ 1. **Handlers return data only.** Never `directive`, `instructions`, or a
217
+ paragraph telling the copilot what to say — those live in Agent Studio.
218
+ Never return `ApiResult.detail` from `execute`; it is for the console.
219
+ Validate parameters and return sanitised errors.
220
+ 2. **`render` receives `execute`'s return value**, not the input params. A
221
+ widget that lacks the ids the card needs passes in isolation and fails in the
222
+ real chat. <https://docs.foldspace.ai/guides/in-chat-ui/>
223
+ 3. **Say what you verified and what you assumed.** "The docs say X" and "I
224
+ think X" are different claims — mark which. Search `docs.foldspace.ai` over
225
+ MCP (`search_docs`, `fetch_page`) before asserting how the platform behaves;
226
+ cite the page, or say plainly that the docs do not cover it and write down
227
+ what you learned.
228
+ 4. **Do not send chat messages to test.** Every message is a real conversation
229
+ in the customer's list. `runTask` creates no conversation record — use it.
230
+ 5. **Test mode is off by default.** A new agent has no traffic to pollute, and
231
+ watching real conversations appear is the point of the first run. Arm it
232
+ only when the tenant already has real conversations — and then on **every**
233
+ instance via `armAllInstances`, because `foldspace.agent({apiName})` returns
234
+ the overlay handle, which is not the one serving an embedded chat. Nothing
235
+ can confirm it is on afterwards; never send probe messages to find out.
236
+ 6. **The human is not necessarily a developer.** Never hand over a terminal
237
+ line, a stack trace, or a choice between flags. Name experiences by outcome
238
+ — *"show my plan and credits"*, not `get_account_status`. Their two jobs:
239
+ sign in, and talk to the agent.
240
+ 7. **Be short.** No recap of what was established, no restating the plan before
241
+ each step. Progress is one line: what you are doing, the level reached, and
242
+ the question if there is one.
243
+ 8. **Subagents investigate; they do not build.** A read-only investigation
244
+ that returns a summary belongs in a subagent because it floods context.
245
+ Never delegate a handler or a gate — a summary of a gate is not a gate.
246
+
247
+ ## Product defaults
248
+
249
+ Lint catches some of these in handler code; Agent Studio copy is on you.
250
+
251
+ - **No emoji** in copilot replies, cards or handler strings. Put that in the
252
+ agent's Behavior instructions, and in each new action's Studio `instructions`.
253
+ - **Never send the user out of the host app.** No "open in <product>" button,
254
+ no pasted URLs — navigate with a route, same tab.
255
+
256
+ ## Layout
257
+
258
+ - `agent/actions/` — one handler per action (`execute`, optional `render`),
259
+ registered in `index.ts`. The key must equal the action key in Agent Studio;
260
+ a mismatch does not error, the action simply never fires.
261
+ - `agent/api/` — one HTTP helper per endpoint
262
+ - `agent/constants.ts` — agent, product, domain, plus empty `API_BASE` /
263
+ `AUTH_SOURCE` and `LOAD_MODE`
264
+ - `agent/utils.ts` — configures the harness runtime and re-exports it. **The
265
+ only import surface for actions.**
266
+ - `agent/views/` — branded cards
267
+ - `docs/app-profile.md` — what is known about this app, and how it was established
268
+ - `foldspace.dev.json` — local harness target configuration
269
+ - `CLAUDE.md` — this file, imported — plus what is specific to this product
270
+
271
+ Do not introduce another bundler or bundle format.
272
+
273
+ ## Working examples to copy from
274
+
275
+ Before writing a handler or a card, read
276
+ `node_modules/@foldspace_npm/harness/recipes/INDEX.md` — the whole table, it is
277
+ short — and open the recipe closest to the outcome. Each is a finished
278
+ piece of a build, generalised from production builds with the customer taken
279
+ out: the API helper, the handler or card, and a recorded response per state. Copy its `agent/` files across, then replace the
280
+ placeholder path and fields with what you observed. They match the harness
281
+ version you have installed, and every one is built and run on each release.
282
+
283
+ L0 → `who-is-the-user`. L2 → `find-by-name`, and `pick-from-a-list` when the
284
+ user has to choose. App does not use a bearer token → `swap-the-login-method`.
285
+ Each says how many production builds carry the pattern and what they learned.
286
+ If nothing fits — there is no L1 card recipe yet — say so and build from
287
+ `agent/actions/_example.ts`.
288
+
289
+ ## What you already have
290
+
291
+ Import from `../utils`. `generate_action_handler` may emit a skeleton that does
292
+ not import it — fix the import when you implement.
293
+
294
+ | Helper | Use when | Do not use when |
295
+ |---|---|---|
296
+ | `apiFetch` / `apiFetchBinary` | The app's own JSON/file API, after a real 200 | Public marketing hosts (`publicFetch`); custom auth headers |
297
+ | `publicFetch` | Unauthenticated / marketing origin | Signed-in product APIs |
298
+ | `getAuthToken` / `parseJwt` | `AUTH_SOURCE` is `localStorage` or `cookie` | Custom header schemes — override `apiFetch` in `utils.ts` |
299
+ | `rankBy` | User typed a name; API search is exact or ignored | Domain ranking (invoices, coverage, industry classification codes) |
300
+ | `renderLoading` / `Empty` / `Error` / `Fatal` | Chatterblock states | Branded cards — those stay in `agent/views/` |
301
+ | `renderFailure` | Any `{ ok: false }` from `apiFetch` — it picks the state from `reason` | You need bespoke copy for one reason; then branch on `reason` yourself |
302
+ | `httpFailure` / `networkFailure` | Your own `apiFetch` (custom header, cookies) — keeps the same `reason`s | The default `apiFetch` fits; then you never call these |
303
+ | `getAgent` | Talking to Foldspace | You need a specific instance — then `agentIds()` |
304
+ | `armAllInstances` | Attach/bootstrap setup | Inside `execute` |
305
+ | `redact` / `mapWithConcurrency` | Logging tokens; batching fetches | — |
306
+
307
+ Every failed call carries a `reason` — `signed_out`, `forbidden`, `not_found`,
308
+ `rate_limited`, `timeout`, `unreachable`, `config`, `server`. Branch on that,
309
+ not on `status`, and do not re-detect it. The session you observed while
310
+ building is not the session at run time: **expect `signed_out`** and let
311
+ `renderFailure` show it. `not_found` and zero rows are both `renderEmpty` with
312
+ the query named — never an error, never a blank card.
313
+
314
+ If the observed auth is not Bearer + `localStorage`/`cookie`, stop re-exporting
315
+ that one function and keep the rest:
316
+
317
+ ```ts
318
+ export { getAgent, rankBy, renderEmpty } from "@foldspace_npm/harness/runtime";
319
+ export { apiFetch } from "./api";
320
+ ```
321
+
322
+ ## Safety
323
+
324
+ - Do not commit secrets, cookies, HAR files, browser storage, or
325
+ `.foldspace-dev/`. Nothing transient goes in `docs/`.
326
+ - Local handler changes are not cloud publication.
327
+ - Creating or publishing Foldspace resources requires explicit approval.
328
+ - Action keys and parameter names must match the published schema.
package/README.md CHANGED
@@ -26,7 +26,7 @@ constraint this package exists to protect.
26
26
  ### What is genuinely shared
27
27
 
28
28
  - `agent/actions/*` and `agent/api/*` — a handler is `fetch` plus `runTask`,
29
- nothing more. The actions built for Figma run unmodified in either track.
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
32
  - `foldspace deploy` — publish to `agent/actions/<env>/<productId>/<agentApiName>`
@@ -97,7 +97,7 @@ Non-interactive / CI form:
97
97
  npx --yes @foldspace_npm/harness init foldspace-agent \
98
98
  --product-id FR8JUQZAQRZB \
99
99
  --agent-key my-agent \
100
- --domain app.example.com \
100
+ --app-domain app.example.com \
101
101
  --name "My Agent"
102
102
  ```
103
103
 
@@ -107,14 +107,15 @@ When running directly from a harness checkout during development:
107
107
  node bin/cli.mjs init ../foldspace-agent \
108
108
  --product-id FR8JUQZAQRZB \
109
109
  --agent-key my-agent \
110
- --domain app.example.com
110
+ --app-domain app.example.com
111
111
  ```
112
112
 
113
113
  The Agent Key is the value shown in Agent Studio, such as `my-agent`. It is
114
114
  not the sidecar directory name. `--agent-api-name` remains a deprecated alias
115
- for `--agent-key`. `--name` is optional and defaults to the target directory
115
+ for `--agent-key`. `--domain` remains a deprecated alias for `--app-domain`.
116
+ `--name` is optional and defaults to the target directory
116
117
  name (`foldspace-agent` unless you pass a directory). The product ID must be
117
- the bare ID, not the `EU-…-1-1` SDK loader key. The domain may be a hostname
118
+ the bare ID, not the `EU-…-1-1` SDK loader key. The app domain may be a hostname
118
119
  or an HTTP(S) URL without a port or path.
119
120
 
120
121
  For safety, `init` requires a target path that does not exist. It does not
@@ -132,6 +133,24 @@ npm run attach
132
133
  The generated npm scripts intentionally remain the normal project interface;
133
134
  `foldspace init` is the one-time project creation command.
134
135
 
136
+ Generated `CLAUDE.md` imports platform instructions from
137
+ `@foldspace_npm/harness` instead of copying them. Existing projects can switch
138
+ to that import with `foldspace upgrade --refresh-instructions`.
139
+
140
+ ### Keep the harness current
141
+
142
+ Tenant projects pin an exact `@foldspace_npm/harness` version. Publishing a
143
+ newer package does not move them. Check, ask the user, then bump:
144
+
145
+ ```bash
146
+ npx --yes @foldspace_npm/harness@latest upgrade --check
147
+ foldspace upgrade --yes # only after the user agrees
148
+ ```
149
+
150
+ `--check` (and a non-interactive `upgrade` with no `--yes`) never installs.
151
+ `--yes` rewrites the exact pin and runs `npm install --ignore-scripts`. Rebuild
152
+ afterward; that is still not a deploy.
153
+
135
154
  ### Lint handlers before they ship
136
155
 
137
156
  `foldspace build` runs `foldspace lint` first. Errors skip bundling; warnings
package/bin/cli.mjs CHANGED
@@ -5,6 +5,12 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { runInit } from "../src/init.mjs";
8
+ import {
9
+ checkForUpdate,
10
+ nagIfOutdated,
11
+ resolveProjectDir,
12
+ runUpgrade,
13
+ } from "../src/upgrade.mjs";
8
14
  import {
9
15
  classifyAttachControl,
10
16
  runAttachControl,
@@ -56,10 +62,13 @@ function printHelp(topic, json) {
56
62
  );
57
63
  return;
58
64
  }
65
+ const update = checkForUpdate(resolveProjectDir());
59
66
  if (json) {
60
- console.log(JSON.stringify(document, null, 2));
67
+ const payload = topic ? document : { ...document, update };
68
+ console.log(JSON.stringify(payload, null, 2));
61
69
  return;
62
70
  }
71
+ nagIfOutdated(update);
63
72
  console.log(
64
73
  topic ? renderCommandHelp(registry, topic) : renderGeneralHelp(registry),
65
74
  );
@@ -118,6 +127,7 @@ const [command, ...args] = process.argv.slice(2);
118
127
  if (!command || command === "--help" || command === "-h") {
119
128
  printHelp(null, args.includes("--json"));
120
129
  } else if (command === "--version" || command === "-v") {
130
+ nagIfOutdated(checkForUpdate(resolveProjectDir()));
121
131
  console.log(
122
132
  `${registry.package.name} ${registry.package.version} ` +
123
133
  `(protocol ${registry.protocolVersion}, CLI schema ${registry.schemaVersion})`,
@@ -149,6 +159,24 @@ if (!command || command === "--help" || command === "-h") {
149
159
  fail(error instanceof Error ? error.message : String(error));
150
160
  }
151
161
  }
162
+ } else if (command === "upgrade") {
163
+ if (args.includes("--help") || args.includes("-h")) {
164
+ printHelp("upgrade", args.includes("--json"));
165
+ } else {
166
+ try {
167
+ const normalized = normalizeCommandArgs(commandByName("upgrade"), args);
168
+ Promise.resolve()
169
+ .then(() => runUpgrade(normalized))
170
+ .then((report) => {
171
+ console.log(JSON.stringify(report, null, 2));
172
+ })
173
+ .catch((error) => {
174
+ fail(error instanceof Error ? error.message : String(error));
175
+ });
176
+ } catch (error) {
177
+ fail(error instanceof Error ? error.message : String(error));
178
+ }
179
+ }
152
180
  } else if (commandByName(command)) {
153
181
  if (args.includes("--help") || args.includes("-h")) {
154
182
  printHelp(command, args.includes("--json"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldspace_npm/harness",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Build and verify portable Foldspace action artifacts against a live app.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,9 @@
21
21
  "bin",
22
22
  "src",
23
23
  "templates",
24
- "README.md"
24
+ "recipes",
25
+ "README.md",
26
+ "CLAUDE.md"
25
27
  ],
26
28
  "dependencies": {
27
29
  "esbuild": "^0.20.0",
@@ -0,0 +1,15 @@
1
+ # Recipe index
2
+
3
+ Read this whole table, then open the recipe closest to the outcome.
4
+
5
+ | Recipe | Level | What it gives | Kind | Proven by |
6
+ |---|---|---|---|---|
7
+ | [`who-is-the-user`](who-is-the-user/) | **L0** | Foldspace knows who is signed in — id, email, name, role, subscription — so conversations and analytics are not anonymous | Runs once at start-up. No action, no card | 4 production builds |
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
+ | [`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 action with a card that waits for the user | 1 production build |
10
+ | [`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 (3 builds, 3 different schemes) |
11
+
12
+ **Not here yet — no production build proves it:** the user's own plan or
13
+ settings drawn as a card (L1). In production that data is sent to `identify`
14
+ (`who-is-the-user`), not rendered. Navigation (L3), writes (L4), uploads and
15
+ task agents are also still to come.
@@ -0,0 +1,46 @@
1
+ # Recipes
2
+
3
+ Finished pieces of a build to **read and adapt** — each one generalised from a
4
+ pattern that is live in production builds, with the customer taken out.
5
+ `_example.ts` is an empty skeleton; these are complete.
6
+
7
+ They ship inside the harness on purpose: a recipe always matches the runtime it
8
+ imports, and every one is installed into a freshly scaffolded project, built,
9
+ typechecked and run against its fixtures on every harness change
10
+ (`test/recipes.test.mjs`). A recipe that stops working fails the release instead
11
+ of a customer session.
12
+
13
+ ## Use one
14
+
15
+ 1. Read `INDEX.md` and pick the closest outcome.
16
+ 2. Copy its `agent/` files into your project's `agent/` — the layout is the
17
+ same, so the imports already resolve.
18
+ 3. Replace every `__observe_me` placeholder — paths, storage keys, claim names —
19
+ with what you **observed** in the live app, and record how you know in
20
+ `docs/app-profile.md`. The placeholders are deliberate: none can succeed.
21
+ 4. Register actions in `agent/actions/index.ts` under the **same key** as in
22
+ Agent Studio, then run the six gates.
23
+
24
+ If nothing here fits, say so and build from `_example.ts`.
25
+
26
+ ## Where they come from
27
+
28
+ Each recipe's README says how many production builds carry the pattern and what
29
+ those builds learned the hard way. **What is taken is the shape** — the order of
30
+ calls, what is returned to the agent, where the traps are. **What is not taken**
31
+ is any customer's name, endpoint, identifier, styling or code. Contracts are the
32
+ documented ones: `execute(params)` and
33
+ `render(data, host, header, callback, cancel)`
34
+ (<https://docs.foldspace.ai/guides/in-chat-ui/>), `foldspace.identify(context)`
35
+ (<https://docs.foldspace.ai/start/user-context/>), and the harness runtime.
36
+
37
+ Error handling is **not** taken from those builds. They predate `reason`: one
38
+ turns every failure into "unexpected error", another reports a missing session
39
+ as "no projects found". Recipes use the runtime's failure reasons instead.
40
+
41
+ ## What every recipe demonstrates
42
+
43
+ - `execute` returns **data and facts** — never `detail`, never instructions.
44
+ - A missing or expired session is `signed_out`, not an empty result.
45
+ - DOM is built with `textContent`, never `innerHTML` with API data, and styles
46
+ go in `header` — `host` lives inside the component's frame.
@@ -0,0 +1,47 @@
1
+ # Find by name — L2
2
+
3
+ **The user says** "add it to the Maple Street project". **The agent needs** an id.
4
+
5
+ **Proven by 3 production builds**, all the same shape: a name from the user's
6
+ words → the app's own list → a ranked answer **back to the agent as data**. None
7
+ of them draws a card. A resolver answers questions; it never asks one. When the
8
+ user genuinely has to choose, that is a different action —
9
+ [`pick-from-a-list`](../pick-from-a-list/).
10
+
11
+ In Agent Studio: an action with key `find_project_id` and one optional string
12
+ parameter `name` — *the name, or part of the name, the user said*.
13
+
14
+ ## What it returns
15
+
16
+ | The user's words | The agent gets |
17
+ |---|---|
18
+ | no name ("how many projects do I have?") | the account's **count**, plus a small sample — never the whole account |
19
+ | one clear match | that project, its match strength, and up to three alternatives |
20
+ | several equally good matches | **all of them**, marked `ambiguous` — never a silent pick |
21
+ | nothing matches | `success: false`, plus near-misses for a typo |
22
+ | the account is empty | a count of zero — not an error |
23
+ | signed out, no access, throttled, unreachable | `success: false` with the `reason` |
24
+
25
+ ## Adapt it
26
+
27
+ | File | Change |
28
+ |---|---|
29
+ | `agent/api/projects.ts` | Path, parameter names, envelope — from a captured response |
30
+ | `agent/projects.ts` | What a summary carries, and which keys a user might name out loud |
31
+ | `agent/actions/find_project_id.ts` | The entity. The logic rarely needs touching |
32
+
33
+ ## What those builds learned the hard way
34
+
35
+ - **Hand back every tie.** Picking one of two equally good matches is how a
36
+ later action changes the wrong project.
37
+ - **Count on the server.** Fetching every row to count them is the slowest thing
38
+ an agent can do, and it puts the whole account into the model's context.
39
+ - **Return summaries, not rows.** Two of the three builds send ten raw fields
40
+ per row to the agent; the third sends three and is the one that scales.
41
+ - **A missing session is not "no projects".** One build reports a missing token
42
+ as an empty list, and the agent cheerfully tells a signed-out user they have
43
+ none.
44
+ - **Server search is usually a plain substring**, so a typo finds nothing. Try
45
+ the near-miss pass before saying no.
46
+ - **Probe with a name a real record has.** A search that returns zero proves
47
+ nothing unless a matching record exists.