@fernado03/zoo-flow 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -90
- package/bin/zoo-flow.js +405 -56
- package/docs/architecture.md +380 -0
- package/docs/bloat-control.md +49 -0
- package/docs/command-design.md +38 -0
- package/docs/command-flow.md +133 -0
- package/docs/comparison.md +86 -0
- package/docs/context-packs.md +35 -0
- package/docs/dogfood/01-small-library.md +28 -0
- package/docs/dogfood/02-web-app.md +29 -0
- package/docs/dogfood/03-mixed-monorepo.md +29 -0
- package/docs/mode-rules.md +86 -0
- package/docs/npm-publishing.md +79 -0
- package/docs/out-of-scope/mainstream-issue-trackers-only.md +25 -0
- package/docs/out-of-scope/question-limits.md +18 -0
- package/docs/out-of-scope/setup-skill-verify-mode.md +15 -0
- package/docs/overview.md +61 -0
- package/docs/philosophy.md +73 -0
- package/docs/quality-scorecard.md +23 -0
- package/docs/skill-maintenance.md +32 -0
- package/docs/skills-index.md +61 -0
- package/docs/team-mode.md +46 -0
- package/docs/token-budget.md +22 -0
- package/docs/troubleshooting.md +288 -0
- package/examples/demo-transcripts/01-small-tweak.md +37 -0
- package/examples/demo-transcripts/02-unknown-bug-fix.md +37 -0
- package/examples/demo-transcripts/03-new-feature.md +37 -0
- package/examples/demo-transcripts/04-refactor.md +37 -0
- package/examples/demo-transcripts/05-review-and-verify.md +37 -0
- package/examples/feature-flow.md +117 -0
- package/examples/fix-flow.md +139 -0
- package/package.json +16 -5
- package/quality/scorecard.json +88 -0
- package/quality/token-budget.exceptions.json +13 -0
- package/scripts/bundle.ps1 +135 -0
- package/scripts/check-golden-transcripts.js +69 -0
- package/scripts/check-package-links.js +72 -0
- package/scripts/check-package-manifest.js +70 -0
- package/scripts/eval-routing.js +149 -0
- package/scripts/score-quality.js +292 -0
- package/scripts/test-doctor.js +107 -0
- package/scripts/test-project-shapes.js +99 -0
- package/scripts/token-budget.js +105 -0
- package/templates/full/.roo/commands/caveman.md +1 -1
- package/templates/full/.roo/commands/diagnose.md +2 -1
- package/templates/full/.roo/commands/explore.md +13 -13
- package/templates/full/.roo/commands/feature.md +1 -1
- package/templates/full/.roo/commands/fix.md +1 -1
- package/templates/full/.roo/commands/grill-me.md +2 -1
- package/templates/full/.roo/commands/grill-with-docs.md +2 -1
- package/templates/full/.roo/commands/handoff.md +2 -1
- package/templates/full/.roo/commands/improve-codebase-architecture.md +2 -1
- package/templates/full/.roo/commands/prototype.md +1 -1
- package/templates/full/.roo/commands/refactor.md +1 -1
- package/templates/full/.roo/commands/review.md +11 -0
- package/templates/full/.roo/commands/scaffold-context.md +13 -13
- package/templates/full/.roo/commands/setup-matt-pocock-skills.md +8 -8
- package/templates/full/.roo/commands/tdd.md +1 -1
- package/templates/full/.roo/commands/to-issues.md +2 -1
- package/templates/full/.roo/commands/to-prd.md +2 -1
- package/templates/full/.roo/commands/triage.md +1 -1
- package/templates/full/.roo/commands/tweak.md +1 -1
- package/templates/full/.roo/commands/update-docs.md +22 -22
- package/templates/full/.roo/commands/verify.md +11 -0
- package/templates/full/.roo/commands/write-a-skill.md +2 -1
- package/templates/full/.roo/commands/zoom-out.md +2 -1
- package/templates/full/.roo/rules/01-command-protocol.md +1 -1
- package/templates/full/.roo/rules/04-context-economy.md +27 -29
- package/templates/full/.roo/rules-code-tweaker/01-completion.md +12 -8
- package/templates/full/.roo/rules-custom-orchestrator/00-routing.md +77 -63
- package/templates/full/.roo/rules-custom-orchestrator/01-delegation-message.md +59 -55
- package/templates/full/.roo/rules-system-architect/02-completion.md +6 -2
- package/templates/full/.roo/skills/engineering/README.md +2 -0
- package/templates/full/.roo/skills/engineering/commit-and-document/SKILL.md +1 -2
- package/templates/full/.roo/skills/engineering/grill-with-docs/ADR-FORMAT.md +1 -1
- package/templates/full/.roo/skills/engineering/grill-with-docs/CONTEXT-FORMAT.md +36 -61
- package/templates/full/.roo/skills/engineering/grill-with-docs/SKILL.md +1 -1
- package/templates/full/.roo/skills/engineering/improve-codebase-architecture/SKILL.md +3 -3
- package/templates/full/.roo/skills/engineering/prototype/SKILL.md +37 -37
- package/templates/full/.roo/skills/engineering/review/SKILL.md +111 -0
- package/templates/full/.roo/skills/engineering/scaffold-context/SKILL.md +218 -152
- package/templates/full/.roo/skills/engineering/scaffold-context/templates/writing-patterns.md +17 -0
- package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/SKILL.md +3 -3
- package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/domain.md +2 -3
- package/templates/full/.roo/skills/engineering/tdd/SKILL.md +2 -0
- package/templates/full/.roo/skills/engineering/to-prd/SKILL.md +57 -57
- package/templates/full/.roo/skills/engineering/tweak/SKILL.md +2 -1
- package/templates/full/.roo/skills/engineering/verify/SKILL.md +80 -0
- package/templates/full/.roo/skills/in-progress/README.md +0 -1
- package/templates/full/.roomodes +47 -47
- package/templates/full/.zoo-flow/CONTEXT.md +8 -8
- package/templates/full/.zoo-flow/START_HERE.md +61 -61
- package/templates/full/.zoo-flow/docs/adr/0001-record-architecture-decisions.md +22 -22
- package/templates/full/.zoo-flow/evals/no-regression-checklist.md +26 -24
- package/templates/full/.zoo-flow/evals/routing-cases.jsonl +20 -0
- package/templates/full/.zoo-flow/evals/routing-cases.md +213 -189
- package/templates/full/.zoo-flow/project-profile.json +24 -0
- package/tests/fixtures/bad-routing-cases/bad-json.jsonl +1 -0
- package/tests/fixtures/bad-routing-cases/bad-mode.jsonl +1 -0
- package/tests/fixtures/bad-routing-cases/missing-command.jsonl +1 -0
- package/tests/fixtures/doctor/bad-built-in-delegation/fixture.json +1 -0
- package/tests/fixtures/doctor/bad-mode-slug/fixture.json +1 -0
- package/tests/fixtures/doctor/bad-skill-wrapper/fixture.json +1 -0
- package/tests/fixtures/doctor/bad-zoo-path/fixture.json +1 -0
- package/tests/fixtures/doctor/helper-missing-mode/fixture.json +1 -0
- package/tests/fixtures/doctor/helper-not-permitted/fixture.json +1 -0
- package/tests/fixtures/doctor/manual-good-template/fixture.json +1 -0
- package/tests/fixtures/doctor/missing-command/fixture.json +1 -0
- package/tests/fixtures/doctor/missing-roomodes/fixture.json +1 -0
- package/tests/fixtures/doctor/missing-skill/fixture.json +1 -0
- package/tests/fixtures/project-shapes/cli-tool/cmd/root.go +1 -0
- package/tests/fixtures/project-shapes/cli-tool/fixture.json +1 -0
- package/tests/fixtures/project-shapes/cli-tool/package.json +1 -0
- package/tests/fixtures/project-shapes/data-pipeline/fixture.json +1 -0
- package/tests/fixtures/project-shapes/data-pipeline/pipelines/invoices.py +1 -0
- package/tests/fixtures/project-shapes/data-pipeline/pyproject.toml +2 -0
- package/tests/fixtures/project-shapes/library/fixture.json +1 -0
- package/tests/fixtures/project-shapes/library/package.json +1 -0
- package/tests/fixtures/project-shapes/library/src/index.ts +1 -0
- package/tests/fixtures/project-shapes/monorepo/fixture.json +1 -0
- package/tests/fixtures/project-shapes/monorepo/package.json +1 -0
- package/tests/fixtures/project-shapes/monorepo/packages/core/index.ts +1 -0
- package/tests/fixtures/project-shapes/monorepo/packages/web/index.ts +1 -0
- package/tests/fixtures/project-shapes/serverless/fixture.json +1 -0
- package/tests/fixtures/project-shapes/serverless/functions/webhook.ts +1 -0
- package/tests/fixtures/project-shapes/serverless/package.json +1 -0
- package/tests/fixtures/project-shapes/web-app/app/routes/index.tsx +1 -0
- package/tests/fixtures/project-shapes/web-app/fixture.json +1 -0
- package/tests/fixtures/project-shapes/web-app/package.json +1 -0
- package/tests/golden-transcripts/01-small-tweak-golden.md +21 -0
- package/tests/golden-transcripts/02-diagnosis-golden.md +26 -0
- package/tests/golden-transcripts/03-verification-golden.md +24 -0
- package/tests/golden-transcripts/04-review-golden.md +26 -0
- package/tests/golden-transcripts/05-feature-planning-golden.md +23 -0
- package/templates/full/.roo/skills/in-progress/review/SKILL.md +0 -39
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const templateRoot = path.join(packageRoot, "templates", "full");
|
|
11
|
+
const casesPath = process.argv[2]
|
|
12
|
+
? path.resolve(process.cwd(), process.argv[2])
|
|
13
|
+
: path.join(templateRoot, ".zoo-flow", "evals", "routing-cases.jsonl");
|
|
14
|
+
const routingPath = path.join(templateRoot, ".roo", "rules-custom-orchestrator", "00-routing.md");
|
|
15
|
+
|
|
16
|
+
const allowedModes = new Set(["system-architect", "code-tweaker"]);
|
|
17
|
+
const allowedRiskLevels = new Set(["R1", "R2", "R3", "R4", "R5"]);
|
|
18
|
+
const failures = [];
|
|
19
|
+
|
|
20
|
+
function getFrontmatterMode(text) {
|
|
21
|
+
if (!text.startsWith("---")) return null;
|
|
22
|
+
const lines = text.split(/\r?\n/);
|
|
23
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
24
|
+
if (end === -1) return null;
|
|
25
|
+
const frontmatter = lines.slice(1, end).join("\n");
|
|
26
|
+
const match = frontmatter.match(/^mode:\s*([^\r\n#]+)\s*$/m);
|
|
27
|
+
return match ? match[1].trim().replace(/^['"]|['"]$/g, "") : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(lineNumber, message) {
|
|
31
|
+
failures.push(`Line ${lineNumber}: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const routingText = fs.existsSync(routingPath) ? fs.readFileSync(routingPath, "utf8") : "";
|
|
35
|
+
const approvalPhrases = ["free-form request is never self-approving", "propose", "wait for approval"];
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(casesPath)) {
|
|
38
|
+
failures.push(`Missing ${path.relative(packageRoot, casesPath)}`);
|
|
39
|
+
} else {
|
|
40
|
+
const lines = fs.readFileSync(casesPath, "utf8").split(/\r?\n/);
|
|
41
|
+
|
|
42
|
+
lines.forEach((line, index) => {
|
|
43
|
+
const lineNumber = index + 1;
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed) return;
|
|
46
|
+
|
|
47
|
+
let record;
|
|
48
|
+
try {
|
|
49
|
+
record = JSON.parse(trimmed);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
fail(lineNumber, `invalid JSON: ${error.message}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const key of ["name", "user", "expected_workflow", "expected_command", "expected_mode"]) {
|
|
56
|
+
if (typeof record[key] !== "string" || record[key].trim() === "") {
|
|
57
|
+
fail(lineNumber, `${key} must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof record.must_require_approval !== "boolean") {
|
|
62
|
+
fail(lineNumber, "must_require_approval must be a boolean");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!Array.isArray(record.must_not_include)) {
|
|
66
|
+
fail(lineNumber, "must_not_include must be an array");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!allowedModes.has(record.expected_mode)) {
|
|
70
|
+
fail(lineNumber, `expected_mode must be one of ${Array.from(allowedModes).join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate new fields
|
|
74
|
+
if (record.risk !== undefined && !allowedRiskLevels.has(record.risk)) {
|
|
75
|
+
fail(lineNumber, `risk must be one of ${Array.from(allowedRiskLevels).join(", ")}, got ${record.risk}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (record.context_required !== undefined && typeof record.context_required !== "boolean") {
|
|
79
|
+
fail(lineNumber, "context_required must be a boolean");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (record.no_doc_bloat !== undefined && typeof record.no_doc_bloat !== "boolean") {
|
|
83
|
+
fail(lineNumber, "no_doc_bloat must be a boolean");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (record.forbidden_followups !== undefined) {
|
|
87
|
+
if (!Array.isArray(record.forbidden_followups)) {
|
|
88
|
+
fail(lineNumber, "forbidden_followups must be an array");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (record.recommended_followup !== undefined && typeof record.recommended_followup !== "string") {
|
|
93
|
+
fail(lineNumber, "recommended_followup must be a string");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check that no_doc_bloat cases do not reference architecture/docs reading
|
|
97
|
+
if (record.no_doc_bloat && Array.isArray(record.must_not_include)) {
|
|
98
|
+
for (const phrase of record.must_not_include) {
|
|
99
|
+
if (/doc|architect|context/i.test(phrase)) continue; // explicit no-doc entries are fine
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof record.expected_command === "string") {
|
|
104
|
+
if (!record.expected_command.startsWith("/")) {
|
|
105
|
+
fail(lineNumber, "expected_command must start with /");
|
|
106
|
+
} else {
|
|
107
|
+
const commandName = record.expected_command.slice(1);
|
|
108
|
+
const commandPath = path.join(templateRoot, ".roo", "commands", `${commandName}.md`);
|
|
109
|
+
if (!fs.existsSync(commandPath)) {
|
|
110
|
+
fail(lineNumber, `missing command file for ${record.expected_command}`);
|
|
111
|
+
} else {
|
|
112
|
+
const actualMode = getFrontmatterMode(fs.readFileSync(commandPath, "utf8"));
|
|
113
|
+
if (actualMode !== record.expected_mode) {
|
|
114
|
+
fail(lineNumber, `${record.expected_command} mode must be ${record.expected_mode}, got ${actualMode || "none"}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof record.expected_workflow === "string" && !routingText.includes(record.expected_workflow)) {
|
|
121
|
+
fail(lineNumber, `expected_workflow not found in routing rule: ${record.expected_workflow}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (record.must_require_approval === true) {
|
|
125
|
+
const routingLower = routingText.toLowerCase();
|
|
126
|
+
for (const phrase of approvalPhrases) {
|
|
127
|
+
if (!routingLower.includes(phrase)) {
|
|
128
|
+
fail(lineNumber, `routing rule missing approval-gate phrase: ${phrase}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check that no_doc_bloat cases have context_required: false
|
|
134
|
+
if (record.no_doc_bloat === true && record.context_required !== false) {
|
|
135
|
+
fail(lineNumber, "no_doc_bloat cases must have context_required: false");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (failures.length > 0) {
|
|
141
|
+
console.error("\nRouting eval validation failed:\n");
|
|
142
|
+
for (const failure of failures) {
|
|
143
|
+
console.error(`- ${failure}`);
|
|
144
|
+
}
|
|
145
|
+
console.error("");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`Routing eval validation passed: ${path.relative(packageRoot, casesPath)}`);
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const scorecardPath = path.join(packageRoot, "quality", "scorecard.json");
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(scorecardPath)) {
|
|
13
|
+
console.error("Error: quality/scorecard.json not found");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const scorecard = JSON.parse(fs.readFileSync(scorecardPath, "utf8"));
|
|
18
|
+
const templateRoot = path.join(packageRoot, "templates", "full");
|
|
19
|
+
|
|
20
|
+
const failures = [];
|
|
21
|
+
const results = {};
|
|
22
|
+
|
|
23
|
+
function check(condition, description, category) {
|
|
24
|
+
if (!condition) {
|
|
25
|
+
failures.push(` FAIL: ${description}`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── command_integrity ──
|
|
32
|
+
(function () {
|
|
33
|
+
const r = { pass: 0, total: 0 };
|
|
34
|
+
|
|
35
|
+
const commandsDir = path.join(templateRoot, ".roo", "commands");
|
|
36
|
+
if (fs.existsSync(commandsDir)) {
|
|
37
|
+
const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const text = fs.readFileSync(path.join(commandsDir, file), "utf8");
|
|
40
|
+
const isModeless = file === "caveman.md";
|
|
41
|
+
r.total++;
|
|
42
|
+
if (isModeless) {
|
|
43
|
+
r.pass++; // modeless by design, not a failure
|
|
44
|
+
} else {
|
|
45
|
+
const hasMode = /^mode:\s*\S+/m.test(text);
|
|
46
|
+
r.pass += check(hasMode, `${file} has mode declaration`, "command_integrity") ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
r.total += 5; // required rules
|
|
51
|
+
for (const rule of ["00-paths.md", "01-command-protocol.md", "02-three-failure-rule.md", "03-manual-reply-protocol.md", "04-context-economy.md"]) {
|
|
52
|
+
const p = path.join(templateRoot, ".roo", "rules", rule);
|
|
53
|
+
r.pass += check(fs.existsSync(p), `Global rule ${rule} exists`, "command_integrity") ? 1 : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const modeDirs = ["code-tweaker", "system-architect", "custom-orchestrator"];
|
|
57
|
+
for (const slug of modeDirs) {
|
|
58
|
+
const dir = path.join(templateRoot, ".roo", `rules-${slug}`);
|
|
59
|
+
r.total++;
|
|
60
|
+
r.pass += check(fs.existsSync(dir), `Mode rule folder rules-${slug} exists`, "command_integrity") ? 1 : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
results.command_integrity = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
// ── skill_quality ──
|
|
67
|
+
(function () {
|
|
68
|
+
const r = { pass: 0, total: 0 };
|
|
69
|
+
const commandsDir = path.join(templateRoot, ".roo", "commands");
|
|
70
|
+
const skillRefRegex = /^Skill:\s*`?(\.roo\/skills\/[^\s`]+SKILL\.md)`?\s*$/m;
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(commandsDir)) {
|
|
73
|
+
const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const text = fs.readFileSync(path.join(commandsDir, file), "utf8");
|
|
76
|
+
const refs = text.match(skillRefRegex);
|
|
77
|
+
if (refs) {
|
|
78
|
+
const skillPath = path.join(templateRoot, refs[1]);
|
|
79
|
+
r.total++;
|
|
80
|
+
r.pass += check(fs.existsSync(skillPath), `${file} references existing skill ${refs[1]}`, "skill_quality") ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check canonical marker usage (no non-canonical wrappers)
|
|
86
|
+
if (fs.existsSync(commandsDir)) {
|
|
87
|
+
const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
|
|
88
|
+
// Import thin wrapper check from doctor
|
|
89
|
+
const isThinNonCanonical = (text) => {
|
|
90
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
91
|
+
const runSkillLines = lines.filter((l) => /^run skill:/i.test(l));
|
|
92
|
+
if (runSkillLines.length !== 1) return false;
|
|
93
|
+
const otherLines = lines.filter((l) => !/^run skill:/i.test(l) && l !== "$ARGUMENTS");
|
|
94
|
+
return otherLines.every((l) => l.length <= 120 && !/^(#{1,6}\s|[-*]\s|\d+[.)]\s|[A-Z][A-Z\s]+:)/.test(l));
|
|
95
|
+
};
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const text = fs.readFileSync(path.join(commandsDir, file), "utf8");
|
|
98
|
+
r.total++;
|
|
99
|
+
r.pass += check(!isThinNonCanonical(text), `${file} is not a thin non-canonical wrapper`, "skill_quality") ? 1 : 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
results.skill_quality = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
// ── validation_depth ──
|
|
107
|
+
(function () {
|
|
108
|
+
const r = { pass: 0, total: 0 };
|
|
109
|
+
|
|
110
|
+
const requiredFiles = [
|
|
111
|
+
".roomodes",
|
|
112
|
+
".roo/",
|
|
113
|
+
".roo/commands/",
|
|
114
|
+
".roo/skills/",
|
|
115
|
+
".roo/rules/",
|
|
116
|
+
".roo/rules-custom-orchestrator/00-routing.md",
|
|
117
|
+
".roo/rules-code-tweaker/",
|
|
118
|
+
".roo/rules-system-architect/"
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const file of requiredFiles) {
|
|
122
|
+
r.total++;
|
|
123
|
+
r.pass += check(fs.existsSync(path.join(templateRoot, file)), `Template has ${file}`, "validation_depth") ? 1 : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check for no built-in delegation in routing
|
|
127
|
+
const routingPath = path.join(templateRoot, ".roo", "rules-custom-orchestrator", "00-routing.md");
|
|
128
|
+
if (fs.existsSync(routingPath)) {
|
|
129
|
+
const text = fs.readFileSync(routingPath, "utf8");
|
|
130
|
+
const builtIns = ["ask", "code", "debug", "architect", "orchestrator"];
|
|
131
|
+
for (const bi of builtIns) {
|
|
132
|
+
const regex = new RegExp(`\\bnew_task\\b[^\\n]*\\bslug:\\s*${bi}\\b`, "i");
|
|
133
|
+
r.total++;
|
|
134
|
+
r.pass += check(!regex.test(text), `No delegation to built-in mode: ${bi}`, "validation_depth") ? 1 : 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for .zoo/ leakage
|
|
139
|
+
const allFiles = [];
|
|
140
|
+
const walkDir = (dir) => {
|
|
141
|
+
if (!fs.existsSync(dir)) return;
|
|
142
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
143
|
+
const full = path.join(dir, entry);
|
|
144
|
+
if (fs.statSync(full).isDirectory()) {
|
|
145
|
+
if (!entry.startsWith(".") || entry === ".zoo-flow") walkDir(full);
|
|
146
|
+
} else {
|
|
147
|
+
allFiles.push(full);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walkDir(templateRoot);
|
|
152
|
+
|
|
153
|
+
const textExts = new Set([".md", ".json", ".txt", ".yaml", ".yml", ".jsonl"]);
|
|
154
|
+
for (const file of allFiles) {
|
|
155
|
+
if (!textExts.has(path.extname(file))) continue;
|
|
156
|
+
const text = fs.readFileSync(file, "utf8");
|
|
157
|
+
const rel = path.relative(templateRoot, file);
|
|
158
|
+
r.total++;
|
|
159
|
+
r.pass += check(!text.includes(".zoo/"), `No .zoo/ path in ${rel}`, "validation_depth") ? 1 : 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
results.validation_depth = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
163
|
+
})();
|
|
164
|
+
|
|
165
|
+
// ── token_economy ──
|
|
166
|
+
(function () {
|
|
167
|
+
const r = { pass: 0, total: 0 };
|
|
168
|
+
|
|
169
|
+
const contextEcoPath = path.join(templateRoot, ".roo", "rules", "04-context-economy.md");
|
|
170
|
+
r.total++;
|
|
171
|
+
r.pass += check(fs.existsSync(contextEcoPath), "context-economy rule exists", "token_economy") ? 1 : 0;
|
|
172
|
+
|
|
173
|
+
if (fs.existsSync(contextEcoPath)) {
|
|
174
|
+
const text = fs.readFileSync(contextEcoPath, "utf8");
|
|
175
|
+
r.total++;
|
|
176
|
+
r.pass += check(/Do not read.*by default/i.test(text), "Domain-doc read gate exists", "token_economy") ? 1 : 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
results.token_economy = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
180
|
+
})();
|
|
181
|
+
|
|
182
|
+
// ── verification ──
|
|
183
|
+
(function () {
|
|
184
|
+
const r = { pass: 0, total: 0 };
|
|
185
|
+
|
|
186
|
+
const verifyCmd = path.join(templateRoot, ".roo", "commands", "verify.md");
|
|
187
|
+
r.total++;
|
|
188
|
+
r.pass += check(fs.existsSync(verifyCmd), "verify command exists", "verification") ? 1 : 0;
|
|
189
|
+
|
|
190
|
+
const verifySkill = path.join(templateRoot, ".roo", "skills", "engineering", "verify", "SKILL.md");
|
|
191
|
+
r.total++;
|
|
192
|
+
r.pass += check(fs.existsSync(verifySkill), "verify skill exists", "verification") ? 1 : 0;
|
|
193
|
+
|
|
194
|
+
if (fs.existsSync(verifySkill)) {
|
|
195
|
+
const text = fs.readFileSync(verifySkill, "utf8");
|
|
196
|
+
r.total++;
|
|
197
|
+
r.pass += check(/never say/i.test(text) && /verified/i.test(text), "verify skill has no-claim-unless-run", "verification") ? 1 : 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Doctor fixture tests exist
|
|
201
|
+
const fixtureDir = path.join(packageRoot, "tests", "fixtures", "doctor");
|
|
202
|
+
r.total++;
|
|
203
|
+
r.pass += check(fs.existsSync(fixtureDir), "doctor fixtures exist", "verification") ? 1 : 0;
|
|
204
|
+
|
|
205
|
+
results.verification = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
// ── review_process ──
|
|
209
|
+
(function () {
|
|
210
|
+
const r = { pass: 0, total: 0 };
|
|
211
|
+
|
|
212
|
+
const reviewCmd = path.join(templateRoot, ".roo", "commands", "review.md");
|
|
213
|
+
r.total++;
|
|
214
|
+
r.pass += check(fs.existsSync(reviewCmd), "review command exists", "review_process") ? 1 : 0;
|
|
215
|
+
|
|
216
|
+
const reviewSkill = path.join(templateRoot, ".roo", "skills", "engineering", "review", "SKILL.md");
|
|
217
|
+
r.total++;
|
|
218
|
+
r.pass += check(fs.existsSync(reviewSkill), "review skill exists", "review_process") ? 1 : 0;
|
|
219
|
+
|
|
220
|
+
if (fs.existsSync(reviewSkill)) {
|
|
221
|
+
const text = fs.readFileSync(reviewSkill, "utf8");
|
|
222
|
+
r.total++;
|
|
223
|
+
r.pass += check(/security/i.test(text), "review skill has security axis", "review_process") ? 1 : 0;
|
|
224
|
+
r.total++;
|
|
225
|
+
r.pass += check(/Review result:/i.test(text), "review skill has canonical result line", "review_process") ? 1 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
results.review_process = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
229
|
+
})();
|
|
230
|
+
|
|
231
|
+
// ── release_readiness ──
|
|
232
|
+
(function () {
|
|
233
|
+
const r = { pass: 0, total: 0 };
|
|
234
|
+
|
|
235
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
236
|
+
const scripts = pkg.scripts || {};
|
|
237
|
+
|
|
238
|
+
r.total++;
|
|
239
|
+
r.pass += check(!!scripts["release:check"], "release:check script exists", "release_readiness") ? 1 : 0;
|
|
240
|
+
|
|
241
|
+
r.total++;
|
|
242
|
+
r.pass += check(fs.existsSync(scorecardPath), "quality/scorecard.json exists", "release_readiness") ? 1 : 0;
|
|
243
|
+
|
|
244
|
+
r.total++;
|
|
245
|
+
r.pass += check(fs.existsSync(path.join(packageRoot, "scripts", "test-doctor.js")), "test-doctor.js exists", "release_readiness") ? 1 : 0;
|
|
246
|
+
|
|
247
|
+
r.total++;
|
|
248
|
+
r.pass += check(fs.existsSync(path.join(packageRoot, "scripts", "eval-routing.js")), "eval-routing.js exists", "release_readiness") ? 1 : 0;
|
|
249
|
+
|
|
250
|
+
r.total++;
|
|
251
|
+
r.pass += check(fs.existsSync(path.join(packageRoot, "scripts", "check-package-links.js")), "check-package-links.js exists", "release_readiness") ? 1 : 0;
|
|
252
|
+
|
|
253
|
+
results.release_readiness = r.total > 0 ? Math.round((r.pass / r.total) * 100) / 10 : 0;
|
|
254
|
+
})();
|
|
255
|
+
|
|
256
|
+
// ── Compute weighted score ──
|
|
257
|
+
let totalWeight = 0;
|
|
258
|
+
let weightedSum = 0;
|
|
259
|
+
|
|
260
|
+
for (const [category, data] of Object.entries(scorecard.categories)) {
|
|
261
|
+
const result = results[category];
|
|
262
|
+
if (result !== undefined) {
|
|
263
|
+
totalWeight += data.weight;
|
|
264
|
+
weightedSum += result * data.weight;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const finalScore = totalWeight > 0 ? Math.round((weightedSum / totalWeight) * 100) / 100 : 0;
|
|
269
|
+
const minimum = scorecard.minimum_score || 9.5;
|
|
270
|
+
|
|
271
|
+
console.log("\nQuality scorecard results:\n");
|
|
272
|
+
for (const [category, data] of Object.entries(scorecard.categories)) {
|
|
273
|
+
const result = results[category];
|
|
274
|
+
if (result !== undefined) {
|
|
275
|
+
const status = result >= 9.5 ? "✅" : "❌";
|
|
276
|
+
console.log(` ${status} ${category}: ${result.toFixed(1)}/10 (weight: ${(data.weight * 100).toFixed(0)}%)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(`\n Weighted composite score: ${finalScore.toFixed(2)}/10`);
|
|
281
|
+
console.log(` Minimum required: ${minimum}/10`);
|
|
282
|
+
|
|
283
|
+
if (finalScore < minimum) {
|
|
284
|
+
console.error(`\n ❌ Score ${finalScore.toFixed(2)} is below minimum ${minimum}\n`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const failure of failures) {
|
|
289
|
+
console.error(` ⚠ ${failure}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`\n ✅ Quality scorecard passed\n`);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
12
|
+
const templateRoot = path.join(packageRoot, "templates", "full");
|
|
13
|
+
const fixturesRoot = path.join(packageRoot, "tests", "fixtures", "doctor");
|
|
14
|
+
const cliPath = path.join(packageRoot, "bin", "zoo-flow.js");
|
|
15
|
+
|
|
16
|
+
const failures = [];
|
|
17
|
+
|
|
18
|
+
function copyRecursive(src, dest) {
|
|
19
|
+
const stat = fs.statSync(src);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
22
|
+
for (const entry of fs.readdirSync(src)) {
|
|
23
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
28
|
+
fs.copyFileSync(src, dest);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function replaceInFile(filePath, from, to) {
|
|
32
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
33
|
+
fs.writeFileSync(filePath, text.replace(from, to));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mutate(root, mutation) {
|
|
37
|
+
if (mutation === "none") return;
|
|
38
|
+
|
|
39
|
+
const command = (name) => path.join(root, ".roo", "commands", name);
|
|
40
|
+
const skill = (...parts) => path.join(root, ".roo", "skills", ...parts);
|
|
41
|
+
|
|
42
|
+
if (mutation === "missing-roomodes") {
|
|
43
|
+
fs.rmSync(path.join(root, ".roomodes"));
|
|
44
|
+
} else if (mutation === "missing-command") {
|
|
45
|
+
fs.rmSync(command("review.md"));
|
|
46
|
+
} else if (mutation === "missing-skill") {
|
|
47
|
+
fs.rmSync(skill("engineering", "tweak", "SKILL.md"));
|
|
48
|
+
} else if (mutation === "bad-skill-wrapper") {
|
|
49
|
+
replaceInFile(command("caveman.md"), "Skill:", "Run skill:");
|
|
50
|
+
} else if (mutation === "bad-mode-slug") {
|
|
51
|
+
replaceInFile(command("review.md"), "mode: system-architect", "mode: architect");
|
|
52
|
+
} else if (mutation === "bad-built-in-delegation") {
|
|
53
|
+
fs.appendFileSync(path.join(root, ".roo", "rules-custom-orchestrator", "00-routing.md"), "\nUse new_task to architect for review.\n");
|
|
54
|
+
} else if (mutation === "bad-zoo-path") {
|
|
55
|
+
fs.appendFileSync(path.join(root, ".roo", "rules", "00-paths.md"), "\nBad path: .zoo/commands/example.md\n");
|
|
56
|
+
} else if (mutation === "helper-missing-mode") {
|
|
57
|
+
replaceInFile(command("diagnose.md"), /^mode: system-architect\r?\n/m, "");
|
|
58
|
+
} else if (mutation === "helper-not-permitted") {
|
|
59
|
+
replaceInFile(path.join(root, ".roomodes"), "/diagnose, ", "");
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unknown mutation: ${mutation}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const fixtureName of fs.readdirSync(fixturesRoot)) {
|
|
66
|
+
const fixturePath = path.join(fixturesRoot, fixtureName, "fixture.json");
|
|
67
|
+
if (!fs.existsSync(fixturePath)) continue;
|
|
68
|
+
|
|
69
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
|
|
70
|
+
const tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), "zoo-flow-doctor-"));
|
|
71
|
+
const tmpTemplate = path.join(tmpParent, "template");
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
copyRecursive(templateRoot, tmpTemplate);
|
|
75
|
+
mutate(tmpTemplate, fixture.mutation);
|
|
76
|
+
|
|
77
|
+
const result = spawnSync(process.execPath, [cliPath, "doctor"], {
|
|
78
|
+
cwd: tmpTemplate,
|
|
79
|
+
encoding: "utf8"
|
|
80
|
+
});
|
|
81
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
82
|
+
const passed = result.status === 0;
|
|
83
|
+
|
|
84
|
+
if (fixture.expect === "pass" && !passed) {
|
|
85
|
+
failures.push(`${fixtureName}: expected pass, got failure\n${output}`);
|
|
86
|
+
}
|
|
87
|
+
if (fixture.expect === "fail" && passed) {
|
|
88
|
+
failures.push(`${fixtureName}: expected failure, got pass`);
|
|
89
|
+
}
|
|
90
|
+
if (fixture.message && !output.includes(fixture.message)) {
|
|
91
|
+
failures.push(`${fixtureName}: output missing ${JSON.stringify(fixture.message)}\n${output}`);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
fs.rmSync(tmpParent, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (failures.length > 0) {
|
|
99
|
+
console.error("\nDoctor fixture tests failed:\n");
|
|
100
|
+
for (const failure of failures) {
|
|
101
|
+
console.error(`- ${failure}`);
|
|
102
|
+
}
|
|
103
|
+
console.error("");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log("Doctor fixture tests passed");
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const fixturesDir = path.join(packageRoot, "tests", "fixtures", "project-shapes");
|
|
11
|
+
const templateRoot = path.join(packageRoot, "templates", "full");
|
|
12
|
+
|
|
13
|
+
const failures = [];
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(fixturesDir)) {
|
|
16
|
+
console.log("Project-shape tests skipped: no tests/fixtures/project-shapes directory");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fixtures = fs.readdirSync(fixturesDir).filter((entry) => {
|
|
21
|
+
return fs.statSync(path.join(fixturesDir, entry)).isDirectory();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (fixtures.length === 0) {
|
|
25
|
+
console.log("Project-shape tests skipped: empty tests/fixtures/project-shapes directory");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectShape(root) {
|
|
30
|
+
const hasPkg = fs.existsSync(path.join(root, "package.json"));
|
|
31
|
+
|
|
32
|
+
if (hasPkg) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
35
|
+
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords : [];
|
|
36
|
+
|
|
37
|
+
if (keywords.includes("web-app")) return "web-app";
|
|
38
|
+
if (keywords.includes("library")) return "library";
|
|
39
|
+
if (keywords.includes("cli")) return "cli-tool";
|
|
40
|
+
if (keywords.includes("monorepo")) return "monorepo";
|
|
41
|
+
if (keywords.includes("data-pipeline")) return "data-pipeline";
|
|
42
|
+
if (keywords.includes("serverless")) return "serverless";
|
|
43
|
+
} catch {
|
|
44
|
+
// fall through
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const topFolders = fs.readdirSync(root).filter((entry) => {
|
|
49
|
+
return fs.statSync(path.join(root, entry)).isDirectory() && !entry.startsWith(".");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (topFolders.includes("app") || topFolders.includes("pages")) return "web-app";
|
|
53
|
+
if (topFolders.includes("cmd")) return "cli-tool";
|
|
54
|
+
if (topFolders.includes("packages")) return "monorepo";
|
|
55
|
+
if (topFolders.includes("lib")) return "library";
|
|
56
|
+
if (topFolders.includes("pipelines")) return "data-pipeline";
|
|
57
|
+
if (topFolders.includes("functions") || topFolders.includes("handlers")) return "serverless";
|
|
58
|
+
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const fixture of fixtures) {
|
|
63
|
+
const fixturePath = path.join(fixturesDir, fixture);
|
|
64
|
+
const metaPath = path.join(fixturePath, "fixture.json");
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(metaPath)) {
|
|
67
|
+
failures.push(`${fixture}: missing fixture.json`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let meta;
|
|
72
|
+
try {
|
|
73
|
+
meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
failures.push(`${fixture}: invalid fixture.json — ${error.message}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!meta.expected_shape) {
|
|
80
|
+
failures.push(`${fixture}: fixture.json missing expected_shape`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const detected = detectShape(fixturePath);
|
|
85
|
+
if (detected !== meta.expected_shape) {
|
|
86
|
+
failures.push(`${fixture}: expected shape ${meta.expected_shape}, detected ${detected}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (failures.length > 0) {
|
|
91
|
+
console.error("\nProject-shape fixture tests failed:\n");
|
|
92
|
+
for (const failure of failures) {
|
|
93
|
+
console.error(`- ${failure}`);
|
|
94
|
+
}
|
|
95
|
+
console.error("");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`Project-shape fixture tests passed: ${fixtures.length} shapes`);
|