@mccaskie-labs/workbench-config 0.3.1 → 0.4.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.
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "9aae488ad117";
1
+ export declare const MANIFEST_VERSION: "7ad8cef4de58";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -9,7 +9,7 @@ export declare const capabilities: {
9
9
  readonly status: "built";
10
10
  readonly surfaces: {
11
11
  readonly gateway: readonly ["POST /anthropic/v1/messages", "POST /anthropic/v1/messages/count_tokens"];
12
- readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi", "checkWriting", "writingStops", "writingNotes", "writingFixNote"];
12
+ readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi", "checkWriting", "writingStops", "writingNotes", "writingFixNote", "Ai.reply", "checkGuide"];
13
13
  };
14
14
  readonly config: readonly [{
15
15
  readonly name: "WORKBENCH_GATEWAY_KEY";
@@ -114,6 +114,12 @@ export declare const capabilities: {
114
114
  readonly text: "Name a Workbench voice and spelling with the voice and spelling options, so the Gateway adds the text, or bring the app's own voice. Never paste Workbench's voice text into a prompt.";
115
115
  readonly enforced_by: "gateway";
116
116
  readonly probe: "ai.voice";
117
+ }, {
118
+ readonly id: "ai.reply-facts-from-code";
119
+ readonly kind: "must";
120
+ readonly text: "Draft replies with ai.reply from facts worked out by code (prices and dates from quote(), never from the AI), and show the draft in renderReplyBox for a person to check and copy. Never send a draft automatically, and never let a draft with needs_human_check go out unread.";
121
+ readonly enforced_by: "review";
122
+ readonly probe: "ai.reply";
117
123
  }];
118
124
  readonly examples: readonly [{
119
125
  readonly id: "ai.example.text";
@@ -127,6 +133,12 @@ export declare const capabilities: {
127
133
  readonly lang: "ts";
128
134
  readonly code: "const r = await ai.text({ model: 'claude-default', job: 'article.write', voice: 'conversational', spelling: 'en-AU', prompt })\nconst issues = checkWriting(r.text, { voice: 'conversational' })\nif (writingStops(issues).length > 0) await retryWith(prompt + writingFixNote(issues))";
129
135
  readonly probe: "ai.voice";
136
+ }, {
137
+ readonly id: "ai.example.reply";
138
+ readonly title: "Draft a reply to an enquiry";
139
+ readonly lang: "ts";
140
+ readonly code: "import { REPLY_GUIDES } from './replies.generated'\nconst ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL, replyGuides: REPLY_GUIDES })\nconst q = quote(settings, ask)\nconst draft = await ai.reply({\n guide: 'venue-quote',\n facts: { venue_name: venue.name, date: ask.date, available: q.available, packages: q.options.map((o) => ({ name: o.package, price: o.total.amount })), alternatives: q.alternatives.map((a) => a.date) },\n incoming: { text, intake: read },\n language: account.reply_language,\n})\nreturn html(renderReplyBox(draft, { againAction: `/enquiries/${id}/reply` }))";
141
+ readonly probe: "ai.reply";
130
142
  }];
131
143
  readonly probes: readonly [{
132
144
  readonly id: "ai.text.claude-default";
@@ -193,18 +205,23 @@ export declare const capabilities: {
193
205
  readonly half: "runtime";
194
206
  readonly expect: "pass";
195
207
  readonly cost_hint_usd: 0.008;
208
+ }, {
209
+ readonly id: "ai.reply";
210
+ readonly half: "runtime";
211
+ readonly expect: "pass";
212
+ readonly cost_hint_usd: 0.005;
196
213
  }];
197
214
  readonly offer: {
198
215
  readonly pattern: "feature_or_content";
199
- readonly when: "The client wants the app to write, summarise, sort, answer questions or pull details out of text with AI, as a feature visitors use, or wants wording for the site written once.";
216
+ readonly when: "The client wants the app to write, summarise, sort, answer questions or pull details out of text with AI, as a feature visitors use, or wants wording for the site written once, or wants ready-to-paste replies to the messages the business gets (quote requests, booking questions).";
200
217
  readonly say: "I'll do this with Workbench AI, which uses Claude through Workbench, so the app holds no AI key and spending has a monthly cap.";
201
218
  readonly answers_file: "workbench/ai/<name>.json";
202
219
  readonly per: "feature or piece of wording";
203
- readonly answers_format: "For a feature: `{ \"format\": 1, \"route\": \"feature\", \"name\", \"about\" (the client's words), \"who\", \"cap_usd\", \"keep\", \"job\" }`, where `job` is the `X-Workbench-Job` the feature's calls send (`<name>` with dots, such as `quotes.estimate`), so its spending shows on its own line in Control. Write it, then build the feature with the package. For wording made once: `npx workbench-ai pick <name> <round>-<draft>` writes it from the draft the client picked, with the round's request ids and cost; never write one from nothing. Then put the wording into the strings catalogue (the workbench-strings skill), keyed for where it is shown. Drafts and their previews stay in `workbench/ai/drafts/`, which isn't committed.";
220
+ readonly answers_format: "For a feature: `{ \"format\": 1, \"route\": \"feature\", \"name\", \"about\" (the client's words), \"who\", \"cap_usd\", \"keep\", \"job\" }`, where `job` is the `X-Workbench-Job` the feature's calls send (`<name>` with dots, such as `quotes.estimate`), so its spending shows on its own line in Control. Write it, then build the feature with the package. For wording made once: `npx workbench-ai pick <name> <round>-<draft>` writes it from the draft the client picked, with the round's request ids and cost; never write one from nothing. Then put the wording into the strings catalogue (the workbench-strings skill), keyed for where it is shown. Drafts and their previews stay in `workbench/ai/drafts/`, which isn't committed. For replies: write the guide itself to `workbench/replies/<name>.json` (format 1: `name`, `version`, `voice`, `spelling`, `max_words`, `max_questions`, `facts` as a small JSON Schema with `x-money` on amounts and `format: date` on dates, `ask_order`, `guide` lines, `examples`, `never`), then run `npx workbench-ai replies generate`, which checks it and writes `src/replies.generated.ts`; never edit that file. Offer samples with `npx workbench-ai replies drafts <name> --languages <the client's languages>` (two to four, with the development key; tell the client the cost it prints) and keep the one the client picks with `npx workbench-ai replies pick <name> <round>-<draft>`, which makes it the guide's first example.";
204
221
  readonly questions: readonly [{
205
222
  readonly id: "ai.route";
206
223
  readonly sets: "route";
207
- readonly ask: "Is this something visitors use on the site, or something made once for the site?";
224
+ readonly ask: "Is this something visitors use on the site, something made once for the site, or replies to the messages you get?";
208
225
  readonly style: "cards";
209
226
  readonly answers: readonly [{
210
227
  readonly label: "Visitors use it";
@@ -214,9 +231,13 @@ export declare const capabilities: {
214
231
  readonly label: "Made once for the site";
215
232
  readonly value: "content";
216
233
  readonly means: "Wording for a page, written once and kept: an About page, a welcome, product descriptions.";
234
+ }, {
235
+ readonly label: "Replies to messages";
236
+ readonly value: "reply";
237
+ readonly means: "A ready-to-paste reply to a customer's message, with your prices and dates filled in from your own details. A person checks it and copies it; nothing is sent by itself.";
217
238
  }];
218
239
  readonly must_ask: true;
219
- readonly note: "Ask it even when the client's words seem to say, as a quick check: the two routes are built and paid for differently.";
240
+ readonly note: "Ask it even when the client's words seem to say, as a quick check: the two routes are built and paid for differently. Replies are their own route: the facts (prices, dates) come from the app's own code, so ask where those facts come from before anything else.";
220
241
  }, {
221
242
  readonly id: "ai.who";
222
243
  readonly sets: "feature.who";
@@ -319,6 +340,156 @@ export declare const capabilities: {
319
340
  readonly is: readonly ["content"];
320
341
  };
321
342
  readonly note: "Name the piece after where it goes (about_us, home_blurb): lower case and underscores. The client's own words, with any facts they give (when they started, where they are), go in --about, because the drafts never invent facts and leave [gaps] for the owner instead.";
343
+ }, {
344
+ readonly id: "ai.reply.purpose";
345
+ readonly sets: "reply.purpose";
346
+ readonly ask: "What are these replies answering?";
347
+ readonly style: "cards";
348
+ readonly answers: readonly [{
349
+ readonly label: "Quote and price requests";
350
+ readonly value: "quote";
351
+ readonly means: "\"How much for Dec 12, 15 pax?\": the reply gives the price and asks only for what's missing.";
352
+ }, {
353
+ readonly label: "Availability questions";
354
+ readonly value: "availability";
355
+ readonly means: "\"Available pa po ba sa Sabado?\": the reply says yes or offers other dates.";
356
+ }, {
357
+ readonly label: "Booking or appointment requests";
358
+ readonly value: "booking";
359
+ readonly means: "\"May slot po ba bukas?\": the reply lists the free times and asks which one.";
360
+ }];
361
+ readonly other: {
362
+ readonly kind: "text";
363
+ readonly min: 3;
364
+ readonly max: 200;
365
+ readonly description: "What the messages are, in a few words.";
366
+ };
367
+ readonly must_ask: true;
368
+ readonly when: {
369
+ readonly id: "ai.route";
370
+ readonly is: readonly ["reply"];
371
+ };
372
+ readonly note: "Name the guide after it (venue-quote, appointment-slots): lower case and dashes. Say that prices and dates always come from the app's own details (workbench-availability's quote() where it's a booking), never from the AI.";
373
+ }, {
374
+ readonly id: "ai.reply.facts";
375
+ readonly sets: "reply.facts";
376
+ readonly ask: "Here are the details a reply can use. Keep, change or add?";
377
+ readonly style: "table";
378
+ readonly must_ask: true;
379
+ readonly when: {
380
+ readonly id: "ai.route";
381
+ readonly is: readonly ["reply"];
382
+ };
383
+ readonly note: "Propose one row per fact from the client's words and what the app already holds: its name, what it is, where the app gets it, and whether it is money (x-money with the currency) or a date. Only these can appear in a reply; a fact the app doesn't have can't be quoted. Then ask which details a reply may ask the customer for, most important first (ask_order); intake's missing fields are asked from that list only.";
384
+ }, {
385
+ readonly id: "ai.reply.language";
386
+ readonly sets: "reply.language";
387
+ readonly ask: "Which language should replies use?";
388
+ readonly style: "cards";
389
+ readonly answers: readonly [{
390
+ readonly label: "Match the message";
391
+ readonly value: "match";
392
+ readonly means: "English gets English, Taglish gets Taglish, Tagalog gets Tagalog. When it can't tell, it uses your fallback and asks you to check.";
393
+ }, {
394
+ readonly label: "Always Taglish";
395
+ readonly value: "taglish";
396
+ }, {
397
+ readonly label: "Always English";
398
+ readonly value: "en";
399
+ }, {
400
+ readonly label: "Always Tagalog";
401
+ readonly value: "tl";
402
+ }];
403
+ readonly when: {
404
+ readonly id: "ai.route";
405
+ readonly is: readonly ["reply"];
406
+ };
407
+ readonly note: "This is the default. Where the app has accounts (a venue each), make it each account's own setting in its portal, defaulting to this, and pass it to ai.reply as language; with match, pass the account's fallback as fallbackLanguage.";
408
+ }, {
409
+ readonly id: "ai.reply.voice";
410
+ readonly sets: "reply.voice";
411
+ readonly ask: "Which tone?";
412
+ readonly style: "cards";
413
+ readonly answers: readonly [{
414
+ readonly label: "Conversational";
415
+ readonly value: "conversational";
416
+ readonly means: "Warm and natural, like a helpful person at the front desk.";
417
+ }, {
418
+ readonly label: "Casual";
419
+ readonly value: "casual";
420
+ readonly means: "Relaxed and friendly, like chatting with a regular.";
421
+ }, {
422
+ readonly label: "Plain";
423
+ readonly value: "plain";
424
+ readonly means: "Short and clear, nothing extra.";
425
+ }, {
426
+ readonly label: "Formal";
427
+ readonly value: "formal";
428
+ readonly means: "Polite and proper, for a business that keeps a formal tone.";
429
+ }];
430
+ readonly when: {
431
+ readonly id: "ai.route";
432
+ readonly is: readonly ["reply"];
433
+ };
434
+ }, {
435
+ readonly id: "ai.reply.length";
436
+ readonly sets: "reply.max_words";
437
+ readonly ask: "How long can a reply be, at most?";
438
+ readonly style: "cards";
439
+ readonly answers: readonly [{
440
+ readonly label: "About 90 words";
441
+ readonly value: 90;
442
+ readonly means: "Room for a price or two and one question.";
443
+ }, {
444
+ readonly label: "About 50 words";
445
+ readonly value: 50;
446
+ readonly means: "A quick chat reply.";
447
+ }, {
448
+ readonly label: "About 150 words";
449
+ readonly value: 150;
450
+ readonly means: "Room for several packages or options.";
451
+ }];
452
+ readonly other: {
453
+ readonly kind: "whole";
454
+ readonly min: 20;
455
+ readonly max: 300;
456
+ readonly description: "Words, from 20 to 300.";
457
+ };
458
+ readonly when: {
459
+ readonly id: "ai.route";
460
+ readonly is: readonly ["reply"];
461
+ };
462
+ readonly note: "A reply up to a quarter over passes quietly; one over twice as long is sent back to be written again, then flagged for a person to check.";
463
+ }, {
464
+ readonly id: "ai.reply.never";
465
+ readonly sets: "reply.never";
466
+ readonly ask: "Is there anything a reply must never say?";
467
+ readonly style: "cards";
468
+ readonly answers: readonly [{
469
+ readonly label: "The usual three";
470
+ readonly value: "usual";
471
+ readonly means: "Never promise a discount, confirm a booking, or ask for payment details. Code checks these as well as telling the AI.";
472
+ }, {
473
+ readonly label: "The usual three, and more";
474
+ readonly value: "usual_and_more";
475
+ readonly means: "Those three plus things of your own (type them in Other).";
476
+ }, {
477
+ readonly label: "Nothing";
478
+ readonly value: "none";
479
+ readonly means: "No rules beyond Workbench's own content rules.";
480
+ }];
481
+ readonly other: {
482
+ readonly kind: "list";
483
+ readonly min: 1;
484
+ readonly max: 10;
485
+ readonly description: "Each thing a reply must never do, in a few words (give medical advice, mention the pool).";
486
+ };
487
+ readonly must_ask: true;
488
+ readonly when: {
489
+ readonly id: "ai.route";
490
+ readonly is: readonly ["reply"];
491
+ };
492
+ readonly note: "Write each as a never line in the guide. Lines about payment details, discounts and confirming bookings are also checked by code; others go into the prompt only, so say so if the client's line matters a lot.";
322
493
  }];
323
494
  readonly not_asked: readonly [{
324
495
  readonly sets: "model";
@@ -337,6 +508,9 @@ export declare const capabilities: {
337
508
  readonly command: "npx workbench-ai drafts text <name> --about \"<what it is for, with the client's facts>\"";
338
509
  };
339
510
  };
511
+ readonly styles: {
512
+ readonly prefix: "wb-reply-";
513
+ };
340
514
  readonly delivery: {
341
515
  readonly choice: "built_in";
342
516
  readonly intro: "Writing, summarising and answering questions with Claude, with a monthly spending cap.";
@@ -2721,6 +2895,297 @@ export declare const capabilities: {
2721
2895
  readonly intro: "Pictures made by AI from a description.";
2722
2896
  };
2723
2897
  };
2898
+ readonly intake: {
2899
+ readonly $schema: "./schema.json";
2900
+ readonly format: 1;
2901
+ readonly component: "intake";
2902
+ readonly title: "Message intake through the Gateway";
2903
+ readonly summary: "Reads one social media message (a Messenger chat, a group post or a comment), as a screenshot, pasted text or both, into one of the project's own intake types: the type, its fields, what is missing or reads two ways, and whether a person should check it. The types are files in the project's repo, sent with every call. Workbench keeps only the call's facts, never the image or the text; the project can keep them in its own bucket.";
2904
+ readonly status: "built";
2905
+ readonly surfaces: {
2906
+ readonly gateway: readonly ["POST /v1/intake/read"];
2907
+ readonly package: readonly ["Ai.intake", "defineIntakeTypes", "IntakeTypes", "IntakeResult"];
2908
+ };
2909
+ readonly config: readonly [{
2910
+ readonly name: "WORKBENCH_GATEWAY_KEY";
2911
+ readonly required: true;
2912
+ readonly secret: true;
2913
+ readonly environments: readonly ["staging", "production"];
2914
+ readonly description: "Project key with the ai scope, delivered as a Worker secret by gw_key_issue";
2915
+ }];
2916
+ readonly limits: readonly [{
2917
+ readonly id: "intake.types-per-call";
2918
+ readonly value: 20;
2919
+ readonly unit: "types";
2920
+ readonly per: "request";
2921
+ readonly enforced_by: "gateway";
2922
+ readonly refusal: "invalid_request";
2923
+ }, {
2924
+ readonly id: "intake.properties-per-type";
2925
+ readonly value: 40;
2926
+ readonly unit: "properties";
2927
+ readonly per: "type";
2928
+ readonly enforced_by: "gateway";
2929
+ readonly refusal: "invalid_request";
2930
+ }, {
2931
+ readonly id: "intake.definition-size";
2932
+ readonly value: 16384;
2933
+ readonly unit: "bytes";
2934
+ readonly per: "type";
2935
+ readonly enforced_by: "gateway";
2936
+ readonly refusal: "invalid_request";
2937
+ }, {
2938
+ readonly id: "intake.types-size";
2939
+ readonly value: 65536;
2940
+ readonly unit: "bytes";
2941
+ readonly per: "request";
2942
+ readonly enforced_by: "gateway";
2943
+ readonly refusal: "invalid_request";
2944
+ }, {
2945
+ readonly id: "intake.text-length";
2946
+ readonly value: 20000;
2947
+ readonly unit: "characters";
2948
+ readonly per: "request";
2949
+ readonly enforced_by: "gateway";
2950
+ readonly refusal: "invalid_request";
2951
+ }, {
2952
+ readonly id: "intake.image-size";
2953
+ readonly value: 5242880;
2954
+ readonly unit: "bytes";
2955
+ readonly per: "request";
2956
+ readonly enforced_by: "gateway";
2957
+ readonly refusal: "invalid_request";
2958
+ }, {
2959
+ readonly id: "intake.images-per-call";
2960
+ readonly value: 1;
2961
+ readonly unit: "images";
2962
+ readonly per: "request";
2963
+ readonly enforced_by: "gateway";
2964
+ readonly refusal: "invalid_request";
2965
+ }];
2966
+ readonly errors: readonly [{
2967
+ readonly code: "invalid_request";
2968
+ readonly http: 400;
2969
+ readonly retryable: false;
2970
+ readonly meaning: "No text or image, an image too large or of another kind, or a type definition that breaks the rules (the message names the type and the JSON path)";
2971
+ }, {
2972
+ readonly code: "budget_exceeded";
2973
+ readonly http: 402;
2974
+ readonly retryable: false;
2975
+ readonly meaning: "The project's budget, its intake cap or the global ceiling is reached";
2976
+ }, {
2977
+ readonly code: "content_refused";
2978
+ readonly http: 422;
2979
+ readonly retryable: false;
2980
+ readonly meaning: "The content rules, or Claude itself, refused the message or the reading";
2981
+ }, {
2982
+ readonly code: "content_check_unavailable";
2983
+ readonly http: 503;
2984
+ readonly retryable: true;
2985
+ readonly meaning: "The content checker is down, so nothing was read";
2986
+ }, {
2987
+ readonly code: "upstream_error";
2988
+ readonly http: 502;
2989
+ readonly retryable: true;
2990
+ readonly meaning: "Anthropic failed after the Gateway's retries";
2991
+ }, {
2992
+ readonly code: "upstream_timeout";
2993
+ readonly http: 504;
2994
+ readonly retryable: true;
2995
+ readonly meaning: "Anthropic didn't answer in time";
2996
+ }];
2997
+ readonly rules: readonly [{
2998
+ readonly id: "intake.types-from-files";
2999
+ readonly kind: "must";
3000
+ readonly text: "Keep each intake type as a file, workbench/intake/<name>.json, and pass intakeTypes from src/intake.generated.ts. Run npx workbench-ai intake generate after changing one, and keep npx workbench-ai intake generate --check in the project's Checks. Never write definitions inline.";
3001
+ readonly enforced_by: "types";
3002
+ readonly probe: "intake.read.text";
3003
+ }, {
3004
+ readonly id: "intake.human-check";
3005
+ readonly kind: "must";
3006
+ readonly text: "Act on needsHumanCheck: show the reading to a person (or keep it marked to check) before it becomes a booking, a quote or a reply. It is the Gateway's rule, not the model's score.";
3007
+ readonly enforced_by: "review";
3008
+ readonly probe: "intake.read.unknown";
3009
+ }, {
3010
+ readonly id: "intake.personal-marked";
3011
+ readonly kind: "must";
3012
+ readonly text: "Mark every field that holds a personal detail with x-personal (name, handle, phone, email, address, photo). An unmarked field whose name or format looks personal is refused, and personal details the type doesn't ask for are blanked in rawText.";
3013
+ readonly enforced_by: "gateway";
3014
+ readonly probe: "intake.read.personal-blanked";
3015
+ }, {
3016
+ readonly id: "intake.no-content-kept";
3017
+ readonly kind: "must_not";
3018
+ readonly text: "Expect Workbench to keep the image or the text: it keeps only the call's facts. To keep them, set save_input to project in workbench/intake.json and pass store: env.INTAKE_STORE.";
3019
+ readonly enforced_by: "gateway";
3020
+ readonly probe: "intake.read.text";
3021
+ }, {
3022
+ readonly id: "intake.subject-is-app-user";
3023
+ readonly kind: "must_not";
3024
+ readonly text: "Send the poster's name or number as subject or ref. subject is user:<id> of the app's own user; ref is the app's own record id, such as enquiry:4812.";
3025
+ readonly enforced_by: "review";
3026
+ readonly probe: "intake.read.text";
3027
+ }, {
3028
+ readonly id: "intake.bad-type-refused";
3029
+ readonly kind: "must_not";
3030
+ readonly text: "Send a type that breaks the format: the Gateway refuses it with invalid_request before any spend.";
3031
+ readonly enforced_by: "gateway";
3032
+ readonly probe: "intake.read.bad-type-refused";
3033
+ }];
3034
+ readonly examples: readonly [{
3035
+ readonly id: "intake.example.read";
3036
+ readonly title: "Read a pasted message or a screenshot";
3037
+ readonly lang: "ts";
3038
+ readonly code: "import { intakeTypes } from './intake.generated'\n\nconst r = await ai.intake({ types: intakeTypes, text, image, timeZone: country.timeZone.id, job: 'intake.quote-request', subject: `user:${user.id}`, ref: `enquiry:${enquiry.id}`, store: env.INTAKE_STORE })\nif (r.type === 'venue-quote-request') {\n await saveEnquiry({ ...r.data, toCheck: r.needsHumanCheck, missing: r.missing })\n}";
3039
+ readonly probe: "intake.read.text";
3040
+ }];
3041
+ readonly probes: readonly [{
3042
+ readonly id: "intake.read.text";
3043
+ readonly half: "runtime";
3044
+ readonly expect: "pass";
3045
+ readonly smoke: true;
3046
+ readonly cost_hint_usd: 0.01;
3047
+ readonly note: "One of HmPo's sample messages as text: the type, the data and no human check";
3048
+ }, {
3049
+ readonly id: "intake.read.unknown";
3050
+ readonly half: "runtime";
3051
+ readonly expect: "pass";
3052
+ readonly cost_hint_usd: 0.01;
3053
+ readonly note: "A thank-you review comes back unknown, with needs_human_check and unknown_type";
3054
+ }, {
3055
+ readonly id: "intake.read.personal-blanked";
3056
+ readonly half: "runtime";
3057
+ readonly expect: "pass";
3058
+ readonly cost_hint_usd: 0.01;
3059
+ readonly note: "A message with a phone number the type doesn't ask for: rawText shows [phone]";
3060
+ }, {
3061
+ readonly id: "intake.read.bad-type-refused";
3062
+ readonly half: "runtime";
3063
+ readonly expect: "refused_as_expected";
3064
+ readonly refusal: "invalid_request";
3065
+ readonly cost_hint_usd: 0;
3066
+ readonly note: "A type with an unmarked contact_phone field is refused before any spend";
3067
+ }];
3068
+ readonly offer: {
3069
+ readonly pattern: "settings";
3070
+ readonly when: "The client gets enquiries, orders or posts as messages (Messenger, a Facebook group, comments) and wants them read into a form or a list instead of typed up by hand.";
3071
+ readonly say: "I'll set this up with Workbench message intake: you paste a message or a screenshot and it fills in the details, and flags the ones a person should check. Workbench never keeps the message. Here are the details I'd pull out; tell me what to change.";
3072
+ readonly answers_file: "workbench/intake/<name>.json";
3073
+ readonly per: "kind of message";
3074
+ readonly generator: "npx workbench-ai intake generate";
3075
+ readonly answers_format: "One type per file, docs/message-intake.md §3: `{ \"format\": 1, \"name\", \"version\": 1, \"description\" (the client's words, made plain), \"not_this\", \"examples\" (1 to 5 real-looking messages with the data each should give), \"glossary\" (the trade's own shorthand), \"schema\" (the fields from intake.fields; required lists what the app can't do without), \"confidence_floor\", \"model\", \"save_input\" }`. The project's own setting is workbench/intake.json, `{ \"format\": 1, \"save_input\": \"none\" | \"project\" }`. Then run the generator and commit src/intake.generated.ts.";
3076
+ readonly questions: readonly [{
3077
+ readonly id: "intake.kind";
3078
+ readonly sets: readonly ["name", "description"];
3079
+ readonly ask: "What kind of message do you want read?";
3080
+ readonly style: "table";
3081
+ readonly other: {
3082
+ readonly kind: "text";
3083
+ readonly min: 3;
3084
+ readonly max: 200;
3085
+ readonly description: "In the client's words, such as \"quote requests on Messenger\" or \"orders in comments under our posts\".";
3086
+ };
3087
+ readonly must_ask: true;
3088
+ readonly note: "Only the client knows. Name the type after it in lower case with hyphens (venue-quote-request). Ask for two or three real messages (names blanked) to use as examples.";
3089
+ }, {
3090
+ readonly id: "intake.fields";
3091
+ readonly sets: readonly ["schema", "examples"];
3092
+ readonly ask: "Here are the details I'd pull out of each message. Keep them, change some, or add or remove?";
3093
+ readonly style: "table";
3094
+ readonly other: {
3095
+ readonly kind: "list";
3096
+ readonly min: 1;
3097
+ readonly max: 40;
3098
+ readonly description: "Each detail: its name, what it is, whether the app can't do without it, and the choices if it is one of a list.";
3099
+ };
3100
+ readonly must_ask: true;
3101
+ readonly note: "Propose from intake.kind, as a table of field, meaning, kind (text, number, date, one of a list), and needed. For an enquiry: the date, how many people, what for, budget, extras. Mark as needed only what the app can't act without; the rest are optional.";
3102
+ }, {
3103
+ readonly id: "intake.personal";
3104
+ readonly sets: "schema.x-personal";
3105
+ readonly ask: "Should it keep the person's name or contact details?";
3106
+ readonly style: "cards";
3107
+ readonly answers: readonly [{
3108
+ readonly label: "No, leave them out";
3109
+ readonly value: "none";
3110
+ readonly means: "Names, numbers and handles are blanked in what it reads, and never kept.";
3111
+ }, {
3112
+ readonly label: "Their display name only";
3113
+ readonly value: "name";
3114
+ readonly means: "The name shown on the message, so the owner knows who asked.";
3115
+ }, {
3116
+ readonly label: "Name and how to reach them";
3117
+ readonly value: "contact";
3118
+ readonly means: "The name and a phone number or email when the message gives one.";
3119
+ }];
3120
+ readonly must_ask: true;
3121
+ readonly note: "Each kept detail becomes a field marked x-personal with its kind (name, phone, email). Say that what the app keeps is under its own privacy notice.";
3122
+ }, {
3123
+ readonly id: "intake.save";
3124
+ readonly sets: "save_input";
3125
+ readonly ask: "Should your app keep a copy of each message (the screenshot and its text)?";
3126
+ readonly style: "cards";
3127
+ readonly answers: readonly [{
3128
+ readonly label: "No, read it and let it go";
3129
+ readonly value: "none";
3130
+ readonly means: "Only the filled-in details are kept, by your app.";
3131
+ }, {
3132
+ readonly label: "Yes, in your own storage";
3133
+ readonly value: "project";
3134
+ readonly means: "Your app keeps the screenshot and text in its own private bucket. Workbench never keeps it.";
3135
+ }];
3136
+ readonly must_ask: true;
3137
+ readonly note: "project sets save_input in workbench/intake.json and needs the INTAKE_STORE bucket bound to the app (a private R2 bucket <id>-intake per environment, in the projects account). Pass store: env.INTAKE_STORE on every call. Say who will be able to see the copies and for how long.";
3138
+ }, {
3139
+ readonly id: "intake.check";
3140
+ readonly sets: "app.when_unsure";
3141
+ readonly ask: "When it isn't sure, what should the app do?";
3142
+ readonly style: "cards";
3143
+ readonly answers: readonly [{
3144
+ readonly label: "Ask you to check first";
3145
+ readonly value: "ask";
3146
+ readonly means: "The details wait for you to confirm before they are saved.";
3147
+ }, {
3148
+ readonly label: "Save it, marked to check";
3149
+ readonly value: "mark";
3150
+ readonly means: "It is saved straight away with a \"to check\" mark.";
3151
+ }, {
3152
+ readonly label: "Skip it";
3153
+ readonly value: "skip";
3154
+ readonly means: "Unsure readings aren't saved; you read the message yourself.";
3155
+ }];
3156
+ readonly note: "This is the app's own behaviour when needsHumanCheck is true; nothing in the type file changes.";
3157
+ }, {
3158
+ readonly id: "intake.model";
3159
+ readonly sets: "model";
3160
+ readonly ask: "How careful should the reading be?";
3161
+ readonly style: "cards";
3162
+ readonly answers: readonly [{
3163
+ readonly label: "Careful";
3164
+ readonly value: "claude-default";
3165
+ readonly means: "About US$0.01 a message.";
3166
+ }, {
3167
+ readonly label: "Quick and cheap";
3168
+ readonly value: "claude-fast";
3169
+ readonly means: "Cheaper, and misses more in long or messy chats.";
3170
+ }];
3171
+ readonly note: "Recommend careful. Leave model out of the file for claude-default.";
3172
+ }];
3173
+ readonly not_asked: readonly [{
3174
+ readonly sets: "confidence_floor";
3175
+ readonly why: "0.75 suits a start; it is tuned from real results, not asked.";
3176
+ }, {
3177
+ readonly sets: "glossary";
3178
+ readonly why: "Claude adds the trade's own shorthand from the client's example messages (DU for day use); the Gateway's built-in Filipino and Taglish glossary covers everyday words.";
3179
+ }, {
3180
+ readonly sets: "budget";
3181
+ readonly why: "Intake has its own monthly cap in the Gateway, set with Control's gw_budget_set (tool intake); offer it only when the client asks about cost.";
3182
+ }];
3183
+ };
3184
+ readonly delivery: {
3185
+ readonly choice: "optional";
3186
+ readonly intro: "Turns a pasted message or a screenshot of a chat into a filled-in form, so enquiries don't have to be typed up by hand.";
3187
+ };
3188
+ };
2724
3189
  readonly pdf: {
2725
3190
  readonly $schema: "./schema.json";
2726
3191
  readonly format: 1;