e56ddd6a1b
Backlog item G. The `coverage:` line under-reported a skill chosen in a PRIOR turn: enforce-coverage-verify credited channel=skill only if the Skill tool ran in the CURRENT turn, so an honest `skill:X` continuation line was BLOCKED -> the controller learned to under-report as direct/chain. Two-sided systemic fix, no weakening: - enforce-coverage-verify: decide() also accepts skill:X when X was invoked anywhere earlier in THIS session (new priorSkillNames param; main() collects them via sessionToolUses). Still unforgeable -- a real Skill tool_use must exist in the transcript. The only residual is possibly-stale attribution, far better than the forced dishonest direct-reporting it replaces. - enforce-prompt-injection: the §17 reminder now lists active skills carried over from earlier turns (read from the transcript) and tells the controller to report `coverage: skill:<name>` when work continues under one -- the proactive half, so the correct line is not merely allowed but prompted. TDD: RED -> GREEN per behavior. tools-vitest 2032 passed / 2 skipped. Plan docs/superpowers/plans/2026-05-31-discipline-guard-backlog.md (item G). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
4.3 KiB
JavaScript
112 lines
4.3 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { decide } from './enforce-coverage-verify.mjs';
|
|
|
|
// Cross-turn skill credit (backlog item G, 2026-05-31): a skill chosen in a PRIOR
|
|
// turn stays active; an honest `skill:X` line on a continuation turn must NOT be
|
|
// blocked just because the Skill tool was not re-invoked this turn. decide() takes
|
|
// priorSkillNames (real Skill tool_uses from earlier in the session transcript).
|
|
describe('enforce-coverage-verify / decide — cross-turn active skill (enforce-coverage-verify.mjs)', () => {
|
|
it('credits skill:X when X was invoked in a PRIOR turn (priorSkillNames)', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development\nработаю',
|
|
priorSkillNames: ['superpowers:test-driven-development'],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('normalizes the superpowers: prefix for prior-turn skills too', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development',
|
|
priorSkillNames: ['test-driven-development'],
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('still blocks skill:X when X is neither in this turn nor any prior turn', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development',
|
|
priorSkillNames: ['some-other-skill'],
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/never invoked/);
|
|
});
|
|
});
|
|
|
|
describe('enforce-coverage-verify / decide', () => {
|
|
it('allows turn with no mutating tools (pure conversational)', () => {
|
|
const r = decide({ toolUses: [{ name: 'Read', input: {} }], assistantText: 'just talking' });
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('blocks mutating turn with no coverage line', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'just did some work',
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/no.*coverage/);
|
|
// 1A (2026-05-31): не рекламировать мёртвые override-фразы (findOverride — заглушка v4).
|
|
expect(r.message).not.toMatch(/Override:/);
|
|
expect(r.message).not.toMatch(/без скилов|direct ok/);
|
|
});
|
|
|
|
it('blocks when coverage says skill but Skill tool not invoked', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development\nдалее…',
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/Skill tool was never invoked/);
|
|
});
|
|
|
|
it('allows when coverage says skill and Skill tool invoked with matching name', () => {
|
|
const r = decide({
|
|
toolUses: [
|
|
{ name: 'Skill', input: { skill: 'superpowers:test-driven-development' } },
|
|
{ name: 'Edit', input: { file_path: 'foo.mjs' } },
|
|
],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development\nок',
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows when coverage matches without superpowers: prefix in tool input', () => {
|
|
const r = decide({
|
|
toolUses: [
|
|
{ name: 'Skill', input: { skill: 'test-driven-development' } },
|
|
{ name: 'Edit', input: { file_path: 'foo.mjs' } },
|
|
],
|
|
assistantText: 'coverage: skill:superpowers:test-driven-development',
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows direct coverage', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'memory/foo.md' } }],
|
|
assistantText: 'coverage: direct:memory-sync',
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows node coverage', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.vue' } }],
|
|
assistantText: 'coverage: node:#19',
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows when override phrase present', () => {
|
|
const r = decide({
|
|
toolUses: [{ name: 'Edit', input: { file_path: 'foo.mjs' } }],
|
|
assistantText: 'no coverage',
|
|
override: { phrase: 'без скилов', suppresses: ['coverage-skill-match'] },
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
});
|