Enforce the ask_instruction sentence-initial contract

Review on #576 caught document.md:71 splicing {{ask_instruction}} after
"then", which is the same defect this branch set out to fix. Rendered for
Codex it produced "Show the user the existing file, then STOP and use Codex's
structured user-input/question tool...". The line now starts a new sentence.

The comment added to PROVIDER_PLACEHOLDERS asserted the contract without
enforcing it, which is exactly how four reference files shipped the splice in
the first place. validateAskInstructionSites() in build.js now checks every
call site and fails the build on a mid-sentence interpolation, and the comment
points at the gate instead of asking authors to remember.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-13 13:24:29 -04:00
co-authored by Claude Opus 5
parent 0e5c6cbe17
commit d4e1b0902f
3 changed files with 61 additions and 4 deletions
+56 -1
View File
@@ -371,6 +371,57 @@ function validateSkillProse(rootDir) {
return errors;
}
/**
* Validate that every `{{ask_instruction}}` interpolation starts a sentence.
*
* The placeholder's per-provider values are complete capitalized sentences
* ("STOP and call the AskUserQuestion tool to clarify."), so a call site that
* splices it mid-sentence ships malformed guidance to every provider at once:
* `stop and STOP and call the AskUserQuestion tool to clarify. before expanding
* it`. Four reference files shipped exactly that before this gate existed, and
* a comment in PROVIDER_PLACEHOLDERS asking authors to keep the contract is
* what failed to prevent it.
*
* Returns the number of validation errors. Build fails if > 0.
*/
function validateAskInstructionSites(rootDir) {
const dir = path.join(rootDir, 'skill', 'reference');
const token = '{{ask_instruction}}';
let errors = 0;
let sites = 0;
if (!fs.existsSync(dir)) return 0;
for (const file of fs.readdirSync(dir)) {
if (path.extname(file) !== '.md') continue;
const rel = path.join('skill/reference', file);
fs.readFileSync(path.join(dir, file), 'utf-8')
.split('\n')
.forEach((line, i) => {
let idx = line.indexOf(token);
while (idx !== -1) {
sites++;
// Bold/italic markers may sit between the punctuation and the token.
const before = line.slice(0, idx).replace(/[*_`]+\s*$/, '').trimEnd();
if (before !== '' && !/[.!?:]$/.test(before)) {
console.error(`${rel}:${i + 1}: ${token} is spliced mid-sentence`);
console.error(` ...${before.slice(-60)} ${token}`);
console.error(` Provider values are full sentences. Start a new one.`);
errors++;
}
idx = line.indexOf(token, idx + 1);
}
});
}
if (errors === 0) {
console.log(`✓ ask_instruction call sites: ${sites} sentence-initial`);
} else {
console.error(`\n${errors} of ${sites} {{ask_instruction}} site(s) spliced mid-sentence.`);
}
return errors;
}
/**
* Validate that every hand-authored HTML page carries the shared site header.
* The partial is stamped with `<!-- site-header v1 -->` so drift is loud.
@@ -738,7 +789,11 @@ async function build() {
// that has no technical reading. Hardening repetition is intentionally allowed.
const skillProseErrors = validateSkillProse(ROOT_DIR);
if (countErrors > 0 || versionErrors > 0 || manifestShapeErrors > 0 || proseErrors > 0 || skillProseErrors > 0) {
// Placeholder values are full sentences; a mid-sentence splice ships broken
// guidance to every provider at once.
const askSiteErrors = validateAskInstructionSites(ROOT_DIR);
if (countErrors > 0 || versionErrors > 0 || manifestShapeErrors > 0 || proseErrors > 0 || skillProseErrors > 0 || askSiteErrors > 0) {
process.exit(1);
}
+4 -2
View File
@@ -477,8 +477,10 @@ export const PROVIDER_PLACEHOLDERS = {
'codex': {
model: 'GPT',
config_file: 'AGENTS.md',
// Every {{ask_instruction}} call site is sentence-initial, so each value is a
// complete capitalized sentence. Keep it that way when adding a provider.
// Each value is a complete capitalized sentence, because every
// {{ask_instruction}} call site is sentence-initial. That is enforced by
// validateAskInstructionSites() in scripts/build.js, not left to authors:
// four reference files had already spliced the placeholder mid-sentence.
ask_instruction: "STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.",
command_prefix: '$'
},