@fernado03/zoo-flow 0.5.3 → 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.
Files changed (127) hide show
  1. package/README.md +102 -79
  2. package/bin/zoo-flow.js +398 -50
  3. package/docs/architecture.md +380 -0
  4. package/docs/bloat-control.md +49 -0
  5. package/docs/command-design.md +38 -0
  6. package/docs/command-flow.md +133 -0
  7. package/docs/comparison.md +86 -0
  8. package/docs/context-packs.md +35 -0
  9. package/docs/dogfood/01-small-library.md +28 -0
  10. package/docs/dogfood/02-web-app.md +29 -0
  11. package/docs/dogfood/03-mixed-monorepo.md +29 -0
  12. package/docs/mode-rules.md +86 -0
  13. package/docs/npm-publishing.md +79 -0
  14. package/docs/out-of-scope/mainstream-issue-trackers-only.md +25 -0
  15. package/docs/out-of-scope/question-limits.md +18 -0
  16. package/docs/out-of-scope/setup-skill-verify-mode.md +15 -0
  17. package/docs/overview.md +61 -0
  18. package/docs/philosophy.md +73 -0
  19. package/docs/quality-scorecard.md +23 -0
  20. package/docs/skill-maintenance.md +32 -0
  21. package/docs/skills-index.md +61 -0
  22. package/docs/team-mode.md +46 -0
  23. package/docs/token-budget.md +22 -0
  24. package/docs/troubleshooting.md +288 -0
  25. package/examples/demo-transcripts/01-small-tweak.md +37 -0
  26. package/examples/demo-transcripts/02-unknown-bug-fix.md +37 -0
  27. package/examples/demo-transcripts/03-new-feature.md +37 -0
  28. package/examples/demo-transcripts/04-refactor.md +37 -0
  29. package/examples/demo-transcripts/05-review-and-verify.md +37 -0
  30. package/examples/feature-flow.md +117 -0
  31. package/examples/fix-flow.md +139 -0
  32. package/package.json +16 -5
  33. package/quality/scorecard.json +88 -0
  34. package/quality/token-budget.exceptions.json +13 -0
  35. package/scripts/bundle.ps1 +135 -0
  36. package/scripts/check-golden-transcripts.js +69 -0
  37. package/scripts/check-package-links.js +72 -0
  38. package/scripts/check-package-manifest.js +70 -0
  39. package/scripts/eval-routing.js +149 -0
  40. package/scripts/score-quality.js +292 -0
  41. package/scripts/test-doctor.js +107 -0
  42. package/scripts/test-project-shapes.js +99 -0
  43. package/scripts/token-budget.js +105 -0
  44. package/templates/full/.roo/commands/caveman.md +1 -1
  45. package/templates/full/.roo/commands/diagnose.md +2 -1
  46. package/templates/full/.roo/commands/feature.md +1 -1
  47. package/templates/full/.roo/commands/fix.md +1 -1
  48. package/templates/full/.roo/commands/grill-me.md +2 -1
  49. package/templates/full/.roo/commands/grill-with-docs.md +2 -1
  50. package/templates/full/.roo/commands/handoff.md +2 -1
  51. package/templates/full/.roo/commands/improve-codebase-architecture.md +2 -1
  52. package/templates/full/.roo/commands/prototype.md +1 -1
  53. package/templates/full/.roo/commands/refactor.md +1 -1
  54. package/templates/full/.roo/commands/review.md +11 -0
  55. package/templates/full/.roo/commands/setup-matt-pocock-skills.md +1 -1
  56. package/templates/full/.roo/commands/tdd.md +1 -1
  57. package/templates/full/.roo/commands/to-issues.md +2 -1
  58. package/templates/full/.roo/commands/to-prd.md +2 -1
  59. package/templates/full/.roo/commands/triage.md +1 -1
  60. package/templates/full/.roo/commands/tweak.md +1 -1
  61. package/templates/full/.roo/commands/verify.md +11 -0
  62. package/templates/full/.roo/commands/write-a-skill.md +2 -1
  63. package/templates/full/.roo/commands/zoom-out.md +2 -1
  64. package/templates/full/.roo/rules/01-command-protocol.md +1 -1
  65. package/templates/full/.roo/rules/04-context-economy.md +3 -5
  66. package/templates/full/.roo/rules-code-tweaker/01-completion.md +12 -8
  67. package/templates/full/.roo/rules-custom-orchestrator/00-routing.md +23 -9
  68. package/templates/full/.roo/rules-custom-orchestrator/01-delegation-message.md +11 -7
  69. package/templates/full/.roo/rules-system-architect/02-completion.md +6 -2
  70. package/templates/full/.roo/skills/engineering/README.md +2 -0
  71. package/templates/full/.roo/skills/engineering/commit-and-document/SKILL.md +1 -2
  72. package/templates/full/.roo/skills/engineering/grill-with-docs/ADR-FORMAT.md +1 -1
  73. package/templates/full/.roo/skills/engineering/grill-with-docs/CONTEXT-FORMAT.md +7 -32
  74. package/templates/full/.roo/skills/engineering/grill-with-docs/SKILL.md +1 -1
  75. package/templates/full/.roo/skills/engineering/improve-codebase-architecture/SKILL.md +3 -3
  76. package/templates/full/.roo/skills/engineering/review/SKILL.md +111 -0
  77. package/templates/full/.roo/skills/engineering/scaffold-context/SKILL.md +66 -0
  78. package/templates/full/.roo/skills/engineering/scaffold-context/templates/writing-patterns.md +17 -0
  79. package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/SKILL.md +3 -3
  80. package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/domain.md +2 -3
  81. package/templates/full/.roo/skills/engineering/tdd/SKILL.md +2 -0
  82. package/templates/full/.roo/skills/engineering/tweak/SKILL.md +2 -1
  83. package/templates/full/.roo/skills/engineering/verify/SKILL.md +80 -0
  84. package/templates/full/.roo/skills/in-progress/README.md +0 -1
  85. package/templates/full/.roomodes +3 -3
  86. package/templates/full/.zoo-flow/evals/no-regression-checklist.md +4 -2
  87. package/templates/full/.zoo-flow/evals/routing-cases.jsonl +20 -0
  88. package/templates/full/.zoo-flow/evals/routing-cases.md +27 -3
  89. package/templates/full/.zoo-flow/project-profile.json +24 -0
  90. package/tests/fixtures/bad-routing-cases/bad-json.jsonl +1 -0
  91. package/tests/fixtures/bad-routing-cases/bad-mode.jsonl +1 -0
  92. package/tests/fixtures/bad-routing-cases/missing-command.jsonl +1 -0
  93. package/tests/fixtures/doctor/bad-built-in-delegation/fixture.json +1 -0
  94. package/tests/fixtures/doctor/bad-mode-slug/fixture.json +1 -0
  95. package/tests/fixtures/doctor/bad-skill-wrapper/fixture.json +1 -0
  96. package/tests/fixtures/doctor/bad-zoo-path/fixture.json +1 -0
  97. package/tests/fixtures/doctor/helper-missing-mode/fixture.json +1 -0
  98. package/tests/fixtures/doctor/helper-not-permitted/fixture.json +1 -0
  99. package/tests/fixtures/doctor/manual-good-template/fixture.json +1 -0
  100. package/tests/fixtures/doctor/missing-command/fixture.json +1 -0
  101. package/tests/fixtures/doctor/missing-roomodes/fixture.json +1 -0
  102. package/tests/fixtures/doctor/missing-skill/fixture.json +1 -0
  103. package/tests/fixtures/project-shapes/cli-tool/cmd/root.go +1 -0
  104. package/tests/fixtures/project-shapes/cli-tool/fixture.json +1 -0
  105. package/tests/fixtures/project-shapes/cli-tool/package.json +1 -0
  106. package/tests/fixtures/project-shapes/data-pipeline/fixture.json +1 -0
  107. package/tests/fixtures/project-shapes/data-pipeline/pipelines/invoices.py +1 -0
  108. package/tests/fixtures/project-shapes/data-pipeline/pyproject.toml +2 -0
  109. package/tests/fixtures/project-shapes/library/fixture.json +1 -0
  110. package/tests/fixtures/project-shapes/library/package.json +1 -0
  111. package/tests/fixtures/project-shapes/library/src/index.ts +1 -0
  112. package/tests/fixtures/project-shapes/monorepo/fixture.json +1 -0
  113. package/tests/fixtures/project-shapes/monorepo/package.json +1 -0
  114. package/tests/fixtures/project-shapes/monorepo/packages/core/index.ts +1 -0
  115. package/tests/fixtures/project-shapes/monorepo/packages/web/index.ts +1 -0
  116. package/tests/fixtures/project-shapes/serverless/fixture.json +1 -0
  117. package/tests/fixtures/project-shapes/serverless/functions/webhook.ts +1 -0
  118. package/tests/fixtures/project-shapes/serverless/package.json +1 -0
  119. package/tests/fixtures/project-shapes/web-app/app/routes/index.tsx +1 -0
  120. package/tests/fixtures/project-shapes/web-app/fixture.json +1 -0
  121. package/tests/fixtures/project-shapes/web-app/package.json +1 -0
  122. package/tests/golden-transcripts/01-small-tweak-golden.md +21 -0
  123. package/tests/golden-transcripts/02-diagnosis-golden.md +26 -0
  124. package/tests/golden-transcripts/03-verification-golden.md +24 -0
  125. package/tests/golden-transcripts/04-review-golden.md +26 -0
  126. package/tests/golden-transcripts/05-feature-planning-golden.md +23 -0
  127. 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 --template-only
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 bundled template
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 validateTemplate(rootDir) {
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
- if (pathExists(roomodesPath)) {
186
- try {
187
- const parsed = readJson(roomodesPath);
188
- if (!Array.isArray(parsed.customModes)) {
189
- failures.push(".roomodes must contain customModes array");
190
- }
191
- } catch (error) {
192
- failures.push(error.message);
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 skill-wrapper command references. Each command file in
197
- // .roo/commands/ may either declare a skill via `Skill:
198
- // .roo/skills/.../SKILL.md` (skill-wrapper command) or contain its
199
- // workflow steps directly. Direct-workflow commands are valid; we only
200
- // verify referenced skills exist.
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 (!match) continue;
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 allFiles = walkFiles(rootDir);
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 rootToCheck = args.has("--template-only") ? templateRoot : process.cwd();
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
- console.log(`Zoo Flow doctor passed: ${rootToCheck}`);
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));