toolfactory 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/bindings/typescript.js +1 -1
  3. package/dist/bindings/typescript.test.js +29 -1
  4. package/dist/commands.d.ts +6 -0
  5. package/dist/commands.js +13 -2
  6. package/dist/hosts/openclaw.d.ts +8 -0
  7. package/dist/hosts/openclaw.js +70 -4
  8. package/dist/hosts/openclaw.test.d.ts +1 -0
  9. package/dist/hosts/openclaw.test.js +58 -0
  10. package/dist/hosts/web.d.ts +1 -0
  11. package/dist/hosts/web.js +6 -3
  12. package/dist/hosts/web.test.d.ts +1 -0
  13. package/dist/hosts/web.test.js +8 -0
  14. package/dist/model.d.ts +12 -0
  15. package/dist/model.js +4 -0
  16. package/dist/ops.d.ts +26 -0
  17. package/dist/ops.js +25 -1
  18. package/dist/project/gate.js +7 -1
  19. package/dist/project/history.d.ts +1 -0
  20. package/dist/project/load.js +19 -2
  21. package/dist/prompt/review.d.ts +23 -0
  22. package/dist/prompt/review.js +103 -0
  23. package/dist/prompt/review.test.d.ts +1 -0
  24. package/dist/prompt/review.test.js +55 -0
  25. package/dist/prompt/standard.d.ts +26 -0
  26. package/dist/prompt/standard.js +68 -0
  27. package/dist/surfaces/openclaw-native.d.ts +19 -2
  28. package/dist/surfaces/openclaw-native.js +43 -14
  29. package/dist/surfaces/openclaw-native.test.js +15 -5
  30. package/dist/surfaces/web.d.ts +6 -6
  31. package/dist/surfaces/web.js +7 -7
  32. package/dist/surfaces/web.test.js +1 -1
  33. package/dist/surfaces/workflows.js +2 -2
  34. package/dist/toolfactory/cli.js +1 -1
  35. package/dist/toolfactory/mcp.js +1 -1
  36. package/package.json +1 -1
  37. package/src/bindings/typescript.test.ts +32 -1
  38. package/src/bindings/typescript.ts +1 -1
  39. package/src/commands.ts +14 -2
  40. package/src/hosts/openclaw.test.ts +62 -0
  41. package/src/hosts/openclaw.ts +73 -5
  42. package/src/hosts/web.test.ts +9 -0
  43. package/src/hosts/web.ts +6 -3
  44. package/src/model.ts +15 -0
  45. package/src/ops.ts +30 -1
  46. package/src/project/gate.ts +7 -1
  47. package/src/project/load.ts +23 -2
  48. package/src/prompt/review.test.ts +68 -0
  49. package/src/prompt/review.ts +148 -0
  50. package/src/prompt/standard.ts +93 -0
  51. package/src/surfaces/openclaw-native.test.ts +15 -5
  52. package/src/surfaces/openclaw-native.ts +48 -14
  53. package/src/surfaces/web.test.ts +1 -1
  54. package/src/surfaces/web.ts +7 -7
  55. package/src/surfaces/workflows.ts +2 -2
  56. package/src/toolfactory/cli.ts +1 -1
  57. package/src/toolfactory/mcp.ts +1 -1
  58. package/web/dist/assets/{index-BHR6gCfm.js → index-D95aMjZ6.js} +2 -2
  59. package/web/dist/index.html +1 -1
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The draft -> evaluate -> iterate loop's evaluator, as `review-prompt`. Pure and deterministic:
3
+ * it scans the draft (and, when given, the fetched reference prompt) against the
4
+ * codex-prompt-standard rubric and returns findings plus concrete next-iteration directives.
5
+ * The taste stays with the calling model; this makes the checklist the skill teaches impossible
6
+ * to skip.
7
+ */
8
+ import { ANATOMY, type PromptSection } from "./standard.js";
9
+
10
+ export interface PromptReviewInput {
11
+ prompt: string;
12
+ /** The closest reference prompt's full text (canon §1: fetch it before evaluating). */
13
+ reference?: string;
14
+ }
15
+
16
+ export interface CheckResult {
17
+ id: string;
18
+ status: "pass" | "warn" | "fail";
19
+ detail: string;
20
+ }
21
+
22
+ export interface PromptReviewReport {
23
+ verdict: "pass" | "polish" | "revise";
24
+ sections: { present: string[]; missing: string[] };
25
+ checks: CheckResult[];
26
+ directives: string[];
27
+ reference?: { sectionsMissingHere: string[] };
28
+ }
29
+
30
+ const HARD_WORDS = /\b(?:MUST|NEVER|ALWAYS)\b/g;
31
+ const HEDGES =
32
+ /\b(?:try to|tries to|consider|maybe|perhaps|probably|if possible|ideally|when appropriate|as needed)\b/gi;
33
+ const PRECEDENCE =
34
+ /\bpreceden|overrid|takes? precedence|in case of (?:a )?conflict|conflicts? with|direct instructions/i;
35
+ const EXAMPLES = /\b(?:example|for instance|good:|bad:)\b/i;
36
+ const DONE_WHEN = /done when|definition of done|success criteria|checkable|acceptance criteria/i;
37
+ const INJECTION = /ignore (?:all |any |the )?(?:previous|prior|above|earlier)\s+instructions/i;
38
+ const CODEX_SPECIFIC = /\bapply_patch\b|\bcodex-rs\b|\bopenai codex cli\b/i;
39
+
40
+ const has = (text: string, section: PromptSection): boolean =>
41
+ section.anchors.some((anchor) => anchor.test(text));
42
+
43
+ export function reviewPrompt(input: PromptReviewInput): PromptReviewReport {
44
+ const { prompt, reference } = input;
45
+ const present = ANATOMY.filter((section) => has(prompt, section));
46
+ const missing = ANATOMY.filter((section) => !has(prompt, section));
47
+
48
+ const checks: CheckResult[] = [];
49
+ const hard = prompt.match(HARD_WORDS)?.length ?? 0;
50
+ checks.push(
51
+ hard > 15
52
+ ? {
53
+ id: "must-never-discipline",
54
+ status: "fail",
55
+ detail: `${hard} hard imperatives: MUST/NEVER are reserved for the few things that must never happen — demote the rest to positive imperatives.`,
56
+ }
57
+ : hard > 8
58
+ ? {
59
+ id: "must-never-discipline",
60
+ status: "warn",
61
+ detail: `${hard} hard imperatives is close to noise; keep MUST/NEVER for the few things that must never happen.`,
62
+ }
63
+ : { id: "must-never-discipline", status: "pass", detail: `${hard} hard imperatives.` },
64
+ );
65
+
66
+ const hedges = prompt.match(HEDGES)?.length ?? 0;
67
+ checks.push(
68
+ hedges >= 5
69
+ ? {
70
+ id: "positive-imperatives",
71
+ status: "warn",
72
+ detail: `${hedges} hedge phrases — every sentence should change behavior; prefer positive imperatives ("run X, then verify Y").`,
73
+ }
74
+ : { id: "positive-imperatives", status: "pass", detail: `${hedges} hedge phrases.` },
75
+ );
76
+
77
+ if (prompt.length > 2000 && !PRECEDENCE.test(prompt))
78
+ checks.push({
79
+ id: "precedence",
80
+ status: "warn",
81
+ detail:
82
+ "No instruction-precedence rule in a prompt of this size — say what wins when sources conflict (direct instructions > project files > defaults).",
83
+ });
84
+ if (prompt.length > 1500 && !EXAMPLES.test(prompt))
85
+ checks.push({
86
+ id: "examples",
87
+ status: "warn",
88
+ detail:
89
+ "No examples in a prompt this long — good-vs-bad examples are how subjective qualities (tone, plan quality) get taught.",
90
+ });
91
+ if (!DONE_WHEN.test(prompt))
92
+ checks.push({
93
+ id: "checkable-criteria",
94
+ status: "warn",
95
+ detail: 'No "done when" — every branch of the task should have a checkable criterion.',
96
+ });
97
+ if (INJECTION.test(prompt))
98
+ checks.push({
99
+ id: "injection-hygiene",
100
+ status: "fail",
101
+ detail:
102
+ "The draft instructs its reader to discard instructions — a prompt must never undermine itself; state precedence instead.",
103
+ });
104
+ if (CODEX_SPECIFIC.test(prompt))
105
+ checks.push({
106
+ id: "harness-adaptation",
107
+ status: "warn",
108
+ detail:
109
+ "Codex-specific names appear — adapt to the target harness's tools and delivery rules; never copy Codex-specific tool names.",
110
+ });
111
+ if (prompt.length < 200)
112
+ checks.push({
113
+ id: "length",
114
+ status: "warn",
115
+ detail:
116
+ "Thinner than any Codex prompt section — say who the agent is, what it may do, and how it validates.",
117
+ });
118
+ else if (prompt.length > 40_000)
119
+ checks.push({
120
+ id: "length",
121
+ status: "warn",
122
+ detail:
123
+ "Very long — output depth and context are budget; cut anything the reference would not need.",
124
+ });
125
+
126
+ const referenceMissing = reference
127
+ ? ANATOMY.filter((section) => has(reference, section) && !has(prompt, section))
128
+ : [];
129
+
130
+ const directives = [
131
+ ...missing.map((section) => section.directive),
132
+ ...referenceMissing.map(
133
+ (section) => `The reference prompt treats ${section.title}; the draft has no such section.`,
134
+ ),
135
+ ...checks.filter((check) => check.status !== "pass").map((check) => check.detail),
136
+ ];
137
+ const revise =
138
+ missing.length > 0 || referenceMissing.length > 0 || checks.some((c) => c.status === "fail");
139
+ const verdict = revise ? "revise" : checks.some((c) => c.status === "warn") ? "polish" : "pass";
140
+
141
+ return {
142
+ verdict,
143
+ sections: { present: present.map((s) => s.id), missing: missing.map((s) => s.id) },
144
+ checks,
145
+ directives,
146
+ ...(reference ? { reference: { sectionsMissingHere: referenceMissing.map((s) => s.id) } } : {}),
147
+ };
148
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The codex-prompt-standard rubric, as data. The prose canon is
3
+ * `.agents/skills/codex-prompt-standard/SKILL.md` — it ships inside the OpenClaw plugin and
4
+ * syncs to every dev harness; this module is the same guidance shaped for `review-prompt` to
5
+ * evaluate against. `review.test.ts` pins every anchor below to the canon text so the two
6
+ * cannot drift apart silently.
7
+ */
8
+
9
+ export interface PromptSection {
10
+ id: string;
11
+ title: string;
12
+ /** Substring of the canon SKILL.md this section is pinned to (drift guard). */
13
+ canonAnchor: string;
14
+ /** Any hit marks the section present in a draft or a reference prompt. */
15
+ anchors: RegExp[];
16
+ /** The next-iteration instruction when the section is missing. */
17
+ directive: string;
18
+ }
19
+
20
+ /** The anatomy a Codex-class prompt reliably contains, in order (canon §2). */
21
+ export const ANATOMY: PromptSection[] = [
22
+ {
23
+ id: "identity",
24
+ title: "one-sentence identity",
25
+ canonAnchor: "One-sentence identity",
26
+ anchors: [/\byou are\b/i, /\bacting (?:for|on behalf of|as)\b/i],
27
+ directive:
28
+ "Open with one sentence of identity: who the model is, where it runs, how it is expected to behave.",
29
+ },
30
+ {
31
+ id: "capabilities",
32
+ title: "capability inventory",
33
+ canonAnchor: "Capability inventory",
34
+ anchors: [/\byou can\b/i, /\bcapabilit/i, /\b(?:available|your) tools?\b/i],
35
+ directive: "State the capability inventory: what the agent can receive and emit.",
36
+ },
37
+ {
38
+ id: "how-you-work",
39
+ title: '"How you work" sections',
40
+ canonAnchor: '"How you work" sections',
41
+ anchors: [/how you work/i, /\bpreceden/i, /\bpersisten/i, /\bautonom/i, /\btone\b/i],
42
+ directive:
43
+ 'Add "how you work" sections: personality and tone, instruction precedence, the autonomy and persistence contract.',
44
+ },
45
+ {
46
+ id: "planning",
47
+ title: "planning guidance",
48
+ canonAnchor: "Planning guidance",
49
+ anchors: [/\bplan\b/i, /\bpersist until\b/i, /before (?:acting|starting|diving)/i],
50
+ directive:
51
+ "Add planning guidance with explicit high-quality vs low-quality examples for what a good plan is.",
52
+ },
53
+ {
54
+ id: "execution-criteria",
55
+ title: "execution criteria",
56
+ canonAnchor: "Execution criteria",
57
+ anchors: [/\bMUST\b/, /\bNEVER\b/, /scope creep/i, /no unrelated changes/i],
58
+ directive:
59
+ "Add execution criteria: hard MUST/NEVER lists, root-cause fixes, no scope creep, no unrelated changes.",
60
+ },
61
+ {
62
+ id: "validation",
63
+ title: "validation philosophy",
64
+ canonAnchor: "Validation philosophy",
65
+ anchors: [/\bverify\b/i, /\bvalidat/i, /before claiming done/i, /\bdone when\b/i],
66
+ directive:
67
+ "Add the validation philosophy: start specific, broaden as confidence grows, verify before claiming done.",
68
+ },
69
+ {
70
+ id: "final-message",
71
+ title: "final-message guidance",
72
+ canonAnchor: "Final-message guidance",
73
+ anchors: [/final message/i, /\boutput depth\b/i, /\bfinal reply\b/i],
74
+ directive:
75
+ "Add final-message guidance: output depth matches task size; skip formatting for trivial replies.",
76
+ },
77
+ ];
78
+
79
+ export interface PromptConvention {
80
+ id: string;
81
+ /** Substring of the canon SKILL.md this convention is pinned to (drift guard). */
82
+ canonAnchor: string;
83
+ }
84
+
85
+ /** Convention anchors the canon must mention; the checks in review.ts implement them. */
86
+ export const CONVENTIONS: PromptConvention[] = [
87
+ { id: "positive-imperatives", canonAnchor: "Positive imperatives" },
88
+ { id: "precedence", canonAnchor: "Explicit precedence" },
89
+ { id: "must-never-discipline", canonAnchor: "MUST/NEVER reserved" },
90
+ { id: "examples", canonAnchor: "Good-vs-bad examples" },
91
+ { id: "harness-adaptation", canonAnchor: "Adapt to the target harness" },
92
+ { id: "checkable-criteria", canonAnchor: "checkable criterion" },
93
+ ];
@@ -122,7 +122,7 @@ describe("openclaw-native", () => {
122
122
  it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
123
123
  const files = emitted(project());
124
124
  const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
125
- expect(manifest.contracts).toEqual({ tools: ["echo"] });
125
+ expect(manifest.contracts).toEqual({ tools: ["hello_echo"] });
126
126
  expect(manifest.activation).toEqual({ onStartup: true });
127
127
  expect(surface.verdict?.(shoot, project())).toEqual({
128
128
  kind: "excluded",
@@ -133,6 +133,7 @@ describe("openclaw-native", () => {
133
133
  expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
134
134
 
135
135
  const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
136
+ expect(pkg.devDependencies).toMatchObject(OPENCLAW_SCAFFOLD.devDependencies);
136
137
  expect(pkg.openclaw).toEqual({
137
138
  extensions: ["./dist/index.js"],
138
139
  compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
@@ -168,7 +169,7 @@ describe("openclaw-native", () => {
168
169
  expect(index).toContain("api.registerControlUiDescriptor(descriptor)");
169
170
  // Neither registrar is a manifest contract key; both are what the inspector expects to see.
170
171
  expect(JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}").contracts).toEqual({
171
- tools: ["echo"],
172
+ tools: ["hello_echo"],
172
173
  });
173
174
  expect(
174
175
  JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}").pluginInspector.plugin.expect
@@ -212,6 +213,10 @@ describe("openclaw-native", () => {
212
213
 
213
214
  it("drives manifest, inspector and test from one `registers` declaration", () => {
214
215
  const target = voice();
216
+ target.tool.openclaw = {
217
+ ...target.tool.openclaw,
218
+ devDependencies: { openclaw: "2026.9.2", "@types/ws": "^8.18.0" },
219
+ };
215
220
  const files = emitted(target);
216
221
  const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
217
222
  expect(manifest.contracts).toEqual({ tools: [], realtimeVoiceProviders: ["codex"] });
@@ -223,6 +228,11 @@ describe("openclaw-native", () => {
223
228
  });
224
229
  // The author's extra dependency merges after the scaffold's; the core is not imported at all.
225
230
  expect(pkg.dependencies).toEqual({ ...OPENCLAW_SCAFFOLD.dependencies, ws: "^8.18.0" });
231
+ expect(pkg.devDependencies).toEqual({
232
+ ...OPENCLAW_SCAFFOLD.devDependencies,
233
+ openclaw: "2026.9.2",
234
+ "@types/ws": "^8.18.0",
235
+ });
226
236
  expect(pkg.peerDependencies.openclaw).toBe(">=2026.8.2");
227
237
  expect(pkg.openclaw.compat.pluginApi).toBe(">=2026.8.2");
228
238
 
@@ -249,8 +259,8 @@ describe("openclaw-native", () => {
249
259
  // when the tool's own result carries a name its output schema promises; leg 3 catches the rest.
250
260
  expect(fixtures.fixtures).toEqual([
251
261
  {
252
- match: { toolName: "echo", hasToolResult: false },
253
- response: { toolCalls: [{ name: "echo", arguments: { text: "hello" } }] },
262
+ match: { toolName: "hello_echo", hasToolResult: false },
263
+ response: { toolCalls: [{ name: "hello_echo", arguments: { text: "hello" } }] },
254
264
  },
255
265
  {
256
266
  match: { hasToolResult: true, toolResultContains: "echoed" },
@@ -312,7 +322,7 @@ function hasOpenclaw(): boolean {
312
322
  }
313
323
 
314
324
  describe.skipIf(!hasOpenclaw())("openclaw scaffold", () => {
315
- it("still matches `openclaw plugins init --type tool`", () => {
325
+ it("still matches `openclaw plugins init --type tool`", { timeout: 60_000 }, () => {
316
326
  expect(scaffoldDrift(project())).toEqual([]);
317
327
  // A declared `pluginApi` is an intentional override, not drift.
318
328
  expect(scaffoldDrift(voice())).toEqual([]);
@@ -35,7 +35,7 @@ import {
35
35
  /** Transcribed from `openclaw plugins init --type tool`; `validate()` proves them still current. */
36
36
  export const OPENCLAW_SCAFFOLD = {
37
37
  /** The OpenClaw release the scaffold was taken from (`package.json openclaw.build`). */
38
- openclawVersion: "2026.9.2",
38
+ openclawVersion: "2026.9.4",
39
39
  /** `peerDependencies.openclaw` and `openclaw.compat.pluginApi`. */
40
40
  pluginApi: ">=2026.5.17",
41
41
  private: true,
@@ -86,6 +86,27 @@ export default defineConfig({
86
86
  * survive into the projection but allows extra ones: upstream stays the source of truth for
87
87
  * everything it does write.
88
88
  */
89
+ /**
90
+ * The version `openclaw plugins init` stamps into its scaffold is the CLI's own version, so
91
+ * the generated manifest carries the openclaw this project actually pins (author override via
92
+ * `openclaw.devDependencies.openclaw`), and the scaffold default only when the project rides
93
+ * `latest`. hosts/openclaw.ts runs the same pin in its drift probe, so the upstream/generated
94
+ * comparison is identical in every environment.
95
+ */
96
+ export function openclawVersion(project: Project): string {
97
+ return project.tool.openclaw?.devDependencies?.openclaw ?? OPENCLAW_SCAFFOLD.openclawVersion;
98
+ }
99
+
100
+ /**
101
+ * The gateway has one flat tool namespace, so the plugin's tool names are the tool id prefix
102
+ * plus the operation name with dots flattened: unprefixed names collide across plugins (two
103
+ * `web`s) and dotted ones fold onto core tools (`sessions.list` → `sessions_list`). The
104
+ * kernel, CLI and MCP server keep the bare operation names; only this host renames.
105
+ */
106
+ export function toolName(project: Project, operation: Operation): string {
107
+ return `${project.identity.name}_${operation.name.replace(/\./g, "_")}`;
108
+ }
109
+
89
110
  export const OPENCLAW_ADDITIONS = {
90
111
  /** Without it `tsc` emits a `dist/` from source that failed type-checking. */
91
112
  compilerOptions: { noEmitOnError: true },
@@ -123,6 +144,11 @@ export function registrations(project: Project): Registration[] {
123
144
  return project.tool.openclaw?.registers ?? [];
124
145
  }
125
146
 
147
+ /** Whether the plugin ships `.agents/skills/` directories (`openclaw.skills` in tool.json). */
148
+ export function shipsSkills(project: Project): boolean {
149
+ return (project.tool.openclaw?.skills?.length ?? 0) > 0;
150
+ }
151
+
126
152
  /**
127
153
  * The gateway route and the Control UI tab the plugin adds when the tool also has a `web`
128
154
  * surface. Only for the in-process binding: the route hands requests to the core package's own
@@ -257,7 +283,7 @@ function packageJson(
257
283
  type: "module",
258
284
  private: OPENCLAW_SCAFFOLD.private,
259
285
  scripts: { ...OPENCLAW_SCAFFOLD.scripts, ...e2e },
260
- files: OPENCLAW_SCAFFOLD.files,
286
+ files: shipsSkills(project) ? [...OPENCLAW_SCAFFOLD.files, "skills"] : OPENCLAW_SCAFFOLD.files,
261
287
  peerDependencies: { openclaw: pluginApi(project), ...extra?.peerDependencies },
262
288
  dependencies: { ...OPENCLAW_SCAFFOLD.dependencies, ...core, ...extra?.dependencies },
263
289
  devDependencies: {
@@ -268,7 +294,7 @@ function packageJson(
268
294
  openclaw: {
269
295
  extensions: [OPENCLAW_SCAFFOLD.entry],
270
296
  compat: { pluginApi: pluginApi(project) },
271
- build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
297
+ build: { openclawVersion: openclawVersion(project) },
272
298
  },
273
299
  pluginInspector: expectedRegistrations(project, operations).length
274
300
  ? pluginInspector(project, operations)
@@ -299,8 +325,9 @@ function pluginManifest(project: Project, operations: Operation[]): Record<strin
299
325
  configSchema: configSchema(project),
300
326
  uiHints: sensitive.length ? Object.fromEntries(sensitive) : undefined,
301
327
  activation: activation(project),
328
+ skills: shipsSkills(project) ? ["./skills"] : undefined,
302
329
  contracts: {
303
- tools: operations.map((operation) => operation.name),
330
+ tools: operations.map((operation) => toolName(project, operation)),
304
331
  ...Object.fromEntries(declared),
305
332
  },
306
333
  }) as Record<string, unknown>;
@@ -404,7 +431,7 @@ const execFileAsync = promisify(execFile);
404
431
  ${pythonKernel(project)}`;
405
432
  const tools = operations.map(
406
433
  (operation) => ` tool({
407
- name: ${JSON.stringify(operation.name)},
434
+ name: ${JSON.stringify(toolName(project, operation))},
408
435
  description: ${JSON.stringify(operation.description ?? operation.name)},
409
436
  parameters: Type.Unsafe(${literal(normalizeSchema(operation.inputSchema), 6)}),
410
437
  ${executeBody(project, operation)}
@@ -565,7 +592,7 @@ import entry from "./index.js";
565
592
 
566
593
  describe(${JSON.stringify(project.identity.name)}, () => {
567
594
  it("declares tool metadata", () => {
568
- expect(getToolPluginMetadata(entry)?.tools.map((tool) => tool.name)).toEqual(${JSON.stringify(operations.map((operation) => operation.name))});
595
+ expect(getToolPluginMetadata(entry)?.tools.map((tool) => tool.name)).toEqual(${JSON.stringify(operations.map((operation) => toolName(project, operation)))});
569
596
  });
570
597
  ${registrationTest}${web}});
571
598
  `;
@@ -611,8 +638,8 @@ function e2eCase(project: Project, operations: Operation[]): E2eCase | undefined
611
638
  * would answer OK even for a tool that threw, because the host reports that in a tool message too.
612
639
  * An operation that promises no output names has nothing to gate on, so it gets no failure leg.
613
640
  */
614
- function e2eFixtures(kase: E2eCase): string {
615
- const name = kase.operation.name;
641
+ function e2eFixtures(project: Project, kase: E2eCase): string {
642
+ const name = toolName(project, kase.operation);
616
643
  return json({
617
644
  fixtures: [
618
645
  {
@@ -758,19 +785,19 @@ describe(${JSON.stringify(`${id} in a real OpenClaw agent turn`)}, () => {
758
785
  it("registers its tools at runtime", async () => {
759
786
  const inspected = JSON.parse(await oc(["plugins", "inspect", ${JSON.stringify(id)}, "--runtime", "--json"]));
760
787
  expect(inspected.plugin.status).toBe("loaded");
761
- expect(inspected.plugin.toolNames).toEqual(${JSON.stringify(operations.map((operation) => operation.name))});
788
+ expect(inspected.plugin.toolNames).toEqual(${JSON.stringify(operations.map((operation) => toolName(project, operation)))});
762
789
  });
763
790
 
764
- it(${JSON.stringify(`the model calls ${kase.operation.name} and the tool's result reaches the reply`)}, async () => {
791
+ it(${JSON.stringify(`the model calls ${toolName(project, kase.operation)} and the tool's result reaches the reply`)}, async () => {
765
792
  const turn = JSON.parse(
766
793
  await oc([
767
794
  "agent", "--local", "--agent", "main", "--session-id", \`e2e-\${Date.now()}\`,
768
795
  "--model", MODEL, "--thinking", "off", "--timeout", "120", "--json",
769
796
  "--message",
770
- ${JSON.stringify(`Call the tool named ${kase.operation.name}. Reply with only the exact text returned by that tool.`)},
797
+ ${JSON.stringify(`Call the tool named ${toolName(project, kase.operation)}. Reply with only the exact text returned by that tool.`)},
771
798
  ]),
772
799
  );
773
- expect(turn.meta.agentMeta.terminalReceipt.successfulToolNames).toContain(${JSON.stringify(kase.operation.name)});
800
+ expect(turn.meta.agentMeta.terminalReceipt.successfulToolNames).toContain(${JSON.stringify(toolName(project, kase.operation))});
774
801
  expect(turn.payloads[0].text).toBe(${JSON.stringify(kase.ok)});
775
802
  }, 180_000);
776
803
  });
@@ -860,7 +887,7 @@ function readme(project: Project, operations: Operation[], kase?: E2eCase): stri
860
887
  "`npm run test:e2e` runs one real OpenClaw agent turn against a scripted OpenAI-compatible",
861
888
  "model (`@copilotkit/aimock`, the same package OpenClaw's own QA lane uses). `e2e/fixtures.json`",
862
889
  "is projected from `dev.toolfactory/ops.json` and `tool.json`'s `tests.examples`: the model asks",
863
- `for \`${kase.operation.name}\` with those arguments, and answers \`${kase.ok}\` only when the tool's own`,
890
+ `for \`${toolName(project, kase.operation)}\` with those arguments, and answers \`${kase.ok}\` only when the tool's own`,
864
891
  "result comes back carrying what its output schema promises.",
865
892
  "",
866
893
  );
@@ -955,6 +982,13 @@ export const surface: Surface = {
955
982
  path: `${HOST_DIR}/vitest.config.ts`,
956
983
  content: OPENCLAW_SCAFFOLD.vitestConfig,
957
984
  },
985
+ ...(project.skills ?? []).flatMap((skill) =>
986
+ skill.files.map((file) => ({
987
+ kind: "file" as const,
988
+ path: `${HOST_DIR}/skills/${skill.name}/${file.path}`,
989
+ content: file.content,
990
+ })),
991
+ ),
958
992
  {
959
993
  kind: "merge",
960
994
  path: `${HOST_DIR}/openclaw.plugin.json`,
@@ -984,7 +1018,7 @@ export const surface: Surface = {
984
1018
  {
985
1019
  kind: "file" as const,
986
1020
  path: `${HOST_DIR}/e2e/fixtures.json`,
987
- content: e2eFixtures(kase),
1021
+ content: e2eFixtures(project, kase),
988
1022
  },
989
1023
  {
990
1024
  kind: "file" as const,
@@ -192,7 +192,7 @@ describe("web", () => {
192
192
  expect(app).toContain("Authorization: `Bearer ${TOKEN}`");
193
193
  // The Secrets panel is the developer's `.env`, and it is the kernel's `/env` that answers.
194
194
  expect(app).toContain('new URL("env", document.baseURI).href');
195
- expect(app).toContain("toolfactory secrets check");
195
+ expect(app).toContain("toolfactory secrets-usage check");
196
196
  expect(app).toContain('type="password"');
197
197
 
198
198
  // The smoke drives the real kernel over --http instead of a static server of its own.
@@ -153,8 +153,8 @@ export default defineConfig({
153
153
  "@fontsource-variable/geist": "^5.3.0",
154
154
  "@tailwindcss/vite": "^4.3.3",
155
155
  "class-variance-authority": "^0.7.1",
156
- cn: "^0.2.6",
157
- "lucide-react": "^1.43.0",
156
+ cn: "^0.3.0",
157
+ "lucide-react": "^1.46.0",
158
158
  react: "^19.2.8",
159
159
  "react-dom": "^19.2.8",
160
160
  shadcn: "^4.21.0",
@@ -164,11 +164,11 @@ export default defineConfig({
164
164
  devDependencies: {
165
165
  "@types/node": "^24.13.3",
166
166
  "@types/react": "^19.2.18",
167
- "@types/react-dom": "^19.2.4",
168
- "@vitejs/plugin-react": "^6.1.0",
169
- oxlint: "^1.79.0",
167
+ "@types/react-dom": "^19.2.7",
168
+ "@vitejs/plugin-react": "^6.1.1",
169
+ oxlint: "^1.81.0",
170
170
  typescript: "~6.0.2",
171
- vite: "^8.2.2",
171
+ vite: "^8.3.0",
172
172
  },
173
173
  },
174
174
  componentsJson: {
@@ -637,7 +637,7 @@ function SecretsPanel() {
637
637
  <CardTitle>Secrets</CardTitle>
638
638
  <CardDescription>
639
639
  Written to this checkout's gitignored <code>.env</code>, which the live tests and
640
- <code> toolfactory bootstrap-repo</code> read. Run <code>toolfactory secrets check</code>{" "}
640
+ <code> toolfactory bootstrap-repo</code> read. Run <code>toolfactory secrets-usage check</code>{" "}
641
641
  to ask each registry whether it accepts them.
642
642
  </CardDescription>
643
643
  </CardHeader>
@@ -625,7 +625,7 @@ function releaseDocument(
625
625
  "`gh workflow run release.yml -f tag=vX.Y.Z` — never `gh run rerun`, which replays the",
626
626
  "original run's secret snapshot. `release` first retracts every registry a surface dropped",
627
627
  "since the previous tag published to (git is the ledger; `toolfactory unpublish`), and",
628
- "`toolfactory secrets status` / `bootstrap-repo` cover the one-time steps that are left.",
628
+ "`toolfactory secrets-usage status` / `bootstrap-repo` cover the one-time steps that are left.",
629
629
  ];
630
630
  if (clawhubSelected) {
631
631
  jobs["publish-clawhub"] = compact({
@@ -904,7 +904,7 @@ function envExample(project: Project): string {
904
904
  lines.push(
905
905
  "",
906
906
  "# Release credentials (repository secrets; `toolfactory bootstrap-repo` pushes them from .env,",
907
- "# `toolfactory secrets status` explains each). Leave one empty to skip that registry.",
907
+ "# `toolfactory secrets-usage status` explains each). Leave one empty to skip that registry.",
908
908
  );
909
909
  for (const row of release) {
910
910
  lines.push("", `# ${row.id}: ${row.url}`, ...row.secrets.map((name) => `${name}=`));
@@ -13,7 +13,7 @@ const operations = [...authored, web];
13
13
  const program = new Command()
14
14
  .name("toolfactory")
15
15
  .description("Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.")
16
- .version("0.2.1");
16
+ .version("0.3.1");
17
17
 
18
18
  for (const op of operations) {
19
19
  if (!serves(op, "cli")) continue;
@@ -33,7 +33,7 @@ const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
33
33
 
34
34
  /** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
35
35
  function createServer(): McpServer {
36
- const server = new McpServer({ name: "toolfactory", version: "0.2.1" });
36
+ const server = new McpServer({ name: "toolfactory", version: "0.3.1" });
37
37
  for (const op of operations) {
38
38
  if (!introspecting && !serves(op, "mcp")) continue;
39
39
  server.registerTool(