@fernado03/zoo-flow 0.5.3 → 0.7.1
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 +81 -78
- package/bin/zoo-flow.js +398 -50
- 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 +2 -2
- 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/setup-matt-pocock-skills.md +1 -1
- 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 +1 -1
- 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 +3 -5
- package/templates/full/.roo/rules-code-tweaker/01-completion.md +14 -8
- package/templates/full/.roo/rules-custom-orchestrator/00-routing.md +43 -11
- package/templates/full/.roo/rules-custom-orchestrator/01-delegation-message.md +11 -7
- 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 +7 -32
- 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/review/SKILL.md +111 -0
- package/templates/full/.roo/skills/engineering/scaffold-context/SKILL.md +66 -0
- 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/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 +3 -3
- package/templates/full/.zoo-flow/START_HERE.md +8 -61
- package/templates/full/.zoo-flow/evals/no-regression-checklist.md +4 -2
- package/templates/full/.zoo-flow/evals/routing-cases.jsonl +20 -0
- package/templates/full/.zoo-flow/evals/routing-cases.md +27 -3
- 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
package/bin/zoo-flow.js
CHANGED
|
@@ -5,27 +5,74 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
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");
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const templateRoot = path.join(packageRoot, "templates", "full");
|
|
11
|
+
|
|
12
|
+
const COMMAND_POLICY = {
|
|
13
|
+
routed: {
|
|
14
|
+
"tweak": "code-tweaker",
|
|
15
|
+
"tdd": "code-tweaker",
|
|
16
|
+
"update-docs": "code-tweaker",
|
|
17
|
+
"commit-and-document": "code-tweaker",
|
|
18
|
+
"prototype": "code-tweaker",
|
|
19
|
+
"scaffold-context": "code-tweaker",
|
|
20
|
+
"verify": "code-tweaker",
|
|
21
|
+
"fix": "system-architect",
|
|
22
|
+
"feature": "system-architect",
|
|
23
|
+
"refactor": "system-architect",
|
|
24
|
+
"explore": "system-architect",
|
|
25
|
+
"triage": "system-architect",
|
|
26
|
+
"review": "system-architect"
|
|
27
|
+
},
|
|
28
|
+
helpers: {
|
|
29
|
+
"diagnose": "system-architect",
|
|
30
|
+
"grill-with-docs": "system-architect",
|
|
31
|
+
"improve-codebase-architecture": "system-architect",
|
|
32
|
+
"to-prd": "system-architect",
|
|
33
|
+
"to-issues": "system-architect",
|
|
34
|
+
"zoom-out": "system-architect",
|
|
35
|
+
"handoff": "system-architect",
|
|
36
|
+
"grill-me": "system-architect",
|
|
37
|
+
"write-a-skill": "code-tweaker",
|
|
38
|
+
"setup-matt-pocock-skills": "code-tweaker"
|
|
39
|
+
},
|
|
40
|
+
modelessUtilities: ["caveman"]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ALLOWED_COMMAND_MODES = new Set(["system-architect", "code-tweaker", "custom-orchestrator"]);
|
|
44
|
+
const BUILT_IN_MODE_NAMES = new Set([
|
|
45
|
+
"Ask",
|
|
46
|
+
"Code",
|
|
47
|
+
"Debug",
|
|
48
|
+
"Architect",
|
|
49
|
+
"Orchestrator",
|
|
50
|
+
"ask",
|
|
51
|
+
"code",
|
|
52
|
+
"debug",
|
|
53
|
+
"architect",
|
|
54
|
+
"orchestrator"
|
|
55
|
+
]);
|
|
11
56
|
|
|
12
57
|
const command = process.argv[2];
|
|
13
58
|
|
|
14
59
|
const HELP = `
|
|
15
60
|
Zoo Flow
|
|
16
61
|
|
|
17
|
-
Usage:
|
|
62
|
+
Usage:
|
|
18
63
|
npx @fernado03/zoo-flow@latest init
|
|
19
64
|
npx @fernado03/zoo-flow@latest init --force
|
|
20
65
|
npx @fernado03/zoo-flow@latest update
|
|
21
66
|
npx @fernado03/zoo-flow@latest update --dry-run
|
|
22
|
-
npx @fernado03/zoo-flow@latest update --force
|
|
23
|
-
npx @fernado03/zoo-flow@latest doctor
|
|
67
|
+
npx @fernado03/zoo-flow@latest update --force
|
|
68
|
+
npx @fernado03/zoo-flow@latest doctor
|
|
69
|
+
npx @fernado03/zoo-flow@latest doctor --template-only
|
|
24
70
|
|
|
25
71
|
Commands:
|
|
26
72
|
init Install Zoo Flow into the current project
|
|
27
73
|
update Back up current config and copy the latest template
|
|
28
|
-
doctor Validate the
|
|
74
|
+
doctor Validate Zoo Flow in the current project
|
|
75
|
+
doctor --template-only Validate this package's bundled template
|
|
29
76
|
|
|
30
77
|
Options:
|
|
31
78
|
--force Overwrite existing .roomodes and .roo after backup
|
|
@@ -144,7 +191,7 @@ function readJson(filePath) {
|
|
|
144
191
|
}
|
|
145
192
|
}
|
|
146
193
|
|
|
147
|
-
function walkFiles(rootDir) {
|
|
194
|
+
function walkFiles(rootDir) {
|
|
148
195
|
const files = [];
|
|
149
196
|
|
|
150
197
|
function walk(current) {
|
|
@@ -164,10 +211,110 @@ function walkFiles(rootDir) {
|
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
walk(rootDir);
|
|
167
|
-
return files;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function stripFrontmatter(text) {
|
|
218
|
+
if (!text.startsWith("---")) return text;
|
|
219
|
+
|
|
220
|
+
const lines = text.split(/\r?\n/);
|
|
221
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
222
|
+
if (lines[index].trim() === "---") {
|
|
223
|
+
return lines.slice(index + 1).join("\n");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return text;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getFrontmatter(text) {
|
|
231
|
+
if (!text.startsWith("---")) return "";
|
|
232
|
+
|
|
233
|
+
const lines = text.split(/\r?\n/);
|
|
234
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
235
|
+
if (lines[index].trim() === "---") {
|
|
236
|
+
return lines.slice(1, index).join("\n");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getFrontmatterMode(text) {
|
|
244
|
+
const frontmatter = getFrontmatter(text);
|
|
245
|
+
const match = frontmatter.match(/^mode:\s*([^\r\n#]+)\s*$/m);
|
|
246
|
+
return match ? match[1].trim().replace(/^['"]|['"]$/g, "") : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getCommandPolicyMode(commandName) {
|
|
250
|
+
return COMMAND_POLICY.routed[commandName] || COMMAND_POLICY.helpers[commandName] || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getPolicyCommandEntries() {
|
|
254
|
+
return Object.entries({ ...COMMAND_POLICY.routed, ...COMMAND_POLICY.helpers });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isThinNonCanonicalSkillWrapper(text) {
|
|
258
|
+
const body = stripFrontmatter(text);
|
|
259
|
+
const lines = body
|
|
260
|
+
.split(/\r?\n/)
|
|
261
|
+
.map((line) => line.trim())
|
|
262
|
+
.filter(Boolean);
|
|
263
|
+
|
|
264
|
+
const runSkillLines = lines.filter((line) =>
|
|
265
|
+
/^run skill:\s*`?\.roo\/skills\/[^\s`]+SKILL\.md`?\s*$/i.test(line)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (runSkillLines.length !== 1) return false;
|
|
269
|
+
|
|
270
|
+
const otherLines = lines.filter((line) =>
|
|
271
|
+
!/^run skill:/i.test(line) && line !== "$ARGUMENTS"
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
return otherLines.every(
|
|
275
|
+
(line) => line.length <= 120 && !/^(#{1,6}\s|[-*]\s|\d+[.)]\s|[A-Z][A-Z\s]+:)/.test(line)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getRoutedCommandRows(routingText) {
|
|
280
|
+
const routedSection = routingText.match(/## Routed commands\s*\n([\s\S]*?)(?=\n## |$)/);
|
|
281
|
+
if (!routedSection) return [];
|
|
282
|
+
|
|
283
|
+
return routedSection[1]
|
|
284
|
+
.split(/\r?\n/)
|
|
285
|
+
.map((line) => line.trim())
|
|
286
|
+
.filter((line) => line.startsWith("|") && !/^\|\s*-+/.test(line))
|
|
287
|
+
.map((line) => line.split("|").slice(1, -1).map((cell) => cell.trim()))
|
|
288
|
+
.filter((cells) => cells.length >= 3 && cells[1].includes("`/"));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function findPositiveBuiltInDelegation(text) {
|
|
292
|
+
const findings = [];
|
|
293
|
+
const target = "(ask|code|debug|architect|orchestrator)";
|
|
294
|
+
const end = "(?=$|[\\s.,;:)\\]`])";
|
|
295
|
+
const optionalBacktick = "[`]?";
|
|
296
|
+
const patterns = [
|
|
297
|
+
new RegExp("\\bnew_task\\b[^\\n]*(?:to|target|slug:)\\s*" + optionalBacktick + target + optionalBacktick + end, "i"),
|
|
298
|
+
new RegExp("\\bdelegate(?:s|d)?\\b[^\\n]*(?:to|into|with)\\s*" + optionalBacktick + target + optionalBacktick + end, "i"),
|
|
299
|
+
new RegExp("\\bswitch_mode\\b[^\\n]*(?:to|->|slug:)\\s*" + optionalBacktick + target + optionalBacktick + end, "i"),
|
|
300
|
+
new RegExp("\\bswitch\\s+to\\s*" + optionalBacktick + target + "(?:\\s+mode)?" + optionalBacktick + end, "i")
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const negativePattern = /\b(never|do not|don't|must not|no valid|not fall back|outside zoo flow)\b/i;
|
|
304
|
+
|
|
305
|
+
for (const line of text.split(/\r?\n/)) {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
if (!trimmed || negativePattern.test(trimmed)) continue;
|
|
308
|
+
|
|
309
|
+
if (patterns.some((pattern) => pattern.test(trimmed))) {
|
|
310
|
+
findings.push(trimmed);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return findings;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function validateTemplate(rootDir) {
|
|
171
318
|
const roomodesPath = path.join(rootDir, ".roomodes");
|
|
172
319
|
const rooDir = path.join(rootDir, ".roo");
|
|
173
320
|
const commandsDir = path.join(rooDir, "commands");
|
|
@@ -182,33 +329,38 @@ function validateTemplate(rootDir) {
|
|
|
182
329
|
if (!pathExists(skillsDir)) failures.push("Missing .roo/skills/");
|
|
183
330
|
if (!pathExists(rulesDir)) failures.push("Missing .roo/rules/");
|
|
184
331
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
332
|
+
let roomodes = null;
|
|
333
|
+
|
|
334
|
+
if (pathExists(roomodesPath)) {
|
|
335
|
+
try {
|
|
336
|
+
roomodes = readJson(roomodesPath);
|
|
337
|
+
if (!Array.isArray(roomodes.customModes)) {
|
|
338
|
+
failures.push(".roomodes must contain customModes array");
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
failures.push(error.message);
|
|
342
|
+
}
|
|
194
343
|
}
|
|
195
344
|
|
|
196
|
-
// Validate
|
|
197
|
-
// .roo/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (pathExists(commandsDir)) {
|
|
202
|
-
const skillRefRegex = /^Skill:\s*`?(\.roo\/skills\/[^\s`]+SKILL\.md)`?\s*$/m;
|
|
203
|
-
|
|
345
|
+
// Validate command references. Commands may either declare a skill via
|
|
346
|
+
// `Skill: .roo/skills/.../SKILL.md` or contain workflow steps directly.
|
|
347
|
+
if (pathExists(commandsDir)) {
|
|
348
|
+
const skillRefRegex = /^Skill:\s*`?(\.roo\/skills\/[^\s`]+SKILL\.md)`?\s*$/m;
|
|
349
|
+
|
|
204
350
|
for (const entry of fs.readdirSync(commandsDir)) {
|
|
205
351
|
if (!entry.endsWith(".md")) continue;
|
|
206
352
|
|
|
207
|
-
const commandFile = path.join(commandsDir, entry);
|
|
208
|
-
const text = fs.readFileSync(commandFile, "utf8");
|
|
209
|
-
const match = text.match(skillRefRegex);
|
|
210
|
-
|
|
211
|
-
if (
|
|
353
|
+
const commandFile = path.join(commandsDir, entry);
|
|
354
|
+
const text = fs.readFileSync(commandFile, "utf8");
|
|
355
|
+
const match = text.match(skillRefRegex);
|
|
356
|
+
|
|
357
|
+
if (isThinNonCanonicalSkillWrapper(text)) {
|
|
358
|
+
failures.push(
|
|
359
|
+
`Command ${path.relative(rootDir, commandFile)} uses non-canonical skill wrapper. Use \`Skill: .roo/skills/.../SKILL.md\`.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!match) continue;
|
|
212
364
|
|
|
213
365
|
const skillRelative = match[1];
|
|
214
366
|
const skillAbsolute = path.join(rootDir, skillRelative);
|
|
@@ -217,14 +369,112 @@ function validateTemplate(rootDir) {
|
|
|
217
369
|
failures.push(
|
|
218
370
|
`Command ${path.relative(rootDir, commandFile)} references missing skill: ${skillRelative}`
|
|
219
371
|
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const routingPath = path.join(rootDir, ".roo", "rules-custom-orchestrator", "00-routing.md");
|
|
377
|
+
const allowedRoutedModes = new Set(["system-architect", "code-tweaker"]);
|
|
378
|
+
|
|
379
|
+
if (pathExists(routingPath)) {
|
|
380
|
+
const routingText = fs.readFileSync(routingPath, "utf8");
|
|
381
|
+
const routedRows = getRoutedCommandRows(routingText);
|
|
382
|
+
|
|
383
|
+
if (routedRows.length === 0) {
|
|
384
|
+
failures.push("Routing table missing routed command rows");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const cells of routedRows) {
|
|
388
|
+
const commandMatch = cells[1].match(/`\/(.+?)`/);
|
|
389
|
+
const mode = cells[2].replace(/`/g, "");
|
|
390
|
+
|
|
391
|
+
if (commandMatch) {
|
|
392
|
+
const commandName = commandMatch[1];
|
|
393
|
+
const commandPath = path.join(commandsDir, `${commandName}.md`);
|
|
394
|
+
if (!pathExists(commandPath)) {
|
|
395
|
+
failures.push(`Routed command /${commandName} is missing .roo/commands/${commandName}.md`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!allowedRoutedModes.has(mode)) {
|
|
400
|
+
failures.push(`Routed command table uses invalid mode slug: ${mode}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
failures.push("Missing .roo/rules-custom-orchestrator/00-routing.md");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (roomodes && Array.isArray(roomodes.customModes)) {
|
|
408
|
+
const slugs = new Set(roomodes.customModes.map((mode) => mode.slug));
|
|
409
|
+
for (const slug of ["custom-orchestrator", "system-architect", "code-tweaker"]) {
|
|
410
|
+
if (!slugs.has(slug)) {
|
|
411
|
+
failures.push(`Missing required mode slug in .roomodes: ${slug}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const modesBySlug = new Map(roomodes.customModes.map((mode) => [mode.slug, mode]));
|
|
416
|
+
for (const [commandName, modeSlug] of getPolicyCommandEntries()) {
|
|
417
|
+
const mode = modesBySlug.get(modeSlug);
|
|
418
|
+
const instructions = mode && typeof mode.customInstructions === "string" ? mode.customInstructions : "";
|
|
419
|
+
if (!instructions.includes(`/${commandName}`)) {
|
|
420
|
+
failures.push(`.roomodes ${modeSlug} does not permit documented command /${commandName}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (pathExists(commandsDir)) {
|
|
426
|
+
for (const [commandName, expectedMode] of getPolicyCommandEntries()) {
|
|
427
|
+
const commandPath = path.join(commandsDir, `${commandName}.md`);
|
|
428
|
+
if (!pathExists(commandPath)) {
|
|
429
|
+
failures.push(`Command policy references missing command file: .roo/commands/${commandName}.md`);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const commandText = fs.readFileSync(commandPath, "utf8");
|
|
434
|
+
const actualMode = getFrontmatterMode(commandText);
|
|
435
|
+
if (actualMode !== expectedMode) {
|
|
436
|
+
failures.push(`Command .roo/commands/${commandName}.md must declare mode: ${expectedMode}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const commandName of COMMAND_POLICY.modelessUtilities) {
|
|
441
|
+
const commandPath = path.join(commandsDir, `${commandName}.md`);
|
|
442
|
+
if (!pathExists(commandPath)) {
|
|
443
|
+
failures.push(`Command policy references missing modeless utility: .roo/commands/${commandName}.md`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const entry of fs.readdirSync(commandsDir)) {
|
|
448
|
+
if (!entry.endsWith(".md")) continue;
|
|
449
|
+
|
|
450
|
+
const commandName = entry.slice(0, -3);
|
|
451
|
+
const commandPath = path.join(commandsDir, entry);
|
|
452
|
+
const commandText = fs.readFileSync(commandPath, "utf8");
|
|
453
|
+
const mode = getFrontmatterMode(commandText);
|
|
454
|
+
|
|
455
|
+
if (mode && !ALLOWED_COMMAND_MODES.has(mode)) {
|
|
456
|
+
failures.push(`Command .roo/commands/${entry} uses invalid mode: ${mode}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (mode && BUILT_IN_MODE_NAMES.has(mode)) {
|
|
460
|
+
failures.push(`Command .roo/commands/${entry} uses built-in/default mode: ${mode}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!getCommandPolicyMode(commandName) && !COMMAND_POLICY.modelessUtilities.includes(commandName)) {
|
|
464
|
+
failures.push(`Command .roo/commands/${entry} is missing from command policy`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const allFiles = [];
|
|
470
|
+
for (const validationRoot of [roomodesPath, rooDir, path.join(rootDir, ".zoo-flow")]) {
|
|
471
|
+
if (pathExists(validationRoot)) {
|
|
472
|
+
allFiles.push(...walkFiles(validationRoot));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
225
475
|
const textFileExtensions = new Set([".md", ".json", ".txt", ".yaml", ".yml"]);
|
|
226
476
|
|
|
227
|
-
for (const file of allFiles) {
|
|
477
|
+
for (const file of allFiles) {
|
|
228
478
|
const ext = path.extname(file);
|
|
229
479
|
if (!textFileExtensions.has(ext)) continue;
|
|
230
480
|
|
|
@@ -234,15 +484,34 @@ function validateTemplate(rootDir) {
|
|
|
234
484
|
".roo/skills///SKILL.md",
|
|
235
485
|
".roo/commands/.md",
|
|
236
486
|
"your-org/roo-flow",
|
|
237
|
-
".zoo/"
|
|
487
|
+
".zoo/",
|
|
488
|
+
"CONTEXT-MAP.md",
|
|
489
|
+
"FLOW.md",
|
|
490
|
+
"APP_MAP.md"
|
|
238
491
|
];
|
|
239
492
|
|
|
240
|
-
for (const pattern of badPatterns) {
|
|
241
|
-
if (text.includes(pattern)) {
|
|
242
|
-
failures.push(`Bad pattern "${pattern}" in ${path.relative(rootDir, file)}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
493
|
+
for (const pattern of badPatterns) {
|
|
494
|
+
if (text.includes(pattern)) {
|
|
495
|
+
failures.push(`Bad pattern "${pattern}" in ${path.relative(rootDir, file)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const runtimeFiles = allFiles.filter((file) => {
|
|
501
|
+
const relative = path.relative(rootDir, file).replace(/\\/g, "/");
|
|
502
|
+
return relative === ".roomodes" || /^\.roo\/rules(?:-|\/)/.test(relative);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
for (const file of runtimeFiles) {
|
|
506
|
+
const text = fs.readFileSync(file, "utf8");
|
|
507
|
+
const findings = findPositiveBuiltInDelegation(text);
|
|
508
|
+
|
|
509
|
+
for (const finding of findings) {
|
|
510
|
+
failures.push(
|
|
511
|
+
`Built-in/default delegation target in ${path.relative(rootDir, file)}: ${finding}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
246
515
|
|
|
247
516
|
const requiredRules = [
|
|
248
517
|
".roo/rules/00-paths.md",
|
|
@@ -349,20 +618,99 @@ When workflow choices appear, type the number manually, e.g. 1.
|
|
|
349
618
|
|
|
350
619
|
function doctor() {
|
|
351
620
|
const args = new Set(process.argv.slice(3));
|
|
352
|
-
const
|
|
621
|
+
const templateOnly = args.has("--template-only");
|
|
622
|
+
const rootToCheck = templateOnly ? templateRoot : process.cwd();
|
|
353
623
|
const failures = validateTemplate(rootToCheck);
|
|
624
|
+
const optionalInfo = [];
|
|
625
|
+
|
|
626
|
+
if (!templateOnly) {
|
|
627
|
+
// Installed-project checks
|
|
628
|
+
const projectRoot = process.cwd();
|
|
629
|
+
|
|
630
|
+
// .gitignore check
|
|
631
|
+
const gi = path.join(projectRoot, ".gitignore");
|
|
632
|
+
if (pathExists(gi)) {
|
|
633
|
+
const giContent = fs.readFileSync(gi, "utf8");
|
|
634
|
+
if (!giContent.includes(ZOO_FLOW_GITIGNORE_MARKER)) {
|
|
635
|
+
optionalInfo.push("info: .gitignore does not contain Zoo Flow entries (.roomodes, .roo/, .zoo-flow/)");
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
optionalInfo.push("info: .gitignore missing — run `npx @fernado03/zoo-flow@latest init` to create it");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// START_HERE.md check
|
|
642
|
+
const startHere = path.join(projectRoot, ".zoo-flow", "START_HERE.md");
|
|
643
|
+
if (!pathExists(startHere)) {
|
|
644
|
+
optionalInfo.push("info: .zoo-flow/START_HERE.md missing — run `npx @fernado03/zoo-flow@latest init` to install starter docs");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// CONTEXT.md check
|
|
648
|
+
const contextMd = path.join(projectRoot, ".zoo-flow", "CONTEXT.md");
|
|
649
|
+
if (!pathExists(contextMd)) {
|
|
650
|
+
optionalInfo.push("info: .zoo-flow/CONTEXT.md missing — run `/scaffold-context` to create project context");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Project-profile check
|
|
654
|
+
const profile = path.join(projectRoot, ".zoo-flow", "project-profile.json");
|
|
655
|
+
if (!pathExists(profile)) {
|
|
656
|
+
optionalInfo.push("info: .zoo-flow/project-profile.json missing — run `/setup-matt-pocock-skills` to configure");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// LESSONS check
|
|
660
|
+
const lessons = path.join(projectRoot, ".zoo-flow", "LESSONS.md");
|
|
661
|
+
if (!pathExists(lessons)) {
|
|
662
|
+
optionalInfo.push("info: .zoo-flow/LESSONS.md missing — optional, created on demand");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Verify three modes exist in .roomodes
|
|
666
|
+
const roomodesPath = path.join(projectRoot, ".roomodes");
|
|
667
|
+
if (pathExists(roomodesPath)) {
|
|
668
|
+
try {
|
|
669
|
+
const roomodes = readJson(roomodesPath);
|
|
670
|
+
const slugs = (roomodes.customModes || []).map((m) => m.slug);
|
|
671
|
+
const required = ["custom-orchestrator", "system-architect", "code-tweaker"];
|
|
672
|
+
for (const slug of required) {
|
|
673
|
+
if (!slugs.includes(slug)) {
|
|
674
|
+
failures.push(`.roomodes missing required mode slug: ${slug}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch (error) {
|
|
678
|
+
failures.push(`.roomodes parse error: ${error.message}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// .roo/commands and .roo/rules exist
|
|
683
|
+
if (!pathExists(path.join(projectRoot, ".roo", "commands"))) {
|
|
684
|
+
failures.push(".roo/commands/ missing — run `npx @fernado03/zoo-flow@latest update`");
|
|
685
|
+
}
|
|
686
|
+
if (!pathExists(path.join(projectRoot, ".roo", "rules"))) {
|
|
687
|
+
failures.push(".roo/rules/ missing — run `npx @fernado03/zoo-flow@latest update`");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
354
690
|
|
|
355
691
|
if (failures.length > 0) {
|
|
356
692
|
console.error("\nZoo Flow doctor found problems:\n");
|
|
357
693
|
for (const failure of failures) {
|
|
358
694
|
console.error(`- ${failure}`);
|
|
359
695
|
}
|
|
696
|
+
if (!templateOnly) {
|
|
697
|
+
console.error("\nRun `npx @fernado03/zoo-flow@latest update --dry-run` to preview replacing local Zoo Flow config.");
|
|
698
|
+
}
|
|
360
699
|
console.error("");
|
|
361
700
|
process.exit(1);
|
|
362
701
|
}
|
|
363
702
|
|
|
364
|
-
|
|
365
|
-
}
|
|
703
|
+
const targetName = templateOnly ? "bundled template" : "current project";
|
|
704
|
+
console.log(`Zoo Flow doctor passed for ${targetName}: ${rootToCheck}`);
|
|
705
|
+
|
|
706
|
+
if (optionalInfo.length > 0) {
|
|
707
|
+
console.log("\nOptional info:\n");
|
|
708
|
+
for (const info of optionalInfo) {
|
|
709
|
+
console.log(` ${info}`);
|
|
710
|
+
}
|
|
711
|
+
console.log("");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
366
714
|
|
|
367
715
|
function update() {
|
|
368
716
|
const args = new Set(process.argv.slice(3));
|