@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
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
|
|
@@ -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 —
|
|
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}
|
|
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}
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
196
|
-
// .roo/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|