diff --git a/static/commands.js b/static/commands.js index 756dcd55..f1b197c6 100644 --- a/static/commands.js +++ b/static/commands.js @@ -949,8 +949,11 @@ async function cmdUse(args){ } return; } + const detail = await api(`/api/skills/content?name=${encodeURIComponent(match.name)}`); + const skillContent = detail&&typeof detail.content==='string' ? detail.content.trim() : ''; + if(!skillContent) throw new Error(`Skill \`${match.name}\` has no readable content.`); const directive = `[USER OVERRIDE] You MUST consult skill '${match.name}' via skill_view before responding to the next message.`; - resolve(directive); + resolve({name:match.name,directive,content:skillContent}); if(isCurrentSession()){ S.messages.push({role:'assistant', content:`Next turn: skill \`${match.name}\` will be forced.`}); renderMessages(); diff --git a/static/messages.js b/static/messages.js index c9973fb8..320287eb 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1063,10 +1063,22 @@ async function send(){ if(_forcedSkillDirectivePending){ const _pending=_forcedSkillDirectivePending; if(!_pending.sessionId||_pending.sessionId===activeSid){ - const _directive = await _pending.promise; + const _directivePayload = await _pending.promise; if(_forcedSkillDirectivePending===_pending)_forcedSkillDirectivePending = null; - if(typeof _directive==='string'&&_directive){ - msgText=`${_directive}\n\n${msgText||''}`.trim(); + if(_directivePayload){ + const _directive = typeof _directivePayload==='string' + ? _directivePayload + : String(_directivePayload.directive||'').trim(); + const _forcedSkillName = typeof _directivePayload==='string' + ? '' + : String(_directivePayload.name||'').trim(); + const _forcedSkillContent = typeof _directivePayload==='string' + ? '' + : String(_directivePayload.content||'').trim(); + const _forcedSkillBlock = _forcedSkillName&&_forcedSkillContent + ? `[FORCED SKILL CONTEXT: ${_forcedSkillName}]\n${_forcedSkillContent}\n[/FORCED SKILL CONTEXT]` + : ''; + msgText=`${_directive}${_forcedSkillBlock?`\n\n${_forcedSkillBlock}`:''}\n\n${msgText||''}`.trim(); } } } diff --git a/tests/test_issue2977_use_command.py b/tests/test_issue2977_use_command.py index 6e32823d..12818351 100644 --- a/tests/test_issue2977_use_command.py +++ b/tests/test_issue2977_use_command.py @@ -34,6 +34,8 @@ def test_forced_skill_directive_set_in_cmdUse(): src = read("static/commands.js") assert "pending.promise = new Promise" in src, "cmdUse must create a pending Promise" assert "_forcedSkillDirectivePending = pending;" in src, "cmdUse must publish the pending directive before awaiting" + assert "resolve({name:match.name,directive,content:skillContent});" in src, \ + "cmdUse must resolve the pending payload with skill name, directive, and fetched content" def test_use_entry_has_noEcho(): @@ -59,7 +61,7 @@ def test_directive_consumed_at_injection_site(): finally_part = src.split("finally")[1] if "finally" in src else "" assert "_forcedSkillDirectivePending = null;" not in finally_part, \ "_forcedSkillDirectivePending must NOT be cleared in the finally block" - assert "const _directive = await _pending.promise;" in src, \ + assert "const _directivePayload = await _pending.promise;" in src, \ "consume site must await the pending promise" assert "_forcedSkillDirectivePending = null;" in src, \ "_forcedSkillDirectivePending must be cleared somewhere in messages.js" @@ -78,6 +80,14 @@ def test_directive_text_uses_match_name(): assert "[USER OVERRIDE] You MUST consult skill '" in src, "directive text must match the specified format" +def test_use_fetches_canonical_skill_content(): + src = read("static/commands.js") + assert "api(`/api/skills/content?name=${encodeURIComponent(match.name)}`)" in src, \ + "cmdUse must fetch the canonical skill content after resolving the canonical skill name" + assert "typeof detail.content==='string' ? detail.content.trim() : ''" in src, \ + "cmdUse must reject missing or non-string skill content" + + def test_pending_promise_set_synchronously(): """_forcedSkillDirectivePending must be set before the first await in cmdUse.""" src = read("static/commands.js") @@ -114,3 +124,5 @@ def test_directive_only_consumed_by_matching_session(): "send() must only consume /use directives issued for the active session" assert "if(_forcedSkillDirectivePending===_pending)_forcedSkillDirectivePending = null;" in src, \ "send() must not clear a newer pending directive created while awaiting" + assert "[FORCED SKILL CONTEXT: ${_forcedSkillName}]" in src, \ + "send() must prepend deterministic forced-skill content before the user message"