Skip to content

Commit 817f3cb

Browse files
committed
web: the playbooks say what is true in a tab
Each SKILL.md in the browser build gets one preamble: the shell is a simulated bash with no git, npm, node, python or test runner and cannot run the code, so the steps that say to run them are skipped, no result is claimed for them, and the summary says which checks were not run. The steps and the menu entries are unchanged; the desktop's files are untouched. Also: the verify.command example means what it says (`! grep -rn TODO src`).
1 parent 4e3cdce commit 817f3cb

4 files changed

Lines changed: 94 additions & 1 deletion

File tree

‎web/ai-extension/build.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ if (!nodePaths.some((p) => fs.existsSync(path.join(p, 'buffer')))) { console.err
3838

3939
const { applyCopy } = createRequire(import.meta.url)('./copy.js');
4040
const { skinChat } = createRequire(import.meta.url)('./skin.js');
41+
const { browserSkill } = createRequire(import.meta.url)('./skills.js');
4142

4243
const outDir = path.join(outRoot, 'levelcode-ai');
4344
fs.rmSync(outDir, { recursive: true, force: true });
@@ -56,6 +57,8 @@ function collect(rel, filter) {
5657
let text = TEXT.has(path.extname(r)) ? applyCopy(r, buf.toString('utf8')) : null;
5758
// The browser's look for the chat goes into this embedded copy and nowhere else (skin.js).
5859
if (text !== null && r === 'media/chat.html') { text = skinChat(text); }
60+
// The playbooks assume git and a test runner; the browser's shell has neither (skills.js).
61+
if (text !== null) { text = browserSkill(r, text); }
5962
out[r] = text !== null ? text : { base64: buf.toString('base64') };
6063
}
6164
};

‎web/ai-extension/manifest.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const DESCRIPTION_OVERRIDES = {
6565
description: 'Backstop timeout in milliseconds before a command in LevelCode Shell is stopped. The Stop button on the command\'s card is the primary control; set 0 to disable the backstop.',
6666
},
6767
'levelcode.ai.verify.command': {
68-
markdownDescription: 'Optional one-shot command run in **LevelCode Shell** to verify the agent\'s edits (e.g. `grep -rn TODO src && exit 1`, or `sh check.sh` for a script of your own). It must exit on its own. Leave empty to verify with editor diagnostics only.',
68+
markdownDescription: 'Optional one-shot command run in **LevelCode Shell** to verify the agent\'s edits (e.g. `! grep -rn TODO src`, which fails while any TODO is left, or `sh check.sh` for a script of your own). It must exit on its own. Leave empty to verify with editor diagnostics only.',
6969
},
7070
'levelcode.ai.agent.autopilot': {
7171
description: 'Autopilot: let the agent run commands without asking, and prefer verifying its own work over pausing to ask you. It still stops and asks before deleting files. File edits stay reviewable with Keep/Undo either way. Off by default.',

‎web/ai-extension/skills.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// The playbooks (extensions/levelcode-ai/skills/*/SKILL.md) as the browser build ships them.
2+
//
3+
// They were written for a machine with git, a test runner and a build: "run git diff", "npm test", "the suite is
4+
// green". In a tab the shell is a simulated bash over the workspace's files, and a model that follows those steps
5+
// literally either burns turns on commands that answer "not found" or — worse — ends with "all tests passing" for
6+
// tests that never ran. One preamble at the top of each body says what is true here; the steps stay as written and
7+
// the desktop's files are untouched (this is applied to the embedded copy, like copy.js and skin.js).
8+
//
9+
// Plain CommonJS; web/test/unit/skills.test.js pins it.
10+
'use strict';
11+
12+
const PREAMBLE = [
13+
'> **BROWSER EDITION.** The shell here is a simulated bash over the workspace files: no git, npm, node, python, pytest or any test runner,',
14+
'> and it cannot run your code. Where the steps below say to run git, tests, a linter, a build or a script: skip that step, do not try it, and do not',
15+
'> claim a result for it. Use read_file, search and the editor\'s diagnostics instead, and end your summary by saying which checks were not run and the',
16+
'> exact command the user can run on their own computer. Never write "passing", "green" or "builds" unless a command you ran in this shell, or the',
17+
'> editor\'s diagnostics, actually showed it.',
18+
'',
19+
].join('\n');
20+
21+
const FRONT = /^---\r?\n[\s\S]*?\r?\n---\r?\n/;
22+
23+
/**
24+
* @param {string} rel path relative to extensions/levelcode-ai, forward slashes
25+
* @param {string} text
26+
* @returns {string}
27+
*/
28+
function browserSkill(rel, text) {
29+
if (!/^skills\/[^/]+\/SKILL\.md$/.test(rel)) { return text; }
30+
const m = FRONT.exec(text);
31+
// The front matter (name, description) is the skill's menu entry and must stay the first thing in the file.
32+
if (!m) { return PREAMBLE + '\n' + text; }
33+
return text.slice(0, m[0].length) + '\n' + PREAMBLE + text.slice(m[0].length);
34+
}
35+
36+
module.exports = { browserSkill, PREAMBLE };

‎web/test/unit/skills.test.js‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* The playbooks in the browser build — run: node web/test/unit/skills.test.js
3+
*
4+
* Each SKILL.md gets one preamble that says what is true in a tab; the desktop's files are never edited.
5+
*--------------------------------------------------------------------------------------------*/
6+
// @ts-check
7+
'use strict';
8+
9+
const assert = require('assert');
10+
const fs = require('fs');
11+
const path = require('path');
12+
const { browserSkill, PREAMBLE } = require('../../ai-extension/skills.js');
13+
14+
const SKILLS = path.join(__dirname, '..', '..', '..', 'extensions', 'levelcode-ai', 'skills');
15+
let n = 0;
16+
function test(name, fn) { fn(); n++; console.log(' ok - ' + name); }
17+
18+
const names = fs.readdirSync(SKILLS).filter((d) => fs.existsSync(path.join(SKILLS, d, 'SKILL.md')));
19+
20+
test('there are skills to treat, and each keeps its front matter as the first lines (that is the menu entry)', () => {
21+
assert.ok(names.length >= 3);
22+
for (const name of names) {
23+
const src = fs.readFileSync(path.join(SKILLS, name, 'SKILL.md'), 'utf8');
24+
const out = browserSkill('skills/' + name + '/SKILL.md', src);
25+
const front = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(src)[0];
26+
assert.ok(out.startsWith(front), name + ': the front matter moved');
27+
assert.ok(out.includes(PREAMBLE), name + ': no preamble');
28+
assert.strictEqual(out.split(PREAMBLE).length - 1, 1, name + ': more than one preamble');
29+
assert.ok(out.endsWith(src.slice(front.length)), name + ': the steps changed');
30+
}
31+
});
32+
33+
test('the preamble forbids claiming a result that never ran, and names what is missing', () => {
34+
const flat = PREAMBLE.replace(/\n> /g, ' ');
35+
for (const word of ['git', 'npm', 'node', 'python', 'test runner', 'do not claim a result', 'Never write "passing"']) {
36+
assert.ok(flat.includes(word), 'the preamble does not say: ' + word);
37+
}
38+
assert.ok(!/hackable/i.test(PREAMBLE));
39+
});
40+
41+
test('only SKILL.md bodies are touched; a text without front matter gets the preamble on top', () => {
42+
assert.strictEqual(browserSkill('media/chat.html', 'x'), 'x');
43+
assert.strictEqual(browserSkill('skills/a/README.md', 'x'), 'x');
44+
assert.ok(browserSkill('skills/a/SKILL.md', 'no front matter').startsWith(PREAMBLE));
45+
});
46+
47+
test('the desktop sources are not modified by it', () => {
48+
for (const name of names) {
49+
const src = fs.readFileSync(path.join(SKILLS, name, 'SKILL.md'), 'utf8');
50+
assert.ok(!src.includes('BROWSER EDITION'), name);
51+
}
52+
});
53+
54+
console.log(`\n${n} tests passed`);

0 commit comments

Comments
 (0)