@mhd-ghaith-abtah/flow 0.7.2-beta.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 (53) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/adapters/e2e/_interface.md +42 -0
  5. package/adapters/e2e/none.md +15 -0
  6. package/adapters/e2e/playwright-mcp.md +86 -0
  7. package/adapters/issue-tracker/_interface.md +46 -0
  8. package/adapters/issue-tracker/github-issues.md +103 -0
  9. package/adapters/issue-tracker/linear.md +65 -0
  10. package/adapters/issue-tracker/none.md +26 -0
  11. package/adapters/pr/_interface.md +29 -0
  12. package/adapters/pr/github.md +61 -0
  13. package/adapters/pr/none.md +32 -0
  14. package/adapters/verify/_interface.md +26 -0
  15. package/adapters/verify/custom.md +27 -0
  16. package/adapters/verify/make.md +30 -0
  17. package/adapters/verify/pnpm.md +27 -0
  18. package/bin/flow.js +129 -0
  19. package/catalog.yaml +364 -0
  20. package/docs/adapters.md +99 -0
  21. package/docs/migrate-from-bmad.md +95 -0
  22. package/docs/profiles.md +81 -0
  23. package/docs/quickstart.md +82 -0
  24. package/lib/catalog.js +164 -0
  25. package/lib/commands/add.js +147 -0
  26. package/lib/commands/doctor.js +392 -0
  27. package/lib/commands/init.js +86 -0
  28. package/lib/commands/install.js +181 -0
  29. package/lib/commands/plan.js +108 -0
  30. package/lib/commands/remove.js +87 -0
  31. package/lib/commands/uninstall.js +157 -0
  32. package/lib/repo-root.js +53 -0
  33. package/package.json +62 -0
  34. package/schemas/catalog.schema.json +155 -0
  35. package/schemas/flow-config.schema.json +85 -0
  36. package/schemas/install-state.schema.json +79 -0
  37. package/skills/flow-doctor/SKILL.md +15 -0
  38. package/skills/flow-doctor/workflow.md +157 -0
  39. package/skills/flow-init/SKILL.md +10 -0
  40. package/skills/flow-init/workflow.md +420 -0
  41. package/skills/flow-sprint/SKILL.md +10 -0
  42. package/skills/flow-sprint/workflow.md +394 -0
  43. package/skills/flow-story/SKILL.md +12 -0
  44. package/skills/flow-story/workflow.md +531 -0
  45. package/templates/claude-md-section.md.tmpl +55 -0
  46. package/templates/flow-readme.md.tmpl +34 -0
  47. package/templates/flow.config.yaml.tmpl +94 -0
  48. package/templates/pr.md.tmpl +40 -0
  49. package/templates/retro.md.tmpl +58 -0
  50. package/templates/sprint.yaml.tmpl +18 -0
  51. package/templates/story.md.tmpl +35 -0
  52. package/tools/fix-caveman-shrink.sh +68 -0
  53. package/tools/lint-changelog.js +98 -0
@@ -0,0 +1,108 @@
1
+ // lib/commands/plan.js — `flow plan` dry-run resolver.
2
+ //
3
+ // Prints the resolved install plan for a given profile: which Flow components,
4
+ // adapters, MCPs, and upstream subsets would be installed. Does NOT execute
5
+ // anything — purely a preview / scriptable plan emitter.
6
+ //
7
+ // Usage:
8
+ // flow plan --profile standard
9
+ // flow plan --profile team --json
10
+ // flow plan --profile mini --without adapter:e2e-playwright-mcp
11
+
12
+ import chalk from 'chalk';
13
+ import { loadCatalog, resolveProfile, listProfiles } from '../catalog.js';
14
+ import { resolveRepoRoot } from '../repo-root.js';
15
+
16
+ /**
17
+ * @param {Object} args - parsed yargs args
18
+ * @param {Object} ctx
19
+ */
20
+ export default async function plan(args, ctx) {
21
+ const repoRoot = ctx.repoRoot ?? resolveRepoRoot(import.meta.url);
22
+ const catalog = loadCatalog(repoRoot);
23
+
24
+ const profileName = args.profile ?? 'standard';
25
+ if (!catalog.profiles[profileName]) {
26
+ console.error(chalk.red(`✗ Unknown profile: ${profileName}`));
27
+ console.error(` Available: ${listProfiles(catalog).join(', ')}`);
28
+ return 1;
29
+ }
30
+
31
+ const profile = resolveProfile(catalog, profileName);
32
+
33
+ // Apply --with / --without overrides (no-op if empty).
34
+ const withList = toArray(args.with);
35
+ const withoutList = toArray(args.without);
36
+ const adapters = applyOverrides(profile.adapters, withList, withoutList, catalog);
37
+
38
+ const planSpec = {
39
+ profile: profileName,
40
+ description: profile.description,
41
+ flow_components: profile.flow_components,
42
+ adapters,
43
+ mcps: profile.mcps,
44
+ upstreams: {
45
+ bmad: profile.bmad_subset,
46
+ ecc: profile.ecc_subset,
47
+ caveman: profile.caveman_subset
48
+ },
49
+ features: profile.features ?? {}
50
+ };
51
+
52
+ if (args.json) {
53
+ console.log(JSON.stringify(planSpec, null, 2));
54
+ return 0;
55
+ }
56
+
57
+ renderHuman(planSpec, ctx);
58
+ return 0;
59
+ }
60
+
61
+ function toArray(v) {
62
+ if (v == null) return [];
63
+ return Array.isArray(v) ? v : [v];
64
+ }
65
+
66
+ function applyOverrides(adapters, withList, withoutList, catalog) {
67
+ let result = [...adapters];
68
+ for (const id of withList) result.push(id);
69
+ result = result.filter(id => !withoutList.includes(id));
70
+
71
+ // De-dupe by family (later wins).
72
+ const byFamily = new Map();
73
+ for (const id of result) {
74
+ const a = catalog.adapters.find(x => x.id === id);
75
+ const family = a?.family ?? id;
76
+ byFamily.set(family, id);
77
+ }
78
+ return [...byFamily.values()];
79
+ }
80
+
81
+ function renderHuman(p, ctx) {
82
+ const lines = [];
83
+ lines.push(chalk.bold(`━━━ flow plan ━━━`));
84
+ lines.push(`Profile: ${chalk.cyan(p.profile)} — ${p.description}`);
85
+ lines.push('');
86
+ lines.push(chalk.bold('Flow components:'));
87
+ for (const c of p.flow_components) lines.push(` ${chalk.green('+')} ${c}`);
88
+ lines.push('');
89
+ lines.push(chalk.bold('Adapters:'));
90
+ for (const a of p.adapters) lines.push(` ${chalk.green('+')} ${a}`);
91
+ lines.push('');
92
+ lines.push(chalk.bold('MCPs:'));
93
+ if (p.mcps.length === 0) lines.push(` ${chalk.dim('(none)')}`);
94
+ for (const m of p.mcps) lines.push(` ${chalk.green('+')} ${m}`);
95
+ lines.push('');
96
+ lines.push(chalk.bold('Upstreams:'));
97
+ lines.push(` BMad: ${p.upstreams.bmad}`);
98
+ lines.push(` ECC: ${p.upstreams.ecc}`);
99
+ lines.push(` Caveman: ${p.upstreams.caveman}`);
100
+ if (Object.keys(p.features).length > 0) {
101
+ lines.push('');
102
+ lines.push(chalk.bold('Features:'));
103
+ for (const [k, v] of Object.entries(p.features)) lines.push(` ${k}: ${v}`);
104
+ }
105
+ lines.push('');
106
+ lines.push(chalk.dim('This is a preview. Run `/flow-init` inside Claude Code to execute.'));
107
+ console.log(lines.join('\n'));
108
+ }
@@ -0,0 +1,87 @@
1
+ // lib/commands/remove.js — `flow remove <component-id>` removes an adapter.
2
+ //
3
+ // Counterpart to `flow add`. For adapters, sets the family back to the `none`
4
+ // variant (which keeps Flow's flow-story phase decision happy — it expects
5
+ // SOME adapter per family). Does NOT remove the adapter file from disk (other
6
+ // projects might still use it via the home-scope install).
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { resolve, join } from 'node:path';
10
+ import chalk from 'chalk';
11
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
12
+ import { loadCatalog } from '../catalog.js';
13
+ import { resolveRepoRoot } from '../repo-root.js';
14
+
15
+ export default async function remove(args, ctx) {
16
+ const repoRoot = ctx.repoRoot ?? resolveRepoRoot(import.meta.url);
17
+ const catalog = loadCatalog(repoRoot);
18
+ const yes = Boolean(args.yes);
19
+ const dryRun = Boolean(args['dry-run']);
20
+
21
+ const componentId = args._?.[0];
22
+ if (!componentId) {
23
+ console.error(chalk.red('✗ Missing component id. Example: flow remove adapter:e2e-playwright-mcp'));
24
+ return 1;
25
+ }
26
+
27
+ const adapter = catalog.adapters?.find((a) => a.id === componentId);
28
+ if (!adapter) {
29
+ console.error(chalk.red(`✗ Unknown adapter (or not an adapter — only adapters are removable via this command): ${componentId}`));
30
+ return 1;
31
+ }
32
+
33
+ // Find the "none" sibling for the same family.
34
+ const noneId = `adapter:${adapter.family}-none`;
35
+ const noneExists = catalog.adapters?.some((a) => a.id === noneId);
36
+ if (!noneExists) {
37
+ console.error(chalk.red(`✗ No '${noneId}' fallback in catalog. Refusing to remove without a fallback.`));
38
+ return 1;
39
+ }
40
+
41
+ console.log(chalk.bold(`━━━ flow remove ${componentId} ━━━`));
42
+ console.log();
43
+ console.log(`Family: ${adapter.family}`);
44
+ console.log(`Fallback: ${noneId} (flow-story expects SOME adapter per family)`);
45
+ console.log();
46
+
47
+ const configPath = join(ctx.cwd, 'flow.config.yaml');
48
+ if (!existsSync(configPath)) {
49
+ console.error(chalk.yellow('⚠'), `No flow.config.yaml in ${ctx.cwd}. Nothing to update.`);
50
+ return 0;
51
+ }
52
+
53
+ const config = parseYaml(readFileSync(configPath, 'utf-8')) || {};
54
+ config.adapters = config.adapters || {};
55
+ const configKey = adapter.family.replace(/-/g, '_');
56
+ const currentShort = config.adapters[configKey];
57
+ const shortIdOfThis = adapter.id.replace(`adapter:${adapter.family}-`, '');
58
+
59
+ if (currentShort !== shortIdOfThis) {
60
+ console.log(chalk.yellow('ℹ'), `flow.config.yaml.adapters.${configKey} is '${currentShort}', not '${shortIdOfThis}'.`);
61
+ console.log(chalk.dim(' Nothing to remove.'));
62
+ return 0;
63
+ }
64
+
65
+ console.log(`Would set flow.config.yaml.adapters.${configKey}: ${shortIdOfThis} → none`);
66
+
67
+ if (dryRun) {
68
+ console.log();
69
+ console.log(chalk.dim('--dry-run: stopping before write.'));
70
+ return 0;
71
+ }
72
+
73
+ if (!yes) {
74
+ console.log();
75
+ console.log(chalk.yellow('?'), 'Re-run with --yes to confirm.');
76
+ return 0;
77
+ }
78
+
79
+ config.adapters[configKey] = 'none';
80
+ writeFileSync(configPath, stringifyYaml(config));
81
+ console.log();
82
+ console.log(` ${chalk.cyan('↻')} flow.config.yaml: adapters.${configKey} ${shortIdOfThis} → none`);
83
+ console.log();
84
+ console.log(chalk.dim(`Note: ${adapter.id}'s files at ~/.claude/skills/flow-story/adapters/${adapter.family}/`));
85
+ console.log(chalk.dim(`were NOT removed — other projects may still use them. Re-add with: flow add ${adapter.id}`));
86
+ return 0;
87
+ }
@@ -0,0 +1,157 @@
1
+ // lib/commands/uninstall.js — `flow uninstall` removal path.
2
+ //
3
+ // Removes Flow's own skills, adapters, and per-scope state. Does NOT touch
4
+ // BMad, ECC, or Caveman — those are owned by their own installers and Flow
5
+ // only records that they were used.
6
+ //
7
+ // Default: --scope project (only this project's .claude/flow/). Pass
8
+ // --scope home to remove the user-level install. Pass --scope both to do both.
9
+ //
10
+ // Default: dry-run. Prints what WOULD be removed. Pass --execute to actually
11
+ // remove. Refuses to run without --execute unless --yes is also passed.
12
+
13
+ import { existsSync, rmSync, readFileSync } from 'node:fs';
14
+ import { resolve, join } from 'node:path';
15
+ import chalk from 'chalk';
16
+ import { loadCatalog } from '../catalog.js';
17
+ import { resolveRepoRoot } from '../repo-root.js';
18
+
19
+ const HOME_PATHS = [
20
+ '$HOME/.claude/skills/flow-init',
21
+ '$HOME/.claude/skills/flow-sprint',
22
+ '$HOME/.claude/skills/flow-story',
23
+ '$HOME/.claude/skills/flow-doctor',
24
+ '$HOME/.claude/flow'
25
+ ];
26
+
27
+ const PROJECT_PATHS = [
28
+ '.claude/flow',
29
+ 'flow.config.yaml',
30
+ 'flow.config.local.yaml'
31
+ ];
32
+
33
+ const PROJECT_PATHS_KEEP_BY_DEFAULT = [
34
+ // These are user content — keep unless --remove-stories is passed.
35
+ 'docs/flow'
36
+ ];
37
+
38
+ /**
39
+ * @param {Object} args
40
+ * @param {Object} ctx
41
+ */
42
+ export default async function uninstall(args, ctx) {
43
+ const repoRoot = ctx.repoRoot ?? resolveRepoRoot(import.meta.url);
44
+ loadCatalog(repoRoot); // validate parseable
45
+
46
+ const scope = args.scope ?? 'project';
47
+ const execute = Boolean(args.execute);
48
+ const yes = Boolean(args.yes);
49
+ const removeStories = Boolean(args['remove-stories']);
50
+ const removeBackups = Boolean(args['remove-backups']);
51
+
52
+ if (!['project', 'home', 'both'].includes(scope)) {
53
+ console.error(chalk.red(`✗ Unknown scope: ${scope}. Use project | home | both.`));
54
+ return 1;
55
+ }
56
+
57
+ const plan = buildPlan({ scope, removeStories, removeBackups, cwd: ctx.cwd, home: ctx.home });
58
+
59
+ renderPlan(plan, { execute });
60
+
61
+ if (!execute) {
62
+ console.log();
63
+ console.log(chalk.dim('Dry run. Re-run with --execute to actually remove. Add --yes to skip the confirm.'));
64
+ return 0;
65
+ }
66
+
67
+ if (!yes) {
68
+ console.log();
69
+ console.log(chalk.yellow('⚠'), 'About to remove the above. Re-run with --execute --yes to confirm.');
70
+ return 0;
71
+ }
72
+
73
+ // Execute.
74
+ console.log();
75
+ console.log(chalk.bold('Removing:'));
76
+ let removed = 0;
77
+ let kept = 0;
78
+ for (const item of plan.toRemove) {
79
+ if (!existsSync(item.resolved)) {
80
+ console.log(` ${chalk.dim('skip (not present)')} ${item.path}`);
81
+ kept++;
82
+ continue;
83
+ }
84
+ try {
85
+ rmSync(item.resolved, { recursive: true, force: true });
86
+ console.log(` ${chalk.green('removed')} ${item.path}`);
87
+ removed++;
88
+ } catch (err) {
89
+ console.log(` ${chalk.red('failed')} ${item.path}: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ console.log();
94
+ console.log(chalk.bold(`Done: ${removed} removed, ${kept} not present.`));
95
+ console.log();
96
+ console.log(chalk.dim('Flow does NOT remove BMad, ECC, or Caveman — they were installed by their own'));
97
+ console.log(chalk.dim('installers and Flow only recorded that they were used. Remove them via:'));
98
+ console.log(chalk.dim(' BMad: rm -rf _bmad/ docs/_bmad-output/ (or `npx bmad-method uninstall`)'));
99
+ console.log(chalk.dim(' ECC: ~/.claude/rules/uninstall.sh (or rm -rf ~/.claude/rules/)'));
100
+ console.log(chalk.dim(' Caveman: rm -rf ~/.claude/plugins/cache/caveman/ and `claude mcp remove caveman-shrink`'));
101
+
102
+ return 0;
103
+ }
104
+
105
+ function buildPlan({ scope, removeStories, removeBackups, cwd, home }) {
106
+ const toRemove = [];
107
+
108
+ if (scope === 'home' || scope === 'both') {
109
+ for (const p of HOME_PATHS) {
110
+ toRemove.push({ path: p, resolved: resolveHomePath(p, home), kind: 'home' });
111
+ }
112
+ }
113
+
114
+ if (scope === 'project' || scope === 'both') {
115
+ for (const p of PROJECT_PATHS) {
116
+ toRemove.push({ path: p, resolved: resolve(cwd, p), kind: 'project' });
117
+ }
118
+ if (removeStories) {
119
+ for (const p of PROJECT_PATHS_KEEP_BY_DEFAULT) {
120
+ toRemove.push({ path: p, resolved: resolve(cwd, p), kind: 'project-user-content' });
121
+ }
122
+ }
123
+ }
124
+
125
+ const toKeep = [];
126
+ if (scope !== 'home' && !removeStories) {
127
+ for (const p of PROJECT_PATHS_KEEP_BY_DEFAULT) {
128
+ toKeep.push({ path: p, resolved: resolve(cwd, p), reason: 'user content — pass --remove-stories to drop' });
129
+ }
130
+ }
131
+ toKeep.push({ path: 'BMad', reason: 'owned by BMad installer; not removed' });
132
+ toKeep.push({ path: 'ECC', reason: 'owned by ECC installer; not removed' });
133
+ toKeep.push({ path: 'Caveman', reason: 'owned by Caveman installer; not removed' });
134
+
135
+ return { scope, toRemove, toKeep, removeBackups };
136
+ }
137
+
138
+ function resolveHomePath(p, home) {
139
+ return p.replace(/\$HOME/g, home);
140
+ }
141
+
142
+ function renderPlan(plan, { execute }) {
143
+ console.log(chalk.bold(`━━━ flow uninstall plan (scope: ${plan.scope}) ━━━`));
144
+ console.log();
145
+ console.log(chalk.bold(execute ? 'Will remove:' : 'Would remove:'));
146
+ for (const item of plan.toRemove) {
147
+ const present = existsSync(item.resolved);
148
+ const tag = present ? chalk.red('-') : chalk.dim('-');
149
+ const suffix = present ? '' : chalk.dim(' (not present)');
150
+ console.log(` ${tag} ${item.path}${suffix}`);
151
+ }
152
+ console.log();
153
+ console.log(chalk.bold('Keeping:'));
154
+ for (const item of plan.toKeep) {
155
+ console.log(` ${chalk.green('+')} ${item.path} ${chalk.dim('— ' + item.reason)}`);
156
+ }
157
+ }
@@ -0,0 +1,53 @@
1
+ // lib/repo-root.js — resolve Flow's repo root the same way the skills do.
2
+ //
3
+ // Resolution order (matches skills/flow-init/workflow.md step 1):
4
+ // 1. $FLOW_REPO_ROOT env (explicit override).
5
+ // 2. Walk up from import.meta.url's directory looking for catalog.yaml
6
+ // (works whether installed via npm at ~/.npm-global/lib/node_modules/...
7
+ // or via dev-link symlinks to a checked-out clone).
8
+ // 3. CWD if it contains catalog.yaml (dev mode).
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { dirname, resolve } from 'node:path';
12
+
13
+ /**
14
+ * @param {string} startDir
15
+ * @returns {string|null}
16
+ */
17
+ function walkUpForCatalog(startDir) {
18
+ let dir = startDir;
19
+ while (dir && dir !== dirname(dir)) {
20
+ if (existsSync(resolve(dir, 'catalog.yaml'))) return dir;
21
+ dir = dirname(dir);
22
+ }
23
+ return null;
24
+ }
25
+
26
+ /**
27
+ * @param {string} importMetaUrl - typically `import.meta.url` from the caller
28
+ * @returns {string}
29
+ */
30
+ export function resolveRepoRoot(importMetaUrl) {
31
+ if (process.env.FLOW_REPO_ROOT) {
32
+ if (!existsSync(resolve(process.env.FLOW_REPO_ROOT, 'catalog.yaml'))) {
33
+ throw new Error(`FLOW_REPO_ROOT=${process.env.FLOW_REPO_ROOT} but no catalog.yaml found there`);
34
+ }
35
+ return process.env.FLOW_REPO_ROOT;
36
+ }
37
+
38
+ // Walk up from the caller's file location (works for npm install + symlinks).
39
+ const callerPath = new URL(importMetaUrl).pathname;
40
+ const fromCaller = walkUpForCatalog(dirname(callerPath));
41
+ if (fromCaller) return fromCaller;
42
+
43
+ // Fall back to CWD (dev mode).
44
+ const fromCwd = walkUpForCatalog(process.cwd());
45
+ if (fromCwd) return fromCwd;
46
+
47
+ throw new Error(
48
+ "Couldn't locate catalog.yaml. Either:\n" +
49
+ " • Set FLOW_REPO_ROOT to the directory containing catalog.yaml, or\n" +
50
+ " • Run `flow` from inside a Flow clone, or\n" +
51
+ " • Re-install Flow (clone https://github.com/mhd-ghaith-abtah/flow; tools/dev-link.sh)."
52
+ );
53
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@mhd-ghaith-abtah/flow",
3
+ "version": "0.7.2-beta.0",
4
+ "description": "Token-light per-story workflow for Claude Code. Delegates to BMad + ECC.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "flow": "bin/flow.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib/**/*.js",
13
+ "!lib/**/*.test.js",
14
+ "skills",
15
+ "adapters",
16
+ "templates",
17
+ "schemas",
18
+ "tools/lint-changelog.js",
19
+ "tools/fix-caveman-shrink.sh",
20
+ "catalog.yaml",
21
+ "docs/quickstart.md",
22
+ "docs/adapters.md",
23
+ "docs/profiles.md",
24
+ "docs/migrate-from-bmad.md",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "scripts": {
30
+ "test": "node --test",
31
+ "smoke": "node bin/flow.js plan --profile standard --dry-run"
32
+ },
33
+ "dependencies": {
34
+ "@inquirer/prompts": "^7.0.0",
35
+ "ajv": "^8.17.1",
36
+ "chalk": "^5.3.0",
37
+ "execa": "^9.5.1",
38
+ "ora": "^8.1.1",
39
+ "yaml": "^2.6.1",
40
+ "yargs-parser": "^21.1.1"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/mhd-ghaith-abtah/flow.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/mhd-ghaith-abtah/flow/issues"
51
+ },
52
+ "homepage": "https://github.com/mhd-ghaith-abtah/flow#readme",
53
+ "keywords": [
54
+ "claude-code",
55
+ "workflow",
56
+ "sprint",
57
+ "story",
58
+ "bmad",
59
+ "ecc",
60
+ "spec-driven"
61
+ ]
62
+ }
@@ -0,0 +1,155 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/mhd-ghaith-abtah/flow/schemas/catalog.schema.json",
4
+ "title": "Flow catalog",
5
+ "description": "The single source of truth for what Flow can install. Validated on every install.",
6
+ "type": "object",
7
+ "required": ["version", "flow_components", "adapters", "profiles"],
8
+ "properties": {
9
+ "version": { "type": "integer", "minimum": 1 },
10
+ "flow_version_compat": { "type": "string" },
11
+
12
+ "flow_components": {
13
+ "type": "array",
14
+ "items": { "$ref": "#/definitions/flow_component" }
15
+ },
16
+
17
+ "adapters": {
18
+ "type": "array",
19
+ "items": { "$ref": "#/definitions/adapter" }
20
+ },
21
+
22
+ "mcps": {
23
+ "type": "array",
24
+ "items": { "$ref": "#/definitions/mcp" }
25
+ },
26
+
27
+ "upstreams": {
28
+ "type": "object",
29
+ "additionalProperties": { "$ref": "#/definitions/upstream" }
30
+ },
31
+
32
+ "profiles": {
33
+ "type": "object",
34
+ "minProperties": 1,
35
+ "additionalProperties": { "$ref": "#/definitions/profile" }
36
+ }
37
+ },
38
+ "additionalProperties": false,
39
+
40
+ "definitions": {
41
+ "flow_component": {
42
+ "type": "object",
43
+ "required": ["id", "family"],
44
+ "properties": {
45
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+:[a-z0-9-]+$" },
46
+ "family": { "type": "string", "enum": ["core", "adapter", "mcp"] },
47
+ "description": { "type": "string" },
48
+ "required": { "type": "boolean" },
49
+ "operations": {
50
+ "type": "array",
51
+ "items": { "$ref": "#/definitions/operation" }
52
+ }
53
+ },
54
+ "additionalProperties": true
55
+ },
56
+
57
+ "adapter": {
58
+ "type": "object",
59
+ "required": ["id", "family"],
60
+ "properties": {
61
+ "id": { "type": "string", "pattern": "^adapter:[a-z0-9-]+$" },
62
+ "family": {
63
+ "type": "string",
64
+ "enum": ["issue-tracker", "pr", "e2e", "verify", "notification"]
65
+ },
66
+ "description": { "type": "string" },
67
+ "needs_mcp": { "type": "array", "items": { "type": "string" } },
68
+ "needs_cli": { "type": "array", "items": { "type": "string" } },
69
+ "detect": { "type": "string" },
70
+ "config_keys": { "type": "array" },
71
+ "runtime_deps": { "type": "array" },
72
+ "operations": {
73
+ "type": "array",
74
+ "items": { "$ref": "#/definitions/operation" }
75
+ }
76
+ },
77
+ "additionalProperties": true
78
+ },
79
+
80
+ "mcp": {
81
+ "type": "object",
82
+ "required": ["id"],
83
+ "properties": {
84
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
85
+ "name": { "type": "string" },
86
+ "description": { "type": "string" },
87
+ "package": { "type": "string" },
88
+ "install_cmd": { "type": "string" },
89
+ "detect_cmd": { "type": "string" },
90
+ "scope_default": { "type": "string", "enum": ["home", "project"] },
91
+ "auth": {
92
+ "type": "string",
93
+ "enum": ["none", "api_token", "oauth_browser", "oauth_device"]
94
+ },
95
+ "auth_instructions": { "type": "string" },
96
+ "env": { "type": "array" },
97
+ "optional": { "type": "boolean" }
98
+ },
99
+ "additionalProperties": true
100
+ },
101
+
102
+ "upstream": {
103
+ "type": "object",
104
+ "required": ["name"],
105
+ "properties": {
106
+ "name": { "type": "string" },
107
+ "repo": { "type": "string" },
108
+ "homepage": { "type": "string" },
109
+ "description": { "type": "string" },
110
+ "detect": { "type": "object" },
111
+ "installer": { "type": "object" },
112
+ "available_modules": { "type": "array" },
113
+ "available_profiles": { "type": "array" },
114
+ "available_modes": { "type": "array" },
115
+ "curated_subsets": { "type": "object" }
116
+ },
117
+ "additionalProperties": true
118
+ },
119
+
120
+ "profile": {
121
+ "type": "object",
122
+ "required": ["description"],
123
+ "properties": {
124
+ "description": { "type": "string" },
125
+ "extends": { "type": "string" },
126
+ "flow_components": { "type": "array", "items": { "type": "string" } },
127
+ "adapters": { "type": "array", "items": { "type": "string" } },
128
+ "mcps": { "type": "array", "items": { "type": "string" } },
129
+ "bmad_subset": { "type": "string" },
130
+ "ecc_subset": { "type": "string" },
131
+ "caveman_subset": { "type": "string" },
132
+ "features": { "type": "object" }
133
+ },
134
+ "additionalProperties": false
135
+ },
136
+
137
+ "operation": {
138
+ "type": "object",
139
+ "properties": {
140
+ "copy": {
141
+ "type": "object",
142
+ "required": ["from", "to"],
143
+ "properties": {
144
+ "from": { "type": "string" },
145
+ "to": { "type": "string" }
146
+ }
147
+ },
148
+ "ensure_dir": { "type": "string" },
149
+ "touch": { "type": "string" },
150
+ "run": { "type": "string" }
151
+ },
152
+ "additionalProperties": false
153
+ }
154
+ }
155
+ }