@mccaskie-labs/workbench-config 0.2.5 → 0.3.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Every component now ships its client questions: email (which emails, where replies go, drafts of the wording), inbound email (which addresses, attachments), strings (other languages) and the database (what to store and who sees it). `OFFER_PENDING` is removed, and so is `delivery.when`: a component's `offer.when` says when its tool applies, and a manifest with neither `offer` nor `no_offer` fails the checks.
8
+
3
9
  ## 0.2.5
4
10
 
5
11
  ### Patch Changes
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The app's own database, one for testing and one for the live site.",
11
- "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration."
10
+ "intro": "The app's own database, one for testing and one for the live site."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -237,5 +236,35 @@
237
236
  "cost_hint_usd": 0,
238
237
  "note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
239
238
  }
240
- ]
239
+ ],
240
+ "offer": {
241
+ "pattern": "settings",
242
+ "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.",
243
+ "say": "The app's data goes in its own Workbench database, one for testing and one for the live site. I'll propose what to store and who can see it, and change it with a migration you can undo.",
244
+ "answers_file": "workbench/database.json",
245
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"tables\": [{ \"name\": \"orders\", \"row\": \"one order\", \"columns\": [{ \"name\": \"total\", \"kind\": \"money\", \"required\": true }], \"read\": \"own\", \"change\": \"own\" }] }`. Then write one migration (`supabase/migrations/<14 digits>_<name>.sql`) with its down file, turning on row-level security with one policy per answer, apply it on staging with Workbench Control (`sb_migration_apply`), and refresh `supabase/staging-columns.json`.",
246
+ "questions": [
247
+ {
248
+ "id": "database.tables",
249
+ "sets": "tables",
250
+ "ask": "What should the app store, and who can see and change it?",
251
+ "style": "table",
252
+ "note": "Propose the tables the request needs, each with what one row is, its columns (name, kind such as text, number, money, date, yes/no, a link to another table; and whether it is required), and who may read and who may change its rows: `own` (the person who made the row), `admins`, `signed_in` (anyone signed in) or `public` (anyone, read only). Recommend `own` for anything about a person. Show money as whole cents, never a fraction. The client accepts, removes or adds tables and columns."
253
+ }
254
+ ],
255
+ "not_asked": [
256
+ {
257
+ "sets": "keys",
258
+ "why": "The app reaches the database as the signed-in person (`dbAs`), so row-level security decides what each person sees; the template holds no secret key."
259
+ },
260
+ {
261
+ "sets": "hosting",
262
+ "why": "Each project has its own staging and production databases, made by Workbench Control in the region set for the install."
263
+ },
264
+ {
265
+ "sets": "sign_in_settings",
266
+ "why": "The database's sign-in settings follow the Workbench baseline (the workbench-sign-in skill)."
267
+ }
268
+ ]
269
+ }
241
270
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "optional",
10
- "intro": "Receiving email at the project's own addresses, for the app to act on.",
11
- "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles."
10
+ "intro": "Receiving email at the project's own addresses, for the app to act on."
12
11
  },
13
12
  "surfaces": {
14
13
  "tools": [
@@ -129,5 +128,50 @@
129
128
  "two_step": true,
130
129
  "note": "cf_email_routing_rule_delete returns a confirmation and changes nothing until Robert approves"
131
130
  }
132
- ]
131
+ ],
132
+ "offer": {
133
+ "pattern": "switch_on",
134
+ "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.",
135
+ "say": "The project can already receive email with Workbench inbound email: each address goes straight to the app, which decides what to do with every message. I'll switch on the addresses you want.",
136
+ "answers_file": "workbench/email-inbound.json",
137
+ "answers_format": "`{ \"format\": 1, \"addresses\": [{ \"name\": \"hello\", \"does\": \"saves it as a message the admins see\" }] }`. Then set `INBOUND_EMAIL` to `\"on\"` in `wrangler.jsonc`, write `handleEmail` in `src/app.ts` to do what each address `does`, and make the addresses with Workbench Control: `cf_email_routing_enable`, then `cf_email_routing_rule_create` for each address on staging first (the capabilities guide below has the steps).",
138
+ "questions": [
139
+ {
140
+ "id": "email-inbound.addresses",
141
+ "sets": "addresses",
142
+ "ask": "Which addresses should the app receive, and what should it do with each message?",
143
+ "style": "table",
144
+ "note": "Propose the addresses the request needs, each with its name (the part before @, lower case: `hello`, `bookings`) and what the app does with a message, in the client's words: save it for the admins to read, add it to a booking, answer it automatically. One address, one job. Say plainly that the app reads the mail itself: nothing is forwarded to anyone's inbox."
145
+ },
146
+ {
147
+ "id": "email-inbound.attachments",
148
+ "sets": "attachments",
149
+ "ask": "What should happen to files attached to these emails?",
150
+ "style": "cards",
151
+ "answers": [
152
+ {
153
+ "label": "Leave them out",
154
+ "value": "drop",
155
+ "means": "The app keeps the words of each email, not its files. Nothing extra to store or check."
156
+ },
157
+ {
158
+ "label": "Keep them",
159
+ "value": "keep",
160
+ "means": "The app stores each file for the admins. Files can be large or unsafe, so keep only the types the job needs."
161
+ }
162
+ ],
163
+ "note": "With keep, ask which types (photos, PDFs) and store them outside the Worker, in file storage the app already has, or ask the owner before adding some; refuse other types with `reject`."
164
+ }
165
+ ],
166
+ "not_asked": [
167
+ {
168
+ "sets": "domain",
169
+ "why": "Addresses are on the project's own email domain: its staging name first, and the client's own domain once it is attached for production (`cf_zone_attach`)."
170
+ },
171
+ {
172
+ "sets": "forwarding",
173
+ "why": "Workbench never forwards mail outside the project or catches every address: each literal address goes to the app."
174
+ }
175
+ ]
176
+ }
133
177
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes.",
11
- "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code."
10
+ "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes."
12
11
  },
13
12
  "surfaces": {
14
13
  "gateway": ["POST /v1/email/send"],
@@ -71,5 +70,70 @@
71
70
  ],
72
71
  "styles": {
73
72
  "prefix": "wb-email-"
73
+ },
74
+ "offer": {
75
+ "pattern": "settings",
76
+ "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.",
77
+ "say": "I'll send these with Workbench email, which sends from the app's own address in your brand, with no email key in the app. The sign-in codes are already there; I'll add yours beside them.",
78
+ "answers_file": "workbench/email.json",
79
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"reply_to\": null, \"emails\": [{ \"id\": \"order_confirmed\", \"when\": \"a customer sends an order\", \"to\": \"the customer\", \"parts\": [\"heading\", \"paragraph\", \"button\"], \"link_changes_something\": false }] }`. Declare each email in `src/lib/emails.ts` with `defineEmails` (stream `transactional`, shell `account`, `decidedBy` naming this answers file), put its words in the strings under `email.<id>.<key>`, and send it with `mailerFor(env)` and an `idempotencyKey` whenever a retry could repeat it.",
80
+ "questions": [
81
+ {
82
+ "id": "email.emails",
83
+ "sets": "emails",
84
+ "ask": "Which emails should the app send?",
85
+ "style": "table",
86
+ "note": "Propose the emails the request needs, each with an id (snake_case: `order_confirmed`), when it is sent, who gets it, its parts in order (heading, paragraphs, a button and its link, small print) and whether its link changes something (then the link lands on a confirmation page first). Only emails a person's own action or account causes: receipts, confirmations, notices. The client accepts, removes or adds. The wording is drafted afterwards, one email at a time."
87
+ },
88
+ {
89
+ "id": "email.reply-to",
90
+ "sets": "reply_to",
91
+ "ask": "Where should a reply to these emails go?",
92
+ "style": "cards",
93
+ "answers": [
94
+ {
95
+ "label": "Nowhere",
96
+ "value": null,
97
+ "means": "The emails come from an address that isn't read. Say so in the small print, with where to get help instead."
98
+ },
99
+ {
100
+ "label": "To the app",
101
+ "value": "app",
102
+ "means": "Replies go to an address the app reads itself (Workbench inbound email)."
103
+ }
104
+ ],
105
+ "other": {
106
+ "kind": "text",
107
+ "min": 6,
108
+ "max": 254,
109
+ "description": "An email address someone at the business reads."
110
+ },
111
+ "must_ask": true,
112
+ "note": "A typed address goes in each send as `replyTo`; check it looks like an email address. With the app, the workbench-email-inbound skill sets up the address."
113
+ }
114
+ ],
115
+ "not_asked": [
116
+ {
117
+ "sets": "sender",
118
+ "why": "Emails come from the project's own sending address (`FROM` in `src/lib/email.ts`) on its own domain, which Workbench Control sets up."
119
+ },
120
+ {
121
+ "sets": "marketing",
122
+ "why": "Only transactional email exists: newsletters and other bulk mail are refused until Workbench has a sender for them, so offer none."
123
+ },
124
+ {
125
+ "sets": "look",
126
+ "why": "Emails take the brand's name, colour and logo, and the project's `project.css` (see Restyling)."
127
+ }
128
+ ],
129
+ "drafts": {
130
+ "of": "wording for one email",
131
+ "surface": "ai",
132
+ "alias": "claude-default",
133
+ "count": 3,
134
+ "max_rounds": 3,
135
+ "job": "ai.drafts",
136
+ "command": "npx workbench-ai drafts text email_<id> --about \"<what the email is for, who gets it and its parts>\""
137
+ }
74
138
  }
75
139
  }
@@ -745,10 +745,6 @@
745
745
  "type": "string",
746
746
  "minLength": 1,
747
747
  "maxLength": 160
748
- },
749
- "when": {
750
- "type": "string",
751
- "minLength": 1
752
748
  }
753
749
  },
754
750
  "required": [
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The words on the app's screens and in its emails, kept in one place for each language.",
11
- "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language."
10
+ "intro": "The words on the app's screens and in its emails, kept in one place for each language."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -132,5 +131,49 @@
132
131
  "cost_hint_usd": 0,
133
132
  "note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
134
133
  }
135
- ]
134
+ ],
135
+ "offer": {
136
+ "pattern": "project",
137
+ "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.",
138
+ "say": "Every word the app shows is kept in one place with Workbench strings, so changing wording never means hunting through code, and other languages can be added later.",
139
+ "answers_file": "workbench/strings.json",
140
+ "answers_format": "`{ \"format\": 1, \"languages\": [\"en-AU\"], \"later\": [\"fil\"] }`: the language the site is written in first, and any the client wants later. New wording goes into `strings/strings.<base>.json` with one key per sentence, and `npm run check:strings` checks it.",
141
+ "questions": [
142
+ {
143
+ "id": "strings.languages",
144
+ "sets": "languages",
145
+ "ask": "Does the site need to be in any language besides English?",
146
+ "style": "cards",
147
+ "answers": [
148
+ {
149
+ "label": "English only",
150
+ "value": "none",
151
+ "means": "Every page and email is in English."
152
+ },
153
+ {
154
+ "label": "Another language later",
155
+ "value": "later",
156
+ "means": "English now. The words are kept so a translation can be added, checked by a native speaker, without changing the code."
157
+ }
158
+ ],
159
+ "other": {
160
+ "kind": "text",
161
+ "min": 2,
162
+ "max": 120,
163
+ "description": "The languages, such as Filipino or Spanish."
164
+ },
165
+ "note": "Translations come with a Workbench tool that isn't built yet: record the languages under `later`, keep every word in the catalogue, and tell the client a translation goes live only once a named native speaker has checked it."
166
+ }
167
+ ],
168
+ "not_asked": [
169
+ {
170
+ "sets": "base",
171
+ "why": "The site is written first in English, in the readers' spelling from the country answers, so it isn't asked again."
172
+ },
173
+ {
174
+ "sets": "wording",
175
+ "why": "Wording is content, not a setting: the client says what they want in their own words, or asks for drafts (the workbench-ai skill), and it goes into the catalogue."
176
+ }
177
+ ]
178
+ }
136
179
  }
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "b74fc43c59b7";
1
+ export declare const MANIFEST_VERSION: "de365af27674";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -909,10 +909,33 @@ export declare const capabilities: {
909
909
  readonly cost_hint_usd: 0;
910
910
  readonly note: "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does";
911
911
  }];
912
+ readonly offer: {
913
+ readonly pattern: "settings";
914
+ readonly when: "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.";
915
+ readonly say: "The app's data goes in its own Workbench database, one for testing and one for the live site. I'll propose what to store and who can see it, and change it with a migration you can undo.";
916
+ readonly answers_file: "workbench/database.json";
917
+ readonly answers_format: "`{ \"format\": 1, \"status\": \"draft\", \"tables\": [{ \"name\": \"orders\", \"row\": \"one order\", \"columns\": [{ \"name\": \"total\", \"kind\": \"money\", \"required\": true }], \"read\": \"own\", \"change\": \"own\" }] }`. Then write one migration (`supabase/migrations/<14 digits>_<name>.sql`) with its down file, turning on row-level security with one policy per answer, apply it on staging with Workbench Control (`sb_migration_apply`), and refresh `supabase/staging-columns.json`.";
918
+ readonly questions: readonly [{
919
+ readonly id: "database.tables";
920
+ readonly sets: "tables";
921
+ readonly ask: "What should the app store, and who can see and change it?";
922
+ readonly style: "table";
923
+ readonly note: "Propose the tables the request needs, each with what one row is, its columns (name, kind such as text, number, money, date, yes/no, a link to another table; and whether it is required), and who may read and who may change its rows: `own` (the person who made the row), `admins`, `signed_in` (anyone signed in) or `public` (anyone, read only). Recommend `own` for anything about a person. Show money as whole cents, never a fraction. The client accepts, removes or adds tables and columns.";
924
+ }];
925
+ readonly not_asked: readonly [{
926
+ readonly sets: "keys";
927
+ readonly why: "The app reaches the database as the signed-in person (`dbAs`), so row-level security decides what each person sees; the template holds no secret key.";
928
+ }, {
929
+ readonly sets: "hosting";
930
+ readonly why: "Each project has its own staging and production databases, made by Workbench Control in the region set for the install.";
931
+ }, {
932
+ readonly sets: "sign_in_settings";
933
+ readonly why: "The database's sign-in settings follow the Workbench baseline (the workbench-sign-in skill).";
934
+ }];
935
+ };
912
936
  readonly delivery: {
913
937
  readonly choice: "built_in";
914
938
  readonly intro: "The app's own database, one for testing and one for the live site.";
915
- readonly when: "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.";
916
939
  };
917
940
  };
918
941
  readonly email: {
@@ -1157,13 +1180,67 @@ export declare const capabilities: {
1157
1180
  readonly cost_hint_usd: 0;
1158
1181
  readonly note: "GET brand.json's logo.url from outside with no sign-in answers 200 with an image content type";
1159
1182
  }];
1183
+ readonly offer: {
1184
+ readonly pattern: "settings";
1185
+ readonly when: "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.";
1186
+ readonly say: "I'll send these with Workbench email, which sends from the app's own address in your brand, with no email key in the app. The sign-in codes are already there; I'll add yours beside them.";
1187
+ readonly answers_file: "workbench/email.json";
1188
+ readonly answers_format: "`{ \"format\": 1, \"status\": \"draft\", \"reply_to\": null, \"emails\": [{ \"id\": \"order_confirmed\", \"when\": \"a customer sends an order\", \"to\": \"the customer\", \"parts\": [\"heading\", \"paragraph\", \"button\"], \"link_changes_something\": false }] }`. Declare each email in `src/lib/emails.ts` with `defineEmails` (stream `transactional`, shell `account`, `decidedBy` naming this answers file), put its words in the strings under `email.<id>.<key>`, and send it with `mailerFor(env)` and an `idempotencyKey` whenever a retry could repeat it.";
1189
+ readonly questions: readonly [{
1190
+ readonly id: "email.emails";
1191
+ readonly sets: "emails";
1192
+ readonly ask: "Which emails should the app send?";
1193
+ readonly style: "table";
1194
+ readonly note: "Propose the emails the request needs, each with an id (snake_case: `order_confirmed`), when it is sent, who gets it, its parts in order (heading, paragraphs, a button and its link, small print) and whether its link changes something (then the link lands on a confirmation page first). Only emails a person's own action or account causes: receipts, confirmations, notices. The client accepts, removes or adds. The wording is drafted afterwards, one email at a time.";
1195
+ }, {
1196
+ readonly id: "email.reply-to";
1197
+ readonly sets: "reply_to";
1198
+ readonly ask: "Where should a reply to these emails go?";
1199
+ readonly style: "cards";
1200
+ readonly answers: readonly [{
1201
+ readonly label: "Nowhere";
1202
+ readonly value: null;
1203
+ readonly means: "The emails come from an address that isn't read. Say so in the small print, with where to get help instead.";
1204
+ }, {
1205
+ readonly label: "To the app";
1206
+ readonly value: "app";
1207
+ readonly means: "Replies go to an address the app reads itself (Workbench inbound email).";
1208
+ }];
1209
+ readonly other: {
1210
+ readonly kind: "text";
1211
+ readonly min: 6;
1212
+ readonly max: 254;
1213
+ readonly description: "An email address someone at the business reads.";
1214
+ };
1215
+ readonly must_ask: true;
1216
+ readonly note: "A typed address goes in each send as `replyTo`; check it looks like an email address. With the app, the workbench-email-inbound skill sets up the address.";
1217
+ }];
1218
+ readonly not_asked: readonly [{
1219
+ readonly sets: "sender";
1220
+ readonly why: "Emails come from the project's own sending address (`FROM` in `src/lib/email.ts`) on its own domain, which Workbench Control sets up.";
1221
+ }, {
1222
+ readonly sets: "marketing";
1223
+ readonly why: "Only transactional email exists: newsletters and other bulk mail are refused until Workbench has a sender for them, so offer none.";
1224
+ }, {
1225
+ readonly sets: "look";
1226
+ readonly why: "Emails take the brand's name, colour and logo, and the project's `project.css` (see Restyling).";
1227
+ }];
1228
+ readonly drafts: {
1229
+ readonly of: "wording for one email";
1230
+ readonly surface: "ai";
1231
+ readonly alias: "claude-default";
1232
+ readonly count: 3;
1233
+ readonly max_rounds: 3;
1234
+ readonly job: "ai.drafts";
1235
+ readonly command: "npx workbench-ai drafts text email_<id> --about \"<what the email is for, who gets it and its parts>\"";
1236
+ };
1237
+ };
1160
1238
  readonly styles: {
1161
1239
  readonly prefix: "wb-email-";
1162
1240
  };
1163
1241
  readonly delivery: {
1164
1242
  readonly choice: "built_in";
1165
1243
  readonly intro: "The emails the app sends, such as confirmations, receipts and sign-in codes.";
1166
- readonly when: "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.";
1167
1244
  };
1168
1245
  };
1169
1246
  readonly "email-inbound": {
@@ -1266,10 +1343,45 @@ export declare const capabilities: {
1266
1343
  readonly two_step: true;
1267
1344
  readonly note: "cf_email_routing_rule_delete returns a confirmation and changes nothing until Robert approves";
1268
1345
  }];
1346
+ readonly offer: {
1347
+ readonly pattern: "switch_on";
1348
+ readonly when: "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.";
1349
+ readonly say: "The project can already receive email with Workbench inbound email: each address goes straight to the app, which decides what to do with every message. I'll switch on the addresses you want.";
1350
+ readonly answers_file: "workbench/email-inbound.json";
1351
+ readonly answers_format: "`{ \"format\": 1, \"addresses\": [{ \"name\": \"hello\", \"does\": \"saves it as a message the admins see\" }] }`. Then set `INBOUND_EMAIL` to `\"on\"` in `wrangler.jsonc`, write `handleEmail` in `src/app.ts` to do what each address `does`, and make the addresses with Workbench Control: `cf_email_routing_enable`, then `cf_email_routing_rule_create` for each address on staging first (the capabilities guide below has the steps).";
1352
+ readonly questions: readonly [{
1353
+ readonly id: "email-inbound.addresses";
1354
+ readonly sets: "addresses";
1355
+ readonly ask: "Which addresses should the app receive, and what should it do with each message?";
1356
+ readonly style: "table";
1357
+ readonly note: "Propose the addresses the request needs, each with its name (the part before @, lower case: `hello`, `bookings`) and what the app does with a message, in the client's words: save it for the admins to read, add it to a booking, answer it automatically. One address, one job. Say plainly that the app reads the mail itself: nothing is forwarded to anyone's inbox.";
1358
+ }, {
1359
+ readonly id: "email-inbound.attachments";
1360
+ readonly sets: "attachments";
1361
+ readonly ask: "What should happen to files attached to these emails?";
1362
+ readonly style: "cards";
1363
+ readonly answers: readonly [{
1364
+ readonly label: "Leave them out";
1365
+ readonly value: "drop";
1366
+ readonly means: "The app keeps the words of each email, not its files. Nothing extra to store or check.";
1367
+ }, {
1368
+ readonly label: "Keep them";
1369
+ readonly value: "keep";
1370
+ readonly means: "The app stores each file for the admins. Files can be large or unsafe, so keep only the types the job needs.";
1371
+ }];
1372
+ readonly note: "With keep, ask which types (photos, PDFs) and store them outside the Worker, in file storage the app already has, or ask the owner before adding some; refuse other types with `reject`.";
1373
+ }];
1374
+ readonly not_asked: readonly [{
1375
+ readonly sets: "domain";
1376
+ readonly why: "Addresses are on the project's own email domain: its staging name first, and the client's own domain once it is attached for production (`cf_zone_attach`).";
1377
+ }, {
1378
+ readonly sets: "forwarding";
1379
+ readonly why: "Workbench never forwards mail outside the project or catches every address: each literal address goes to the app.";
1380
+ }];
1381
+ };
1269
1382
  readonly delivery: {
1270
1383
  readonly choice: "optional";
1271
1384
  readonly intro: "Receiving email at the project's own addresses, for the app to act on.";
1272
- readonly when: "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.";
1273
1385
  };
1274
1386
  };
1275
1387
  readonly "email-sending": {
@@ -2729,10 +2841,45 @@ export declare const capabilities: {
2729
2841
  readonly cost_hint_usd: 0;
2730
2842
  readonly note: "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI";
2731
2843
  }];
2844
+ readonly offer: {
2845
+ readonly pattern: "project";
2846
+ readonly when: "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.";
2847
+ readonly say: "Every word the app shows is kept in one place with Workbench strings, so changing wording never means hunting through code, and other languages can be added later.";
2848
+ readonly answers_file: "workbench/strings.json";
2849
+ readonly answers_format: "`{ \"format\": 1, \"languages\": [\"en-AU\"], \"later\": [\"fil\"] }`: the language the site is written in first, and any the client wants later. New wording goes into `strings/strings.<base>.json` with one key per sentence, and `npm run check:strings` checks it.";
2850
+ readonly questions: readonly [{
2851
+ readonly id: "strings.languages";
2852
+ readonly sets: "languages";
2853
+ readonly ask: "Does the site need to be in any language besides English?";
2854
+ readonly style: "cards";
2855
+ readonly answers: readonly [{
2856
+ readonly label: "English only";
2857
+ readonly value: "none";
2858
+ readonly means: "Every page and email is in English.";
2859
+ }, {
2860
+ readonly label: "Another language later";
2861
+ readonly value: "later";
2862
+ readonly means: "English now. The words are kept so a translation can be added, checked by a native speaker, without changing the code.";
2863
+ }];
2864
+ readonly other: {
2865
+ readonly kind: "text";
2866
+ readonly min: 2;
2867
+ readonly max: 120;
2868
+ readonly description: "The languages, such as Filipino or Spanish.";
2869
+ };
2870
+ readonly note: "Translations come with a Workbench tool that isn't built yet: record the languages under `later`, keep every word in the catalogue, and tell the client a translation goes live only once a named native speaker has checked it.";
2871
+ }];
2872
+ readonly not_asked: readonly [{
2873
+ readonly sets: "base";
2874
+ readonly why: "The site is written first in English, in the readers' spelling from the country answers, so it isn't asked again.";
2875
+ }, {
2876
+ readonly sets: "wording";
2877
+ readonly why: "Wording is content, not a setting: the client says what they want in their own words, or asks for drafts (the workbench-ai skill), and it goes into the catalogue.";
2878
+ }];
2879
+ };
2732
2880
  readonly delivery: {
2733
2881
  readonly choice: "built_in";
2734
2882
  readonly intro: "The words on the app's screens and in its emails, kept in one place for each language.";
2735
- readonly when: "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.";
2736
2883
  };
2737
2884
  };
2738
2885
  readonly ui: {
@@ -1,6 +1,6 @@
1
1
  // Generated by `npm run capabilities:build` from capabilities/*.json. Do not edit.
2
2
  // check:capabilities fails in Checks when this file is stale.
3
- export const MANIFEST_VERSION = "b74fc43c59b7";
3
+ export const MANIFEST_VERSION = "de365af27674";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -1238,10 +1238,39 @@ export const capabilities = {
1238
1238
  "note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
1239
1239
  }
1240
1240
  ],
1241
+ "offer": {
1242
+ "pattern": "settings",
1243
+ "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.",
1244
+ "say": "The app's data goes in its own Workbench database, one for testing and one for the live site. I'll propose what to store and who can see it, and change it with a migration you can undo.",
1245
+ "answers_file": "workbench/database.json",
1246
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"tables\": [{ \"name\": \"orders\", \"row\": \"one order\", \"columns\": [{ \"name\": \"total\", \"kind\": \"money\", \"required\": true }], \"read\": \"own\", \"change\": \"own\" }] }`. Then write one migration (`supabase/migrations/<14 digits>_<name>.sql`) with its down file, turning on row-level security with one policy per answer, apply it on staging with Workbench Control (`sb_migration_apply`), and refresh `supabase/staging-columns.json`.",
1247
+ "questions": [
1248
+ {
1249
+ "id": "database.tables",
1250
+ "sets": "tables",
1251
+ "ask": "What should the app store, and who can see and change it?",
1252
+ "style": "table",
1253
+ "note": "Propose the tables the request needs, each with what one row is, its columns (name, kind such as text, number, money, date, yes/no, a link to another table; and whether it is required), and who may read and who may change its rows: `own` (the person who made the row), `admins`, `signed_in` (anyone signed in) or `public` (anyone, read only). Recommend `own` for anything about a person. Show money as whole cents, never a fraction. The client accepts, removes or adds tables and columns."
1254
+ }
1255
+ ],
1256
+ "not_asked": [
1257
+ {
1258
+ "sets": "keys",
1259
+ "why": "The app reaches the database as the signed-in person (`dbAs`), so row-level security decides what each person sees; the template holds no secret key."
1260
+ },
1261
+ {
1262
+ "sets": "hosting",
1263
+ "why": "Each project has its own staging and production databases, made by Workbench Control in the region set for the install."
1264
+ },
1265
+ {
1266
+ "sets": "sign_in_settings",
1267
+ "why": "The database's sign-in settings follow the Workbench baseline (the workbench-sign-in skill)."
1268
+ }
1269
+ ]
1270
+ },
1241
1271
  "delivery": {
1242
1272
  "choice": "built_in",
1243
- "intro": "The app's own database, one for testing and one for the live site.",
1244
- "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration."
1273
+ "intro": "The app's own database, one for testing and one for the live site."
1245
1274
  }
1246
1275
  },
1247
1276
  "email": {
@@ -1546,13 +1575,77 @@ export const capabilities = {
1546
1575
  "note": "GET brand.json's logo.url from outside with no sign-in answers 200 with an image content type"
1547
1576
  }
1548
1577
  ],
1578
+ "offer": {
1579
+ "pattern": "settings",
1580
+ "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.",
1581
+ "say": "I'll send these with Workbench email, which sends from the app's own address in your brand, with no email key in the app. The sign-in codes are already there; I'll add yours beside them.",
1582
+ "answers_file": "workbench/email.json",
1583
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"reply_to\": null, \"emails\": [{ \"id\": \"order_confirmed\", \"when\": \"a customer sends an order\", \"to\": \"the customer\", \"parts\": [\"heading\", \"paragraph\", \"button\"], \"link_changes_something\": false }] }`. Declare each email in `src/lib/emails.ts` with `defineEmails` (stream `transactional`, shell `account`, `decidedBy` naming this answers file), put its words in the strings under `email.<id>.<key>`, and send it with `mailerFor(env)` and an `idempotencyKey` whenever a retry could repeat it.",
1584
+ "questions": [
1585
+ {
1586
+ "id": "email.emails",
1587
+ "sets": "emails",
1588
+ "ask": "Which emails should the app send?",
1589
+ "style": "table",
1590
+ "note": "Propose the emails the request needs, each with an id (snake_case: `order_confirmed`), when it is sent, who gets it, its parts in order (heading, paragraphs, a button and its link, small print) and whether its link changes something (then the link lands on a confirmation page first). Only emails a person's own action or account causes: receipts, confirmations, notices. The client accepts, removes or adds. The wording is drafted afterwards, one email at a time."
1591
+ },
1592
+ {
1593
+ "id": "email.reply-to",
1594
+ "sets": "reply_to",
1595
+ "ask": "Where should a reply to these emails go?",
1596
+ "style": "cards",
1597
+ "answers": [
1598
+ {
1599
+ "label": "Nowhere",
1600
+ "value": null,
1601
+ "means": "The emails come from an address that isn't read. Say so in the small print, with where to get help instead."
1602
+ },
1603
+ {
1604
+ "label": "To the app",
1605
+ "value": "app",
1606
+ "means": "Replies go to an address the app reads itself (Workbench inbound email)."
1607
+ }
1608
+ ],
1609
+ "other": {
1610
+ "kind": "text",
1611
+ "min": 6,
1612
+ "max": 254,
1613
+ "description": "An email address someone at the business reads."
1614
+ },
1615
+ "must_ask": true,
1616
+ "note": "A typed address goes in each send as `replyTo`; check it looks like an email address. With the app, the workbench-email-inbound skill sets up the address."
1617
+ }
1618
+ ],
1619
+ "not_asked": [
1620
+ {
1621
+ "sets": "sender",
1622
+ "why": "Emails come from the project's own sending address (`FROM` in `src/lib/email.ts`) on its own domain, which Workbench Control sets up."
1623
+ },
1624
+ {
1625
+ "sets": "marketing",
1626
+ "why": "Only transactional email exists: newsletters and other bulk mail are refused until Workbench has a sender for them, so offer none."
1627
+ },
1628
+ {
1629
+ "sets": "look",
1630
+ "why": "Emails take the brand's name, colour and logo, and the project's `project.css` (see Restyling)."
1631
+ }
1632
+ ],
1633
+ "drafts": {
1634
+ "of": "wording for one email",
1635
+ "surface": "ai",
1636
+ "alias": "claude-default",
1637
+ "count": 3,
1638
+ "max_rounds": 3,
1639
+ "job": "ai.drafts",
1640
+ "command": "npx workbench-ai drafts text email_<id> --about \"<what the email is for, who gets it and its parts>\""
1641
+ }
1642
+ },
1549
1643
  "styles": {
1550
1644
  "prefix": "wb-email-"
1551
1645
  },
1552
1646
  "delivery": {
1553
1647
  "choice": "built_in",
1554
- "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes.",
1555
- "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code."
1648
+ "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes."
1556
1649
  }
1557
1650
  },
1558
1651
  "email-inbound": {
@@ -1682,10 +1775,54 @@ export const capabilities = {
1682
1775
  "note": "cf_email_routing_rule_delete returns a confirmation and changes nothing until Robert approves"
1683
1776
  }
1684
1777
  ],
1778
+ "offer": {
1779
+ "pattern": "switch_on",
1780
+ "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.",
1781
+ "say": "The project can already receive email with Workbench inbound email: each address goes straight to the app, which decides what to do with every message. I'll switch on the addresses you want.",
1782
+ "answers_file": "workbench/email-inbound.json",
1783
+ "answers_format": "`{ \"format\": 1, \"addresses\": [{ \"name\": \"hello\", \"does\": \"saves it as a message the admins see\" }] }`. Then set `INBOUND_EMAIL` to `\"on\"` in `wrangler.jsonc`, write `handleEmail` in `src/app.ts` to do what each address `does`, and make the addresses with Workbench Control: `cf_email_routing_enable`, then `cf_email_routing_rule_create` for each address on staging first (the capabilities guide below has the steps).",
1784
+ "questions": [
1785
+ {
1786
+ "id": "email-inbound.addresses",
1787
+ "sets": "addresses",
1788
+ "ask": "Which addresses should the app receive, and what should it do with each message?",
1789
+ "style": "table",
1790
+ "note": "Propose the addresses the request needs, each with its name (the part before @, lower case: `hello`, `bookings`) and what the app does with a message, in the client's words: save it for the admins to read, add it to a booking, answer it automatically. One address, one job. Say plainly that the app reads the mail itself: nothing is forwarded to anyone's inbox."
1791
+ },
1792
+ {
1793
+ "id": "email-inbound.attachments",
1794
+ "sets": "attachments",
1795
+ "ask": "What should happen to files attached to these emails?",
1796
+ "style": "cards",
1797
+ "answers": [
1798
+ {
1799
+ "label": "Leave them out",
1800
+ "value": "drop",
1801
+ "means": "The app keeps the words of each email, not its files. Nothing extra to store or check."
1802
+ },
1803
+ {
1804
+ "label": "Keep them",
1805
+ "value": "keep",
1806
+ "means": "The app stores each file for the admins. Files can be large or unsafe, so keep only the types the job needs."
1807
+ }
1808
+ ],
1809
+ "note": "With keep, ask which types (photos, PDFs) and store them outside the Worker, in file storage the app already has, or ask the owner before adding some; refuse other types with `reject`."
1810
+ }
1811
+ ],
1812
+ "not_asked": [
1813
+ {
1814
+ "sets": "domain",
1815
+ "why": "Addresses are on the project's own email domain: its staging name first, and the client's own domain once it is attached for production (`cf_zone_attach`)."
1816
+ },
1817
+ {
1818
+ "sets": "forwarding",
1819
+ "why": "Workbench never forwards mail outside the project or catches every address: each literal address goes to the app."
1820
+ }
1821
+ ]
1822
+ },
1685
1823
  "delivery": {
1686
1824
  "choice": "optional",
1687
- "intro": "Receiving email at the project's own addresses, for the app to act on.",
1688
- "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles."
1825
+ "intro": "Receiving email at the project's own addresses, for the app to act on."
1689
1826
  }
1690
1827
  },
1691
1828
  "email-sending": {
@@ -3610,10 +3747,53 @@ export const capabilities = {
3610
3747
  "note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
3611
3748
  }
3612
3749
  ],
3750
+ "offer": {
3751
+ "pattern": "project",
3752
+ "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.",
3753
+ "say": "Every word the app shows is kept in one place with Workbench strings, so changing wording never means hunting through code, and other languages can be added later.",
3754
+ "answers_file": "workbench/strings.json",
3755
+ "answers_format": "`{ \"format\": 1, \"languages\": [\"en-AU\"], \"later\": [\"fil\"] }`: the language the site is written in first, and any the client wants later. New wording goes into `strings/strings.<base>.json` with one key per sentence, and `npm run check:strings` checks it.",
3756
+ "questions": [
3757
+ {
3758
+ "id": "strings.languages",
3759
+ "sets": "languages",
3760
+ "ask": "Does the site need to be in any language besides English?",
3761
+ "style": "cards",
3762
+ "answers": [
3763
+ {
3764
+ "label": "English only",
3765
+ "value": "none",
3766
+ "means": "Every page and email is in English."
3767
+ },
3768
+ {
3769
+ "label": "Another language later",
3770
+ "value": "later",
3771
+ "means": "English now. The words are kept so a translation can be added, checked by a native speaker, without changing the code."
3772
+ }
3773
+ ],
3774
+ "other": {
3775
+ "kind": "text",
3776
+ "min": 2,
3777
+ "max": 120,
3778
+ "description": "The languages, such as Filipino or Spanish."
3779
+ },
3780
+ "note": "Translations come with a Workbench tool that isn't built yet: record the languages under `later`, keep every word in the catalogue, and tell the client a translation goes live only once a named native speaker has checked it."
3781
+ }
3782
+ ],
3783
+ "not_asked": [
3784
+ {
3785
+ "sets": "base",
3786
+ "why": "The site is written first in English, in the readers' spelling from the country answers, so it isn't asked again."
3787
+ },
3788
+ {
3789
+ "sets": "wording",
3790
+ "why": "Wording is content, not a setting: the client says what they want in their own words, or asks for drafts (the workbench-ai skill), and it goes into the catalogue."
3791
+ }
3792
+ ]
3793
+ },
3613
3794
  "delivery": {
3614
3795
  "choice": "built_in",
3615
- "intro": "The words on the app's screens and in its emails, kept in one place for each language.",
3616
- "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language."
3796
+ "intro": "The words on the app's screens and in its emails, kept in one place for each language."
3617
3797
  }
3618
3798
  },
3619
3799
  "ui": {
@@ -391,17 +391,10 @@ export declare const capabilitySchema: z.ZodObject<{
391
391
  optional: "optional";
392
392
  }>;
393
393
  intro: z.ZodString;
394
- when: z.ZodOptional<z.ZodString>;
395
394
  }, z.core.$strict>>;
396
395
  }, z.core.$strict>;
397
396
  export type Capability = z.infer<typeof capabilitySchema>;
398
397
  export type Offer = z.infer<typeof offer>;
399
- /**
400
- * Components whose client questions are still being written (Robert,
401
- * 11 November 2026: every shared tool ships its questions). The list only
402
- * shrinks; a new component can't join it.
403
- */
404
- export declare const OFFER_PENDING: readonly ["database", "email", "email-inbound", "strings"];
405
398
  export interface ManifestProblem {
406
399
  file: string;
407
400
  check: "schema" | "proof" | "ids" | "surfaces" | "errors" | "generated";
@@ -203,8 +203,6 @@ const delivery = z.strictObject({
203
203
  choice: z.enum(["built_in", "optional"]),
204
204
  /** One plain sentence for the owner, shown when Claude introduces the tools. */
205
205
  intro: z.string().min(1).max(160),
206
- /** When the tool applies, in the client's words. Only for a component with no `offer`, whose `offer.when` says it. */
207
- when: z.string().min(1).optional(),
208
206
  });
209
207
  const baseShape = {
210
208
  $schema: z.literal("./schema.json"),
@@ -262,12 +260,6 @@ export const capabilitySchema = z.strictObject(baseShape).superRefine((m, ctx) =
262
260
  }
263
261
  }
264
262
  });
265
- /**
266
- * Components whose client questions are still being written (Robert,
267
- * 11 November 2026: every shared tool ships its questions). The list only
268
- * shrinks; a new component can't join it.
269
- */
270
- export const OFFER_PENDING = ["database", "email", "email-inbound", "strings"];
271
263
  function checkOffer(file, component, o, seen) {
272
264
  const problems = [];
273
265
  const named = new Map();
@@ -381,21 +373,14 @@ export function checkManifests(files) {
381
373
  problems.push(...checkOffer(file, m.component, m.offer, seen));
382
374
  if (m.offer && m.no_offer)
383
375
  problems.push({ file, check: "schema", message: "has both offer and no_offer" });
384
- const pending = OFFER_PENDING.includes(m.component);
385
- if (!m.offer && !m.no_offer && !pending) {
376
+ if (!m.offer && !m.no_offer) {
386
377
  problems.push({ file, check: "schema", message: "every component ships its client questions: add an offer, or no_offer with the reason a client has nothing to answer" });
387
378
  }
388
- if ((m.offer || m.no_offer) && pending)
389
- problems.push({ file, check: "schema", message: `remove ${m.component} from OFFER_PENDING` });
390
379
  // §9.6: every component a client's Claude Code works with says how it reaches the project.
391
380
  if (m.no_offer && m.delivery)
392
381
  problems.push({ file, check: "schema", message: "has both no_offer and delivery: plumbing gets no skill of its own" });
393
382
  if (!m.no_offer && !m.delivery)
394
383
  problems.push({ file, check: "schema", message: "add delivery: whether the tool is built in or optional, and one sentence that introduces it to the owner" });
395
- if (m.delivery && m.offer && m.delivery.when)
396
- problems.push({ file, check: "schema", message: "delivery.when repeats offer.when: remove it" });
397
- if (m.delivery && !m.offer && !m.delivery.when)
398
- problems.push({ file, check: "schema", message: "delivery needs when, because there is no offer to say when the tool applies" });
399
384
  }
400
385
  const byComponent = new Map(manifests.map((m) => [m.component, m]));
401
386
  for (const { file, m, own } of deferred) {
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { WORKBENCH_ENVS, workbenchEnv, type WorkbenchEnv } from "./env.ts";
2
2
  export { defineConfig, type Config } from "./define-config.ts";
3
3
  export { sharedSecret, workerSecret, redact, redactAll, type SecretsStoreSecretLike } from "./secrets.ts";
4
4
  export { WorkbenchConfigError, type ConfigErrorCode, type ConfigProblem } from "./errors.ts";
5
- export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, type Offer, OFFER_PATTERNS, OFFER_PENDING, } from "./capability-schema.ts";
5
+ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, type Offer, OFFER_PATTERNS, } from "./capability-schema.ts";
6
6
  export { projectNotes, renderSkill, renderSkills, renderStart, skillName, SKILLS_DIR, START_SKILL, TOOLS_FILE } from "./skills.ts";
7
7
  export { toolsState } from "./tools.ts";
8
8
  export type { ToolChoices, ToolsState } from "./tools.ts";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { WORKBENCH_ENVS, workbenchEnv } from "./env.js";
2
2
  export { defineConfig } from "./define-config.js";
3
3
  export { sharedSecret, workerSecret, redact, redactAll } from "./secrets.js";
4
4
  export { WorkbenchConfigError } from "./errors.js";
5
- export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, OFFER_PATTERNS, OFFER_PENDING, } from "./capability-schema.js";
5
+ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, OFFER_PATTERNS, } from "./capability-schema.js";
6
6
  export { projectNotes, renderSkill, renderSkills, renderStart, skillName, SKILLS_DIR, START_SKILL, TOOLS_FILE } from "./skills.js";
7
7
  export { toolsState } from "./tools.js";
8
8
  export { capabilities, MANIFEST_VERSION } from "./capabilities.gen.js";
package/dist/skills.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare const TOOLS_FILE = "workbench/tools.json";
6
6
  export declare const START_SKILL = "workbench-start";
7
7
  /** The project's own notes on a tool, beside its generated skill: where this project wires it in. Never generated. */
8
8
  export declare const projectNotes: (component: string) => string;
9
- /** The SKILL.md for one manifest, or undefined when it has no `delivery` (plumbing). `guide` is its `capabilities/<component>.md`, when there is one. */
9
+ /** The SKILL.md for one manifest, or undefined for plumbing (no `delivery` or no `offer`). `guide` is its `capabilities/<component>.md`, when there is one. */
10
10
  export declare function renderSkill(m: Capability, guide?: string): string | undefined;
11
11
  /** The `workbench-start` skill: every tool introduced to the owner, and how their choices are recorded (§9.6). */
12
12
  export declare function renderStart(manifests: readonly Capability[]): string;
package/dist/skills.js CHANGED
@@ -120,14 +120,14 @@ function useBlock(m, guide) {
120
120
  out.push(`Read \`${projectNotes(m.component)}\` next, if it is there: it is this project's own note on where the tool is already wired in and how to call it here. It is the project's to edit; add to it when the wiring changes. Its limits, rules and error codes are in ${manifest}, and each package's API is in its \`README.md\` under \`node_modules/@mccaskie-labs/\`.`);
121
121
  return out;
122
122
  }
123
- /** The SKILL.md for one manifest, or undefined when it has no `delivery` (plumbing). `guide` is its `capabilities/<component>.md`, when there is one. */
123
+ /** The SKILL.md for one manifest, or undefined for plumbing (no `delivery` or no `offer`). `guide` is its `capabilities/<component>.md`, when there is one. */
124
124
  export function renderSkill(m, guide) {
125
125
  const o = m.offer;
126
126
  const d = m.delivery;
127
- if (!d)
127
+ if (!d || !o)
128
128
  return undefined;
129
- const when = o?.when ?? d.when ?? m.summary;
130
- const description = (o ? `${when} Offers Workbench ${m.title} and asks the client for its settings.` : `${when} Use Workbench ${m.title} for it.`).replaceAll("\n", " ");
129
+ const when = o.when;
130
+ const description = `${when} Offers Workbench ${m.title} and asks the client for its settings.`.replaceAll("\n", " ");
131
131
  const out = [
132
132
  "---",
133
133
  `name: ${skillName(m.component)}`,
@@ -146,10 +146,6 @@ export function renderSkill(m, guide) {
146
146
  "",
147
147
  ...beforeBlock(m),
148
148
  ];
149
- if (!o) {
150
- out.push("", "## Questions", "", "This tool's questions for the client aren't written yet. Use it with its defaults, tell the client that is what you did, and invent no setting. A choice only the client can make goes to them as a plain question.", ...(m.styles ? ["", "## Restyling", "", restyleLine(m.styles.prefix)] : []), "", ...useBlock(m, guide));
151
- return `${out.join("\n")}\n`;
152
- }
153
149
  out.push("", "## What to say", "", d.choice === "optional"
154
150
  ? `Once the owner's choice is yes, say: "${o.say}"`
155
151
  : `Say: "${o.say}" Don't ask whether to use Workbench or something of the client's own. If the client says they don't want it, stop and say what they give up.`, "", "## How to ask", "", ...(o.needs?.length ? [`- First check the project setup this needs: ${o.needs.map((n) => `\`workbench/${n}.json\``).join(", ")}. If one is missing, use that component's \`workbench-<component>\` skill first.`] : []), ...PATTERN_STEPS[o.pattern].map((step) => `- ${step}`), "- Ask with the AskUserQuestion tool, up to four questions per call. Give each question's answers in the order below; the first is recommended, so label it \"(Recommended)\", unless the question says none is. The tool adds \"Other\" itself.", "- Where the client accepts every recommendation, still ask each \"Must ask\" question.", "- An \"Other\" answer must meet the rule under its question. When it doesn't, say why and ask again. Never store a value outside it.", "- Ask nothing that isn't listed here, and invent no setting. A need the questions don't cover goes back to the client as a plain question, and isn't written to the answers file.", `- Write the answers to \`${o.answers_file}\`${o.per ? `, one entry per ${o.per}` : ""}: ${o.answers_format ?? `for each question its id, the value, and the client's own words when they typed "Other" or changed a recommendation.`}${o.generator ? ` Then run \`${o.generator}\`, which writes the code and refuses anything the package can't apply (it doesn't check the "Other" rules: you do). It builds nothing while an answers file is a draft. Never edit what it writes; change the answers file and run it again.` : ""}`, "", "## Questions", "", o.questions.map((q, i) => questionBlock(q, i + 1)).join("\n\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",