@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.
Files changed (135) hide show
  1. package/README.md +105 -90
  2. package/bin/zoo-flow.js +405 -56
  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/explore.md +13 -13
  47. package/templates/full/.roo/commands/feature.md +1 -1
  48. package/templates/full/.roo/commands/fix.md +1 -1
  49. package/templates/full/.roo/commands/grill-me.md +2 -1
  50. package/templates/full/.roo/commands/grill-with-docs.md +2 -1
  51. package/templates/full/.roo/commands/handoff.md +2 -1
  52. package/templates/full/.roo/commands/improve-codebase-architecture.md +2 -1
  53. package/templates/full/.roo/commands/prototype.md +1 -1
  54. package/templates/full/.roo/commands/refactor.md +1 -1
  55. package/templates/full/.roo/commands/review.md +11 -0
  56. package/templates/full/.roo/commands/scaffold-context.md +13 -13
  57. package/templates/full/.roo/commands/setup-matt-pocock-skills.md +8 -8
  58. package/templates/full/.roo/commands/tdd.md +1 -1
  59. package/templates/full/.roo/commands/to-issues.md +2 -1
  60. package/templates/full/.roo/commands/to-prd.md +2 -1
  61. package/templates/full/.roo/commands/triage.md +1 -1
  62. package/templates/full/.roo/commands/tweak.md +1 -1
  63. package/templates/full/.roo/commands/update-docs.md +22 -22
  64. package/templates/full/.roo/commands/verify.md +11 -0
  65. package/templates/full/.roo/commands/write-a-skill.md +2 -1
  66. package/templates/full/.roo/commands/zoom-out.md +2 -1
  67. package/templates/full/.roo/rules/01-command-protocol.md +1 -1
  68. package/templates/full/.roo/rules/04-context-economy.md +27 -29
  69. package/templates/full/.roo/rules-code-tweaker/01-completion.md +12 -8
  70. package/templates/full/.roo/rules-custom-orchestrator/00-routing.md +77 -63
  71. package/templates/full/.roo/rules-custom-orchestrator/01-delegation-message.md +59 -55
  72. package/templates/full/.roo/rules-system-architect/02-completion.md +6 -2
  73. package/templates/full/.roo/skills/engineering/README.md +2 -0
  74. package/templates/full/.roo/skills/engineering/commit-and-document/SKILL.md +1 -2
  75. package/templates/full/.roo/skills/engineering/grill-with-docs/ADR-FORMAT.md +1 -1
  76. package/templates/full/.roo/skills/engineering/grill-with-docs/CONTEXT-FORMAT.md +36 -61
  77. package/templates/full/.roo/skills/engineering/grill-with-docs/SKILL.md +1 -1
  78. package/templates/full/.roo/skills/engineering/improve-codebase-architecture/SKILL.md +3 -3
  79. package/templates/full/.roo/skills/engineering/prototype/SKILL.md +37 -37
  80. package/templates/full/.roo/skills/engineering/review/SKILL.md +111 -0
  81. package/templates/full/.roo/skills/engineering/scaffold-context/SKILL.md +218 -152
  82. package/templates/full/.roo/skills/engineering/scaffold-context/templates/writing-patterns.md +17 -0
  83. package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/SKILL.md +3 -3
  84. package/templates/full/.roo/skills/engineering/setup-matt-pocock-skills/domain.md +2 -3
  85. package/templates/full/.roo/skills/engineering/tdd/SKILL.md +2 -0
  86. package/templates/full/.roo/skills/engineering/to-prd/SKILL.md +57 -57
  87. package/templates/full/.roo/skills/engineering/tweak/SKILL.md +2 -1
  88. package/templates/full/.roo/skills/engineering/verify/SKILL.md +80 -0
  89. package/templates/full/.roo/skills/in-progress/README.md +0 -1
  90. package/templates/full/.roomodes +47 -47
  91. package/templates/full/.zoo-flow/CONTEXT.md +8 -8
  92. package/templates/full/.zoo-flow/START_HERE.md +61 -61
  93. package/templates/full/.zoo-flow/docs/adr/0001-record-architecture-decisions.md +22 -22
  94. package/templates/full/.zoo-flow/evals/no-regression-checklist.md +26 -24
  95. package/templates/full/.zoo-flow/evals/routing-cases.jsonl +20 -0
  96. package/templates/full/.zoo-flow/evals/routing-cases.md +213 -189
  97. package/templates/full/.zoo-flow/project-profile.json +24 -0
  98. package/tests/fixtures/bad-routing-cases/bad-json.jsonl +1 -0
  99. package/tests/fixtures/bad-routing-cases/bad-mode.jsonl +1 -0
  100. package/tests/fixtures/bad-routing-cases/missing-command.jsonl +1 -0
  101. package/tests/fixtures/doctor/bad-built-in-delegation/fixture.json +1 -0
  102. package/tests/fixtures/doctor/bad-mode-slug/fixture.json +1 -0
  103. package/tests/fixtures/doctor/bad-skill-wrapper/fixture.json +1 -0
  104. package/tests/fixtures/doctor/bad-zoo-path/fixture.json +1 -0
  105. package/tests/fixtures/doctor/helper-missing-mode/fixture.json +1 -0
  106. package/tests/fixtures/doctor/helper-not-permitted/fixture.json +1 -0
  107. package/tests/fixtures/doctor/manual-good-template/fixture.json +1 -0
  108. package/tests/fixtures/doctor/missing-command/fixture.json +1 -0
  109. package/tests/fixtures/doctor/missing-roomodes/fixture.json +1 -0
  110. package/tests/fixtures/doctor/missing-skill/fixture.json +1 -0
  111. package/tests/fixtures/project-shapes/cli-tool/cmd/root.go +1 -0
  112. package/tests/fixtures/project-shapes/cli-tool/fixture.json +1 -0
  113. package/tests/fixtures/project-shapes/cli-tool/package.json +1 -0
  114. package/tests/fixtures/project-shapes/data-pipeline/fixture.json +1 -0
  115. package/tests/fixtures/project-shapes/data-pipeline/pipelines/invoices.py +1 -0
  116. package/tests/fixtures/project-shapes/data-pipeline/pyproject.toml +2 -0
  117. package/tests/fixtures/project-shapes/library/fixture.json +1 -0
  118. package/tests/fixtures/project-shapes/library/package.json +1 -0
  119. package/tests/fixtures/project-shapes/library/src/index.ts +1 -0
  120. package/tests/fixtures/project-shapes/monorepo/fixture.json +1 -0
  121. package/tests/fixtures/project-shapes/monorepo/package.json +1 -0
  122. package/tests/fixtures/project-shapes/monorepo/packages/core/index.ts +1 -0
  123. package/tests/fixtures/project-shapes/monorepo/packages/web/index.ts +1 -0
  124. package/tests/fixtures/project-shapes/serverless/fixture.json +1 -0
  125. package/tests/fixtures/project-shapes/serverless/functions/webhook.ts +1 -0
  126. package/tests/fixtures/project-shapes/serverless/package.json +1 -0
  127. package/tests/fixtures/project-shapes/web-app/app/routes/index.tsx +1 -0
  128. package/tests/fixtures/project-shapes/web-app/fixture.json +1 -0
  129. package/tests/fixtures/project-shapes/web-app/package.json +1 -0
  130. package/tests/golden-transcripts/01-small-tweak-golden.md +21 -0
  131. package/tests/golden-transcripts/02-diagnosis-golden.md +26 -0
  132. package/tests/golden-transcripts/03-verification-golden.md +24 -0
  133. package/tests/golden-transcripts/04-review-golden.md +26 -0
  134. package/tests/golden-transcripts/05-feature-planning-golden.md +23 -0
  135. 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
@@ -80,20 +127,21 @@ function backupIfExists(projectRoot, backupDir, relativePath) {
80
127
  return true;
81
128
  }
82
129
 
83
- const ZOO_FLOW_GITIGNORE_MARKER = "# Zoo Flow — local context scaffolding";
130
+ const ZOO_FLOW_GITIGNORE_MARKER = "# Zoo Flow — generated config (never committed)";
84
131
 
85
132
  function appendZooFlowToGitignore(projectRoot) {
86
133
  const gi = path.join(projectRoot, ".gitignore");
134
+ const entries = [".roomodes", ".roo/", ".zoo-flow/"];
87
135
 
88
136
  if (pathExists(gi)) {
89
137
  const content = fs.readFileSync(gi, "utf8");
90
138
  if (content.includes(ZOO_FLOW_GITIGNORE_MARKER)) return false;
91
- const addition = `\n${ZOO_FLOW_GITIGNORE_MARKER} (never committed)\n.zoo-flow/\n`;
139
+ const addition = `\n${ZOO_FLOW_GITIGNORE_MARKER}\n${entries.join("\n")}\n`;
92
140
  fs.appendFileSync(gi, addition);
93
141
  return true;
94
142
  }
95
143
 
96
- const fresh = `${ZOO_FLOW_GITIGNORE_MARKER} (never committed)\n.zoo-flow/\n`;
144
+ const fresh = `${ZOO_FLOW_GITIGNORE_MARKER}\n${entries.join("\n")}\n`;
97
145
  fs.writeFileSync(gi, fresh);
98
146
  return true;
99
147
  }
@@ -143,7 +191,7 @@ function readJson(filePath) {
143
191
  }
144
192
  }
145
193
 
146
- function walkFiles(rootDir) {
194
+ function walkFiles(rootDir) {
147
195
  const files = [];
148
196
 
149
197
  function walk(current) {
@@ -163,10 +211,110 @@ function walkFiles(rootDir) {
163
211
  }
164
212
 
165
213
  walk(rootDir);
166
- return files;
167
- }
168
-
169
- 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) {
170
318
  const roomodesPath = path.join(rootDir, ".roomodes");
171
319
  const rooDir = path.join(rootDir, ".roo");
172
320
  const commandsDir = path.join(rooDir, "commands");
@@ -181,33 +329,38 @@ function validateTemplate(rootDir) {
181
329
  if (!pathExists(skillsDir)) failures.push("Missing .roo/skills/");
182
330
  if (!pathExists(rulesDir)) failures.push("Missing .roo/rules/");
183
331
 
184
- if (pathExists(roomodesPath)) {
185
- try {
186
- const parsed = readJson(roomodesPath);
187
- if (!Array.isArray(parsed.customModes)) {
188
- failures.push(".roomodes must contain customModes array");
189
- }
190
- } catch (error) {
191
- failures.push(error.message);
192
- }
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
+ }
193
343
  }
194
344
 
195
- // Validate skill-wrapper command references. Each command file in
196
- // .roo/commands/ may either declare a skill via `Skill:
197
- // .roo/skills/.../SKILL.md` (skill-wrapper command) or contain its
198
- // workflow steps directly. Direct-workflow commands are valid; we only
199
- // verify referenced skills exist.
200
- if (pathExists(commandsDir)) {
201
- const skillRefRegex = /^Skill:\s*`?(\.roo\/skills\/[^\s`]+SKILL\.md)`?\s*$/m;
202
-
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
+
203
350
  for (const entry of fs.readdirSync(commandsDir)) {
204
351
  if (!entry.endsWith(".md")) continue;
205
352
 
206
- const commandFile = path.join(commandsDir, entry);
207
- const text = fs.readFileSync(commandFile, "utf8");
208
- const match = text.match(skillRefRegex);
209
-
210
- 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;
211
364
 
212
365
  const skillRelative = match[1];
213
366
  const skillAbsolute = path.join(rootDir, skillRelative);
@@ -216,14 +369,112 @@ function validateTemplate(rootDir) {
216
369
  failures.push(
217
370
  `Command ${path.relative(rootDir, commandFile)} references missing skill: ${skillRelative}`
218
371
  );
219
- }
220
- }
221
- }
222
-
223
- 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
+ }
224
475
  const textFileExtensions = new Set([".md", ".json", ".txt", ".yaml", ".yml"]);
225
476
 
226
- for (const file of allFiles) {
477
+ for (const file of allFiles) {
227
478
  const ext = path.extname(file);
228
479
  if (!textFileExtensions.has(ext)) continue;
229
480
 
@@ -233,15 +484,34 @@ function validateTemplate(rootDir) {
233
484
  ".roo/skills///SKILL.md",
234
485
  ".roo/commands/.md",
235
486
  "your-org/roo-flow",
236
- ".zoo/"
487
+ ".zoo/",
488
+ "CONTEXT-MAP.md",
489
+ "FLOW.md",
490
+ "APP_MAP.md"
237
491
  ];
238
492
 
239
- for (const pattern of badPatterns) {
240
- if (text.includes(pattern)) {
241
- failures.push(`Bad pattern "${pattern}" in ${path.relative(rootDir, file)}`);
242
- }
243
- }
244
- }
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
+ }
245
515
 
246
516
  const requiredRules = [
247
517
  ".roo/rules/00-paths.md",
@@ -325,7 +595,7 @@ Run again with --force to back up and overwrite:
325
595
  copiedLines.push(" - .zoo-flow/docs/adr/0001-record-architecture-decisions.md");
326
596
  }
327
597
  if (didAddGitignore) {
328
- copiedLines.push(" - appended .zoo-flow/ to .gitignore");
598
+ copiedLines.push(" - appended .roo/, .roomodes, .zoo-flow/ to .gitignore");
329
599
  }
330
600
 
331
601
  console.log(`
@@ -348,20 +618,99 @@ When workflow choices appear, type the number manually, e.g. 1.
348
618
 
349
619
  function doctor() {
350
620
  const args = new Set(process.argv.slice(3));
351
- const rootToCheck = args.has("--template-only") ? templateRoot : process.cwd();
621
+ const templateOnly = args.has("--template-only");
622
+ const rootToCheck = templateOnly ? templateRoot : process.cwd();
352
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
+ }
353
690
 
354
691
  if (failures.length > 0) {
355
692
  console.error("\nZoo Flow doctor found problems:\n");
356
693
  for (const failure of failures) {
357
694
  console.error(`- ${failure}`);
358
695
  }
696
+ if (!templateOnly) {
697
+ console.error("\nRun `npx @fernado03/zoo-flow@latest update --dry-run` to preview replacing local Zoo Flow config.");
698
+ }
359
699
  console.error("");
360
700
  process.exit(1);
361
701
  }
362
702
 
363
- console.log(`Zoo Flow doctor passed: ${rootToCheck}`);
364
- }
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
+ }
365
714
 
366
715
  function update() {
367
716
  const args = new Set(process.argv.slice(3));
@@ -406,7 +755,7 @@ Would replace with latest template:
406
755
  - .roomodes
407
756
  - .roo/
408
757
  ${wouldMigrate.length > 0 ? `\nWould migrate to .zoo-flow/:\n${wouldMigrate.map((p) => ` - ${p}\n`).join("")}` : ""}
409
- Would append .zoo-flow/ to .gitignore (idempotent).
758
+ Would append .roo/, .roomodes, .zoo-flow/ to .gitignore (idempotent).
410
759
 
411
760
  Run this to update:
412
761
  npx @fernado03/zoo-flow@latest update
@@ -442,7 +791,7 @@ ${didBackup ? `Backup:\n ${backupDir}\n\nTo restore the previous config:\n rm
442
791
  2. Open Zoo Code
443
792
  3. Confirm the three custom modes still appear
444
793
  `);
445
- }
794
+ }
446
795
 
447
796
  if (!command || command === "--help" || command === "-h") {
448
797
  console.log(HELP);