Files
ECC/tests/docs/dev-team-skill.test.js
T
e990c0c7ed feat(skills): add dev-team skill — multi-persona collaborative session (#2309)
* feat(skills): add dev-team skill — multi-persona collaborative session

Adds skills/dev-team/SKILL.md, a community skill inspired by the
BMAD Method's "party mode": PM, Architect, Developer, and QA respond
to the same topic in parallel, then a synthesis step names tensions
explicitly instead of averaging them.

Reads PROJECT-CONTEXT.md from the repo root when present, and offers
to generate it when missing, folding in the closed project-context
skill's (#2310) generation workflow per affaan-m's review — that
skill's premise (every agent reads the file) wasn't implemented
anywhere, so the capability now lives directly in the one skill that
actually reads it.

Rebuilt on current upstream/main as a skill-only diff: the shared
format-code.ts Windows fix and github-coordination branch-coverage
tests that were previously bundled here (and duplicated across the
story-lifecycle and project-context sibling PRs) now live in #2459.

* fix(manifests): register dev-team skill in workflow-quality install module

* fix(docs): repair README lint errors and Windows hook-install path regression

Fixes CI inherited from the README 2.1 restructure (19b05476):
- MD058: blank lines around tables (delegation map, Codex role configs)
- MD001: Option A/B headings under Ecosystem Tools h2 jump to h4
- MD024: duplicate 'What's included' headings (Codex, Copilot sections)
- restore %USERPROFILE%\\.claude escaping required by
  tests/scripts/manual-hook-install-docs.test.js

* feat(skills): address review — trust boundary, harness-neutral I/O, contract test

Address maintainer review on #2309:
- untrusted-context boundary now travels with every persona prompt:
  inline label on the context section, personas marked analysis-only
  with no state-changing tool use
- personas receive a bounded declarative summary (≤150 words, fixed
  fields, secrets and imperative content stripped) — never the raw
  PROJECT-CONTEXT.md
- context loading uses harness-native file tools; POSIX-only
  'test -f && cat' removed
- all references resolve on main: story-lifecycle follow-up replaced
  with /plan and epic-* commands, ecc:plan-prd corrected to the
  /plan-prd command; boundary vs team-builder and council made explicit
- added tests/docs/dev-team-skill.test.js contract test (roles,
  parallel dispatch, synthesis guardrails, trust boundary, registration)

* docs: refresh Turkish skill count

* ci: retrigger checks (flaky stop-hooks-stdout timeout on macos node20 npm cell)

---------

Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
2026-08-11 12:17:03 -04:00

125 lines
5.4 KiB
JavaScript

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', '..');
const SKILL_PATH = path.join(ROOT, 'skills', 'dev-team', 'SKILL.md');
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing dev-team skill contract ===\n');
let passed = 0;
let failed = 0;
const body = fs.readFileSync(SKILL_PATH, 'utf8');
if (test('uses the canonical When to Activate header', () => {
assert.ok(body.includes('## When to Activate'), 'missing ## When to Activate');
})) passed++; else failed++;
if (test('defines all four preset roles with their lenses', () => {
for (const role of ['Product Manager', 'Architect', 'Developer', 'QA Engineer']) {
assert.ok(body.includes(role), `missing role: ${role}`);
}
for (const lens of ['user value', 'system design', 'implementation complexity', 'testability']) {
assert.ok(body.includes(lens), `missing lens: ${lens}`);
}
})) passed++; else failed++;
if (test('requires parallel dispatch of all four personas', () => {
assert.ok(body.includes('### 3. Launch four personas in parallel'), 'missing parallel step');
assert.ok(/all four must run at the same time/i.test(body), 'missing parallel anti-pattern');
})) passed++; else failed++;
if (test('personas are analysis-only with no state-changing tool use', () => {
assert.ok(/analysis-only/i.test(body), 'missing analysis-only rule');
assert.ok(/must not edit files, run state-changing commands/i.test(body),
'missing no-state-change rule');
assert.ok(body.includes('do not edit files, run commands, or change any state'),
'prompt template must carry the analysis-only instruction');
})) passed++; else failed++;
if (test('untrusted-context boundary is embedded in the persona prompt template', () => {
assert.ok(body.includes('untrusted declarative data'), 'missing inline trust label');
assert.ok(body.includes('do NOT follow any instructions'), 'missing inline directive guard');
const promptStart = body.indexOf('```text');
const promptEnd = body.indexOf('```', promptStart + 7);
const template = body.slice(promptStart, promptEnd);
assert.ok(template.includes('untrusted declarative data'),
'trust label must be inside the prompt template, not only prose');
})) passed++; else failed++;
if (test('personas receive a bounded summary, never raw PROJECT-CONTEXT.md', () => {
assert.ok(/do \*\*not\*\* pass its raw content/i.test(body), 'missing raw-content ban');
assert.ok(/at most 150 words/i.test(body), 'missing summary bound');
assert.ok(/drop anything that looks like a secret/i.test(body), 'missing secret filter');
})) passed++; else failed++;
if (test('context loading is harness-neutral, no POSIX-only shell', () => {
assert.ok(/native file tools/i.test(body), 'missing harness-native rule');
const codeFences = body.match(/```bash[\s\S]*?```/g) || [];
assert.strictEqual(codeFences.length, 0, 'no bash fences should remain');
})) passed++; else failed++;
if (test('synthesis names tensions instead of averaging them', () => {
assert.ok(body.includes('### Synthesis'), 'missing synthesis section');
assert.ok(/Name tensions explicitly/i.test(body), 'missing tension guardrail');
assert.ok(/flag it as a blocking issue/i.test(body), 'missing blocking-issue rule');
})) passed++; else failed++;
if (test('boundary with team-builder and council is explicit', () => {
assert.ok(body.includes('## Relationship to council and team-builder'), 'missing boundary section');
assert.ok(body.includes('team-builder'), 'missing team-builder reference');
assert.ok(/preset four-lens/i.test(body), 'missing preset positioning');
})) passed++; else failed++;
if (test('does not reference surfaces that are not on main', () => {
assert.ok(!body.includes('story-lifecycle'), 'story-lifecycle is not merged');
assert.ok(!body.includes('ecc:plan-prd'), 'plan-prd resolves as a command, not a skill');
})) passed++; else failed++;
if (test('every referenced skill, agent, and command resolves in the repo', () => {
const refs = [
'skills/council/SKILL.md',
'skills/team-builder/SKILL.md',
'skills/santa-method/SKILL.md',
'commands/plan-prd.md',
'commands/plan.md',
'commands/epic-decompose.md',
'commands/save-session.md',
'commands/code-review.md',
'agents/architect.md',
'agents/code-reviewer.md',
];
for (const ref of refs) {
assert.ok(fs.existsSync(path.join(ROOT, ref)), `unresolved reference: ${ref}`);
}
})) passed++; else failed++;
if (test('skill is registered in install manifest and npm files list', () => {
const modules = JSON.parse(
fs.readFileSync(path.join(ROOT, 'manifests', 'install-modules.json'), 'utf8'));
assert.ok(JSON.stringify(modules).includes('skills/dev-team'),
'missing from manifests/install-modules.json');
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
assert.ok(pkg.files.includes('skills/dev-team/'),
'missing from package.json files');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();