@mccaskie-labs/workbench-config 0.1.28 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.1.29
4
+
5
+ ### Patch Changes
6
+
7
+ - Offer patterns (docs/capabilities-manifest.md §9.5): `offer.pattern` (settings, drafts, feature_or_content, project, switch_on), `question.when`, `offer.drafts`, `offer.needs` and a top-level `no_offer`. Every manifest needs an offer or a no_offer reason, except components in `OFFER_PENDING`. gateway, config and email-sending state why a client has nothing to answer. Generated skills gain per-pattern steps, branches and a Drafts section.
8
+
3
9
  ## 0.1.28
4
10
 
5
11
  ### Patch Changes
@@ -59,5 +59,6 @@
59
59
  { "id": "config.env-required", "half": "runtime", "expect": "pass", "cost_hint_usd": 0 },
60
60
  { "id": "config.brand-file", "half": "runtime", "expect": "pass", "cost_hint_usd": 0, "note": "The app's brand.json passes defineBrand and a bad one is refused with every bad field named" },
61
61
  { "id": "config.dev-vars-check", "half": "runtime", "expect": "pass", "cost_hint_usd": 0, "note": "Manual: run npx workbench-config check in the project" }
62
- ]
62
+ ],
63
+ "no_offer": "Plumbing: WORKBENCH_ENV and typed variables. The template sets them; there is nothing for a client to choose."
63
64
  }
@@ -34,5 +34,6 @@
34
34
  { "id": "email-sending.status", "half": "drill", "expect": "pass", "cost_hint_usd": 0 },
35
35
  { "id": "email-sending.sender-add", "half": "drill", "expect": "pass", "cost_hint_usd": 0, "note": "Then a runtime send from the new address (email.send.transactional)" },
36
36
  { "id": "email-sending.out-of-scope", "half": "drill", "expect": "refused_as_expected", "refusal": "out_of_scope", "cost_hint_usd": 0, "note": "Another project's namespace" }
37
- ]
37
+ ],
38
+ "no_offer": "Plumbing: sender domain onboarding is done through Control by the owner. The client's sender address is asked by the email component."
38
39
  }
@@ -79,5 +79,6 @@
79
79
  { "id": "gateway.revoked-key", "half": "runtime", "expect": "refused_as_expected", "refusal": "unauthorised", "cost_hint_usd": 0, "note": "Stops within 60 seconds of revocation" },
80
80
  { "id": "gateway.budget-enforced", "half": "runtime", "expect": "refused_as_expected", "refusal": "budget_exceeded", "cost_hint_usd": 0, "note": "Uses the development key's US$0 enforce budget, so it is refused before any spend (docs/phase-4-plan.md §3 Q10)" },
81
81
  { "id": "gateway.no-vendor-keys.scan", "half": "runtime", "expect": "pass", "cost_hint_usd": 0, "note": "Manual: grep of the repo and cf_worker_secret_list show only WORKBENCH_GATEWAY_* secrets" }
82
- ]
82
+ ],
83
+ "no_offer": "Plumbing: keys, scopes, headers and budgets shared by every Gateway endpoint. Budgets are set through Control, not by the client's answers."
83
84
  }
@@ -405,6 +405,16 @@
405
405
  "offer": {
406
406
  "type": "object",
407
407
  "properties": {
408
+ "pattern": {
409
+ "type": "string",
410
+ "enum": [
411
+ "settings",
412
+ "drafts",
413
+ "feature_or_content",
414
+ "project",
415
+ "switch_on"
416
+ ]
417
+ },
408
418
  "when": {
409
419
  "type": "string",
410
420
  "minLength": 1
@@ -540,6 +550,32 @@
540
550
  "must_ask": {
541
551
  "type": "boolean"
542
552
  },
553
+ "when": {
554
+ "type": "object",
555
+ "properties": {
556
+ "id": {
557
+ "type": "string",
558
+ "pattern": "^[a-z0-9][a-z0-9.-]*$"
559
+ },
560
+ "is": {
561
+ "minItems": 1,
562
+ "type": "array",
563
+ "items": {
564
+ "type": [
565
+ "string",
566
+ "number",
567
+ "boolean",
568
+ "null"
569
+ ]
570
+ }
571
+ }
572
+ },
573
+ "required": [
574
+ "id",
575
+ "is"
576
+ ],
577
+ "additionalProperties": false
578
+ },
543
579
  "note": {
544
580
  "type": "string",
545
581
  "minLength": 1
@@ -575,15 +611,76 @@
575
611
  ],
576
612
  "additionalProperties": false
577
613
  }
614
+ },
615
+ "drafts": {
616
+ "type": "object",
617
+ "properties": {
618
+ "of": {
619
+ "type": "string",
620
+ "minLength": 1
621
+ },
622
+ "surface": {
623
+ "type": "string",
624
+ "enum": [
625
+ "ai",
626
+ "images",
627
+ "image-search"
628
+ ]
629
+ },
630
+ "alias": {
631
+ "type": "string",
632
+ "pattern": "^[a-z][a-z0-9-]*$"
633
+ },
634
+ "count": {
635
+ "type": "integer",
636
+ "minimum": 2,
637
+ "maximum": 4
638
+ },
639
+ "max_rounds": {
640
+ "type": "integer",
641
+ "minimum": 1,
642
+ "maximum": 5
643
+ },
644
+ "job": {
645
+ "type": "string",
646
+ "pattern": "^[a-z0-9._-]{1,64}$"
647
+ },
648
+ "command": {
649
+ "type": "string",
650
+ "minLength": 1
651
+ }
652
+ },
653
+ "required": [
654
+ "of",
655
+ "surface",
656
+ "count",
657
+ "max_rounds",
658
+ "job",
659
+ "command"
660
+ ],
661
+ "additionalProperties": false
662
+ },
663
+ "needs": {
664
+ "minItems": 1,
665
+ "type": "array",
666
+ "items": {
667
+ "type": "string",
668
+ "pattern": "^[a-z][a-z0-9-]*$"
669
+ }
578
670
  }
579
671
  },
580
672
  "required": [
673
+ "pattern",
581
674
  "when",
582
675
  "say",
583
676
  "answers_file",
584
677
  "questions"
585
678
  ],
586
679
  "additionalProperties": false
680
+ },
681
+ "no_offer": {
682
+ "type": "string",
683
+ "minLength": 1
587
684
  }
588
685
  },
589
686
  "required": [
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "c83a7c612319";
1
+ export declare const MANIFEST_VERSION: "4538a59cfe87";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -355,6 +355,7 @@ export declare const capabilities: {
355
355
  readonly cost_hint_usd: 0;
356
356
  readonly note: "Manual: run npx workbench-config check in the project";
357
357
  }];
358
+ readonly no_offer: "Plumbing: WORKBENCH_ENV and typed variables. The template sets them; there is nothing for a client to choose.";
358
359
  };
359
360
  readonly country: {
360
361
  readonly $schema: "./schema.json";
@@ -1083,6 +1084,7 @@ export declare const capabilities: {
1083
1084
  readonly cost_hint_usd: 0;
1084
1085
  readonly note: "Another project's namespace";
1085
1086
  }];
1087
+ readonly no_offer: "Plumbing: sender domain onboarding is done through Control by the owner. The client's sender address is asked by the email component.";
1086
1088
  };
1087
1089
  readonly forms: {
1088
1090
  readonly $schema: "./schema.json";
@@ -1380,6 +1382,7 @@ export declare const capabilities: {
1380
1382
  readonly cost_hint_usd: 0;
1381
1383
  readonly note: "Manual: grep of the repo and cf_worker_secret_list show only WORKBENCH_GATEWAY_* secrets";
1382
1384
  }];
1385
+ readonly no_offer: "Plumbing: keys, scopes, headers and budgets shared by every Gateway endpoint. Budgets are set through Control, not by the client's answers.";
1383
1386
  };
1384
1387
  readonly "image-search": {
1385
1388
  readonly $schema: "./schema.json";
@@ -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 = "c83a7c612319";
3
+ export const MANIFEST_VERSION = "4538a59cfe87";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -521,7 +521,8 @@ export const capabilities = {
521
521
  "cost_hint_usd": 0,
522
522
  "note": "Manual: run npx workbench-config check in the project"
523
523
  }
524
- ]
524
+ ],
525
+ "no_offer": "Plumbing: WORKBENCH_ENV and typed variables. The template sets them; there is nothing for a client to choose."
525
526
  },
526
527
  "country": {
527
528
  "$schema": "./schema.json",
@@ -1473,7 +1474,8 @@ export const capabilities = {
1473
1474
  "cost_hint_usd": 0,
1474
1475
  "note": "Another project's namespace"
1475
1476
  }
1476
- ]
1477
+ ],
1478
+ "no_offer": "Plumbing: sender domain onboarding is done through Control by the owner. The client's sender address is asked by the email component."
1477
1479
  },
1478
1480
  "forms": {
1479
1481
  "$schema": "./schema.json",
@@ -1872,7 +1874,8 @@ export const capabilities = {
1872
1874
  "cost_hint_usd": 0,
1873
1875
  "note": "Manual: grep of the repo and cf_worker_secret_list show only WORKBENCH_GATEWAY_* secrets"
1874
1876
  }
1875
- ]
1877
+ ],
1878
+ "no_offer": "Plumbing: keys, scopes, headers and budgets shared by every Gateway endpoint. Budgets are set through Control, not by the client's answers."
1876
1879
  },
1877
1880
  "image-search": {
1878
1881
  "$schema": "./schema.json",
@@ -87,7 +87,16 @@ export declare const allowedSchemas: {
87
87
  }>>;
88
88
  }, z.core.$strict>;
89
89
  };
90
+ /** How Claude works with the client (docs/capabilities-manifest.md §9.5). */
91
+ export declare const OFFER_PATTERNS: readonly ["settings", "drafts", "feature_or_content", "project", "switch_on"];
90
92
  declare const offer: z.ZodObject<{
93
+ pattern: z.ZodEnum<{
94
+ drafts: "drafts";
95
+ feature_or_content: "feature_or_content";
96
+ project: "project";
97
+ settings: "settings";
98
+ switch_on: "switch_on";
99
+ }>;
91
100
  when: z.ZodString;
92
101
  say: z.ZodString;
93
102
  answers_file: z.ZodString;
@@ -120,12 +129,30 @@ declare const offer: z.ZodObject<{
120
129
  }, z.core.$strict>>;
121
130
  for: z.ZodOptional<z.ZodArray<z.ZodString>>;
122
131
  must_ask: z.ZodOptional<z.ZodBoolean>;
132
+ when: z.ZodOptional<z.ZodObject<{
133
+ id: z.ZodString;
134
+ is: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
135
+ }, z.core.$strict>>;
123
136
  note: z.ZodOptional<z.ZodString>;
124
137
  }, z.core.$strict>>;
125
138
  not_asked: z.ZodOptional<z.ZodArray<z.ZodObject<{
126
139
  sets: z.ZodString;
127
140
  why: z.ZodString;
128
141
  }, z.core.$strict>>>;
142
+ drafts: z.ZodOptional<z.ZodObject<{
143
+ of: z.ZodString;
144
+ surface: z.ZodEnum<{
145
+ ai: "ai";
146
+ "image-search": "image-search";
147
+ images: "images";
148
+ }>;
149
+ alias: z.ZodOptional<z.ZodString>;
150
+ count: z.ZodNumber;
151
+ max_rounds: z.ZodNumber;
152
+ job: z.ZodString;
153
+ command: z.ZodString;
154
+ }, z.core.$strict>>;
155
+ needs: z.ZodOptional<z.ZodArray<z.ZodString>>;
129
156
  }, z.core.$strict>;
130
157
  /** One file in `capabilities/`. Parsing checks shape only; cross-references are checked by `checkManifests`. */
131
158
  export declare const capabilitySchema: z.ZodObject<{
@@ -262,6 +289,13 @@ export declare const capabilitySchema: z.ZodObject<{
262
289
  note: z.ZodOptional<z.ZodString>;
263
290
  }, z.core.$strict>>;
264
291
  offer: z.ZodOptional<z.ZodObject<{
292
+ pattern: z.ZodEnum<{
293
+ drafts: "drafts";
294
+ feature_or_content: "feature_or_content";
295
+ project: "project";
296
+ settings: "settings";
297
+ switch_on: "switch_on";
298
+ }>;
265
299
  when: z.ZodString;
266
300
  say: z.ZodString;
267
301
  answers_file: z.ZodString;
@@ -294,16 +328,41 @@ export declare const capabilitySchema: z.ZodObject<{
294
328
  }, z.core.$strict>>;
295
329
  for: z.ZodOptional<z.ZodArray<z.ZodString>>;
296
330
  must_ask: z.ZodOptional<z.ZodBoolean>;
331
+ when: z.ZodOptional<z.ZodObject<{
332
+ id: z.ZodString;
333
+ is: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
334
+ }, z.core.$strict>>;
297
335
  note: z.ZodOptional<z.ZodString>;
298
336
  }, z.core.$strict>>;
299
337
  not_asked: z.ZodOptional<z.ZodArray<z.ZodObject<{
300
338
  sets: z.ZodString;
301
339
  why: z.ZodString;
302
340
  }, z.core.$strict>>>;
341
+ drafts: z.ZodOptional<z.ZodObject<{
342
+ of: z.ZodString;
343
+ surface: z.ZodEnum<{
344
+ ai: "ai";
345
+ "image-search": "image-search";
346
+ images: "images";
347
+ }>;
348
+ alias: z.ZodOptional<z.ZodString>;
349
+ count: z.ZodNumber;
350
+ max_rounds: z.ZodNumber;
351
+ job: z.ZodString;
352
+ command: z.ZodString;
353
+ }, z.core.$strict>>;
354
+ needs: z.ZodOptional<z.ZodArray<z.ZodString>>;
303
355
  }, z.core.$strict>>;
356
+ no_offer: z.ZodOptional<z.ZodString>;
304
357
  }, z.core.$strict>;
305
358
  export type Capability = z.infer<typeof capabilitySchema>;
306
359
  export type Offer = z.infer<typeof offer>;
360
+ /**
361
+ * Components whose client questions are still being written (Robert,
362
+ * 11 November 2026: every shared tool ships its questions). The list only
363
+ * shrinks; a new component can't join it.
364
+ */
365
+ export declare const OFFER_PENDING: readonly ["ai", "analytics", "country", "database", "email", "email-inbound", "forms", "image-search", "images", "pdf", "strings", "ui"];
307
366
  export interface ManifestProblem {
308
367
  file: string;
309
368
  check: "schema" | "proof" | "ids" | "surfaces" | "errors" | "generated";
@@ -161,9 +161,24 @@ const question = z.strictObject({
161
161
  other: otherRule.optional(),
162
162
  for: z.array(z.string().min(1)).min(1).optional(),
163
163
  must_ask: z.boolean().optional(),
164
+ /** Ask only when an earlier question's answer is one of `is` (the branch, e.g. feature or one-off). */
165
+ when: z.strictObject({ id, is: z.array(answerValue).min(1) }).optional(),
164
166
  note: z.string().min(1).optional(),
165
167
  });
168
+ /** How Claude works with the client (docs/capabilities-manifest.md §9.5). */
169
+ export const OFFER_PATTERNS = ["settings", "drafts", "feature_or_content", "project", "switch_on"];
170
+ const drafts = z.strictObject({
171
+ /** What is drafted, in plain words: "invoice layouts", "a hero image". */
172
+ of: z.string().min(1),
173
+ surface: z.enum(["ai", "images", "image-search"]),
174
+ alias: z.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
175
+ count: z.number().int().min(2).max(4),
176
+ max_rounds: z.number().int().min(1).max(5),
177
+ job: z.string().regex(/^[a-z0-9._-]{1,64}$/),
178
+ command: z.string().min(1),
179
+ });
166
180
  const offer = z.strictObject({
181
+ pattern: z.enum(OFFER_PATTERNS),
167
182
  when: z.string().min(1),
168
183
  say: z.string().min(1),
169
184
  answers_file: z.string().regex(/^workbench\/[a-z0-9<>/._-]+\.json$/, "a JSON file under workbench/"),
@@ -171,6 +186,9 @@ const offer = z.strictObject({
171
186
  generator: z.string().min(1).optional(),
172
187
  questions: z.array(question).min(1),
173
188
  not_asked: z.array(z.strictObject({ sets: z.string().min(1), why: z.string().min(1) })).min(1).optional(),
189
+ drafts: drafts.optional(),
190
+ /** Project setup answers that must exist first: their components' answers files. */
191
+ needs: z.array(z.string().regex(/^[a-z][a-z0-9-]*$/)).min(1).optional(),
174
192
  });
175
193
  const baseShape = {
176
194
  $schema: z.literal("./schema.json"),
@@ -195,6 +213,8 @@ const baseShape = {
195
213
  examples: z.array(example).min(1).optional(),
196
214
  probes: z.array(probe).min(1),
197
215
  offer: offer.optional(),
216
+ /** Why a client has nothing to answer for this component (plumbing). A manifest has `offer` or `no_offer`. */
217
+ no_offer: z.string().min(1).optional(),
198
218
  };
199
219
  /** One file in `capabilities/`. Parsing checks shape only; cross-references are checked by `checkManifests`. */
200
220
  export const capabilitySchema = z.strictObject(baseShape).superRefine((m, ctx) => {
@@ -212,6 +232,12 @@ export const capabilitySchema = z.strictObject(baseShape).superRefine((m, ctx) =
212
232
  }
213
233
  }
214
234
  });
235
+ /**
236
+ * Components whose client questions are still being written (Robert,
237
+ * 11 November 2026: every shared tool ships its questions). The list only
238
+ * shrinks; a new component can't join it.
239
+ */
240
+ export const OFFER_PENDING = ["ai", "analytics", "country", "database", "email", "email-inbound", "forms", "image-search", "images", "pdf", "strings", "ui"];
215
241
  function checkOffer(file, component, o, seen) {
216
242
  const problems = [];
217
243
  const named = new Map();
@@ -222,7 +248,13 @@ function checkOffer(file, component, o, seen) {
222
248
  else
223
249
  named.set(option, by);
224
250
  };
251
+ const asked = new Set();
252
+ if (o.pattern === "drafts" && !o.drafts)
253
+ problems.push({ file, check: "schema", message: "offer: the drafts pattern needs drafts" });
225
254
  for (const q of o.questions) {
255
+ if (q.when && !asked.has(q.when.id))
256
+ problems.push({ file, check: "ids", message: `question ${q.id} depends on ${q.when.id}, which isn't an earlier question` });
257
+ asked.add(q.id);
226
258
  seen("question", q.id);
227
259
  if (!q.id.startsWith(`${component}.`))
228
260
  problems.push({ file, check: "ids", message: `question ${q.id} must start with ${component}.` });
@@ -316,6 +348,14 @@ export function checkManifests(files) {
316
348
  }
317
349
  if (m.offer)
318
350
  problems.push(...checkOffer(file, m.component, m.offer, seen));
351
+ if (m.offer && m.no_offer)
352
+ problems.push({ file, check: "schema", message: "has both offer and no_offer" });
353
+ const pending = OFFER_PENDING.includes(m.component);
354
+ if (!m.offer && !m.no_offer && !pending) {
355
+ 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" });
356
+ }
357
+ if ((m.offer || m.no_offer) && pending)
358
+ problems.push({ file, check: "schema", message: `remove ${m.component} from OFFER_PENDING` });
319
359
  }
320
360
  return { manifests, problems };
321
361
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { WORKBENCH_ENVS, DEFAULT_GATEWAY_URL, workbenchEnv, type WorkbenchEnv }
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, } from "./capability-schema.ts";
5
+ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, type Offer, OFFER_PATTERNS, OFFER_PENDING, } from "./capability-schema.ts";
6
6
  export { renderSkill, renderSkills, skillName, SKILLS_DIR } from "./skills.ts";
7
7
  export { capabilities, MANIFEST_VERSION, type Alias, type GatewayScope } from "./capabilities.gen.ts";
8
8
  export { compareDevVars, parseDevVarNames } from "./dev-vars.ts";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { WORKBENCH_ENVS, DEFAULT_GATEWAY_URL, 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, } from "./capability-schema.js";
5
+ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, OFFER_PATTERNS, OFFER_PENDING, } from "./capability-schema.js";
6
6
  export { renderSkill, renderSkills, skillName, SKILLS_DIR } from "./skills.js";
7
7
  export { capabilities, MANIFEST_VERSION } from "./capabilities.gen.js";
8
8
  export { compareDevVars, parseDevVarNames } from "./dev-vars.js";
package/dist/skills.js CHANGED
@@ -21,6 +21,8 @@ function questionBlock(q, n) {
21
21
  const lines = [`### ${n}. ${q.ask}`, "", `Id \`${q.id}\`. Sets ${sets(q)}.`];
22
22
  if (q.for)
23
23
  lines.push(`Only for: ${q.for.map((f) => `\`${f}\``).join(", ")}.`);
24
+ if (q.when)
25
+ lines.push(`Ask only when \`${q.when.id}\` is ${q.when.is.map(show).join(" or ")}.`);
24
26
  if (q.must_ask)
25
27
  lines.push("**Must ask:** there is no safe default, so always ask it, even when the client accepts every recommendation.");
26
28
  lines.push("");
@@ -36,6 +38,34 @@ function questionBlock(q, n) {
36
38
  lines.push("", q.note);
37
39
  return lines.join("\n");
38
40
  }
41
+ const PATTERN_STEPS = {
42
+ settings: [
43
+ "Propose everything at once: a table of every setting with its recommended value. Then ask one question: \"Accept all (Recommended)\", \"Change some\" or \"Add or remove\".",
44
+ "The client edits the list in plain words (\"delete this\", \"add a dropdown with these values, required\"). Update the answers file and show the table again until they accept it.",
45
+ ],
46
+ drafts: [
47
+ "Ask what the thing is first, then offer examples to pick from (see Drafts below). Show them, let the client pick one or ask for another round, and keep only the one picked.",
48
+ ],
49
+ feature_or_content: [
50
+ "Ask the routing question first: is this a feature visitors use on the site, or something made once for the site? Then ask only the questions for that route.",
51
+ "A feature for visitors spends money every time someone uses it: always ask who may use it and its monthly cap.",
52
+ ],
53
+ project: [
54
+ "These are project-wide answers, asked once. If the answers file already exists, use it and don't ask again; offer to change it only if the client asks.",
55
+ ],
56
+ switch_on: [
57
+ "The template already has this. Confirm the client wants it, ask the few questions below, and name any step the owner takes in Workbench Control.",
58
+ ],
59
+ };
60
+ function draftsBlock(d) {
61
+ return [
62
+ "## Drafts",
63
+ "",
64
+ `Offer ${d.count} examples of ${d.of} to pick from, at most ${d.max_rounds} round${d.max_rounds === 1 ? "" : "s"}. Make them with \`${d.command}\`: it calls the Workbench Gateway (${d.surface}${d.alias ? `, \`${d.alias}\`` : ""}, job \`${d.job}\`) with the project's development key from \`.dev.vars\`, spends from the development budget, and prints what each round cost. Tell the client the cost it prints.`,
65
+ "",
66
+ "Never write drafts yourself and never call a vendor directly. If the command says there is no development key, say so and offer the standard version instead; never ask for a key in chat. Record the picked draft's request id, model and cost in the answers file.",
67
+ ].join("\n");
68
+ }
39
69
  /** The SKILL.md for one manifest, or undefined when it has no offer. */
40
70
  export function renderSkill(m) {
41
71
  const o = m.offer;
@@ -64,6 +94,8 @@ export function renderSkill(m) {
64
94
  "",
65
95
  "## How to ask",
66
96
  "",
97
+ ...(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.`] : []),
98
+ ...PATTERN_STEPS[o.pattern].map((step) => `- ${step}`),
67
99
  "- 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)\". The tool adds \"Other\" itself.",
68
100
  "- Where the client accepts every recommendation, still ask each \"Must ask\" question.",
69
101
  "- 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.",
@@ -74,6 +106,8 @@ export function renderSkill(m) {
74
106
  "",
75
107
  o.questions.map((q, i) => questionBlock(q, i + 1)).join("\n\n"),
76
108
  ];
109
+ if (o.drafts)
110
+ out.push("", draftsBlock(o.drafts));
77
111
  if (o.not_asked?.length) {
78
112
  out.push("", "## Not asked", "", ...o.not_asked.map((n) => `- \`${n.sets}\`: ${n.why}`));
79
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
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",