@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,82 @@
1
+ # Quickstart
2
+
3
+ A 10-minute path from zero to first shipped story.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Claude Code](https://claude.com/claude-code) installed (`claude` on PATH)
8
+ - Node 20+ (for the `flow` CLI; not strictly required if you only use slash commands)
9
+ - Git repo, with `origin` set if you want PRs
10
+
11
+ ## Install
12
+
13
+ Inside Claude Code, in your project root:
14
+
15
+ ```
16
+ /flow-init
17
+ ```
18
+
19
+ This launches an interactive installer. It will:
20
+
21
+ 1. **Detect project shape** — package manager, framework, existing BMad/ECC install, CLAUDE.md presence
22
+ 2. **Ask ~8 questions** — profile (mini/standard/team), adapters (issue tracker, PR platform, E2E, verify), upstream subsets (BMad, ECC, Caveman)
23
+ 3. **Delegate to upstream installers** — `npx bmad-method install --modules <curated>`, `ECC ./install.sh --profile <curated>`, plus Caveman
24
+ 4. **Set up MCP servers** — context7, playwright, linear (if selected)
25
+ 5. **Scaffold docs/flow/ + flow.config.yaml** — sprint.yaml, stories/, journeys/, retros/, deferred.md
26
+ 6. **Optionally migrate** an existing BMad `sprint-status.yaml`
27
+
28
+ Re-running is idempotent — `/flow-init --update` will diff against the recorded state and apply only deltas.
29
+
30
+ ## First story
31
+
32
+ ```
33
+ /flow-sprint add "Wire up Stripe webhook handler" --epic E1 --tags payments,auth
34
+ /flow-sprint next
35
+ /flow-story
36
+ ```
37
+
38
+ `/flow-story` auto-detects the current phase (plan / implement / review / verify / e2e / docs / commit / pr) from `sprint.yaml` + git branch + commit state, and invokes the right ECC primitive at each step. It chains phases automatically until it hits a destructive boundary (commit, PR) or a CRITICAL/HIGH review finding, then pauses for your CONFIRM.
39
+
40
+ **Useful flags:**
41
+
42
+ | Flag | Effect |
43
+ |---|---|
44
+ | `--auto` | No CONFIRM gates. Use for low-risk stories. |
45
+ | `--auto-merge` | After PR opens, poll `gh pr merge --auto` until CI passes (15-min cap). |
46
+ | `--hard-review` | Force adversarial + edge-case reviewers regardless of tags. |
47
+ | `--no-review` | Skip code review. Risky. Use for trivial config tweaks. |
48
+ | `--no-verify` | Skip the verify command. Risky. |
49
+ | `--no-e2e` | Skip the E2E adapter. |
50
+ | `--strict-plan` | Block on plan CONFIRM even with `--auto`. |
51
+ | `--skip-plan` | Treat the story as pre-planned. |
52
+ | `--advise-only` | Print phase + suggested next action; don't execute. |
53
+
54
+ ## Closing out a story
55
+
56
+ After a PR merges, `/flow-story` auto-detects the `merge-done` phase and asks for CONFIRM to flip the story to `done`. Or you can do it manually:
57
+
58
+ ```
59
+ /flow-sprint done E1-001
60
+ ```
61
+
62
+ ## End-of-sprint
63
+
64
+ ```
65
+ /flow-sprint retro
66
+ ```
67
+
68
+ Generates `docs/flow/retros/<date>.md` from your last sprint's stories: what shipped, what got deferred, review-finding rollup, verify failure rate.
69
+
70
+ ## Health check
71
+
72
+ ```
73
+ /flow-doctor
74
+ ```
75
+
76
+ Verifies catalog / state / adapter files / MCP registration / required CLIs / upstream installations. Probes for known bugs (caveman-shrink mis-registered, severity-label stripping, loose marker matches). Add `--fix` for safe auto-repairs (prints the commands; doesn't auto-run anything destructive).
77
+
78
+ ## Next
79
+
80
+ - [profiles.md](profiles.md) — choosing mini vs standard vs team
81
+ - [adapters.md](adapters.md) — picking + swapping integrations
82
+ - [migrate-from-bmad.md](migrate-from-bmad.md) — porting an existing BMad project
package/lib/catalog.js ADDED
@@ -0,0 +1,164 @@
1
+ // lib/catalog.js — load and resolve flow's catalog.yaml.
2
+ //
3
+ // Minimal v0.7 implementation: parse YAML, resolve profile inheritance, expose
4
+ // helpers for `flow plan` and `flow init`. JSON-schema validation lands when
5
+ // schemas/catalog.schema.json is finalized (tracked in task #9).
6
+
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import { parse as parseYaml } from 'yaml';
10
+ import Ajv from 'ajv';
11
+
12
+ /**
13
+ * @typedef {Object} Profile
14
+ * @property {string} description
15
+ * @property {string[]} flow_components
16
+ * @property {string[]} adapters
17
+ * @property {string[]} mcps
18
+ * @property {string} bmad_subset
19
+ * @property {string} ecc_subset
20
+ * @property {string} caveman_subset
21
+ * @property {Object} [features]
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} Catalog
26
+ * @property {number} version
27
+ * @property {string} flow_version_compat
28
+ * @property {Array<Object>} flow_components
29
+ * @property {Array<Object>} adapters
30
+ * @property {Object} [mcps]
31
+ * @property {Object} [upstreams]
32
+ * @property {Record<string, RawProfile>} profiles
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} RawProfile
37
+ * @property {string} description
38
+ * @property {string} [extends]
39
+ * @property {string[]} [flow_components]
40
+ * @property {string[]} [adapters]
41
+ * @property {string[]} [mcps]
42
+ * @property {string} [bmad_subset]
43
+ * @property {string} [ecc_subset]
44
+ * @property {string} [caveman_subset]
45
+ * @property {Object} [features]
46
+ */
47
+
48
+ /**
49
+ * Load and parse catalog.yaml from the given repo root. Validates against
50
+ * schemas/catalog.schema.json if present (issue #11).
51
+ * @param {string} repoRoot
52
+ * @param {Object} [opts]
53
+ * @param {boolean} [opts.validate=true] - run ajv validation if schema present
54
+ * @returns {Catalog}
55
+ */
56
+ export function loadCatalog(repoRoot, opts = {}) {
57
+ const { validate = true } = opts;
58
+ const catalogPath = resolve(repoRoot, 'catalog.yaml');
59
+ const raw = readFileSync(catalogPath, 'utf-8');
60
+ const catalog = parseYaml(raw);
61
+
62
+ if (!catalog || typeof catalog !== 'object') {
63
+ throw new Error(`catalog.yaml at ${catalogPath} did not parse to an object`);
64
+ }
65
+ if (!catalog.profiles || typeof catalog.profiles !== 'object') {
66
+ throw new Error(`catalog.yaml has no 'profiles' section`);
67
+ }
68
+
69
+ if (validate) {
70
+ const schemaPath = resolve(repoRoot, 'schemas', 'catalog.schema.json');
71
+ if (existsSync(schemaPath)) {
72
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
73
+ const ajv = new Ajv({ allErrors: true, strict: false });
74
+ const check = ajv.compile(schema);
75
+ if (!check(catalog)) {
76
+ const errs = (check.errors || []).map(e => ` ${e.instancePath || '/'}: ${e.message}`).join('\n');
77
+ throw new Error(`catalog.yaml failed schema validation:\n${errs}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ return catalog;
83
+ }
84
+
85
+ /**
86
+ * Resolve a profile name to its fully-merged spec (walks `extends:` chain).
87
+ * Adapter lists override family-by-family (later profile wins per family).
88
+ * @param {Catalog} catalog
89
+ * @param {string} profileName
90
+ * @returns {Profile}
91
+ */
92
+ export function resolveProfile(catalog, profileName) {
93
+ const seen = new Set();
94
+ const chain = [];
95
+ let cursor = profileName;
96
+ while (cursor) {
97
+ if (seen.has(cursor)) {
98
+ throw new Error(`Profile inheritance cycle detected at '${cursor}'`);
99
+ }
100
+ seen.add(cursor);
101
+ const profile = catalog.profiles[cursor];
102
+ if (!profile) {
103
+ throw new Error(`Profile '${cursor}' not found in catalog. Available: ${Object.keys(catalog.profiles).join(', ')}`);
104
+ }
105
+ chain.unshift(profile);
106
+ cursor = profile.extends;
107
+ }
108
+
109
+ // Merge: later entries in the chain override earlier ones. For adapters,
110
+ // override by family (one adapter per family — later wins).
111
+ const merged = {
112
+ description: '',
113
+ flow_components: [],
114
+ adapters: [],
115
+ mcps: [],
116
+ bmad_subset: 'none',
117
+ ecc_subset: 'none',
118
+ caveman_subset: 'full',
119
+ features: {}
120
+ };
121
+
122
+ for (const layer of chain) {
123
+ if (layer.description) merged.description = layer.description;
124
+ if (layer.flow_components) merged.flow_components = uniq([...merged.flow_components, ...layer.flow_components]);
125
+ if (layer.adapters) merged.adapters = mergeAdapters(merged.adapters, layer.adapters, catalog);
126
+ if (layer.mcps) merged.mcps = uniq([...merged.mcps, ...layer.mcps]);
127
+ if (layer.bmad_subset) merged.bmad_subset = layer.bmad_subset;
128
+ if (layer.ecc_subset) merged.ecc_subset = layer.ecc_subset;
129
+ if (layer.caveman_subset) merged.caveman_subset = layer.caveman_subset;
130
+ if (layer.features) merged.features = { ...merged.features, ...layer.features };
131
+ }
132
+ return merged;
133
+ }
134
+
135
+ /**
136
+ * Adapters are keyed by family — replacing one adapter for a family overrides
137
+ * any previously-merged adapter for the same family.
138
+ * @param {string[]} current
139
+ * @param {string[]} incoming
140
+ * @param {Catalog} catalog
141
+ * @returns {string[]}
142
+ */
143
+ function mergeAdapters(current, incoming, catalog) {
144
+ const byFamily = new Map();
145
+ for (const id of [...current, ...incoming]) {
146
+ const adapter = catalog.adapters.find(a => a.id === id);
147
+ const family = adapter ? adapter.family : id.split(':')[1]?.split('-')[0] ?? id;
148
+ byFamily.set(family, id);
149
+ }
150
+ return [...byFamily.values()];
151
+ }
152
+
153
+ function uniq(arr) {
154
+ return [...new Set(arr)];
155
+ }
156
+
157
+ /**
158
+ * List all available profile names.
159
+ * @param {Catalog} catalog
160
+ * @returns {string[]}
161
+ */
162
+ export function listProfiles(catalog) {
163
+ return Object.keys(catalog.profiles);
164
+ }
@@ -0,0 +1,147 @@
1
+ // lib/commands/add.js — `flow add <component-id>` adds a single component.
2
+ //
3
+ // Most useful for adapter swaps: `flow add adapter:e2e-playwright-mcp` to
4
+ // install the Playwright E2E adapter alongside the existing setup. Updates
5
+ // flow.config.yaml's adapters block too — picking the new adapter as active
6
+ // for its family (replacing the previous one for that family).
7
+ //
8
+ // Refuses without --yes. Refuses if catalog has no such component.
9
+
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'node:fs';
11
+ import { resolve, join, dirname } from 'node:path';
12
+ import chalk from 'chalk';
13
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
14
+ import { loadCatalog } from '../catalog.js';
15
+ import { resolveRepoRoot } from '../repo-root.js';
16
+
17
+ export default async function add(args, ctx) {
18
+ const repoRoot = ctx.repoRoot ?? resolveRepoRoot(import.meta.url);
19
+ const catalog = loadCatalog(repoRoot);
20
+ const yes = Boolean(args.yes);
21
+ const dryRun = Boolean(args['dry-run']);
22
+
23
+ // The component id is the first positional. bin/flow.js strips `add` from
24
+ // argv before passing to yargs-parser, so args._[0] is the component id.
25
+ const componentId = args._?.[0];
26
+ if (!componentId) {
27
+ console.error(chalk.red('✗ Missing component id. Example: flow add adapter:e2e-playwright-mcp'));
28
+ return 1;
29
+ }
30
+
31
+ // Find it in the catalog. Could be in flow_components or adapters.
32
+ const adapter = catalog.adapters?.find((a) => a.id === componentId);
33
+ const component = catalog.flow_components?.find((c) => c.id === componentId);
34
+ const item = adapter || component;
35
+ if (!item) {
36
+ console.error(chalk.red(`✗ Unknown component: ${componentId}`));
37
+ console.error(` Available adapters: ${catalog.adapters?.map((a) => a.id).join(', ') || '(none)'}`);
38
+ return 1;
39
+ }
40
+
41
+ const homeRoot = ctx.home || process.env.HOME;
42
+ const home = (p) => resolve(p.replace(/\$HOME/g, homeRoot));
43
+
44
+ console.log(chalk.bold(`━━━ flow add ${componentId} ━━━`));
45
+ console.log();
46
+ console.log(`Description: ${item.description}`);
47
+ if (adapter) {
48
+ console.log(`Family: ${adapter.family}`);
49
+ if (adapter.needs_mcp?.length) console.log(`Needs MCPs: ${adapter.needs_mcp.join(', ')}`);
50
+ if (adapter.needs_cli?.length) console.log(`Needs CLIs: ${adapter.needs_cli.join(', ')}`);
51
+ }
52
+ console.log();
53
+
54
+ const operations = [];
55
+ for (const op of item.operations || []) {
56
+ if (op.copy) {
57
+ operations.push({
58
+ kind: 'copy',
59
+ from: join(repoRoot, op.copy.from),
60
+ to: home(op.copy.to),
61
+ });
62
+ }
63
+ }
64
+
65
+ console.log(chalk.bold('Operations:'));
66
+ for (const op of operations) {
67
+ console.log(` ${chalk.green('+')} ${op.kind} ${chalk.dim(`${op.from} → ${op.to}`)}`);
68
+ }
69
+ console.log();
70
+
71
+ if (dryRun) {
72
+ console.log(chalk.dim('--dry-run: stopping before execution.'));
73
+ return 0;
74
+ }
75
+
76
+ if (!yes) {
77
+ console.log(chalk.yellow('?'), 'Re-run with --yes to execute.');
78
+ return 0;
79
+ }
80
+
81
+ // Execute file ops.
82
+ let executed = 0;
83
+ for (const op of operations) {
84
+ try {
85
+ if (!existsSync(op.from)) {
86
+ throw new Error(`source missing: ${op.from}`);
87
+ }
88
+ const isDir = statSync(op.from).isDirectory();
89
+ if (isDir) {
90
+ copyDirRecursive(op.from, op.to);
91
+ } else {
92
+ mkdirSync(dirname(op.to), { recursive: true });
93
+ copyFileSync(op.from, op.to);
94
+ }
95
+ console.log(` ${chalk.green('✓')} copy ${op.to}`);
96
+ executed++;
97
+ } catch (err) {
98
+ console.error(` ${chalk.red('✗')} ${op.kind} failed: ${err.message}`);
99
+ return 2;
100
+ }
101
+ }
102
+
103
+ // If it's an adapter, update flow.config.yaml's adapters block.
104
+ if (adapter) {
105
+ const configPath = join(ctx.cwd, 'flow.config.yaml');
106
+ if (existsSync(configPath)) {
107
+ const config = parseYaml(readFileSync(configPath, 'utf-8')) || {};
108
+ config.adapters = config.adapters || {};
109
+ // The family in catalog is hyphenated (e.g. "issue-tracker"); the config
110
+ // key is snake_case (e.g. "issue_tracker"). Normalize.
111
+ const configKey = adapter.family.replace(/-/g, '_');
112
+ // Strip "adapter:" prefix and the family prefix to get the short id.
113
+ // e.g. "adapter:issue-tracker-linear" → "linear"
114
+ const shortId = adapter.id.replace(`adapter:${adapter.family}-`, '');
115
+ const previous = config.adapters[configKey];
116
+ config.adapters[configKey] = shortId;
117
+ writeFileSync(configPath, stringifyYaml(config));
118
+ console.log();
119
+ if (previous && previous !== shortId) {
120
+ console.log(` ${chalk.cyan('↻')} flow.config.yaml: adapters.${configKey} ${previous} → ${shortId}`);
121
+ } else {
122
+ console.log(` ${chalk.cyan('+')} flow.config.yaml: adapters.${configKey} = ${shortId}`);
123
+ }
124
+ } else {
125
+ console.log();
126
+ console.log(chalk.yellow('⚠'), `No flow.config.yaml in ${ctx.cwd}. Add this manually:`);
127
+ console.log(` adapters.${adapter.family.replace(/-/g, '_')}: ${adapter.id.replace(`adapter:${adapter.family}-`, '')}`);
128
+ }
129
+ }
130
+
131
+ console.log();
132
+ console.log(chalk.bold(`Done: ${executed} operations executed.`));
133
+ return 0;
134
+ }
135
+
136
+ function copyDirRecursive(src, dest) {
137
+ mkdirSync(dest, { recursive: true });
138
+ for (const entry of readdirSync(src)) {
139
+ const srcPath = join(src, entry);
140
+ const destPath = join(dest, entry);
141
+ if (statSync(srcPath).isDirectory()) {
142
+ copyDirRecursive(srcPath, destPath);
143
+ } else {
144
+ copyFileSync(srcPath, destPath);
145
+ }
146
+ }
147
+ }