feat(cli): improve agent prompts/skills and workspace fork workflow

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-06-11 12:15:30 +02:00
co-authored by Claude Opus 4.8
parent ce6e2f7ade
commit 1c744d2809
27 changed files with 494 additions and 120 deletions
+46 -4
View File
@@ -1,3 +1,6 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Select } from "@cliffy/prompt/select";
@@ -53,11 +56,19 @@ export async function refreshPrompts(opts: {
},
});
log.info(colors.green("Refreshed AGENTS.cli.md"));
log.info(colors.green("Refreshed AGENTS.wmill.md"));
if (result.legacyManagedRemoved) {
log.info(
colors.yellow(
"Migrated legacy AGENTS.cli.md → AGENTS.wmill.md (removed the old file and rewrote any @AGENTS.cli.md include)."
)
);
}
reportReconciliation({
file: "AGENTS.md",
includeLine: "@AGENTS.cli.md",
includeLine: "@AGENTS.wmill.md",
created: result.agentsCreated,
migration: result.agentsMigration,
});
@@ -175,13 +186,44 @@ interface CommandOptions {
async function promptsAction(opts: CommandOptions): Promise<void> {
await refreshPrompts({ yes: opts.yes === true });
await refreshRtNamespaceIfPresent(opts);
}
/**
* Keep an existing `rt.d.ts` resource-type namespace in sync when the user
* runs `wmill refresh prompts`. `wmill init` already generates it on first
* bind; here we only refresh it when the file is already present (so we never
* introduce it into projects that don't use it). Best-effort: a missing
* workspace/login or offline run just skips with a warning rather than failing
* the whole refresh.
*
* Not wired into the shared `refreshPrompts` helper on purpose — `wmill init`
* regenerates the namespace itself, and `refreshPrompts` must stay
* network-free for its other callers.
*/
async function refreshRtNamespaceIfPresent(opts: CommandOptions): Promise<void> {
const rtPath = join(process.cwd(), "rt.d.ts");
if (!existsSync(rtPath)) return;
try {
const { generateRTNamespace } = await import(
"../resource-type/resource-type.ts"
);
await generateRTNamespace(opts as any);
} catch (error) {
log.warn(
`Could not refresh rt.d.ts resource type namespace: ${
error instanceof Error ? error.message : error
}`
);
}
}
const command = new Command()
.description("Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.")
.description("Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.")
.option(
"--yes",
"Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched."
"Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched."
)
.action(promptsAction as any);
+1 -1
View File
@@ -4,7 +4,7 @@ import tsconfigCommand from "./tsconfig.ts";
const command = new Command()
.description(
"Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)"
"Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)"
)
.command("prompts", promptsCommand)
.command("tsconfig", tsconfigCommand);
+3 -3
View File
@@ -22,7 +22,7 @@ const WORKSPACE_IMPORT_DIRS = ["f", "u"];
// wmill-managed files holding the recommended config. They are always
// (re)written so we can ship updated recommendations over time; users keep
// their own overrides in tsconfig.json / deno.json, which reference these
// managed files and are never overwritten. This mirrors how AGENTS.cli.md
// managed files and are never overwritten. This mirrors how AGENTS.wmill.md
// (managed) and AGENTS.md (user-owned) work for AI prompts.
const MANAGED_TSCONFIG = "tsconfig.wmill.json";
const MANAGED_IMPORT_MAP = "import_map.wmill.json";
@@ -33,7 +33,7 @@ const MANAGED_NOTICE =
// Embedded in tsconfig.wmill.json so any command can detect a stale managed file
// (the recommended config changed) and nudge the user to `wmill refresh tsconfig`
// — mirroring the prompts freshness marker in AGENTS.cli.md.
// — mirroring the prompts freshness marker in AGENTS.wmill.md.
const TSCONFIG_HASH_PREFIX = "// wmill-tsconfig-hash: ";
const TSCONFIG_HASH_REGEX = /^\/\/ wmill-tsconfig-hash: ([0-9a-f]{12})/m;
@@ -291,7 +291,7 @@ async function refreshManagedDenoImportMap(mode: WireMode) {
/**
* Ensure a user-owned config file references the wmill-managed file. Mirrors how
* `wmill refresh prompts` wires `@AGENTS.cli.md` into AGENTS.md:
* `wmill refresh prompts` wires `@AGENTS.wmill.md` into AGENTS.md:
* - missing → create the minimal file (already linked);
* - exists & linked → leave it alone;
* - exists & unlinked → auto-wire it (parse JSON, apply `wire`, write back).
+87 -9
View File
@@ -5,15 +5,23 @@ import * as log from "../../core/log.ts";
import { setClient } from "../../core/client.ts";
import { allWorkspaces, list, removeWorkspace } from "./workspace.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, isGitRepository } from "../../utils/git.ts";
import {
getCurrentGitBranch,
getOriginalBranchForWorkspaceForks,
gitBranchExists,
isGitRepository,
renameCurrentGitBranch,
} from "../../utils/git.ts";
import { WM_FORK_PREFIX } from "../../core/constants.ts";
import { tryResolveBranchWorkspace } from "../../core/context.ts";
import { findWorkspaceByGitBranch, readConfigFile } from "../../core/conf.ts";
async function createWorkspaceFork(
opts: GlobalOptions & {
createWorkspaceName: string | undefined;
color: string | undefined;
datatableBehavior: string | undefined;
fromBranch: string | undefined;
yes: boolean | undefined;
},
workspaceName: string | undefined,
@@ -23,7 +31,37 @@ async function createWorkspaceFork(
throw new Error("You can only create forks within a git repo. Forks are tracked with git and synced to your instance with the git sync workflow.");
}
const workspace = await tryResolveBranchWorkspace(opts);
const currentBranch = getCurrentGitBranch()
if (!currentBranch) {
throw new Error("Could not get git branch name");
}
// `--from-branch` enables the "already on a working branch" workflow: the
// fork is based on <fromBranch> (the parent), and the current branch is
// later renamed onto the fork branch. Without it, the fork is based on the
// current branch and the user checks out a fresh fork branch.
const fromBranch = opts.fromBranch;
let workspace;
if (fromBranch) {
if (fromBranch === currentBranch) {
throw new Error(
`--from-branch is for converting a *different* working branch into the fork branch, but you are already on \`${currentBranch}\`. ` +
`Either omit --from-branch (a fresh fork branch is created with \`git checkout -b\`), or check out the working branch you want to convert first.`,
);
}
const config = await readConfigFile({ warnIfMissing: false });
const match = findWorkspaceByGitBranch(config.workspaces, fromBranch);
if (!match) {
throw new Error(
`Could not find a workspace mapped to branch \`${fromBranch}\` in wmill.yaml's workspaces section. ` +
`Pass the base branch your fork should be based on (e.g. the branch bound to the parent workspace).`,
);
}
workspace = await tryResolveBranchWorkspace(opts, match[0]);
} else {
workspace = await tryResolveBranchWorkspace(opts);
}
if (!workspace) {
throw new Error("Could not resolve workspace from branch name. Make sure you are in a git repo to use workspace forks");
@@ -31,14 +69,12 @@ async function createWorkspaceFork(
log.info(`You are forking workspace (${workspace.workspaceId})`)
const currentBranch = getCurrentGitBranch()
if (!currentBranch) {
throw new Error("Could not get git branch name");
}
const originalBranchIfForked = getOriginalBranchForWorkspaceForks(currentBranch);
let clonedBranchName: string | null;
if (originalBranchIfForked) {
if (fromBranch) {
clonedBranchName = fromBranch;
} else if (originalBranchIfForked) {
log.info(`You are creating a fork of a fork. The branch will be linked to the original branch this was forked from, i.e. \`${originalBranchIfForked}\`, for all settings and overrides.`);
clonedBranchName = originalBranchIfForked;
} else {
@@ -248,9 +284,51 @@ async function createWorkspaceFork(
const newBranchName = `${WM_FORK_PREFIX}/${clonedBranchName}/${workspaceId}`
log.info(`Created forked workspace ${trueWorkspaceId}. To start contributing to your fork, create and push edits to the branch \`${newBranchName}\` by using the command:
// Workflow B (`--from-branch`): turn the current working branch into the
// fork branch in place so its commits become the fork's. Workflow A: leave
// the user on their base branch and have them check out a fresh fork branch.
let onForkBranch = false;
if (fromBranch) {
if (currentBranch === newBranchName) {
onForkBranch = true;
log.info(colors.green(`Your current branch is already \`${newBranchName}\`.`));
} else if (gitBranchExists(newBranchName)) {
log.warn(
`Branch \`${newBranchName}\` already exists locally, so the current branch \`${currentBranch}\` was not renamed. ` +
`Check out the fork branch yourself (e.g. \`git checkout ${newBranchName}\`).`,
);
} else {
let doRename = opts.yes === true;
if (!doRename) {
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message: `Rename your current branch \`${currentBranch}\`\`${newBranchName}\` so its commits become the fork's branch?`,
options: [
{ name: "Yes, rename it", value: "confirm" },
{ name: "No, I'll switch branches myself", value: "cancel" },
],
});
doRename = choice === "confirm";
}
if (doRename) {
renameCurrentGitBranch(newBranchName);
onForkBranch = true;
log.info(
colors.green(
`Renamed \`${currentBranch}\`\`${newBranchName}\`. Your existing commits are now on the fork branch.`,
),
);
}
}
}
\t`+colors.white(`git checkout -b ${newBranchName}`) + `
const checkoutHint = onForkBranch
? `Created forked workspace ${trueWorkspaceId}. You are on the fork branch \`${newBranchName}\` — push it to sync your fork.`
: `Created forked workspace ${trueWorkspaceId}. To start contributing to your fork, create and push edits to the branch \`${newBranchName}\` by using the command:
\t` + colors.white(`git checkout -b ${newBranchName}`);
log.info(`${checkoutHint}
When doing operations on the forked workspace, it will use the remote setup in the workspaces section for the branch it was forked from.
+5 -1
View File
@@ -812,7 +812,11 @@ const command = new Command()
"--datatable-behavior <behavior:string>",
"How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)"
)
.option("-y --yes", "Skip interactive prompts (defaults datatable behavior to 'skip')")
.option(
"--from-branch <branch:string>",
"Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead."
)
.option("-y --yes", "Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)")
.action(createWorkspaceFork as any)
.command("delete-fork")
.description("Delete a forked workspace and git branch")
+33 -7
View File
@@ -3,18 +3,28 @@
*
* `wmill` writes two files:
*
* - `AGENTS.cli.md` — managed CLI / workspace guidance, refreshed by
* - `AGENTS.wmill.md` — managed CLI / workspace guidance, refreshed by
* `wmill refresh prompts` (and the implicit refresh inside `wmill init`).
* - `AGENTS.md` — user-owned project entry point. The default skeleton
* references `AGENTS.cli.md` via an `@`-include so the managed content is
* references `AGENTS.wmill.md` via an `@`-include so the managed content is
* pulled in automatically.
*
* The managed file used to be named `AGENTS.cli.md`; `wmill init` /
* `wmill refresh prompts` migrate the old name to `AGENTS.wmill.md` (and
* rewrite the `@`-include) automatically. The legacy constants below exist
* solely for that migration.
*/
export const AGENTS_CLI_INCLUDE_LINE = "@AGENTS.cli.md";
export const AGENTS_WMILL_FILENAME = "AGENTS.wmill.md";
export const AGENTS_WMILL_INCLUDE_LINE = "@AGENTS.wmill.md";
/** Legacy managed filename / include line, migrated away from on init/refresh. */
export const LEGACY_AGENTS_CLI_FILENAME = "AGENTS.cli.md";
export const LEGACY_AGENTS_CLI_INCLUDE_LINE = "@AGENTS.cli.md";
/**
* Lightweight, user-owned AGENTS.md skeleton. Written only when no AGENTS.md
* exists in the project. Everything below the `@AGENTS.cli.md` include is for
* exists in the project. Everything below the `@AGENTS.wmill.md` include is for
* the user to edit; nothing in this file is refreshed by `wmill`.
*/
export function generateAgentsMdSkeleton(): string {
@@ -28,7 +38,7 @@ The line below pulls in Windmill's managed CLI guidance (skills, deploy flow,
debugging jobs, etc.). Refresh it with \`wmill refresh prompts\`. Remove the
include line if you don't want the managed guidance in this project.
${AGENTS_CLI_INCLUDE_LINE}
${AGENTS_WMILL_INCLUDE_LINE}
## Project-specific instructions
@@ -41,8 +51,12 @@ ${AGENTS_CLI_INCLUDE_LINE}
}
/**
* Managed AGENTS.cli.md content. Rewritten by `wmill init` and
* Managed AGENTS.wmill.md content. Rewritten by `wmill init` and
* `wmill refresh prompts` every time.
*
* NOTE: `system_prompts/generate.py` extracts this template by anchoring on
* the function name `generateAgentsCliMdContent` — keep the name in sync if
* you rename it.
*/
export function generateAgentsCliMdContent(skillsReference: string): string {
return `# Windmill CLI Agent Instructions
@@ -112,10 +126,11 @@ There are two ways local changes reach the workspace. Pick based on how the repo
Before deploying, check whether this repo has a **GitHub Actions (or other CI) workflow that runs \`wmill sync push\` on push**. That workflow is the signal that pushing a branch will deploy:
- Look for \`.github/workflows/*.yml\` (or other CI configs) that invoke \`wmill sync push\`, \`wmill\` deployment commands, or similar.
- Cache the result for the rest of the session — don't re-scan on every deploy.
If such a workflow exists → **use \`git push\`** (Option A). Otherwise → **use \`wmill sync push\`** directly (Option B).
**Save the preference so you don't re-detect it every session.** Once you've determined which option this repo uses (or the user tells you), record it in the **project-specific instructions** section of \`AGENTS.md\` (user-owned — never overwritten by \`wmill refresh prompts\`), e.g. a line like \`Deploy mode: git push (CI runs wmill sync push)\` or \`Deploy mode: wmill sync push (no CI wiring)\`. On later sessions, read that line first and skip the scan. Re-detect only if the CI wiring visibly changed.
### Option A — \`git push\` (CI is wired to sync)
The CI workflow will pick up the commit and run \`wmill sync push\` on the backend, which is how deployments are intended to happen in this repo. Don't bypass it.
@@ -137,6 +152,17 @@ No CI workflow runs \`wmill sync push\` automatically, so deploy directly from t
Only deploy when the user explicitly asks to deploy, publish, push, or ship — not when they say "run", "try", or "test". For testing local edits use the per-entity \`preview\` commands (\`wmill script preview\`, \`wmill flow preview\`) — they don't deploy.
## Workspace forks
A **fork** is an isolated copy of a workspace for parallel or experimental work — make changes (including to datatables, which are cloned per fork) without touching the parent, then merge back after review. Each fork is paired with a git branch named \`wm-fork/<base>/<id>\`. Forks require a git repo.
Create one with \`wmill workspace fork\`. There are two branch workflows — pick by where you are:
- **Starting from the base branch** (no in-progress work to carry over): run \`wmill workspace fork\`. It bases the fork on your current branch and prints a \`git checkout -b wm-fork/<base>/<id>\` to start the fork branch.
- **Already on a working branch you want to turn into the fork** (e.g. you've branched and already edited a forked datatable): run \`wmill workspace fork --from-branch <base>\`. It bases the fork on \`<base>\` (the parent's branch) and renames your current branch onto \`wm-fork/<base>/<id>\` in place, preserving its commits.
Merge a fork back into its parent with \`wmill workspace merge\` (or the Merge UI on the fork's home page). Full reference: https://www.windmill.dev/docs/advanced/workspace_forks
## Debugging Jobs
When the user reports a script or flow failure, is investigating unexpected output, or asks why something ran the way it did, use the CLI to fetch job details before speculating. See the \`cli-commands\` skill for all flags.
+31 -13
View File
@@ -1,13 +1,13 @@
/**
* Versioning + freshness check for the managed AGENTS.cli.md bundle.
* Versioning + freshness check for the managed AGENTS.wmill.md bundle.
*
* We embed a short hash of "what this CLI would write" into AGENTS.cli.md as
* We embed a short hash of "what this CLI would write" into AGENTS.wmill.md as
* an HTML comment. On every `wmill` command (with a few exceptions), we read
* the stored hash and compare against the current CLI's hash. Mismatch =>
* one-line warning telling the user to `wmill refresh prompts`.
*
* The hash covers all inputs that affect the rendered bundle: the
* AGENTS.cli.md template, every skill body, schemas and schema mappings, and
* AGENTS.wmill.md template, every skill body, schemas and schema mappings, and
* the nonDottedPaths setting. It is *not* tied to the CLI's package version,
* so non-prompt CLI releases don't produce false positives.
*/
@@ -15,7 +15,11 @@ import { createHash } from "node:crypto";
import { stat } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { readTextFile } from "../utils/utils.ts";
import { generateAgentsCliMdContent } from "./core.ts";
import {
AGENTS_WMILL_FILENAME,
LEGACY_AGENTS_CLI_FILENAME,
generateAgentsCliMdContent,
} from "./core.ts";
import {
SCHEMAS,
SCHEMA_MAPPINGS,
@@ -43,7 +47,7 @@ export function extractPromptsHash(content: string): string | null {
}
/**
* Insert the hash marker into rendered AGENTS.cli.md content. The marker
* Insert the hash marker into rendered AGENTS.wmill.md content. The marker
* goes on the line right after the title so it's easy to find and doesn't
* break the rendered Markdown structure.
*/
@@ -73,7 +77,7 @@ export function currentPromptsHash(nonDottedPaths: boolean): string {
hasher.update(generateAgentsCliMdContent("__PLACEHOLDER__"));
// Skill metadata (names + descriptions) — fed into the skills reference
// line in AGENTS.cli.md and the wrapper frontmatter.
// line in AGENTS.wmill.md and the wrapper frontmatter.
hasher.update("\nskills:");
hasher.update(JSON.stringify(SKILLS));
@@ -101,10 +105,14 @@ export function currentPromptsHash(nonDottedPaths: boolean): string {
}
/**
* Read AGENTS.cli.md in the current working directory, compare its embedded
* Read AGENTS.wmill.md in the current working directory, compare its embedded
* hash to the current CLI's hash, and print a one-line warning if they
* differ. Silent on every other code path (no AGENTS.cli.md, no marker,
* differ. Silent on every other code path (no managed file, no marker,
* matching hash, IO error, …) so it never gets in the user's way.
*
* Back-compat: if only the legacy `AGENTS.cli.md` is present (no
* `AGENTS.wmill.md` yet), warn that the managed file should be migrated —
* `wmill refresh prompts` renames it and rewrites the include.
*/
export async function warnIfPromptsStale(opts?: {
cwd?: string;
@@ -114,9 +122,19 @@ export async function warnIfPromptsStale(opts?: {
if (opts?.argv && !shouldRunFreshnessCheck(opts.argv)) return;
const cwd = opts?.cwd ?? process.cwd();
const path = `${cwd}/AGENTS.cli.md`;
const path = `${cwd}/${AGENTS_WMILL_FILENAME}`;
if (!(await stat(path).catch(() => null))) return;
if (!(await stat(path).catch(() => null))) {
// No AGENTS.wmill.md. If the legacy AGENTS.cli.md is still around, nudge
// the user to migrate it; otherwise this project just isn't wmill-managed.
const legacyPath = `${cwd}/${LEGACY_AGENTS_CLI_FILENAME}`;
if (await stat(legacyPath).catch(() => null)) {
emitWarning(
"Your AGENTS.cli.md is using the old managed filename. Run `wmill refresh prompts` to migrate it to AGENTS.wmill.md."
);
}
return;
}
let content: string;
try {
@@ -127,10 +145,10 @@ export async function warnIfPromptsStale(opts?: {
const stored = extractPromptsHash(content);
if (!stored) {
// Older AGENTS.cli.md without a marker. Warn so the user re-runs
// Older AGENTS.wmill.md without a marker. Warn so the user re-runs
// refresh and picks up the new format.
emitWarning(
"Your AGENTS.cli.md predates prompt versioning. Run `wmill refresh prompts` to refresh and add a version marker."
"Your AGENTS.wmill.md predates prompt versioning. Run `wmill refresh prompts` to refresh and add a version marker."
);
return;
}
@@ -152,7 +170,7 @@ export async function warnIfPromptsStale(opts?: {
const current = currentPromptsHash(nonDottedPaths);
if (stored !== current) {
emitWarning(
"Your AGENTS.cli.md is out of date. Run `wmill refresh prompts` to refresh."
"Your AGENTS.wmill.md is out of date. Run `wmill refresh prompts` to refresh."
);
}
}
+20 -9
View File
@@ -291,6 +291,10 @@ import Stripe from "stripe";
import { someFunction } from "some-package";
\`\`\`
## Prefer \`//native\` when the runtime allows it
If a script only needs \`fetch\` and the JavaScript standard library — including when it uses \`windmill-client\` — prefer making it a **native** script: add \`//native\` as the first line and write it with the \`write-script-bunnative\` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. \`windmill-client\` works on the native worker (its calls go over \`fetch\`), so needing the Windmill client is **not** a reason to avoid \`//native\`. Use the regular \`bun\` language only when the code (or a dependency) needs Node/Bun runtime APIs — \`node:*\` modules, the filesystem, child processes, or native addons.
## Windmill Client
Import the windmill client for platform interactions:
@@ -299,7 +303,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
\`\`\`
See the SDK documentation for available methods.
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to \`fetch\`.
## Preprocessor Scripts
@@ -1012,7 +1018,9 @@ export async function main(url: string) {
## Windmill Client
\`windmill-client\` is available for Windmill-specific primitives such as the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). Use \`fetch\` for plain HTTP.
\`windmill-client\` works on the native worker (its calls go over \`fetch\`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). It handles auth, the workspace, and the base URL for you. Reserve raw \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a \`fetch\` against the Windmill API.
## Preprocessor Scripts
@@ -1813,7 +1821,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
\`\`\`
See the SDK documentation for available methods.
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to \`fetch\`.
## Preprocessor Scripts
@@ -4494,7 +4504,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
## CLI Commands running, previewing, deploying
After writing, tell the user which command fits what they want to do:
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (\`wmill flow preview\` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (\`wmill sync push\`, \`wmill generate-metadata\`) so the user can approve them. The options:
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files. Add \`--step <step_id>\` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
- \`wmill flow run <path>\` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
@@ -4960,7 +4970,7 @@ The runnable ID is the filename without extension. For example, \`get_user.ts\`
| C# | \`.cs\` | \`myFunc.cs\` |
| Java | \`.java\` | \`myFunc.java\` |
After creating a runnable, tell the user they can generate lock files by running:
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
\`\`\`bash
wmill generate-metadata
\`\`\`
@@ -6456,12 +6466,12 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)
**Subcommands:**
- \`refresh prompts\` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh prompts\` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh tsconfig\` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- \`--yes\` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
@@ -6752,7 +6762,8 @@ workspace related commands
- \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
- \`--color <color:string>\` - Workspace color (hex code, e.g. #ff0000)
- \`--datatable-behavior <behavior:string>\` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip')
- \`--from-branch <branch:string>\` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace and git branch
- \`-y --yes\` - Skip confirmation prompt
- \`workspace merge\` - Compare and deploy changes between a fork and its parent workspace
+78 -12
View File
@@ -1,8 +1,11 @@
import { cp, mkdir, readdir, stat, writeFile } from "node:fs/promises";
import { cp, mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
import { readTextFile } from "../utils/utils.ts";
import { join } from "node:path";
import {
AGENTS_CLI_INCLUDE_LINE,
AGENTS_WMILL_FILENAME,
AGENTS_WMILL_INCLUDE_LINE,
LEGACY_AGENTS_CLI_FILENAME,
LEGACY_AGENTS_CLI_INCLUDE_LINE,
generateAgentsCliMdContent,
generateAgentsMdSkeleton,
} from "./core.ts";
@@ -25,7 +28,7 @@ type ResolvedSkillMetadata = SkillMetadata & {
/**
* How to reconcile an existing user-owned guidance file (AGENTS.md or
* CLAUDE.md) that doesn't reference the managed file below it
* (`@AGENTS.cli.md` for AGENTS.md, `@AGENTS.md` for CLAUDE.md).
* (`@AGENTS.wmill.md` for AGENTS.md, `@AGENTS.md` for CLAUDE.md).
*
* - `append`: leave the file as-is and append the include line.
* - `overwrite`: replace the file with the managed skeleton.
@@ -45,13 +48,13 @@ export interface WriteAiGuidanceOptions {
nonDottedPaths?: boolean;
/** Skill source override (testing / source-of-truth bundling). */
skillsSourcePath?: string;
/** AGENTS.cli.md source override (testing). */
/** AGENTS.wmill.md source override (testing). */
agentsSourcePath?: string;
/** CLAUDE.md source override (testing). */
claudeSourcePath?: string;
/**
* Optional resolver invoked when an existing AGENTS.md lacks an
* `@AGENTS.cli.md` reference. Callers are expected to prompt the user; if
* `@AGENTS.wmill.md` reference. Callers are expected to prompt the user; if
* omitted, the writer defaults to `append` (non-destructive).
*/
resolveAgentsMdMigration?: () => Promise<AgentsMdMigration>;
@@ -64,6 +67,12 @@ export interface WriteAiGuidanceResult {
claudeCreated: boolean;
claudeMigration: ReconcileOutcome;
skillCount: number;
/**
* True when a legacy `AGENTS.cli.md` was found and removed (its content is
* superseded by `AGENTS.wmill.md`, and any `@AGENTS.cli.md` includes were
* rewritten to `@AGENTS.wmill.md`).
*/
legacyManagedRemoved: boolean;
}
export const WMILL_INIT_AI_SKILLS_SOURCE_ENV = "WMILL_INIT_AI_SKILLS_SOURCE";
@@ -94,7 +103,7 @@ export async function writeAiGuidanceFiles(
? await readSkillMetadataFromDirectory(options.skillsSourcePath)
: getGeneratedSkillMetadata();
// AGENTS.cli.md — always (re)written, this is the managed file.
// AGENTS.wmill.md — always (re)written, this is the managed file.
// We embed a content-hash marker so other `wmill` commands can detect a
// stale bundle and prompt the user to `wmill refresh prompts`.
const rawAgentsCliContent =
@@ -105,23 +114,29 @@ export async function writeAiGuidanceFiles(
rawAgentsCliContent,
currentPromptsHash(nonDottedPaths)
);
const agentsCliPath = join(options.targetDir, "AGENTS.cli.md");
const agentsCliPath = join(options.targetDir, AGENTS_WMILL_FILENAME);
await writeFile(agentsCliPath, agentsCliContent, "utf8");
const agentsCliWritten = true;
// Migrate the legacy `AGENTS.cli.md`: rewrite `@AGENTS.cli.md` includes in
// user-owned files to `@AGENTS.wmill.md`, then remove the stale managed
// file. Done before reconciliation so the rewritten include reads as
// "already-linked" rather than triggering a duplicate append.
const legacyManagedRemoved = await migrateLegacyManagedFile(options.targetDir);
// Cache the user's first migration answer and reuse it for every file
// that needs reconciling in this run — there's never a good reason to ask
// the same question twice in a row.
const resolveMigration = cacheOnce(options.resolveAgentsMdMigration);
// AGENTS.md — user-owned. Three paths:
// 1. doesn't exist → create skeleton (which already includes @AGENTS.cli.md).
// 2. exists and already references @AGENTS.cli.md → leave alone.
// 3. exists but doesn't reference @AGENTS.cli.md → ask caller via
// 1. doesn't exist → create skeleton (which already includes @AGENTS.wmill.md).
// 2. exists and already references @AGENTS.wmill.md → leave alone.
// 3. exists but doesn't reference @AGENTS.wmill.md → ask caller via
// resolveMigration (defaults to append).
const agentsMdResult = await reconcileIncludingFile({
path: join(options.targetDir, "AGENTS.md"),
includeLine: AGENTS_CLI_INCLUDE_LINE,
includeLine: AGENTS_WMILL_INCLUDE_LINE,
skeleton: generateAgentsMdSkeleton(),
resolveMigration,
});
@@ -153,9 +168,60 @@ export async function writeAiGuidanceFiles(
claudeCreated: claudeMdResult.created,
claudeMigration: claudeMdResult.migration,
skillCount: skillMetadata.length,
legacyManagedRemoved,
};
}
/**
* One-time migration from the old managed filename (`AGENTS.cli.md`) to
* `AGENTS.wmill.md`:
*
* 1. Rewrite the `@AGENTS.cli.md` include token → `@AGENTS.wmill.md` in
* AGENTS.md and CLAUDE.md (only when it appears as a standalone
* whitespace-delimited token, so lookalikes like `@AGENTS.cli.md.backup`
* are left intact).
* 2. Delete the stale `AGENTS.cli.md` — its content is fully superseded by
* the freshly written `AGENTS.wmill.md`.
*
* Returns true when a legacy `AGENTS.cli.md` was present and removed.
*/
async function migrateLegacyManagedFile(targetDir: string): Promise<boolean> {
for (const fileName of ["AGENTS.md", "CLAUDE.md"]) {
const filePath = join(targetDir, fileName);
const existing = await readTextFile(filePath).catch(() => null);
if (existing == null) continue;
const rewritten = rewriteIncludeToken(
existing,
LEGACY_AGENTS_CLI_INCLUDE_LINE,
AGENTS_WMILL_INCLUDE_LINE
);
if (rewritten !== existing) {
await writeFile(filePath, rewritten, "utf8");
}
}
const legacyPath = join(targetDir, LEGACY_AGENTS_CLI_FILENAME);
const legacyExists = (await stat(legacyPath).catch(() => null)) != null;
if (legacyExists) {
await rm(legacyPath, { force: true });
}
return legacyExists;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Replace `from` with `to` only where `from` appears as a standalone
* whitespace-delimited token. Line endings and surrounding content are
* preserved (`\s` in the lookahead matches `\r`), so a CRLF file stays CRLF.
*/
function rewriteIncludeToken(content: string, from: string, to: string): string {
const re = new RegExp(`(?<=^|\\s)${escapeRegExp(from)}(?=\\s|$)`, "gm");
return content.replace(re, to);
}
function cacheOnce(
resolver: (() => Promise<AgentsMdMigration>) | undefined
): (() => Promise<AgentsMdMigration>) | undefined {
@@ -212,7 +278,7 @@ function referencesIncludeLine(content: string, includeLine: string): boolean {
// line by itself: our own CLAUDE.md default is `Instructions are in
// @AGENTS.md` (one sentence), and a strict equality check made `wmill
// refresh prompts` re-prompt every run on files wmill itself wrote.
// Skipping comment-bearing lines keeps `<!-- @AGENTS.cli.md -->` from
// Skipping comment-bearing lines keeps `<!-- @AGENTS.wmill.md -->` from
// false-positiving.
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
+1 -1
View File
@@ -302,7 +302,7 @@ async function main() {
return response;
});
// Warn (one line) if AGENTS.cli.md predates this CLI's prompts bundle.
// Warn (one line) if AGENTS.wmill.md predates this CLI's prompts bundle.
// The check is gated on argv parsing (cheap) so the ~360 KB skills.gen.ts
// bundle stays out of the import graph for help/version/init/refresh/etc.
if (shouldRunFreshnessCheck(process.argv)) {
+27
View File
@@ -20,6 +20,33 @@ export function getCurrentGitBranch(): string | null {
}
}
/** Whether a local branch with this exact name exists. */
export function gitBranchExists(branchName: string): boolean {
const r = spawnSync(
"git",
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
{ stdio: "pipe" },
);
return r.status === 0;
}
/**
* Rename the currently checked-out branch (`git branch -m <newName>`). Used by
* `wmill workspace fork --from-branch` to turn an existing working branch into
* the `wm-fork/<base>/<id>` fork branch in place, preserving its commits.
*/
export function renameCurrentGitBranch(newName: string): void {
const r = spawnSync("git", ["branch", "-m", newName], {
encoding: "utf8",
stdio: "pipe",
});
if ((r.status ?? 1) !== 0) {
throw new Error(
`git branch -m ${newName} failed (exit ${r.status}): ${r.stderr ?? ""}`,
);
}
}
export function getOriginalBranchForWorkspaceForks(branchName: string | null): string | null {
if (!branchName || !branchName.startsWith(WM_FORK_PREFIX)) {
return null
+87 -28
View File
@@ -143,7 +143,7 @@ Copied from source bundle.
});
});
test("AGENTS.cli.md gets the skills reference from copied directory names", async () => {
test("AGENTS.wmill.md gets the skills reference from copied directory names", async () => {
await withTempDir(async (tempDir) => {
const sourceSkillsDir = join(tempDir, "source-skills");
await writeSkill(
@@ -163,7 +163,7 @@ Copied from source bundle.
skillsSourcePath: sourceSkillsDir,
});
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
expect(agentsCli).toContain(".agents/skills/custom-folder/SKILL.md");
expect(agentsCli).not.toContain(".agents/skills/write-flow/SKILL.md");
// The skill reference points at the .agents/ tree — not .claude/ —
@@ -172,7 +172,7 @@ Copied from source bundle.
});
});
test("AGENTS.cli.md and CLAUDE.md are written even if skills creation fails", async () => {
test("AGENTS.wmill.md and CLAUDE.md are written even if skills creation fails", async () => {
await withTempDir(async (tempDir) => {
// Create a file at .claude so mkdir of .claude/skills throws.
await writeFile(join(tempDir, ".claude"), "not a directory\n", "utf8");
@@ -181,11 +181,11 @@ Copied from source bundle.
writeAiGuidanceFiles({ targetDir: tempDir })
).rejects.toThrow();
expect(await readFile(join(tempDir, "AGENTS.cli.md"), "utf8")).toContain(
expect(await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")).toContain(
".agents/skills/"
);
expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toContain(
"@AGENTS.cli.md"
"@AGENTS.wmill.md"
);
expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toContain(
"@AGENTS.md"
@@ -195,20 +195,20 @@ Copied from source bundle.
});
describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
test("creates a skeleton AGENTS.md (with @AGENTS.cli.md include) when none exists", async () => {
test("creates a skeleton AGENTS.md (with @AGENTS.wmill.md include) when none exists", async () => {
await withTempDir(async (tempDir) => {
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
expect(result.agentsCreated).toBe(true);
expect(result.agentsMigration).toBe("not-applicable");
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
expect(agentsMd).toContain("@AGENTS.cli.md");
expect(agentsMd).toContain("@AGENTS.wmill.md");
});
});
test("leaves an existing AGENTS.md alone when it already references @AGENTS.cli.md", async () => {
test("leaves an existing AGENTS.md alone when it already references @AGENTS.wmill.md", async () => {
await withTempDir(async (tempDir) => {
const original = "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.cli.md\n";
const original = "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.wmill.md\n";
await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
@@ -219,7 +219,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
});
});
test("appends @AGENTS.cli.md when the resolver returns 'append'", async () => {
test("appends @AGENTS.wmill.md when the resolver returns 'append'", async () => {
await withTempDir(async (tempDir) => {
const original = "# Existing custom AGENTS.md\n\nproject rules here.\n";
await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
@@ -233,7 +233,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
expect(updated).toStartWith(original);
expect(updated).toContain("@AGENTS.cli.md");
expect(updated).toContain("@AGENTS.wmill.md");
});
});
@@ -251,7 +251,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
expect(updated).not.toBe(original);
expect(updated).toContain("@AGENTS.cli.md");
expect(updated).toContain("@AGENTS.wmill.md");
});
});
@@ -281,7 +281,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
expect(updated).toStartWith(original);
expect(updated).toContain("@AGENTS.cli.md");
expect(updated).toContain("@AGENTS.wmill.md");
});
});
});
@@ -385,19 +385,78 @@ describe("writeAiGuidanceFiles — CLAUDE.md reconciliation", () => {
});
});
describe("writeAiGuidanceFiles — legacy AGENTS.cli.md migration", () => {
test("removes a legacy AGENTS.cli.md and rewrites the @AGENTS.cli.md include", async () => {
await withTempDir(async (tempDir) => {
// Simulate a project initialized by an older CLI.
await writeFile(
join(tempDir, "AGENTS.cli.md"),
"# old managed file\n",
"utf8"
);
await writeFile(
join(tempDir, "AGENTS.md"),
"# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.cli.md\n",
"utf8"
);
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
// Legacy file is gone; the new managed file is present.
expect(result.legacyManagedRemoved).toBe(true);
await expect(
readFile(join(tempDir, "AGENTS.cli.md"), "utf8")
).rejects.toThrow();
expect(
await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")
).toContain(".agents/skills/");
// The include was rewritten in place (so it reads as already-linked,
// not a duplicate append).
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
expect(agentsMd).toContain("@AGENTS.wmill.md");
expect(agentsMd).not.toContain("@AGENTS.cli.md");
expect(result.agentsMigration).toBe("already-linked");
});
});
test("legacyManagedRemoved is false when there is no legacy file", async () => {
await withTempDir(async (tempDir) => {
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
expect(result.legacyManagedRemoved).toBe(false);
});
});
test("does not rewrite a @AGENTS.cli.md.backup lookalike token", async () => {
await withTempDir(async (tempDir) => {
await writeFile(
join(tempDir, "AGENTS.md"),
"see @AGENTS.cli.md.backup\n\n@AGENTS.wmill.md\n",
"utf8"
);
await writeAiGuidanceFiles({ targetDir: tempDir });
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
// The standalone backup reference (a different file) is preserved.
expect(agentsMd).toContain("@AGENTS.cli.md.backup");
});
});
});
describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", () => {
test.each([
["bare line", "@AGENTS.cli.md"],
["between blank lines", "before\n\n@AGENTS.cli.md\n\nafter"],
["leading whitespace then include", " @AGENTS.cli.md\n"],
["CRLF line endings", "line one\r\n@AGENTS.cli.md\r\nline three"],
["bare line", "@AGENTS.wmill.md"],
["between blank lines", "before\n\n@AGENTS.wmill.md\n\nafter"],
["leading whitespace then include", " @AGENTS.wmill.md\n"],
["CRLF line endings", "line one\r\n@AGENTS.wmill.md\r\nline three"],
// Mid-sentence include: this is how our own CLAUDE.md default looks
// ("Instructions are in @AGENTS.md"). A strict line-equality check made
// `wmill refresh prompts` re-prompt every run on files wmill wrote.
["mid-sentence include", "Instructions are in @AGENTS.cli.md\n"],
["mid-sentence include", "Instructions are in @AGENTS.wmill.md\n"],
// `>` blockquote prefix doesn't disable Claude's `@`-import expansion,
// so we treat it as a reference too.
["blockquoted include", "> @AGENTS.cli.md"],
["blockquoted include", "> @AGENTS.wmill.md"],
])("treats %s as a reference (no append)", async (_label, content) => {
await withTempDir(async (tempDir) => {
await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
@@ -408,11 +467,11 @@ describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", ()
});
test.each([
["@AGENTS.cli.md.backup", "@AGENTS.cli.md.backup"],
["@AGENTS.cli.mdx", "@AGENTS.cli.mdx"],
["@AGENTS.wmill.md.backup", "@AGENTS.wmill.md.backup"],
["@AGENTS.wmill.mdx", "@AGENTS.wmill.mdx"],
["@AGENTS-cli-md (lookalike)", "@AGENTS-cli-md"],
["@AGENTS.cli.md without surrounding whitespace", "foo@AGENTS.cli.md"],
["commented-out include", "<!-- @AGENTS.cli.md -->"],
["@AGENTS.wmill.md without surrounding whitespace", "foo@AGENTS.wmill.md"],
["commented-out include", "<!-- @AGENTS.wmill.md -->"],
])("does not treat %s as a reference (append happens)", async (_label, content) => {
await withTempDir(async (tempDir) => {
await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
@@ -426,10 +485,10 @@ describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", ()
});
describe("prompts freshness — hash marker", () => {
test("AGENTS.cli.md written by writeAiGuidanceFiles carries a hash marker", async () => {
test("AGENTS.wmill.md written by writeAiGuidanceFiles carries a hash marker", async () => {
await withTempDir(async (tempDir) => {
await writeAiGuidanceFiles({ targetDir: tempDir });
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
const hash = extractPromptsHash(agentsCli);
expect(hash).not.toBeNull();
expect(hash).toMatch(/^[0-9a-f]{12}$/);
@@ -441,7 +500,7 @@ describe("prompts freshness — hash marker", () => {
// writeAiGuidanceFiles defaults nonDottedPaths to `false` (matching
// core/conf.ts's missing-key default).
await writeAiGuidanceFiles({ targetDir: tempDir });
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
expect(extractPromptsHash(agentsCli)).toBe(currentPromptsHash(false));
});
});
@@ -516,9 +575,9 @@ describe("prompts freshness — additional invariants", () => {
test("warnIfPromptsStale writes to stderr (never stdout)", async () => {
await withTempDir(async (tempDir) => {
// Write a tampered AGENTS.cli.md so the freshness check trips.
// Write a tampered AGENTS.wmill.md so the freshness check trips.
await writeFile(
join(tempDir, "AGENTS.cli.md"),
join(tempDir, "AGENTS.wmill.md"),
"# Windmill CLI Agent Instructions\n<!-- wmill-prompts-hash: 000000000000 -->\nbody\n",
"utf8"
);
@@ -430,12 +430,12 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)
**Subcommands:**
- `refresh prompts` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh prompts` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh tsconfig` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- `--yes` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
@@ -726,7 +726,8 @@ workspace related commands
- `--create-workspace-name <workspace_name:string>` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
- `--color <color:string>` - Workspace color (hex code, e.g. #ff0000)
- `--datatable-behavior <behavior:string>` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip')
- `--from-branch <branch:string>` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
- `workspace delete-fork <fork_name:string>` - Delete a forked workspace and git branch
- `-y --yes` - Skip confirmation prompt
- `workspace merge` - Compare and deploy changes between a fork and its parent workspace
+18 -7
View File
@@ -2981,12 +2981,12 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)
**Subcommands:**
- \`refresh prompts\` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh prompts\` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh tsconfig\` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- \`--yes\` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
@@ -3277,7 +3277,8 @@ workspace related commands
- \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
- \`--color <color:string>\` - Workspace color (hex code, e.g. #ff0000)
- \`--datatable-behavior <behavior:string>\` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip')
- \`--from-branch <branch:string>\` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace and git branch
- \`-y --yes\` - Skip confirmation prompt
- \`workspace merge\` - Compare and deploy changes between a fork and its parent workspace
@@ -3451,6 +3452,10 @@ import Stripe from "stripe";
import { someFunction } from "some-package";
\`\`\`
## Prefer \`//native\` when the runtime allows it
If a script only needs \`fetch\` and the JavaScript standard library — including when it uses \`windmill-client\` — prefer making it a **native** script: add \`//native\` as the first line and write it with the \`write-script-bunnative\` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. \`windmill-client\` works on the native worker (its calls go over \`fetch\`), so needing the Windmill client is **not** a reason to avoid \`//native\`. Use the regular \`bun\` language only when the code (or a dependency) needs Node/Bun runtime APIs — \`node:*\` modules, the filesystem, child processes, or native addons.
## Windmill Client
Import the windmill client for platform interactions:
@@ -3459,7 +3464,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
\`\`\`
See the SDK documentation for available methods.
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to \`fetch\`.
## Preprocessor Scripts
@@ -3575,7 +3582,9 @@ export async function main(url: string) {
## Windmill Client
\`windmill-client\` is available for Windmill-specific primitives such as the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). Use \`fetch\` for plain HTTP.
\`windmill-client\` works on the native worker (its calls go over \`fetch\`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). It handles auth, the workspace, and the base URL for you. Reserve raw \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a \`fetch\` against the Windmill API.
## Preprocessor Scripts
@@ -3740,7 +3749,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
\`\`\`
See the SDK documentation for available methods.
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to \`fetch\`.
## Preprocessor Scripts
+13 -3
View File
@@ -162,6 +162,10 @@ import Stripe from "stripe";
import { someFunction } from "some-package";
```
## Prefer `//native` when the runtime allows it
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
## Windmill Client
Import the windmill client for platform interactions:
@@ -170,7 +174,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
## Preprocessor Scripts
@@ -286,7 +292,9 @@ export async function main(url: string) {
## Windmill Client
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
## Preprocessor Scripts
@@ -451,7 +459,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
## Preprocessor Scripts
@@ -435,12 +435,12 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)
**Subcommands:**
- `refresh prompts` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh prompts` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh tsconfig` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- `--yes` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
@@ -731,7 +731,8 @@ workspace related commands
- `--create-workspace-name <workspace_name:string>` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
- `--color <color:string>` - Workspace color (hex code, e.g. #ff0000)
- `--datatable-behavior <behavior:string>` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip')
- `--from-branch <branch:string>` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
- `workspace delete-fork <fork_name:string>` - Delete a forked workspace and git branch
- `-y --yes` - Skip confirmation prompt
- `workspace merge` - Compare and deploy changes between a fork and its parent workspace
@@ -126,7 +126,7 @@ The runnable ID is the filename without extension. For example, `get_user.ts` cr
| C# | `.cs` | `myFunc.cs` |
| Java | `.java` | `myFunc.java` |
After creating a runnable, tell the user they can generate lock files by running:
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
```bash
wmill generate-metadata
```
@@ -44,7 +44,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
## CLI Commands — running, previewing, deploying
After writing, tell the user which command fits what they want to do:
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (`wmill flow preview` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (`wmill sync push`, `wmill generate-metadata`) so the user can approve them. The options:
- `wmill flow preview <flow_path>`**default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
@@ -78,6 +78,10 @@ import Stripe from "stripe";
import { someFunction } from "some-package";
```
## Prefer `//native` when the runtime allows it
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
## Windmill Client
Import the windmill client for platform interactions:
@@ -86,7 +90,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
## Preprocessor Scripts
@@ -87,7 +87,9 @@ export async function main(url: string) {
## Windmill Client
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
## Preprocessor Scripts
@@ -90,7 +90,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
## Preprocessor Scripts
+1 -1
View File
@@ -39,7 +39,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
## CLI Commands — running, previewing, deploying
After writing, tell the user which command fits what they want to do:
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (`wmill flow preview` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (`wmill sync push`, `wmill generate-metadata`) so the user can approve them. The options:
- `wmill flow preview <flow_path>`**default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
+1 -1
View File
@@ -121,7 +121,7 @@ The runnable ID is the filename without extension. For example, `get_user.ts` cr
| C# | `.cs` | `myFunc.cs` |
| Java | `.java` | `myFunc.java` |
After creating a runnable, tell the user they can generate lock files by running:
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
```bash
wmill generate-metadata
```
+4 -4
View File
@@ -1827,7 +1827,7 @@ CONTEXT7_REPO_NAME = "windmill-cli-docs"
def extract_agents_md_template() -> str:
"""Extract the AGENTS.cli.md template string from cli/src/guidance/core.ts.
"""Extract the AGENTS.wmill.md template string from cli/src/guidance/core.ts.
Keeping a single source of truth in TypeScript avoids drift between what
`wmill init` writes locally and what we publish for context7 ingestion.
@@ -1844,7 +1844,7 @@ def extract_agents_md_template() -> str:
)
if not match:
raise RuntimeError(
f"Could not extract AGENTS.cli.md template from {core_ts_path}"
f"Could not extract AGENTS.wmill.md template from {core_ts_path}"
)
return _unescape_ts_template_literal(match.group(1))
@@ -1866,7 +1866,7 @@ def _unescape_ts_template_literal(raw: str) -> str:
def render_agents_md_for_docs(
skills: list[str], skill_desc_map: dict[str, str]
) -> str:
"""Render AGENTS.cli.md exactly as `wmill init` would, for the docs repo.
"""Render AGENTS.wmill.md exactly as `wmill init` would, for the docs repo.
The skill reference paths point at `.agents/skills/` (the canonical tree
that Codex/Pi read directly and that Claude Code mirrors under
@@ -2010,7 +2010,7 @@ def generate_context7_repo(
skill_desc_map = build_skill_desc_map(skills)
# AGENTS.md — the managed CLI guidance (what `wmill init` writes as
# AGENTS.cli.md locally). Kept under the `AGENTS.md` filename here to
# AGENTS.wmill.md locally). Kept under the `AGENTS.md` filename here to
# preserve the existing context7 ingest path; docs consumers read this
# as the canonical AGENTS file.
(target_dir / "AGENTS.md").write_text(
+7 -1
View File
@@ -38,6 +38,10 @@ import Stripe from "stripe";
import { someFunction } from "some-package";
```
## Prefer `//native` when the runtime allows it
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
## Windmill Client
Import the windmill client for platform interactions:
@@ -46,7 +50,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
## Preprocessor Scripts
+3 -1
View File
@@ -47,7 +47,9 @@ export async function main(url: string) {
## Windmill Client
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
## Preprocessor Scripts
+3 -1
View File
@@ -50,7 +50,9 @@ Import the windmill client for platform interactions:
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
## Preprocessor Scripts