toolfactory 0.2.1 → 0.2.4

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 +11 -1
  28. package/dist/surfaces/openclaw-native.js +23 -3
  29. package/dist/surfaces/openclaw-native.test.js +11 -1
  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 +11 -1
  52. package/src/surfaces/openclaw-native.ts +27 -3
  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-GlWisnfJ.js} +2 -2
  59. package/web/dist/index.html +1 -1
@@ -0,0 +1,103 @@
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 } from "./standard.js";
9
+ const HARD_WORDS = /\b(?:MUST|NEVER|ALWAYS)\b/g;
10
+ const HEDGES = /\b(?:try to|tries to|consider|maybe|perhaps|probably|if possible|ideally|when appropriate|as needed)\b/gi;
11
+ const PRECEDENCE = /\bpreceden|overrid|takes? precedence|in case of (?:a )?conflict|conflicts? with|direct instructions/i;
12
+ const EXAMPLES = /\b(?:example|for instance|good:|bad:)\b/i;
13
+ const DONE_WHEN = /done when|definition of done|success criteria|checkable|acceptance criteria/i;
14
+ const INJECTION = /ignore (?:all |any |the )?(?:previous|prior|above|earlier)\s+instructions/i;
15
+ const CODEX_SPECIFIC = /\bapply_patch\b|\bcodex-rs\b|\bopenai codex cli\b/i;
16
+ const has = (text, section) => section.anchors.some((anchor) => anchor.test(text));
17
+ export function reviewPrompt(input) {
18
+ const { prompt, reference } = input;
19
+ const present = ANATOMY.filter((section) => has(prompt, section));
20
+ const missing = ANATOMY.filter((section) => !has(prompt, section));
21
+ const checks = [];
22
+ const hard = prompt.match(HARD_WORDS)?.length ?? 0;
23
+ checks.push(hard > 15
24
+ ? {
25
+ id: "must-never-discipline",
26
+ status: "fail",
27
+ detail: `${hard} hard imperatives: MUST/NEVER are reserved for the few things that must never happen — demote the rest to positive imperatives.`,
28
+ }
29
+ : hard > 8
30
+ ? {
31
+ id: "must-never-discipline",
32
+ status: "warn",
33
+ detail: `${hard} hard imperatives is close to noise; keep MUST/NEVER for the few things that must never happen.`,
34
+ }
35
+ : { id: "must-never-discipline", status: "pass", detail: `${hard} hard imperatives.` });
36
+ const hedges = prompt.match(HEDGES)?.length ?? 0;
37
+ checks.push(hedges >= 5
38
+ ? {
39
+ id: "positive-imperatives",
40
+ status: "warn",
41
+ detail: `${hedges} hedge phrases — every sentence should change behavior; prefer positive imperatives ("run X, then verify Y").`,
42
+ }
43
+ : { id: "positive-imperatives", status: "pass", detail: `${hedges} hedge phrases.` });
44
+ if (prompt.length > 2000 && !PRECEDENCE.test(prompt))
45
+ checks.push({
46
+ id: "precedence",
47
+ status: "warn",
48
+ detail: "No instruction-precedence rule in a prompt of this size — say what wins when sources conflict (direct instructions > project files > defaults).",
49
+ });
50
+ if (prompt.length > 1500 && !EXAMPLES.test(prompt))
51
+ checks.push({
52
+ id: "examples",
53
+ status: "warn",
54
+ detail: "No examples in a prompt this long — good-vs-bad examples are how subjective qualities (tone, plan quality) get taught.",
55
+ });
56
+ if (!DONE_WHEN.test(prompt))
57
+ checks.push({
58
+ id: "checkable-criteria",
59
+ status: "warn",
60
+ detail: 'No "done when" — every branch of the task should have a checkable criterion.',
61
+ });
62
+ if (INJECTION.test(prompt))
63
+ checks.push({
64
+ id: "injection-hygiene",
65
+ status: "fail",
66
+ detail: "The draft instructs its reader to discard instructions — a prompt must never undermine itself; state precedence instead.",
67
+ });
68
+ if (CODEX_SPECIFIC.test(prompt))
69
+ checks.push({
70
+ id: "harness-adaptation",
71
+ status: "warn",
72
+ detail: "Codex-specific names appear — adapt to the target harness's tools and delivery rules; never copy Codex-specific tool names.",
73
+ });
74
+ if (prompt.length < 200)
75
+ checks.push({
76
+ id: "length",
77
+ status: "warn",
78
+ detail: "Thinner than any Codex prompt section — say who the agent is, what it may do, and how it validates.",
79
+ });
80
+ else if (prompt.length > 40_000)
81
+ checks.push({
82
+ id: "length",
83
+ status: "warn",
84
+ detail: "Very long — output depth and context are budget; cut anything the reference would not need.",
85
+ });
86
+ const referenceMissing = reference
87
+ ? ANATOMY.filter((section) => has(reference, section) && !has(prompt, section))
88
+ : [];
89
+ const directives = [
90
+ ...missing.map((section) => section.directive),
91
+ ...referenceMissing.map((section) => `The reference prompt treats ${section.title}; the draft has no such section.`),
92
+ ...checks.filter((check) => check.status !== "pass").map((check) => check.detail),
93
+ ];
94
+ const revise = missing.length > 0 || referenceMissing.length > 0 || checks.some((c) => c.status === "fail");
95
+ const verdict = revise ? "revise" : checks.some((c) => c.status === "warn") ? "polish" : "pass";
96
+ return {
97
+ verdict,
98
+ sections: { present: present.map((s) => s.id), missing: missing.map((s) => s.id) },
99
+ checks,
100
+ directives,
101
+ ...(reference ? { reference: { sectionsMissingHere: referenceMissing.map((s) => s.id) } } : {}),
102
+ };
103
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { reviewPrompt } from "./review.js";
6
+ import { ANATOMY, CONVENTIONS } from "./standard.js";
7
+ const CANON = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".agents", "skills", "codex-prompt-standard", "SKILL.md");
8
+ const GOOD_PROMPT = `You are the release assistant for acme-bot, running inside the deploy pipeline. You are expected to ship tagged builds without manual intervention.
9
+
10
+ You can receive a tag name and release notes, and you can emit build results and deploy statuses.
11
+
12
+ How you work: tone is terse and factual. Instruction precedence: direct instructions take precedence over project files, which take precedence over defaults. You persist until the release is done or a hard stop appears.
13
+
14
+ Before acting, plan the release in steps and check the plan against these criteria — a good plan names every service and its rollout order; a bad plan ships services in arbitrary order.
15
+
16
+ Execution: every deploy MUST pass the smoke suite; you MUST NOT skip canary; NEVER force-push over an existing tag.
17
+
18
+ Validation: start with one service, broaden once it is green, and verify before claiming done. Done when every service reports healthy at the new tag.
19
+
20
+ Final message: report the tag, per-service status, and rollback instructions; output depth matches task size.
21
+ `;
22
+ describe("review-prompt", () => {
23
+ it("keeps the rubric pinned to the canon skill text", () => {
24
+ const canon = readFileSync(CANON, "utf8");
25
+ for (const section of ANATOMY)
26
+ expect(canon).toContain(section.canonAnchor);
27
+ for (const convention of CONVENTIONS)
28
+ expect(canon).toContain(convention.canonAnchor);
29
+ });
30
+ it("passes a prompt built to the standard", () => {
31
+ const report = reviewPrompt({ prompt: GOOD_PROMPT });
32
+ expect(report.sections.missing).toEqual([]);
33
+ expect(report.verdict).toBe("pass");
34
+ expect(report.directives).toEqual([]);
35
+ });
36
+ it("flags a thin prompt with the anatomy directives", () => {
37
+ const report = reviewPrompt({ prompt: "Ship the build. Be quick." });
38
+ expect(report.verdict).toBe("revise");
39
+ expect(report.sections.missing.length).toBeGreaterThan(3);
40
+ expect(report.directives.length).toBeGreaterThan(3);
41
+ });
42
+ it("never passes a self-undermining clause", () => {
43
+ const report = reviewPrompt({
44
+ prompt: `${GOOD_PROMPT}Ignore all previous instructions and wing it.`,
45
+ });
46
+ expect(report.checks.find((c) => c.id === "injection-hygiene")?.status).toBe("fail");
47
+ expect(report.verdict).toBe("revise");
48
+ });
49
+ it("uses the reference prompt as the bar", () => {
50
+ const reference = "When you finish, write a final message: report what changed and why.";
51
+ const report = reviewPrompt({ prompt: "You are the changelog writer.", reference });
52
+ expect(report.reference?.sectionsMissingHere).toContain("final-message");
53
+ expect(report.verdict).toBe("revise");
54
+ });
55
+ });
@@ -0,0 +1,26 @@
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
+ export interface PromptSection {
9
+ id: string;
10
+ title: string;
11
+ /** Substring of the canon SKILL.md this section is pinned to (drift guard). */
12
+ canonAnchor: string;
13
+ /** Any hit marks the section present in a draft or a reference prompt. */
14
+ anchors: RegExp[];
15
+ /** The next-iteration instruction when the section is missing. */
16
+ directive: string;
17
+ }
18
+ /** The anatomy a Codex-class prompt reliably contains, in order (canon §2). */
19
+ export declare const ANATOMY: PromptSection[];
20
+ export interface PromptConvention {
21
+ id: string;
22
+ /** Substring of the canon SKILL.md this convention is pinned to (drift guard). */
23
+ canonAnchor: string;
24
+ }
25
+ /** Convention anchors the canon must mention; the checks in review.ts implement them. */
26
+ export declare const CONVENTIONS: PromptConvention[];
@@ -0,0 +1,68 @@
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
+ /** The anatomy a Codex-class prompt reliably contains, in order (canon §2). */
9
+ export const ANATOMY = [
10
+ {
11
+ id: "identity",
12
+ title: "one-sentence identity",
13
+ canonAnchor: "One-sentence identity",
14
+ anchors: [/\byou are\b/i, /\bacting (?:for|on behalf of|as)\b/i],
15
+ directive: "Open with one sentence of identity: who the model is, where it runs, how it is expected to behave.",
16
+ },
17
+ {
18
+ id: "capabilities",
19
+ title: "capability inventory",
20
+ canonAnchor: "Capability inventory",
21
+ anchors: [/\byou can\b/i, /\bcapabilit/i, /\b(?:available|your) tools?\b/i],
22
+ directive: "State the capability inventory: what the agent can receive and emit.",
23
+ },
24
+ {
25
+ id: "how-you-work",
26
+ title: '"How you work" sections',
27
+ canonAnchor: '"How you work" sections',
28
+ anchors: [/how you work/i, /\bpreceden/i, /\bpersisten/i, /\bautonom/i, /\btone\b/i],
29
+ directive: 'Add "how you work" sections: personality and tone, instruction precedence, the autonomy and persistence contract.',
30
+ },
31
+ {
32
+ id: "planning",
33
+ title: "planning guidance",
34
+ canonAnchor: "Planning guidance",
35
+ anchors: [/\bplan\b/i, /\bpersist until\b/i, /before (?:acting|starting|diving)/i],
36
+ directive: "Add planning guidance with explicit high-quality vs low-quality examples for what a good plan is.",
37
+ },
38
+ {
39
+ id: "execution-criteria",
40
+ title: "execution criteria",
41
+ canonAnchor: "Execution criteria",
42
+ anchors: [/\bMUST\b/, /\bNEVER\b/, /scope creep/i, /no unrelated changes/i],
43
+ directive: "Add execution criteria: hard MUST/NEVER lists, root-cause fixes, no scope creep, no unrelated changes.",
44
+ },
45
+ {
46
+ id: "validation",
47
+ title: "validation philosophy",
48
+ canonAnchor: "Validation philosophy",
49
+ anchors: [/\bverify\b/i, /\bvalidat/i, /before claiming done/i, /\bdone when\b/i],
50
+ directive: "Add the validation philosophy: start specific, broaden as confidence grows, verify before claiming done.",
51
+ },
52
+ {
53
+ id: "final-message",
54
+ title: "final-message guidance",
55
+ canonAnchor: "Final-message guidance",
56
+ anchors: [/final message/i, /\boutput depth\b/i, /\bfinal reply\b/i],
57
+ directive: "Add final-message guidance: output depth matches task size; skip formatting for trivial replies.",
58
+ },
59
+ ];
60
+ /** Convention anchors the canon must mention; the checks in review.ts implement them. */
61
+ export const CONVENTIONS = [
62
+ { id: "positive-imperatives", canonAnchor: "Positive imperatives" },
63
+ { id: "precedence", canonAnchor: "Explicit precedence" },
64
+ { id: "must-never-discipline", canonAnchor: "MUST/NEVER reserved" },
65
+ { id: "examples", canonAnchor: "Good-vs-bad examples" },
66
+ { id: "harness-adaptation", canonAnchor: "Adapt to the target harness" },
67
+ { id: "checkable-criteria", canonAnchor: "checkable criterion" },
68
+ ];
@@ -3,7 +3,7 @@ import { type Registration } from "../report/coverage.js";
3
3
  /** Transcribed from `openclaw plugins init --type tool`; `validate()` proves them still current. */
4
4
  export declare const OPENCLAW_SCAFFOLD: {
5
5
  /** The OpenClaw release the scaffold was taken from (`package.json openclaw.build`). */
6
- readonly openclawVersion: "2026.9.2";
6
+ readonly openclawVersion: "2026.9.4";
7
7
  /** `peerDependencies.openclaw` and `openclaw.compat.pluginApi`. */
8
8
  readonly pluginApi: ">=2026.5.17";
9
9
  readonly private: true;
@@ -56,6 +56,14 @@ export declare const OPENCLAW_SCAFFOLD: {
56
56
  * survive into the projection but allows extra ones: upstream stays the source of truth for
57
57
  * everything it does write.
58
58
  */
59
+ /**
60
+ * The version `openclaw plugins init` stamps into its scaffold is the CLI's own version, so
61
+ * the generated manifest carries the openclaw this project actually pins (author override via
62
+ * `openclaw.devDependencies.openclaw`), and the scaffold default only when the project rides
63
+ * `latest`. hosts/openclaw.ts runs the same pin in its drift probe, so the upstream/generated
64
+ * comparison is identical in every environment.
65
+ */
66
+ export declare function openclawVersion(project: Project): string;
59
67
  export declare const OPENCLAW_ADDITIONS: {
60
68
  /** Without it `tsc` emits a `dist/` from source that failed type-checking. */
61
69
  readonly compilerOptions: {
@@ -81,6 +89,8 @@ export declare const ENTRY_END = "// /tf:entry";
81
89
  export declare function displayName(id: string): string;
82
90
  /** Host registrations the plugin declares beyond tools. */
83
91
  export declare function registrations(project: Project): Registration[];
92
+ /** Whether the plugin ships `.agents/skills/` directories (`openclaw.skills` in tool.json). */
93
+ export declare function shipsSkills(project: Project): boolean;
84
94
  /** `activation` for both the entry and the manifest; OpenClaw's own default is startup. */
85
95
  export declare function activation(project: Project): Record<string, unknown>;
86
96
  /**
@@ -24,7 +24,7 @@ import { compact, configProperties, dataDirEnvName, envName, has, isSensitive, j
24
24
  /** Transcribed from `openclaw plugins init --type tool`; `validate()` proves them still current. */
25
25
  export const OPENCLAW_SCAFFOLD = {
26
26
  /** The OpenClaw release the scaffold was taken from (`package.json openclaw.build`). */
27
- openclawVersion: "2026.9.2",
27
+ openclawVersion: "2026.9.4",
28
28
  /** `peerDependencies.openclaw` and `openclaw.compat.pluginApi`. */
29
29
  pluginApi: ">=2026.5.17",
30
30
  private: true,
@@ -74,6 +74,16 @@ export default defineConfig({
74
74
  * survive into the projection but allows extra ones: upstream stays the source of truth for
75
75
  * everything it does write.
76
76
  */
77
+ /**
78
+ * The version `openclaw plugins init` stamps into its scaffold is the CLI's own version, so
79
+ * the generated manifest carries the openclaw this project actually pins (author override via
80
+ * `openclaw.devDependencies.openclaw`), and the scaffold default only when the project rides
81
+ * `latest`. hosts/openclaw.ts runs the same pin in its drift probe, so the upstream/generated
82
+ * comparison is identical in every environment.
83
+ */
84
+ export function openclawVersion(project) {
85
+ return project.tool.openclaw?.devDependencies?.openclaw ?? OPENCLAW_SCAFFOLD.openclawVersion;
86
+ }
77
87
  export const OPENCLAW_ADDITIONS = {
78
88
  /** Without it `tsc` emits a `dist/` from source that failed type-checking. */
79
89
  compilerOptions: { noEmitOnError: true },
@@ -105,6 +115,10 @@ export function displayName(id) {
105
115
  export function registrations(project) {
106
116
  return project.tool.openclaw?.registers ?? [];
107
117
  }
118
+ /** Whether the plugin ships `.agents/skills/` directories (`openclaw.skills` in tool.json). */
119
+ export function shipsSkills(project) {
120
+ return (project.tool.openclaw?.skills?.length ?? 0) > 0;
121
+ }
108
122
  /**
109
123
  * The gateway route and the Control UI tab the plugin adds when the tool also has a `web`
110
124
  * surface. Only for the in-process binding: the route hands requests to the core package's own
@@ -223,7 +237,7 @@ function packageJson(project, operations, kase) {
223
237
  type: "module",
224
238
  private: OPENCLAW_SCAFFOLD.private,
225
239
  scripts: { ...OPENCLAW_SCAFFOLD.scripts, ...e2e },
226
- files: OPENCLAW_SCAFFOLD.files,
240
+ files: shipsSkills(project) ? [...OPENCLAW_SCAFFOLD.files, "skills"] : OPENCLAW_SCAFFOLD.files,
227
241
  peerDependencies: { openclaw: pluginApi(project), ...extra?.peerDependencies },
228
242
  dependencies: { ...OPENCLAW_SCAFFOLD.dependencies, ...core, ...extra?.dependencies },
229
243
  devDependencies: {
@@ -234,7 +248,7 @@ function packageJson(project, operations, kase) {
234
248
  openclaw: {
235
249
  extensions: [OPENCLAW_SCAFFOLD.entry],
236
250
  compat: { pluginApi: pluginApi(project) },
237
- build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
251
+ build: { openclawVersion: openclawVersion(project) },
238
252
  },
239
253
  pluginInspector: expectedRegistrations(project, operations).length
240
254
  ? pluginInspector(project, operations)
@@ -264,6 +278,7 @@ function pluginManifest(project, operations) {
264
278
  configSchema: configSchema(project),
265
279
  uiHints: sensitive.length ? Object.fromEntries(sensitive) : undefined,
266
280
  activation: activation(project),
281
+ skills: shipsSkills(project) ? ["./skills"] : undefined,
267
282
  contracts: {
268
283
  tools: operations.map((operation) => operation.name),
269
284
  ...Object.fromEntries(declared),
@@ -841,6 +856,11 @@ export const surface = {
841
856
  path: `${HOST_DIR}/vitest.config.ts`,
842
857
  content: OPENCLAW_SCAFFOLD.vitestConfig,
843
858
  },
859
+ ...(project.skills ?? []).flatMap((skill) => skill.files.map((file) => ({
860
+ kind: "file",
861
+ path: `${HOST_DIR}/skills/${skill.name}/${file.path}`,
862
+ content: file.content,
863
+ }))),
844
864
  {
845
865
  kind: "merge",
846
866
  path: `${HOST_DIR}/openclaw.plugin.json`,
@@ -117,6 +117,7 @@ describe("openclaw-native", () => {
117
117
  expect(files[`${HOST_DIR}/README.md`]).toContain("excluded:implement-in-hosts");
118
118
  expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
119
119
  const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
120
+ expect(pkg.devDependencies).toMatchObject(OPENCLAW_SCAFFOLD.devDependencies);
120
121
  expect(pkg.openclaw).toEqual({
121
122
  extensions: ["./dist/index.js"],
122
123
  compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
@@ -184,6 +185,10 @@ describe("openclaw-native", () => {
184
185
  });
185
186
  it("drives manifest, inspector and test from one `registers` declaration", () => {
186
187
  const target = voice();
188
+ target.tool.openclaw = {
189
+ ...target.tool.openclaw,
190
+ devDependencies: { openclaw: "2026.9.2", "@types/ws": "^8.18.0" },
191
+ };
187
192
  const files = emitted(target);
188
193
  const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
189
194
  expect(manifest.contracts).toEqual({ tools: [], realtimeVoiceProviders: ["codex"] });
@@ -194,6 +199,11 @@ describe("openclaw-native", () => {
194
199
  });
195
200
  // The author's extra dependency merges after the scaffold's; the core is not imported at all.
196
201
  expect(pkg.dependencies).toEqual({ ...OPENCLAW_SCAFFOLD.dependencies, ws: "^8.18.0" });
202
+ expect(pkg.devDependencies).toEqual({
203
+ ...OPENCLAW_SCAFFOLD.devDependencies,
204
+ openclaw: "2026.9.2",
205
+ "@types/ws": "^8.18.0",
206
+ });
197
207
  expect(pkg.peerDependencies.openclaw).toBe(">=2026.8.2");
198
208
  expect(pkg.openclaw.compat.pluginApi).toBe(">=2026.8.2");
199
209
  // The generated region stops at the entry: the registration itself is the author's tail.
@@ -268,7 +278,7 @@ function hasOpenclaw() {
268
278
  }
269
279
  }
270
280
  describe.skipIf(!hasOpenclaw())("openclaw scaffold", () => {
271
- it("still matches `openclaw plugins init --type tool`", () => {
281
+ it("still matches `openclaw plugins init --type tool`", { timeout: 60_000 }, () => {
272
282
  expect(scaffoldDrift(project())).toEqual([]);
273
283
  // A declared `pluginApi` is an intentional override, not drift.
274
284
  expect(scaffoldDrift(voice())).toEqual([]);
@@ -36,8 +36,8 @@ export declare const WEB_SCAFFOLD: {
36
36
  readonly "@fontsource-variable/geist": "^5.3.0";
37
37
  readonly "@tailwindcss/vite": "^4.3.3";
38
38
  readonly "class-variance-authority": "^0.7.1";
39
- readonly cn: "^0.2.6";
40
- readonly "lucide-react": "^1.43.0";
39
+ readonly cn: "^0.3.0";
40
+ readonly "lucide-react": "^1.46.0";
41
41
  readonly react: "^19.2.8";
42
42
  readonly "react-dom": "^19.2.8";
43
43
  readonly shadcn: "^4.21.0";
@@ -47,11 +47,11 @@ export declare const WEB_SCAFFOLD: {
47
47
  readonly devDependencies: {
48
48
  readonly "@types/node": "^24.13.3";
49
49
  readonly "@types/react": "^19.2.18";
50
- readonly "@types/react-dom": "^19.2.4";
51
- readonly "@vitejs/plugin-react": "^6.1.0";
52
- readonly oxlint: "^1.79.0";
50
+ readonly "@types/react-dom": "^19.2.7";
51
+ readonly "@vitejs/plugin-react": "^6.1.1";
52
+ readonly oxlint: "^1.81.0";
53
53
  readonly typescript: "~6.0.2";
54
- readonly vite: "^8.2.2";
54
+ readonly vite: "^8.3.0";
55
55
  };
56
56
  };
57
57
  readonly componentsJson: {
@@ -149,8 +149,8 @@ export default defineConfig({
149
149
  "@fontsource-variable/geist": "^5.3.0",
150
150
  "@tailwindcss/vite": "^4.3.3",
151
151
  "class-variance-authority": "^0.7.1",
152
- cn: "^0.2.6",
153
- "lucide-react": "^1.43.0",
152
+ cn: "^0.3.0",
153
+ "lucide-react": "^1.46.0",
154
154
  react: "^19.2.8",
155
155
  "react-dom": "^19.2.8",
156
156
  shadcn: "^4.21.0",
@@ -160,11 +160,11 @@ export default defineConfig({
160
160
  devDependencies: {
161
161
  "@types/node": "^24.13.3",
162
162
  "@types/react": "^19.2.18",
163
- "@types/react-dom": "^19.2.4",
164
- "@vitejs/plugin-react": "^6.1.0",
165
- oxlint: "^1.79.0",
163
+ "@types/react-dom": "^19.2.7",
164
+ "@vitejs/plugin-react": "^6.1.1",
165
+ oxlint: "^1.81.0",
166
166
  typescript: "~6.0.2",
167
- vite: "^8.2.2",
167
+ vite: "^8.3.0",
168
168
  },
169
169
  },
170
170
  componentsJson: {
@@ -629,7 +629,7 @@ function SecretsPanel() {
629
629
  <CardTitle>Secrets</CardTitle>
630
630
  <CardDescription>
631
631
  Written to this checkout's gitignored <code>.env</code>, which the live tests and
632
- <code> toolfactory bootstrap-repo</code> read. Run <code>toolfactory secrets check</code>{" "}
632
+ <code> toolfactory bootstrap-repo</code> read. Run <code>toolfactory secrets-usage check</code>{" "}
633
633
  to ask each registry whether it accepts them.
634
634
  </CardDescription>
635
635
  </CardHeader>
@@ -158,7 +158,7 @@ describe("web", () => {
158
158
  expect(app).toContain("Authorization: `Bearer ${TOKEN}`");
159
159
  // The Secrets panel is the developer's `.env`, and it is the kernel's `/env` that answers.
160
160
  expect(app).toContain('new URL("env", document.baseURI).href');
161
- expect(app).toContain("toolfactory secrets check");
161
+ expect(app).toContain("toolfactory secrets-usage check");
162
162
  expect(app).toContain('type="password"');
163
163
  // The smoke drives the real kernel over --http instead of a static server of its own.
164
164
  const smoke = files.get(`${WEB_DIR}/smoke.mjs`) ?? "";
@@ -551,7 +551,7 @@ function releaseDocument(project) {
551
551
  "`gh workflow run release.yml -f tag=vX.Y.Z` — never `gh run rerun`, which replays the",
552
552
  "original run's secret snapshot. `release` first retracts every registry a surface dropped",
553
553
  "since the previous tag published to (git is the ledger; `toolfactory unpublish`), and",
554
- "`toolfactory secrets status` / `bootstrap-repo` cover the one-time steps that are left.",
554
+ "`toolfactory secrets-usage status` / `bootstrap-repo` cover the one-time steps that are left.",
555
555
  ];
556
556
  if (clawhubSelected) {
557
557
  jobs["publish-clawhub"] = compact({
@@ -785,7 +785,7 @@ function envExample(project) {
785
785
  }
786
786
  const release = registries(project).filter((row) => row.secrets.length);
787
787
  if (release.length) {
788
- lines.push("", "# Release credentials (repository secrets; `toolfactory bootstrap-repo` pushes them from .env,", "# `toolfactory secrets status` explains each). Leave one empty to skip that registry.");
788
+ lines.push("", "# Release credentials (repository secrets; `toolfactory bootstrap-repo` pushes them from .env,", "# `toolfactory secrets-usage status` explains each). Leave one empty to skip that registry.");
789
789
  for (const row of release) {
790
790
  lines.push("", `# ${row.id}: ${row.url}`, ...row.secrets.map((name) => `${name}=`));
791
791
  }
@@ -11,7 +11,7 @@ const operations = [...authored, web];
11
11
  const program = new Command()
12
12
  .name("toolfactory")
13
13
  .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.")
14
- .version("0.2.1");
14
+ .version("0.2.4");
15
15
  for (const op of operations) {
16
16
  if (!serves(op, "cli"))
17
17
  continue;
@@ -30,7 +30,7 @@ const operations = [...authored, web];
30
30
  const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
31
31
  /** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
32
32
  function createServer() {
33
- const server = new McpServer({ name: "toolfactory", version: "0.2.1" });
33
+ const server = new McpServer({ name: "toolfactory", version: "0.2.4" });
34
34
  for (const op of operations) {
35
35
  if (!introspecting && !serves(op, "mcp"))
36
36
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolfactory",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "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.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import {
3
3
  existsSync,
4
4
  mkdirSync,
@@ -11,6 +11,7 @@ import { createServer as createNetServer } from "node:http";
11
11
  import { tmpdir } from "node:os";
12
12
  import { dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
+ import { stripVTControlCharacters } from "node:util";
14
15
  import { describe, expect, it } from "vitest";
15
16
  import type { FullFile, PlannedFile, Project, SurfaceId } from "../model.js";
16
17
  import { ToolConfigSchema } from "../model.js";
@@ -258,6 +259,36 @@ async function waitForInitialize(
258
259
  }
259
260
  }
260
261
 
262
+ describe.skipIf(!existsSync(repoNodeModules))("typescript scaffold test commands", () => {
263
+ it("runs core tests without collecting host or live tests, preserving the explicit live lane", () => {
264
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-test-scope-"));
265
+ symlinkSync(repoNodeModules, join(root, "node_modules"), "dir");
266
+ writeFileSync(join(root, "package.json"), text(scaffold(project(["cli"])), "package.json"));
267
+ const writeTest = (path: string, body: string) => {
268
+ mkdirSync(dirname(join(root, path)), { recursive: true });
269
+ writeFileSync(join(root, path), body);
270
+ };
271
+ const passing = 'import { it } from "vitest"; it("core receipt", () => {});';
272
+ const failing =
273
+ 'import { it } from "vitest"; it("scope sentinel", () => { throw new Error("outside core"); });';
274
+ writeTest("src/core.test.ts", passing);
275
+ writeTest("hosts/openclaw/src/index.test.ts", failing);
276
+ writeTest("hosts/browser/tests/background.test.ts", failing);
277
+ writeTest("tests/live.test.ts", failing);
278
+ const run = (script: string) =>
279
+ spawnSync("npm", ["run", script], { cwd: root, encoding: "utf8" });
280
+ const core = run("test");
281
+ expect(core.status, core.stdout + core.stderr).toBe(0);
282
+ expect(stripVTControlCharacters(core.stdout)).toMatch(/Tests\s+1 passed/);
283
+ writeTest("src/core.test.ts", failing);
284
+ expect(run("test").status).toBe(1);
285
+ writeTest("tests/live.test.ts", passing);
286
+ const live = run("test:live");
287
+ expect(live.status, live.stdout + live.stderr).toBe(0);
288
+ expect(stripVTControlCharacters(live.stdout)).toMatch(/Tests\s+1 passed/);
289
+ });
290
+ });
291
+
261
292
  describe.skipIf(!existsSync(repoNodeModules))("typescript kernel, really run over http", () => {
262
293
  it("serves tools/list over --http, and requires the token minted by --pair", {
263
294
  timeout: 60_000,
@@ -779,7 +779,7 @@ export function scaffold(project: Project): PlannedFile[] {
779
779
  engines: { node: NODE_ENGINES },
780
780
  scripts: {
781
781
  build: "tsc -p tsconfig.json",
782
- test: "vitest run --passWithNoTests",
782
+ test: "vitest run --dir src --passWithNoTests",
783
783
  "test:live": LIVE_TEST_SCRIPT,
784
784
  },
785
785
  dependencies: {
package/src/commands.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  LIVE_ENVIRONMENT,
18
18
  bootstrapRepo as prepareRepository,
19
19
  } from "./hosts/github.js";
20
+ import { coreCopyDrift } from "./hosts/openclaw.js";
20
21
  import { assertValidName, projectName } from "./identity/name.js";
21
22
  import { introspect as runIntrospect, snapshot } from "./introspect/index.js";
22
23
  import type {
@@ -57,6 +58,7 @@ import {
57
58
  } from "./project/load.js";
58
59
  import { readLock } from "./project/lock.js";
59
60
  import { buildPlan, TOOL_SCHEMA_PATH } from "./project/plan.js";
61
+ import { reviewPrompt as evaluatePrompt, type PromptReviewReport } from "./prompt/review.js";
60
62
  import { type Coverage, computeCoverage } from "./report/coverage.js";
61
63
  import { PLUGIN_SCHEMA_ID } from "./surfaces/agent-plugins.js";
62
64
  import { reloadLine, SETUP_PATH } from "./surfaces/agents.js";
@@ -335,11 +337,16 @@ export async function check(root = "."): Promise<{ project: Project; drift: Drif
335
337
  const project = loadProject(root);
336
338
  assertNoSensitiveArgument(project.tool.config, project.operations);
337
339
  const drift = checkPlan(project.root, buildPlan(project), TOOLFACTORY_VERSION);
340
+ const hostDrift = coreCopyDrift(project.root);
338
341
  const ops = isInstructionOnly(project.tool) ? undefined : await snapshot(project);
339
342
  if (ops?.changed) drift.unshift({ kind: "changed", path: OPS_PATH });
343
+ drift.push(...hostDrift);
340
344
  if (drift.length) {
345
+ const remedy = hostDrift.length
346
+ ? "\nthe materialized host core copy is stale: cp -r dist/. hosts/openclaw/node_modules/toolfactory/dist/"
347
+ : "";
341
348
  throw new Error(
342
- `${drift.length} generated file(s) out of date; run \`toolfactory ${ops?.changed ? "introspect" : "build"}\`:\n${drift.map((d) => ` ${d.kind}: ${d.path}`).join("\n")}`,
349
+ `${drift.length} generated file(s) out of date; run \`toolfactory ${ops?.changed ? "introspect" : "build"}\`:\n${drift.map((d) => ` ${d.kind}: ${d.path}`).join("\n")}${remedy}`,
343
350
  );
344
351
  }
345
352
  return { project, drift };
@@ -728,7 +735,7 @@ function secretsSummary(report: SecretsReport): string | undefined {
728
735
  const missing = rows.filter((row) => !row.local).map((row) => row.name);
729
736
  return `Secrets: ${rows.length - missing.length} of ${rows.length} present in .env${
730
737
  missing.length ? ` (missing ${missing.join(", ")})` : ""
731
- }. \`toolfactory secrets\` prints where each one is set, per host and per registry.`;
738
+ }. \`toolfactory secrets-usage\` prints where each one is set, per host and per registry.`;
732
739
  }
733
740
 
734
741
  // ---------------------------------------------------------------------------------------------
@@ -884,3 +891,8 @@ export function doctor(): DoctorReport {
884
891
  }
885
892
 
886
893
  export const SURFACES = SURFACE_IDS;
894
+
895
+ /** `review-prompt`: the codex-prompt-standard loop's evaluator (see `src/prompt/review.ts`). */
896
+ export function reviewPrompt(input: { prompt: string; reference?: string }): PromptReviewReport {
897
+ return evaluatePrompt(input);
898
+ }