feat(run): the run step names Claude Code's run; owner=run is a built-in owner (A2)
The coupling to Claude Code's built-in run skill (wave 4, order A2, decisions 54 and 64). The plan's "run the thing" step, the executor's End to end part and the plan template each say in one sentence that the app is seen and driven with run, and that the verdict is still the End-to-end: command, because run gives no exit code. The template quotes the name instead of backticking it: every backticked span on a template step bullet is read as a runnable example. R2: lib/skills/routing.mjs keeps a short fixed list of Claude Code's own skills a quiet case may name as owner (BUILTIN_OWNERS, only run, seen in 2.1.303). For such an owner only "not taken" is checked, and the report says so; ROUTING_NEGATIVE_WON stands, and an owner on no list is still ROUTING_UNKNOWN_OWNER. skill-writing-criteria-quiet-2 gets owner=run. The trekplan.md paragraph is reflowed to its old line count so the stats line_ref there does not move (order B edits the same file); the trekexecute Phase 9 line_ref fixture moves by two lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cf08df9d55
commit
1989656a9c
12 changed files with 141 additions and 22 deletions
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -22,7 +22,7 @@
|
|||
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1128-1152 | profile, phase_models, profile_source | none |
|
||||
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
|
||||
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1277-1305 | profile, phase_models, parallel_agents, profile_source | none |
|
||||
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2907-2929 | profile, phase_models, profile_source | none |
|
||||
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2909-2931 | profile, phase_models, profile_source | none |
|
||||
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
|
||||
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
|
||||
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:636-686 | profile, phase_models, profile_source | none |
|
||||
|
|
|
|||
59
tests/lib/run-coupling-prose-pins.test.mjs
Normal file
59
tests/lib/run-coupling-prose-pins.test.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// tests/lib/run-coupling-prose-pins.test.mjs
|
||||
// The coupling to Claude Code's built-in `run` skill (wave 4, order A2,
|
||||
// decisions 54 and 64). The plan's "run the thing" step, the executor's
|
||||
// `### End to end` and the plan template's run step each say in one sentence
|
||||
// that the app is seen and driven with `run`, and that the verdict is still
|
||||
// the `End-to-end:` command, because `run` gives no exit code. Each part is
|
||||
// read by its heading or marker, never by line number.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
|
||||
/** From the line holding `marker` to the first line where `stop` is true. */
|
||||
function partAt(rel, marker, stop) {
|
||||
const lines = read(rel).split('\n');
|
||||
const start = lines.findIndex((l) => l.includes(marker));
|
||||
assert.ok(start !== -1, `${rel} must hold "${marker}"`);
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) if (stop(lines[i])) { end = i; break; }
|
||||
return lines.slice(start, end).join(' ').replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const PARTS = [
|
||||
// An indented paragraph of a numbered list item: it ends at a blank or unindented line.
|
||||
['commands/trekplan.md', '**The "run the thing" step.**', (l) => l.trim() === '' || /^\S/.test(l)],
|
||||
['commands/trekexecute.md', '### End to end — "run the thing" (P9 (6))', (l) => l.startsWith('### ')],
|
||||
['templates/plan-template.md', '### Step 2: {description — the "run the thing" step}', (l) => l.startsWith('### ')],
|
||||
];
|
||||
|
||||
// The template quotes the names instead of backticking them: every backticked
|
||||
// span on a template step bullet is read as a runnable example
|
||||
// (tests/lib/skills-criteria-prose-pins.test.mjs), and `run` is no command.
|
||||
const RUN = '(?:`run`|"run")';
|
||||
for (const [rel, marker, stop] of PARTS) {
|
||||
test(`run coupling: ${rel} names Claude Code's run and keeps the verdict on the End-to-end: command`, () => {
|
||||
const p = partAt(rel, marker, stop);
|
||||
assert.match(p, new RegExp(`Claude Code's built-in ${RUN} skill`), `${rel}: the run step must name Claude Code's built-in run`);
|
||||
assert.match(p, /the verdict is still the `?End-to-end:`? command/, `${rel}: the verdict must stay the End-to-end: command`);
|
||||
assert.match(p, new RegExp(`${RUN} gives no exit code`), `${rel}: the reason must be said`);
|
||||
});
|
||||
}
|
||||
|
||||
test('run coupling: the run step still says what it runs, through the criteria runner', () => {
|
||||
const p = partAt(...PARTS[0]);
|
||||
assert.match(p, /criteria runner's allowlist/);
|
||||
assert.match(p, /End-to-end: \\`bash tests\/e2e\.sh\\`/);
|
||||
});
|
||||
|
||||
test('run coupling: the writing-criteria quiet case is owned by run, with no stand-in left in its description', () => {
|
||||
const md = read('evals/skill-writing-criteria-quiet-2/prompt.md');
|
||||
assert.match(md, /^tags: \[[^\]]*\bowner=run\b[^\]]*\]$/m);
|
||||
assert.doesNotMatch(md, /stands in/);
|
||||
assert.doesNotMatch(md, /owner=trekexecute/);
|
||||
});
|
||||
|
|
@ -174,6 +174,40 @@ test('routing: a negative must name an owner that exists and outranks the skill'
|
|||
});
|
||||
});
|
||||
|
||||
// ---- R2: an owner from Claude Code's own skills (wave 4, order A2) ----
|
||||
|
||||
const R2_SPEC = (prompt, owner = 'run') => ({
|
||||
skills: { 'tidy-imports': 'Sorts the import lines of a source file. Use when imports are out of order.' },
|
||||
commands: { 'rotate-logs': 'Compresses and deletes old log files on disk.' },
|
||||
cases: { n1: { tags: ['skill-no-trigger', 'skill=tidy-imports', `owner=${owner}`, 'from=test'], prompt } },
|
||||
});
|
||||
|
||||
test('routing (R2): owner=run, a built-in Claude Code skill, is no unknown owner, and the report says what was checked', () => {
|
||||
withPlugin(R2_SPEC('start the app and click through the new screen'), (root) => {
|
||||
const r = checkRouting(root);
|
||||
assert.ok(!r.errors.some((e) => e.case === 'n1'), JSON.stringify(r.errors));
|
||||
const note = (r.notes || []).find((n) => n.case === 'n1');
|
||||
assert.ok(note, JSON.stringify(r));
|
||||
assert.match(note.message, /owner outside the corpus: only "not taken" checked/);
|
||||
const out = spawnSync('node', [CLI, '--root', root], { encoding: 'utf8' });
|
||||
assert.match(out.stdout, /note ROUTING_OWNER_OUTSIDE_CORPUS: n1: owner=run [^\n]*owner outside the corpus: only "not taken" checked/, out.stdout);
|
||||
});
|
||||
});
|
||||
|
||||
test('routing (R2): a skill that ranks first is still red when the owner is run', () => {
|
||||
withPlugin(R2_SPEC('sort the import lines of the source file'), (root) => {
|
||||
const r = checkRouting(root);
|
||||
assert.ok(r.errors.some((e) => e.code === 'ROUTING_NEGATIVE_WON' && e.case === 'n1'), JSON.stringify(r.errors));
|
||||
});
|
||||
});
|
||||
|
||||
test('routing (R2): an owner neither in the corpus nor on the built-in list is still unknown', () => {
|
||||
withPlugin(R2_SPEC('start the app and click through the new screen', 'nosuchthing'), (root) => {
|
||||
const r = checkRouting(root);
|
||||
assert.ok(r.errors.some((e) => e.code === 'ROUTING_UNKNOWN_OWNER' && e.case === 'n1'), JSON.stringify(r.errors));
|
||||
});
|
||||
});
|
||||
|
||||
test('routing: every case names the event it came from (P10)', () => {
|
||||
withPlugin({
|
||||
skills: { 'tidy-imports': 'Sorts the import lines of a source file. Use when imports are out of order.' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue