@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
@@ -0,0 +1,319 @@
1
+ {
2
+ "accounts": [
3
+ {
4
+ "id": "acct_1",
5
+ "name": "Northwind Traders",
6
+ "industry": "Logistics",
7
+ "city": "Seattle",
8
+ "state": "WA"
9
+ },
10
+ {
11
+ "id": "acct_2",
12
+ "name": "Acme Corporation",
13
+ "industry": "Technology",
14
+ "city": "Austin",
15
+ "state": "TX"
16
+ },
17
+ {
18
+ "id": "acct_3",
19
+ "name": "Acme Logistics",
20
+ "industry": "Logistics",
21
+ "city": "Denver",
22
+ "state": "CO"
23
+ },
24
+ {
25
+ "id": "acct_4",
26
+ "name": "Fabrikam",
27
+ "industry": null,
28
+ "city": null,
29
+ "state": null
30
+ },
31
+ {
32
+ "id": "acct_5",
33
+ "name": "Contoso Branch 01",
34
+ "industry": "Healthcare",
35
+ "city": "Boston",
36
+ "state": "MA"
37
+ },
38
+ {
39
+ "id": "acct_6",
40
+ "name": "Contoso Branch 02",
41
+ "industry": "Retail",
42
+ "city": "Chicago",
43
+ "state": "IL"
44
+ },
45
+ {
46
+ "id": "acct_7",
47
+ "name": "Contoso Branch 03",
48
+ "industry": "Manufacturing",
49
+ "city": "Portland",
50
+ "state": "OR"
51
+ },
52
+ {
53
+ "id": "acct_8",
54
+ "name": "Contoso Branch 04",
55
+ "industry": "Education",
56
+ "city": "Miami",
57
+ "state": "FL"
58
+ },
59
+ {
60
+ "id": "acct_9",
61
+ "name": "Contoso Branch 05",
62
+ "industry": "Finance",
63
+ "city": "Phoenix",
64
+ "state": "AZ"
65
+ },
66
+ {
67
+ "id": "acct_10",
68
+ "name": "Contoso Branch 06",
69
+ "industry": "Healthcare",
70
+ "city": "Boston",
71
+ "state": "MA"
72
+ },
73
+ {
74
+ "id": "acct_11",
75
+ "name": "Contoso Branch 07",
76
+ "industry": "Retail",
77
+ "city": "Chicago",
78
+ "state": "IL"
79
+ },
80
+ {
81
+ "id": "acct_12",
82
+ "name": "Contoso Branch 08",
83
+ "industry": "Manufacturing",
84
+ "city": "Portland",
85
+ "state": "OR"
86
+ },
87
+ {
88
+ "id": "acct_13",
89
+ "name": "Contoso Branch 09",
90
+ "industry": "Education",
91
+ "city": "Miami",
92
+ "state": "FL"
93
+ },
94
+ {
95
+ "id": "acct_14",
96
+ "name": "Contoso Branch 10",
97
+ "industry": "Finance",
98
+ "city": "Phoenix",
99
+ "state": "AZ"
100
+ },
101
+ {
102
+ "id": "acct_15",
103
+ "name": "Contoso Branch 11",
104
+ "industry": "Healthcare",
105
+ "city": "Boston",
106
+ "state": "MA"
107
+ },
108
+ {
109
+ "id": "acct_16",
110
+ "name": "Contoso Branch 12",
111
+ "industry": "Retail",
112
+ "city": "Chicago",
113
+ "state": "IL"
114
+ },
115
+ {
116
+ "id": "acct_17",
117
+ "name": "Contoso Branch 13",
118
+ "industry": "Manufacturing",
119
+ "city": "Portland",
120
+ "state": "OR"
121
+ },
122
+ {
123
+ "id": "acct_18",
124
+ "name": "Contoso Branch 14",
125
+ "industry": "Education",
126
+ "city": "Miami",
127
+ "state": "FL"
128
+ },
129
+ {
130
+ "id": "acct_19",
131
+ "name": "Contoso Branch 15",
132
+ "industry": "Finance",
133
+ "city": "Phoenix",
134
+ "state": "AZ"
135
+ },
136
+ {
137
+ "id": "acct_20",
138
+ "name": "Contoso Branch 16",
139
+ "industry": "Healthcare",
140
+ "city": "Boston",
141
+ "state": "MA"
142
+ },
143
+ {
144
+ "id": "acct_21",
145
+ "name": "Contoso Branch 17",
146
+ "industry": "Retail",
147
+ "city": "Chicago",
148
+ "state": "IL"
149
+ },
150
+ {
151
+ "id": "acct_22",
152
+ "name": "Contoso Branch 18",
153
+ "industry": "Manufacturing",
154
+ "city": "Portland",
155
+ "state": "OR"
156
+ },
157
+ {
158
+ "id": "acct_23",
159
+ "name": "Contoso Branch 19",
160
+ "industry": "Education",
161
+ "city": "Miami",
162
+ "state": "FL"
163
+ },
164
+ {
165
+ "id": "acct_24",
166
+ "name": "Contoso Branch 20",
167
+ "industry": "Finance",
168
+ "city": "Phoenix",
169
+ "state": "AZ"
170
+ },
171
+ {
172
+ "id": "acct_25",
173
+ "name": "Contoso Branch 21",
174
+ "industry": "Healthcare",
175
+ "city": "Boston",
176
+ "state": "MA"
177
+ },
178
+ {
179
+ "id": "acct_26",
180
+ "name": "Contoso Branch 22",
181
+ "industry": "Retail",
182
+ "city": "Chicago",
183
+ "state": "IL"
184
+ },
185
+ {
186
+ "id": "acct_27",
187
+ "name": "Contoso Branch 23",
188
+ "industry": "Manufacturing",
189
+ "city": "Portland",
190
+ "state": "OR"
191
+ },
192
+ {
193
+ "id": "acct_28",
194
+ "name": "Contoso Branch 24",
195
+ "industry": "Education",
196
+ "city": "Miami",
197
+ "state": "FL"
198
+ },
199
+ {
200
+ "id": "acct_29",
201
+ "name": "Contoso Branch 25",
202
+ "industry": "Finance",
203
+ "city": "Phoenix",
204
+ "state": "AZ"
205
+ },
206
+ {
207
+ "id": "acct_30",
208
+ "name": "Contoso Branch 26",
209
+ "industry": "Healthcare",
210
+ "city": "Boston",
211
+ "state": "MA"
212
+ },
213
+ {
214
+ "id": "acct_31",
215
+ "name": "Contoso Branch 27",
216
+ "industry": "Retail",
217
+ "city": "Chicago",
218
+ "state": "IL"
219
+ },
220
+ {
221
+ "id": "acct_32",
222
+ "name": "Contoso Branch 28",
223
+ "industry": "Manufacturing",
224
+ "city": "Portland",
225
+ "state": "OR"
226
+ },
227
+ {
228
+ "id": "acct_33",
229
+ "name": "Contoso Branch 29",
230
+ "industry": "Education",
231
+ "city": "Miami",
232
+ "state": "FL"
233
+ },
234
+ {
235
+ "id": "acct_34",
236
+ "name": "Contoso Branch 30",
237
+ "industry": "Finance",
238
+ "city": "Phoenix",
239
+ "state": "AZ"
240
+ },
241
+ {
242
+ "id": "acct_35",
243
+ "name": "Contoso Branch 31",
244
+ "industry": "Healthcare",
245
+ "city": "Boston",
246
+ "state": "MA"
247
+ },
248
+ {
249
+ "id": "acct_36",
250
+ "name": "Contoso Branch 32",
251
+ "industry": "Retail",
252
+ "city": "Chicago",
253
+ "state": "IL"
254
+ },
255
+ {
256
+ "id": "acct_37",
257
+ "name": "Contoso Branch 33",
258
+ "industry": "Manufacturing",
259
+ "city": "Portland",
260
+ "state": "OR"
261
+ },
262
+ {
263
+ "id": "acct_38",
264
+ "name": "Contoso Branch 34",
265
+ "industry": "Education",
266
+ "city": "Miami",
267
+ "state": "FL"
268
+ },
269
+ {
270
+ "id": "acct_39",
271
+ "name": "Contoso Branch 35",
272
+ "industry": "Finance",
273
+ "city": "Phoenix",
274
+ "state": "AZ"
275
+ },
276
+ {
277
+ "id": "acct_40",
278
+ "name": "Contoso Branch 36",
279
+ "industry": "Healthcare",
280
+ "city": "Boston",
281
+ "state": "MA"
282
+ },
283
+ {
284
+ "id": "acct_41",
285
+ "name": "Contoso Branch 37",
286
+ "industry": "Retail",
287
+ "city": "Chicago",
288
+ "state": "IL"
289
+ },
290
+ {
291
+ "id": "acct_42",
292
+ "name": "Contoso Branch 38",
293
+ "industry": "Manufacturing",
294
+ "city": "Portland",
295
+ "state": "OR"
296
+ },
297
+ {
298
+ "id": "acct_43",
299
+ "name": "Contoso Branch 39",
300
+ "industry": "Education",
301
+ "city": "Miami",
302
+ "state": "FL"
303
+ },
304
+ {
305
+ "id": "acct_44",
306
+ "name": "Contoso Branch 40",
307
+ "industry": "Finance",
308
+ "city": "Phoenix",
309
+ "state": "AZ"
310
+ },
311
+ {
312
+ "id": "acct_45",
313
+ "name": "Contoso Branch 41",
314
+ "industry": "Healthcare",
315
+ "city": "Boston",
316
+ "state": "MA"
317
+ }
318
+ ]
319
+ }
@@ -0,0 +1,122 @@
1
+ {
2
+ "account": {
3
+ "id": "acct_1",
4
+ "name": "Northwind Traders",
5
+ "industry": "Logistics",
6
+ "website": "https://www.northwind.example",
7
+ "city": "Seattle",
8
+ "state": "WA",
9
+ "employeeCount": 500,
10
+ "annualRevenue": 42000000,
11
+ "currency": "USD",
12
+ "owner": {
13
+ "name": "Dana Reyes",
14
+ "email": "dana@example.com"
15
+ }
16
+ },
17
+ "opportunities": [
18
+ {
19
+ "id": "o1",
20
+ "name": "Enterprise Platform Upgrade",
21
+ "amount": 125000,
22
+ "stage": "negotiation",
23
+ "probability": 70,
24
+ "closeDate": "2026-10-14"
25
+ },
26
+ {
27
+ "id": "o2",
28
+ "name": "Q1 License Renewal",
29
+ "amount": "50000",
30
+ "stage": "proposal",
31
+ "probability": 50,
32
+ "closeDate": "2026-09-20"
33
+ },
34
+ {
35
+ "id": "o3",
36
+ "name": "API Integration Package",
37
+ "amount": 35000,
38
+ "stage": "discovery",
39
+ "probability": 20,
40
+ "closeDate": "2026-12-10"
41
+ },
42
+ {
43
+ "id": "o4",
44
+ "name": "Training Add-on",
45
+ "amount": 8000,
46
+ "stage": "qualification",
47
+ "probability": 30,
48
+ "closeDate": null
49
+ },
50
+ {
51
+ "id": "o5",
52
+ "name": "Pilot (lost)",
53
+ "amount": 90000,
54
+ "stage": "closed_lost",
55
+ "probability": 0,
56
+ "closeDate": "2026-06-01"
57
+ }
58
+ ],
59
+ "contacts": [
60
+ {
61
+ "id": "c1",
62
+ "firstName": "Sam",
63
+ "lastName": "Lee",
64
+ "title": "Operations Lead",
65
+ "email": "sam@example.com",
66
+ "phone": "(415) 555-0142"
67
+ },
68
+ {
69
+ "id": "c2",
70
+ "firstName": "Priya",
71
+ "lastName": "Nair",
72
+ "title": "VP Procurement",
73
+ "email": "priya@example.com",
74
+ "phone": null,
75
+ "primary": true
76
+ },
77
+ {
78
+ "id": "c3",
79
+ "firstName": "Jordan",
80
+ "lastName": "Kim",
81
+ "title": null,
82
+ "email": "jordan@example.com"
83
+ },
84
+ {
85
+ "id": "c4",
86
+ "firstName": "Alex",
87
+ "lastName": "Moreno",
88
+ "title": "IT Manager",
89
+ "email": "alex@example.com"
90
+ }
91
+ ],
92
+ "cases": [
93
+ {
94
+ "id": "k1",
95
+ "title": "Invoices not syncing",
96
+ "priority": "critical",
97
+ "status": "open",
98
+ "createdAt": "2026-09-28"
99
+ },
100
+ {
101
+ "id": "k2",
102
+ "title": "SSO login loop",
103
+ "priority": "medium",
104
+ "status": "in_progress",
105
+ "createdAt": "2026-09-21"
106
+ },
107
+ {
108
+ "id": "k3",
109
+ "title": "Old ticket",
110
+ "priority": "low",
111
+ "status": "closed",
112
+ "createdAt": "2026-05-01"
113
+ }
114
+ ],
115
+ "meetings": [
116
+ {
117
+ "id": "m1",
118
+ "title": "Quarterly review",
119
+ "startTime": "2026-10-03T15:00:00Z"
120
+ }
121
+ ]
122
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "account": {
3
+ "id": null,
4
+ "name": null
5
+ },
6
+ "opportunities": [
7
+ {
8
+ "id": "o1",
9
+ "name": "Renewal",
10
+ "amount": 18000,
11
+ "stage": "proposal"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "title": "Look up, then show",
3
+ "level": "L2",
4
+ "family": "lookup-then-view",
5
+ "kind": "action",
6
+ "action": "find_account",
7
+ "actions": ["find_account", "show_account_overview"],
8
+ "entry": "agent/actions/find_account.ts",
9
+ "outcome": "The user names an account. One match: its card, no question asked. Several or none: a searchable, paged list, one click to choose - then the card",
10
+ "provenBy": 1
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "action_key": "find_account",
3
+ "display_name": "Find account",
4
+ "description": "Finds the account the user asked to see and returns its id. With one clear match it answers at once; with several, or none, it shows the user a searchable list and returns the one they choose.",
5
+ "parameters": [
6
+ { "name": "name", "description": "The account name, or part of it, exactly as the user said it.", "dataType": "STRING", "required": true }
7
+ ],
8
+ "confirmation_required": false,
9
+ "instructions": "Call this when the user asks to see an account by name. The result says found, chosen, none_of_these, cancelled or failed. On found or chosen, call show_account_overview with that accountId straight away - do not ask the user to confirm, and do not repeat the account's details. On none_of_these, ask the user which account they mean. On cancelled, stop. On failed, the card already says why: say so in one short sentence and stop. Never guess an id, and never show ids to the user. No emoji."
10
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "action_key": "show_account_overview",
3
+ "display_name": "Show account overview",
4
+ "description": "Shows one account as a card: who it is, open pipeline, deals, people and open cases.",
5
+ "parameters": [
6
+ {
7
+ "name": "accountId",
8
+ "description": "The account's id. Never guessed and never taken from the user's words: get it from find_account.",
9
+ "dataType": "STRING",
10
+ "required": true
11
+ }
12
+ ],
13
+ "confirmation_required": false,
14
+ "instructions": "The result is the data the card was drawn from - every open deal, person and open case, not only the ones it lists - or a failure: success false, with the reason. Answer follow-up questions about this account from the data you receive: quote money and counts exactly as given, and never add them up yourself. Right after the card, one short sentence at most - do not repeat what it shows. On a failure, the card already says why: say so in one short sentence and stop. No emoji."
15
+ }
@@ -6,9 +6,9 @@
6
6
 
7
7
  The app decides what "at risk" means, on the server. In that build it was: stalled 14+ days, past its close date, low probability in a late stage, critical support cases on the account, no recent meetings — each reason comes back as a short label and the card shows them as tags. The action does not re-judge anything; it draws what the API says.
8
8
 
9
- The card is view-only. `awaitUserInput` stays off. For a component that only shows something, the model is told that the information was displayed — nothing `execute` returned and nothing the card drew. So the empty state ("No opportunities are at risk.") is on the card, not in a message. The count, the total and the average are computed in code from the rows.
9
+ A view card: `render` takes no `callback` and the action sets no `awaitUserInput`. As soon as the card is drawn, the agent gets "Shown to the user. The user has not acted on it." plus exactly what `execute` returned - the count, the total, the average and the rows (no rows when the card says "No opportunities are at risk."), or the failure (`success: false`, `error` and `reason`). So `execute` returns what the card draws, with each row's id, and nothing else. The count, the total and the average are computed in code from the rows.
10
10
 
11
- In Agent Studio: an action with key `show_opportunities_at_risk` and no parameters.
11
+ In Agent Studio: an action with key `show_opportunities_at_risk` and no parameters. Its `instructions`: "Answer follow-up questions about these opportunities from the data you receive: quote money and counts exactly as given, and never add them up yourself. Right after the card, one short sentence at most - do not repeat what it shows. On a failure, the card already says why: say so in one short sentence and stop. No emoji."
12
12
 
13
13
  ## Adapt it
14
14
 
@@ -16,11 +16,11 @@ In Agent Studio: an action with key `show_opportunities_at_risk` and no paramete
16
16
  |---|---|
17
17
  | `agent/api/opportunities.ts` | Mock path and envelope. Find the app's own "needs attention" list — the one with reasons per row |
18
18
  | `agent/opportunities.ts` | Which fields a row carries. Totals stay computed here |
19
- | `agent/views/at-risk.ts` | The card. `brand.ts` matches the agent's white panel; put the agent's primary colour in `accent` |
19
+ | `agent/views/at-risk.ts` | The card. `brand.ts` here is a placeholder: keep the `agent/views/brand.ts` that `npx foldspace observe styles --write` wrote - it has every key the recipe cards read. If the project has none yet, run that command first; do not copy this one |
20
20
 
21
21
  ## What that build learned the hard way
22
22
 
23
23
  - **A list has nothing to prove who it belongs to.** An account has an id and a name; a list does not. The build answered signed-out calls with a 401, which `apiFetch` already reports as `signed_out`. What the code still checks is that a 200 is the list at all — the rows array has to be there. ⚠️ If the app you are on answers a signed-out call with a 200 and an empty list, that check is not enough; find something in the response that only a signed-in user gets.
24
24
  - **Empty is an answer.** Zero at risk is drawn as a card that says so — never as a failure, and never as a hint about what to do next.
25
- - **Rows are not links.** The production build made every name clickable through a message its own single-page app listened for. No other app does; on any other product the click would do nothing. Open a record with a navigation route instead.
26
- - **The agent cannot see the card.** In the production build the render also handed the rows to the agent through `callback`, so it could answer "tell me more about the second one". That path is not in this recipe: with `awaitUserInput` off the platform does not deliver it, and a view-only action's data does not reach the model by design. If the agent needs the rows, that is a second, data-only action.
25
+ - **Rows are not links.** The production build made every name clickable through a message its own single-page app listened for. No other app does; on any other product the click would do nothing. Open a record with a navigation route instead, with the id the agent got for that row.
26
+ - **The agent answers from what `execute` returns.** The production build called `callback` with the rows so that the agent could answer "tell me more about the second one". A view card needs no `callback` for that: the agent gets everything `execute` returned, rows included.
@@ -1,7 +1,11 @@
1
- // View-only card. Do not set awaitUserInput: the model is told only that the
2
- // information was displayed, so the card carries every fact — including "nothing
3
- // is at risk". A failed or signed-out call is drawn with renderFailure, never as
4
- // a count of zero.
1
+ // A view card: render takes no callback and the action sets no
2
+ // awaitUserInput. As soon as the card is drawn, the agent is told it was shown
3
+ // and gets everything execute returned. So execute returns what the card
4
+ // draws, with each row's id, and nothing else: the count, the total, the
5
+ // average and the rows (the agent can answer "tell me more about the second
6
+ // one", or say that nothing is at risk), or the failure the card draws
7
+ // instead. A failed or signed-out call is
8
+ // drawn with renderFailure, never as a count of zero.
5
9
 
6
10
  import { getOpportunitiesAtRisk } from "../api/opportunities";
7
11
  import { isAtRiskList, toAtRisk, type AtRiskOverview } from "../opportunities";
@@ -10,7 +14,6 @@ import { mountOpportunitiesAtRisk } from "../views/at-risk";
10
14
 
11
15
  type ShowResult = {
12
16
  success: boolean;
13
- message?: string;
14
17
  error?: string;
15
18
  reason?: FailureReason;
16
19
  data?: AtRiskOverview;
@@ -26,8 +29,7 @@ export const show_opportunities_at_risk = {
26
29
  if (!isAtRiskList(res.data)) {
27
30
  return { success: false, error: "Not signed in.", reason: "signed_out" };
28
31
  }
29
- // `message` never reaches the model for a view-only action; it is for the console.
30
- return { success: true, message: "Displayed.", data: toAtRisk(res.data) };
32
+ return { success: true, data: toAtRisk(res.data) };
31
33
  },
32
34
 
33
35
  render: (result: ShowResult | undefined, host: HTMLElement, header: HTMLElement) => {
@@ -1,6 +1,8 @@
1
1
  // The count, the total and the average are computed here, from the rows the API
2
2
  // returned. The model does not add them, and the card does not trust a total the
3
- // server sent alongside rows that may disagree with it.
3
+ // server sent alongside rows that may disagree with it. A row carries what the
4
+ // card draws, plus the record's id so the agent can open it with a navigation
5
+ // route; the agent gets all of it once the card is drawn. The card shows no ids.
4
6
 
5
7
  import type { AtRiskPayload } from "./api/opportunities";
6
8
 
@@ -10,13 +12,12 @@ export type AtRiskOpportunity = {
10
12
  accountName: string | null;
11
13
  value: number;
12
14
  stage: string | null;
13
- closeDate: string | null;
14
- daysSinceUpdate: number | null;
15
15
  risks: string[];
16
16
  };
17
17
 
18
18
  export type AtRiskOverview = {
19
- metrics: { count: number; totalValue: number; avgDaysStalled: number };
19
+ /** avgDaysStalled is null when no row says how long it has stalled - never 0. */
20
+ metrics: { count: number; totalValue: number; avgDaysStalled: number | null };
20
21
  opportunities: AtRiskOpportunity[];
21
22
  };
22
23
 
@@ -49,20 +50,20 @@ function toNumber(value: number | string | null | undefined): number {
49
50
  }
50
51
 
51
52
  export function toAtRisk(payload: AtRiskPayload): AtRiskOverview {
52
- const opportunities = (payload.deals ?? []).map((row) => ({
53
+ const deals = payload.deals ?? [];
54
+ const opportunities = deals.map((row) => ({
53
55
  id: row.id?.trim() || "",
54
56
  name: row.name?.trim() || "Untitled",
55
57
  accountName: row.account?.name?.trim() || null,
56
58
  value: toNumber(row.value),
57
59
  stage: row.stage?.trim() || null,
58
- closeDate: row.closeDate?.trim() || null,
59
- daysSinceUpdate:
60
- typeof row.daysSinceUpdate === "number" && Number.isFinite(row.daysSinceUpdate) ? row.daysSinceUpdate : null,
61
60
  risks: (row.risks ?? []).filter((risk): risk is string => typeof risk === "string" && risk.trim() !== ""),
62
61
  }));
63
62
 
64
- const stalled = opportunities.map((row) => row.daysSinceUpdate).filter((days): days is number => days !== null);
65
- const avgDaysStalled = stalled.length ? Math.round(stalled.reduce((sum, days) => sum + days, 0) / stalled.length) : 0;
63
+ const stalled = deals
64
+ .map((row) => row.daysSinceUpdate)
65
+ .filter((days): days is number => typeof days === "number" && Number.isFinite(days));
66
+ const avgDaysStalled = stalled.length ? Math.round(stalled.reduce((sum, days) => sum + days, 0) / stalled.length) : null;
66
67
 
67
68
  return {
68
69
  metrics: {
@@ -86,7 +86,7 @@ export function mountOpportunitiesAtRisk(host: HTMLElement, header: HTMLElement,
86
86
  stats.append(
87
87
  stat("At risk", String(metrics.count)),
88
88
  stat("Total value", formatMoney(metrics.totalValue), true),
89
- stat("Avg days stalled", String(metrics.avgDaysStalled)),
89
+ stat("Avg days stalled", metrics.avgDaysStalled === null ? "-" : String(metrics.avgDaysStalled)),
90
90
  );
91
91
  card.append(stats);
92
92
 
@@ -1,24 +1,35 @@
1
- // The card lives in the agent's panel, which is white by default (the agent's
2
- // theme: backgroundColor #FFFFFF, primaryColor for accents) - so the palette
3
- // matches the panel, not the page. Plain hex/rgba only: the panel saves every
4
- // card to history as an image drawn with html2canvas, which cannot read
5
- // oklch()/lab()/color-mix(). Take `accent` from the agent's own primaryColor
6
- // (get_agent_settings); re-sample font and radius from the page if you want
7
- // them to match; swap for a dark set only when the panel itself is dark.
1
+ // Placeholder with the keys of the agent/views/brand.ts that
2
+ // `npx foldspace observe styles --write` writes. When you copy this recipe,
3
+ // keep the project's own brand.ts; if it has none yet, run that command.
8
4
 
9
5
  export const brand = {
10
6
  surface: "#FFFFFF",
11
- headerStart: "#F0F1FE",
12
- border: "#E5E7EB",
7
+ surface2: "#F5F6FA",
13
8
  text: "#374151",
14
- textStrong: "#111827",
15
9
  muted: "#6B7280",
16
- faint: "#9CA3AF",
10
+ border: "#E5E7EB",
17
11
  accent: "#3248F2",
18
- money: "#047857",
19
- warn: "#B45309",
20
- warnInk: "#FEF3C7",
21
- row: "#F9FAFB",
12
+ accentText: "#FFFFFF",
22
13
  font: 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
23
14
  radius: "8px",
15
+ size: { base: "14px", small: "12px" },
16
+ textStrong: "#111827",
17
+ faint: "#9CA3AF",
18
+ row: "#F9FAFB",
19
+ tableHead: "#F3F4F6",
20
+ accentDeep: "#2538D6",
21
+ accentSoft: "#EEF0FE",
22
+ headerStart: "#F0F1FE",
23
+ good: "#047857",
24
+ warn: "#B45309",
25
+ warnInk: "#FEF3C7",
26
+ bad: "#B91C1C",
27
+ primary: "#3248F2",
28
+ tint: "#F3F4F6",
29
+ disabled: "#9CA3AF",
30
+ money: "#047857",
31
+ ok: "#047857",
32
+ deals: "#3248F2",
33
+ contacts: "#2538D6",
34
+ cases: "#B45309",
24
35
  } as const;