aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-27 14:59:06 +0100
committerAgando <roduccomm@gmail.com>2026-10-27 14:59:06 +0100
commit6f2255946a834f2458613450533cd2906eb31e7d (patch)
tree85677c588783e030606c4f82cc99b6d2ce138d75 /modules/ext
parentd2ec6fe99cc6f79013e689732fdc7f77089a8ed7 (diff)
fix(newos4): bound one turn's tool context, honest skill-create, quarantine a corrupt memory graph
The three items the last review and probes left open, each with a test that is red on HEAD. - MAINT-023 (module-aiassist.js): one agentic turn grew without limit — every role:'tool' body was pushed verbatim and only this.context was trimmed BETWEEN turns (measured 6 -> 112 705 tokens across 8 model calls against maxContextTokens 12 000). Observations are now clipped to maxToolResultChars (6 000) with a note saying how much was withheld and to re-read with offset/limit, and _boundToolContext() elides the oldest tool bodies once the running total passes toolContextBudgetChars (24 000), always keeping the newest 4 verbatim. The ceiling is turn-independent: the same ~26 k chars at 8 turns and at 20. - MAINT-024 (modules/ext/skill-pack-core.js): the command printed "✅ Skill created" with fs.writeFile()'s boolean discarded (probe: fresh VFS, success printed, no file), stripped invalid characters instead of rejecting the name (My-Skill -> y-kill, DevOps_Thing -> evpshing), and treated an unreadable /path argument as the skill body. Now: kebab-case is validated (uppercase normalises and the final name is reported; anything else is refused), /skills is created on first use, the write and index results are checked, and an unreadable body path is an error. - MAINT-003 (module-memory-graph.js): a corrupt store looked exactly like a fresh install — fsReadJSON returns null on a parse failure, so 85 nodes could vanish with no warning and no copy. The probe confirmed the loss: seed 6 nodes, truncate nodes.json and export.json, reload -> 1 node, no message, nothing in .trash. _quarantineCorrupt() now keeps the unreadable file as <name>.corrupt, warns, and then loads from whatever remains. Tests: test-tool-context-bound.js (11), test-skill-create.js (16), test-memory-graph-recovery.js (8). RED on HEAD: tool-context crashes on _clipToolResult (exit 1), skill-create 16 assertions fail on the missing file/index, memory-graph B1/B2/C1 fail (no warning, no .corrupt copy). Suite now 13/13 files green; maintain --fix reports "node tests 13/13 pass". Live on v4.6.107: skill-create My-Skill -> ✅ "my-skill"; DevOps_Thing -> Invalid skill name; /does/not/exist.md -> Cannot read skill body; only my-skill in the index.
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/skill-pack-core.js37
1 files changed, 29 insertions, 8 deletions
diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js
index c2c95ba..3d3acf7 100644
--- a/modules/ext/skill-pack-core.js
+++ b/modules/ext/skill-pack-core.js
@@ -729,17 +729,28 @@ after the behaviour itself was fixed in v4.6.58.`),
this.terminal.print('Usage: skill-create <name> <description> <path-or-text> [--tags=a,b] [--category=x]', 'warning');
return;
}
- const name = positional[0].replace(/[^a-z0-9-]/g, '').toLowerCase();
- if (!name) { this.terminal.print('Name must be kebab-case (letters, numbers, hyphens only)', 'error'); return; }
+ const rawName = positional[0];
+ const name = rawName.toLowerCase();
+ // MAINT-024: the old code stripped invalid characters instead of rejecting them, so
+ // "My-Skill" became "y-kill" and "DevOps_Thing" became "evpshing" — a skill created
+ // under a name nobody asked for, reported as success.
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
+ this.terminal.print(`Invalid skill name "${rawName}": use kebab-case (letters, numbers, single hyphens, e.g. my-skill)`, 'error');
+ return;
+ }
const desc = positional[1];
const bodyArg = positional.slice(2).join(' ');
const fs0 = this.terminal.fs;
let content = bodyArg;
if (bodyArg.startsWith('/')) {
- try {
- const fromFile = fs0.readFile(bodyArg);
- if (typeof fromFile === 'string' && fromFile.length) content = fromFile;
- } catch (e) { /* not a file: treat the argument as literal text */ }
+ // A path that cannot be read is an error, not a body: silently using the path text
+ // as the skill body produced a skill whose content was "/skills/nope.md".
+ const fromFile = fs0.readFile(bodyArg);
+ if (typeof fromFile !== 'string' || !fromFile.length) {
+ this.terminal.print(`Cannot read skill body: ${bodyArg} (missing or empty)`, 'error');
+ return;
+ }
+ content = fromFile;
}
const tags = flags.tags;
const fs = this.terminal.fs;
@@ -751,9 +762,19 @@ after the behaviour itself was fixed in v4.6.58.`),
}
const now = new Date().toISOString();
const skillContent = `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: ${flags.category}\n---\n${content}`;
- fs.writeFile(`/skills/${name}.md`, skillContent);
+ // MAINT-024: create /skills when this is the first skill, and do not print ✅ unless the
+ // file actually landed — the old code reported success on a failed write.
+ try { if (!fs.readDir('/skills')) fs.mkdirRecursive('/skills'); } catch (e) { /* write below reports it */ }
+ const wroteFile = fs.writeFile(`/skills/${name}.md`, skillContent);
+ if (!wroteFile) {
+ this.terminal.print(`Failed to write /skills/${name}.md — the skills directory is not writable`, 'error');
+ return;
+ }
index[name] = { name, description: desc, tags, category: flags.category, created: now, updated: now };
- fs.fsWriteJSON('/skills/index.json', index);
+ const wroteIndex = fs.fsWriteJSON('/skills/index.json', index);
+ if (wroteIndex === false) {
+ this.terminal.print(`⚠️ Skill "${name}" written, but /skills/index.json could not be updated — it may not appear in skill lists`, 'warning');
+ }
this.terminal.print(`✅ Skill "${name}" created (${(skillContent.length / 1024).toFixed(1)}KB)`, 'success');
}