Skip to content

Commit f7ebb68

Browse files
johnlindquistclaude
andcommitted
feat(guardrails): enforce _max_prompt_tokens; adhoc engines match the ladder
Both found by executing the field guide's user stories against the CLI: _max_prompt_tokens was declared in types and recommended by docs but enforced nowhere. It now blocks execution before any engine turn when the fully resolved prompt exceeds the limit, with a PROMPT_TOKEN_LIMIT error pointing at --_context. Ad-hoc md.COMMAND aliases were limited to a stale v2 list (no pi, cursor-agent, agy, and no custom engines). Detection now accepts any registered adapter or PATH binary, mirroring the filename rung, and the name pattern allows hyphens (md.cursor-agent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 901ca34 commit f7ebb68

2 files changed

Lines changed: 54 additions & 8 deletions

File tree

src/adhoc-command.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,36 @@
1212
*/
1313

1414
import { basename } from "path";
15+
import { hasAdapter } from "./adapters";
1516

16-
/** Supported command aliases */
17+
/**
18+
* Built-in engines always accepted as md.COMMAND aliases. Beyond this list,
19+
* any registered adapter or PATH binary works, mirroring the filename rung
20+
* of the engine ladder (md.echo, md.pi, md.agy all resolve).
21+
*/
1722
export const SUPPORTED_COMMANDS = [
1823
"claude",
1924
"codex",
2025
"gemini",
2126
"copilot",
2227
"droid",
2328
"opencode",
29+
"pi",
30+
"cursor-agent",
31+
"agy",
2432
] as const;
2533

26-
export type SupportedCommand = typeof SUPPORTED_COMMANDS[number];
34+
export type SupportedCommand = string;
35+
36+
function isRunnableEngine(cmd: string): boolean {
37+
if ((SUPPORTED_COMMANDS as readonly string[]).includes(cmd)) return true;
38+
if (hasAdapter(cmd)) return true;
39+
try {
40+
return Bun.which(cmd) !== null;
41+
} catch {
42+
return false;
43+
}
44+
}
2745

2846
/**
2947
* Result of parsing an ad-hoc command invocation
@@ -85,19 +103,19 @@ function parseAdhocFromName(name: string): { isAdhoc: boolean; command?: Support
85103
const cleanName = name.replace(/\.(ts|js|mjs|cjs)$/, "");
86104

87105
// Pattern: md.i.COMMAND (interactive mode)
88-
const interactiveMatch = cleanName.match(/^md\.i\.([a-z]+)$/i);
106+
const interactiveMatch = cleanName.match(/^md\.i\.([a-z][a-z0-9-]*)$/i);
89107
if (interactiveMatch) {
90-
const cmd = interactiveMatch[1]?.toLowerCase() as SupportedCommand;
91-
if (SUPPORTED_COMMANDS.includes(cmd)) {
108+
const cmd = interactiveMatch[1]!.toLowerCase();
109+
if (isRunnableEngine(cmd)) {
92110
return { isAdhoc: true, command: cmd, interactive: true };
93111
}
94112
}
95113

96114
// Pattern: md.COMMAND
97-
const match = cleanName.match(/^md\.([a-z]+)$/i);
115+
const match = cleanName.match(/^md\.([a-z][a-z0-9-]*)$/i);
98116
if (match) {
99-
const cmd = match[1]?.toLowerCase() as SupportedCommand;
100-
if (SUPPORTED_COMMANDS.includes(cmd)) {
117+
const cmd = match[1]!.toLowerCase();
118+
if (isRunnableEngine(cmd)) {
101119
return { isAdhoc: true, command: cmd, interactive: false };
102120
}
103121
}

src/cli-runner.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,20 @@ export class CliRunner {
696696
}
697697

698698
// Edit before execute
699+
// Hard prompt budget: _max_prompt_tokens blocks execution BEFORE any
700+
// engine turn is spent when the fully resolved prompt exceeds the limit.
701+
const maxPromptTokens = frontmatter._max_prompt_tokens;
702+
if (typeof maxPromptTokens === "number" && maxPromptTokens > 0) {
703+
const promptTokens = await countTokensAsync(finalBody);
704+
if (promptTokens > maxPromptTokens) {
705+
throw new MarkdownAgentError(
706+
`Prompt is ~${promptTokens.toLocaleString()} tokens, over the _max_prompt_tokens limit of ${maxPromptTokens.toLocaleString()}. ` +
707+
`Narrow the imports, raise the limit, or inspect with --_context.`,
708+
{ errorCode: "PROMPT_TOKEN_LIMIT", exitCode: 1 }
709+
);
710+
}
711+
}
712+
699713
let promptToRun = finalBody;
700714
if (parsed.editFlag && !parsed.jsonMode) {
701715
const editResult = await editPrompt(finalBody);
@@ -982,6 +996,20 @@ export class CliRunner {
982996
}
983997

984998
// Edit before execute
999+
// Hard prompt budget: _max_prompt_tokens blocks execution BEFORE any
1000+
// engine turn is spent when the fully resolved prompt exceeds the limit.
1001+
const maxPromptTokens = frontmatter._max_prompt_tokens;
1002+
if (typeof maxPromptTokens === "number" && maxPromptTokens > 0) {
1003+
const promptTokens = await countTokensAsync(finalBody);
1004+
if (promptTokens > maxPromptTokens) {
1005+
throw new MarkdownAgentError(
1006+
`Prompt is ~${promptTokens.toLocaleString()} tokens, over the _max_prompt_tokens limit of ${maxPromptTokens.toLocaleString()}. ` +
1007+
`Narrow the imports, raise the limit, or inspect with --_context.`,
1008+
{ errorCode: "PROMPT_TOKEN_LIMIT", exitCode: 1 }
1009+
);
1010+
}
1011+
}
1012+
9851013
let promptToRun = finalBody;
9861014
if (parsed.editFlag && !parsed.jsonMode) {
9871015
const editResult = await editPrompt(finalBody);

0 commit comments

Comments
 (0)