flow-workflows 0.93.0 → 0.94.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 (46) hide show
  1. package/README.md +4 -4
  2. package/adapters/codex/CORE.md +50 -16
  3. package/adapters/codex/skills/flow-bug-validate/SKILL.md +1 -1
  4. package/adapters/codex/skills/flow-doctor/SKILL.md +33 -15
  5. package/adapters/codex/skills/flow-feat-design/SKILL.md +2 -2
  6. package/adapters/codex/skills/flow-feat-ship/SKILL.md +3 -1
  7. package/adapters/codex/skills/flow-feat-start/SKILL.md +1 -1
  8. package/adapters/codex/skills/flow-feat-validate/SKILL.md +1 -1
  9. package/adapters/codex/skills/flow-init/SKILL.md +12 -8
  10. package/adapters/codex/skills/flow-next/SKILL.md +5 -4
  11. package/adapters/codex/skills/flow-work-README/SKILL.md +5 -2
  12. package/adapters/gemini/CORE.md +50 -16
  13. package/adapters/gemini/commands/flow/bug/validate.toml +1 -1
  14. package/adapters/gemini/commands/flow/doctor.toml +33 -15
  15. package/adapters/gemini/commands/flow/feat/design.toml +2 -2
  16. package/adapters/gemini/commands/flow/feat/ship.toml +3 -1
  17. package/adapters/gemini/commands/flow/feat/start.toml +1 -1
  18. package/adapters/gemini/commands/flow/feat/validate.toml +1 -1
  19. package/adapters/gemini/commands/flow/init.toml +12 -8
  20. package/adapters/gemini/commands/flow/next.toml +5 -4
  21. package/adapters/gemini/commands/flow/work/README.toml +5 -2
  22. package/adapters/hermes/CORE.md +50 -16
  23. package/adapters/hermes/skills/flow-bug-validate/SKILL.md +1 -1
  24. package/adapters/hermes/skills/flow-doctor/SKILL.md +33 -15
  25. package/adapters/hermes/skills/flow-feat-design/SKILL.md +2 -2
  26. package/adapters/hermes/skills/flow-feat-ship/SKILL.md +3 -1
  27. package/adapters/hermes/skills/flow-feat-start/SKILL.md +1 -1
  28. package/adapters/hermes/skills/flow-feat-validate/SKILL.md +1 -1
  29. package/adapters/hermes/skills/flow-init/SKILL.md +12 -8
  30. package/adapters/hermes/skills/flow-next/SKILL.md +5 -4
  31. package/adapters/hermes/skills/flow-work-README/SKILL.md +5 -2
  32. package/adapters/opencode/CORE.md +50 -16
  33. package/adapters/opencode/commands/flow-bug-validate.md +1 -1
  34. package/adapters/opencode/commands/flow-doctor.md +33 -15
  35. package/adapters/opencode/commands/flow-feat-design.md +2 -2
  36. package/adapters/opencode/commands/flow-feat-ship.md +3 -1
  37. package/adapters/opencode/commands/flow-feat-start.md +1 -1
  38. package/adapters/opencode/commands/flow-feat-validate.md +1 -1
  39. package/adapters/opencode/commands/flow-init.md +12 -8
  40. package/adapters/opencode/commands/flow-next.md +5 -4
  41. package/adapters/opencode/commands/flow-work-README.md +5 -2
  42. package/bin/cli.mjs +160 -65
  43. package/package.json +1 -1
  44. package/plugins/flow/.claude-plugin/plugin.json +1 -1
  45. package/plugins/flow/CHANGELOG.md +33 -0
  46. package/plugins/flow/examples/FLOW.template.md +13 -4
package/bin/cli.mjs CHANGED
@@ -284,32 +284,114 @@ function parseFlow(text) {
284
284
  return out
285
285
  }
286
286
 
287
- // Where the FLOW files actually are. A phase run inside a linked worktree has its own root,
288
- // and a `FLOW.md` the repo git-ignores exists only in the main checkout — so a worktree would
289
- // resolve every key to its fallback and say nothing. Prefer the root we are in (a repo that
290
- // commits its config gets it in every worktree, and a worktree may legitimately differ), and
291
- // fall back to the main checkout, which `--git-common-dir` points into from anywhere.
287
+ // Where a FLOW file actually is. A phase run inside a linked worktree has its own root, and a
288
+ // `FLOW.md` the repo git-ignores exists only in the main checkout — so a worktree would resolve
289
+ // every key to its fallback and say nothing. Prefer the root we are in (a repo that commits its
290
+ // config gets it in every worktree, and a worktree may legitimately differ), and fall back to the
291
+ // main checkout, which `--git-common-dir` points into from anywhere.
292
+ // The main checkout is asked of git once per run: every FLOW layer and overlay probe needs it.
293
+ const mainCheckouts = new Map()
294
+ function mainCheckout(repo) {
295
+ if (!mainCheckouts.has(repo)) {
296
+ const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repo).trim()
297
+ mainCheckouts.set(repo, common ? dirname(common) : repo)
298
+ }
299
+ return mainCheckouts.get(repo)
300
+ }
301
+
292
302
  function rootHolding(repo, probe) {
293
303
  if (existsSync(join(repo, probe))) return repo
294
- const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repo).trim()
295
- if (!common) return repo
296
- const main = dirname(common)
304
+ const main = mainCheckout(repo)
297
305
  return main !== repo && existsSync(join(main, probe)) ? main : repo
298
306
  }
299
307
 
300
- const configRoot = (repo) => rootHolding(repo, 'FLOW.md')
308
+ // Exactly this name, case included. A case-insensitive filesystem answers `existsSync` for
309
+ // `FLOW.CI.md` with `FLOW.ci.md`, which neither the agent's listing nor a Linux CI would read.
310
+ function hasFile(root, name) {
311
+ try { return readdirSync(root).includes(name) } catch { return false }
312
+ }
313
+
314
+ // A FLOW file is taken from the main checkout only when that checkout's HEAD does not track it —
315
+ // the git-ignored personal file a worktree never carries. One it tracks is committed, so its
316
+ // absence here is this branch's own state (deleted, renamed, not added yet), and the main
317
+ // checkout's copy would be another branch's version. When git cannot answer, nothing is borrowed:
318
+ // a guess here reads another branch's config, and the note says why the file was not read.
319
+ function flowRoot(repo, name) {
320
+ if (hasFile(repo, name)) return { root: repo, note: '' }
321
+ const main = mainCheckout(repo)
322
+ if (main === repo || !hasFile(main, name)) return { root: repo, note: '' }
323
+ let tracked
324
+ try {
325
+ tracked = execFileSync('git', ['ls-tree', '--name-only', 'HEAD', '--', name], {
326
+ cwd: main, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
327
+ }).trim()
328
+ } catch {
329
+ return { root: repo, note: `${name} in ${main} not read: git could not say whether that checkout tracks it` }
330
+ }
331
+ return { root: tracked ? repo : main, note: '' }
332
+ }
301
333
 
302
334
  // The overlay names flow-core §0 recognises. A `--harness` outside this set reads no
303
335
  // overlay and looks exactly like a repo that has none, so it stops the run instead: a typo
304
336
  // here is worth one error message and never worth a review round nobody can trust.
305
337
  const OVERLAY_HARNESSES = ['claude', 'codex', 'gemini', 'hermes', 'opencode', 'zcode']
306
338
 
307
- // Every overlay sitting next to the base, whether or not this run reads one.
308
- function overlaysPresent(root) {
339
+ // The context layer, `FLOW.<FLOW_CONTEXT>.md`, is chosen by whoever launches flow. The name
340
+ // becomes part of a path, so it is held to one plain ASCII segment, and a harness name would read an
341
+ // overlay as the wrong layer — compared ignoring case, because `FLOW.Claude.md` opens
342
+ // `FLOW.claude.md` on a case-insensitive filesystem. Nothing is trimmed: the agent reading the
343
+ // same variable by flow-core §0 takes it as written, and the two must resolve the same layer. A
344
+ // name that fails is skipped and named, never fatal: the phases calling this must not stop on
345
+ // configuration.
346
+ const CONTEXT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/
347
+
348
+ function flowContext() {
349
+ const value = process.env.FLOW_CONTEXT ?? ''
350
+ if (value === '') return { name: null, note: '' }
351
+ if (CONTEXT_NAME.test(value) && !OVERLAY_HARNESSES.includes(value.toLowerCase())) {
352
+ return { name: value, note: '' }
353
+ }
354
+ // Echoed into the pack's header, which reviewers read as Markdown: a refused value is shown as
355
+ // inline code, escaped so that no newline, backtick or `<!--` in it can open anything there.
356
+ const shown = JSON.stringify(value).replace(/`/g, '\\u0060')
357
+ return {
358
+ name: null,
359
+ note: `FLOW_CONTEXT=\`${shown}\` ignored: a context is an ASCII name matching`
360
+ + ` [A-Za-z0-9][A-Za-z0-9_-]* other than a harness (${OVERLAY_HARNESSES.join(', ')})`,
361
+ }
362
+ }
363
+
364
+ // The layers this run reads, in merge order, each located on its own. One root for all of
365
+ // them misses one whenever they live apart: a committed `FLOW.<context>.md` is in the worktree
366
+ // while a git-ignored `FLOW.md` or overlay exists only in the main checkout — and reading the
367
+ // main checkout's copy of a committed file reads another branch's version of it.
368
+ //
369
+ // Under `--rev` a layer the frozen revision holds is read from it: the context file is tracked,
370
+ // so it belongs to the revision being reviewed as much as the diff does, and a round editing it
371
+ // mid-flight must not hand each reader a different exclude list. An untracked layer is in no
372
+ // revision and is read live, as before.
373
+ function flowLayers(repo, harness, rev = null) {
374
+ const context = flowContext()
375
+ const contextFile = context.name && `FLOW.${context.name}.md`
376
+ const layers = ['FLOW.md', contextFile, harness && `FLOW.${harness}.md`].filter(Boolean).map((name) => {
377
+ const layer = { name, context: name === contextFile, note: '' }
378
+ if (rev) {
379
+ const text = gitBlob(repo, rev, name)
380
+ if (text !== null) return { ...layer, root: repo, at: rev, text, found: true }
381
+ }
382
+ const { root, note } = flowRoot(repo, name)
383
+ return { ...layer, root, note, found: hasFile(root, name) }
384
+ })
385
+ return { layers, context }
386
+ }
387
+
388
+ function gitBlob(repo, rev, name) {
309
389
  try {
310
- return readdirSync(root).filter((n) => /^FLOW\.[^.]+\.md$/.test(n)).sort()
390
+ return execFileSync('git', ['show', `${rev}:${name}`], {
391
+ cwd: repo, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
392
+ })
311
393
  } catch {
312
- return []
394
+ return null
313
395
  }
314
396
  }
315
397
 
@@ -321,33 +403,35 @@ function overlaysPresent(root) {
321
403
  // header then says `FLOW.md`, which is what a repo with no overlay prints — the same silence
322
404
  // one level down, and the one the line exists to break. So a run that skipped an overlay names
323
405
  // the file it skipped, because that file is where `agents.exec_cmd` and a harness's models
324
- // live and their absence is indistinguishable from a decision not to set them.
325
- function configFiles(repo, harness) {
326
- const root = configRoot(repo)
327
- const names = ['FLOW.md', harness && `FLOW.${harness}.md`].filter(Boolean)
328
- const found = names.filter((n) => existsSync(join(root, n)))
329
- const where = root === repo ? '' : ` (from ${root})`
330
- const read = found.length ? found.join(' + ') + where : `none found${where || ` in ${repo}`}`
331
- const skipped = harness ? [] : overlaysPresent(root)
332
- return skipped.length
333
- ? `${read} — no \`--harness\` was given, so ${skipped.join(', ')} went unread`
334
- : read
406
+ // live and their absence is indistinguishable from a decision not to set them. A context file
407
+ // left unread is not in that list: no `FLOW_CONTEXT` is how a laptop leaves it out on purpose.
408
+ function configFiles(repo, harness, rev = null) {
409
+ const { layers, context } = flowLayers(repo, harness, rev)
410
+ const found = layers.filter((l) => l.found)
411
+ const label = (l) => l.name + (l.context ? ` [FLOW_CONTEXT=${context.name}]` : '')
412
+ + (l.at ? ` (at ${l.at.slice(0, 12)})` : l.root === repo ? '' : ` (from ${l.root})`)
413
+ const notes = [found.length ? found.map(label).join(' + ') : `none found in ${repo}`]
414
+ if (context.note) notes.push(context.note)
415
+ for (const l of layers) if (l.note) notes.push(l.note)
416
+ for (const l of layers) if (l.context && !l.found) notes.push(`FLOW_CONTEXT=${context.name}: ${l.name} not found`)
417
+ const skipped = harness ? []
418
+ : OVERLAY_HARNESSES.map((h) => `FLOW.${h}.md`).filter((n) => hasFile(flowRoot(repo, n).root, n))
419
+ if (skipped.length) notes.push(`no \`--harness\` was given, so ${skipped.join(', ')} went unread`)
420
+ return notes.join(' — ')
335
421
  }
336
422
 
337
- // Base file plus the overlay of the harness running this, exactly as flow-core §0 resolves
338
- // it: a key present in the overlay replaces the base value.
339
- function flowConfig(repo, harness) {
340
- const root = configRoot(repo)
341
- const read = (name) => {
342
- const p = join(root, name)
343
- return existsSync(p) ? parseFlow(readFileSync(p, 'utf8')) : {}
344
- }
345
- const base = read('FLOW.md')
346
- const overlay = harness ? read(`FLOW.${harness}.md`) : {}
347
- for (const [section, keys] of Object.entries(overlay)) {
348
- base[section] = { ...(base[section] ?? {}), ...keys }
423
+ // Base, then the context layer, then the overlay of the harness running this, exactly as
424
+ // flow-core §0 resolves it: a key present in a later layer replaces the earlier value.
425
+ function flowConfig(repo, harness, rev = null) {
426
+ const merged = {}
427
+ for (const l of flowLayers(repo, harness, rev).layers) {
428
+ if (!l.found) continue
429
+ const text = l.text ?? readFileSync(join(l.root, l.name), 'utf8')
430
+ for (const [section, keys] of Object.entries(parseFlow(text))) {
431
+ merged[section] = { ...(merged[section] ?? {}), ...keys }
432
+ }
349
433
  }
350
- return base
434
+ return merged
351
435
  }
352
436
 
353
437
  // `.claude/work` is git-ignored like the FLOW files, so a worktree does not carry it either — and
@@ -393,12 +477,28 @@ function buildBundle(argv) {
393
477
  console.error(`recognise. One of: ${OVERLAY_HARNESSES.join(', ')}.`)
394
478
  process.exit(1)
395
479
  }
396
- const cfg = flowConfig(repo, harness)
480
+ const resolves = (ref) => Boolean(git(['rev-parse', '--verify', '--quiet', ref], repo).trim())
481
+ // `--rev` freezes what is read. Without it the pack is the live checkout, which is the
482
+ // right answer for a phase that is about to edit it and the wrong one for a review: the
483
+ // round applies its own fixes while its reviewers are still reading, so "the tree" is a
484
+ // different tree for each of them and for the sha recorded at the end. A caller that has
485
+ // resolved a candidate — a commit, or a `git stash create` object holding a dirty tree —
486
+ // passes it here and every section below reads that one revision, the config it commits
487
+ // included.
488
+ const rev = flag('--rev')
489
+ if (rev && !resolves(rev)) {
490
+ console.error(`flow bundle: --rev \`${rev}\` does not resolve in this checkout.`)
491
+ console.error('Pass a commit, a tag, or the object `git stash create` printed. A pack')
492
+ console.error('built from a revision nobody can name is evidence about nothing.')
493
+ process.exit(1)
494
+ }
495
+ const to = rev ? git(['rev-parse', rev], repo).trim() : null
496
+ const cfg = flowConfig(repo, harness, to)
497
+ const configs = configFiles(repo, harness, to)
397
498
  const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], repo).trim()
398
499
  const maxFile = Number(flag('--max-file-bytes', MAX_FILE_BYTES)) || MAX_FILE_BYTES
399
500
  const maxDiffLines = Number(flag('--max-diff-lines', MAX_DIFF_LINES)) || MAX_DIFF_LINES
400
501
 
401
- const resolves = (ref) => Boolean(git(['rev-parse', '--verify', '--quiet', ref], repo).trim())
402
502
  let base = flag('--base') || cfg.git?.default_base || ''
403
503
  if (!base) {
404
504
  // What the remote says it defaults to, then the usual names — remote first, but a repo
@@ -414,22 +514,9 @@ function buildBundle(argv) {
414
514
  console.error(`flow bundle: base \`${base}\` does not resolve in this checkout.`)
415
515
  console.error('Set `git.default_base` in FLOW.md, or pass --base. A silently empty bundle')
416
516
  console.error('reads exactly like a branch with no changes, which is worse than this error.')
517
+ console.error(`Config read: ${configs}.`)
417
518
  process.exit(1)
418
519
  }
419
- // `--rev` freezes what is read. Without it the pack is the live checkout, which is the
420
- // right answer for a phase that is about to edit it and the wrong one for a review: the
421
- // round applies its own fixes while its reviewers are still reading, so "the tree" is a
422
- // different tree for each of them and for the sha recorded at the end. A caller that has
423
- // resolved a candidate — a commit, or a `git stash create` object holding a dirty tree —
424
- // passes it here and every section below reads that one revision.
425
- const rev = flag('--rev')
426
- if (rev && !resolves(rev)) {
427
- console.error(`flow bundle: --rev \`${rev}\` does not resolve in this checkout.`)
428
- console.error('Pass a commit, a tag, or the object `git stash create` printed. A pack')
429
- console.error('built from a revision nobody can name is evidence about nothing.')
430
- process.exit(1)
431
- }
432
- const to = rev ? git(['rev-parse', rev], repo).trim() : null
433
520
  const from = git(['merge-base', base, to ?? 'HEAD'], repo).trim() || base
434
521
  // Every diff below runs `from` → `ends`, and an empty `ends` means the working tree.
435
522
  const ends = to ? [to] : []
@@ -499,7 +586,7 @@ function buildBundle(argv) {
499
586
  out.push(`# flow bundle — ${branch}`)
500
587
  out.push('')
501
588
  out.push(`Base \`${base}\` · merge base \`${from.slice(0, 12)}\` · generated by \`flow bundle\`.`)
502
- out.push(`Config read: ${configFiles(repo, harness)}.`)
589
+ out.push(`Config read: ${configs}.`)
503
590
  if (to) {
504
591
  out.push(`Revision \`${to.slice(0, 12)}\` (\`--rev ${rev}\`) — **frozen**. Every diff and every`)
505
592
  out.push('file below is that one revision, not the checkout, which may have moved since.')
@@ -611,7 +698,7 @@ function buildBundle(argv) {
611
698
  }
612
699
  }
613
700
 
614
- return { text: out.join('\n'), repo, cfg, branch, work, configs: configFiles(repo, harness) }
701
+ return { text: out.join('\n'), repo, cfg, branch, work, configs }
615
702
  }
616
703
 
617
704
  // --- tier -------------------------------------------------------------------------------
@@ -666,21 +753,23 @@ function tier(argv) {
666
753
  console.error(`flow tier: unknown harness \`${harness}\`. One of: ${OVERLAY_HARNESSES.join(', ')}.`)
667
754
  process.exit(1)
668
755
  }
669
- const cfg = flowConfig(repo, harness)
670
756
  const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], repo).trim()
671
757
  const resolves = (ref) => Boolean(git(['rev-parse', '--verify', '--quiet', ref], repo).trim())
672
-
673
- const base = flag('--base') || cfg.git?.default_base || 'origin/main'
674
- if (!resolves(base)) {
675
- console.error(`flow tier: base \`${base}\` does not resolve. Set git.default_base or pass --base.`)
676
- process.exit(1)
677
- }
678
758
  const rev = flag('--rev')
679
759
  if (rev && !resolves(rev)) {
680
760
  console.error(`flow tier: --rev \`${rev}\` does not resolve in this checkout.`)
681
761
  process.exit(1)
682
762
  }
683
763
  const to = rev ? git(['rev-parse', rev], repo).trim() : null
764
+ const cfg = flowConfig(repo, harness, to)
765
+ const configs = configFiles(repo, harness, to)
766
+
767
+ const base = flag('--base') || cfg.git?.default_base || 'origin/main'
768
+ if (!resolves(base)) {
769
+ console.error(`flow tier: base \`${base}\` does not resolve. Set git.default_base or pass --base.`)
770
+ console.error(`Config read: ${configs}`)
771
+ process.exit(1)
772
+ }
684
773
  const from = git(['merge-base', base, to ?? 'HEAD'], repo).trim() || base
685
774
  const ends = to ? [to] : []
686
775
 
@@ -723,7 +812,7 @@ function tier(argv) {
723
812
  const out = []
724
813
  out.push(`Changed lines: ${changedLines} over ${files.length} file(s), \`${from.slice(0, 12)}\`..`
725
814
  + `${to ? `\`${to.slice(0, 12)}\`` : 'working tree'} (base \`${base}\`)`)
726
- out.push(`Config read: ${configFiles(repo, harness)}`)
815
+ out.push(`Config read: ${configs}`)
727
816
  out.push(`review_depth: ${depth}`)
728
817
  out.push(`Diff size: ${diffSize} · meta.json.size: ${recorded ?? 'not recorded'}`
729
818
  + ` · Effective size: ${effective}`)
@@ -1144,8 +1233,9 @@ Claude Code, Codex CLI and zcode have their own marketplaces:
1144
1233
  Two limits, two questions: --max-file-bytes decides what is too big to print in full,
1145
1234
  --max-diff-lines what is too big to read as a diff. A large file with a small change
1146
1235
  keeps its diff; the worklist always shows the whole change, and anything held back is
1147
- named there rather than at the end. FLOW.md is read from the main checkout when this
1148
- runs inside a worktree that does not carry one.
1236
+ named there rather than at the end. Each FLOW file is read from the main checkout when
1237
+ this runs inside a worktree that does not carry it — unless the main checkout tracks it,
1238
+ since a committed file missing here is this branch's own state.
1149
1239
  --rev freezes what is read: the diff and the files come from that one revision instead
1150
1240
  of the live checkout, so every reader of the pack reads the same bytes however long
1151
1241
  they take. Pass a commit, or the object "git stash create" prints for a dirty tree.
@@ -1154,6 +1244,11 @@ Claude Code, Codex CLI and zcode have their own marketplaces:
1154
1244
  --harness names the overlay to merge on top of it (${OVERLAY_HARNESSES.join(' · ')}).
1155
1245
  Without it no overlay is read at all, which the pack's header says out loud: a key set
1156
1246
  only there — agents.exec_cmd, a harness's models — would otherwise read as unset.
1247
+ FLOW_CONTEXT=<name> in the environment adds FLOW.<name>.md between FLOW.md and the
1248
+ overlay — a layer the repo commits for where flow runs (ci, server…). The name is an ASCII
1249
+ letter or digit, then ASCII letters, digits, - or _, and never a harness name; anything
1250
+ else, spaces included, is skipped and named in the pack's header. Under --rev, a FLOW file
1251
+ the revision holds is read from it.
1157
1252
 
1158
1253
  "tier" resolves the review depth from the same config: changed lines against the base,
1159
1254
  diff size, meta.json.size, effective size, the built-in effort with and without the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flow-workflows",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "description": "Guided feat/bug workflows (start→ship), post-deploy monitoring and multi-agent code review, stack-agnostic. Each repo uses a shared FLOW.md plus optional harness overlay.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@
3
3
  "name": "flow",
4
4
  "displayName": "Flow — guided dev workflows",
5
5
  "description": "Guided feat/bug workflows (start→ship), post-deploy monitoring and multi-agent code review, stack-agnostic. Each repo uses a shared FLOW.md plus optional harness overlay.",
6
- "version": "0.93.0",
6
+ "version": "0.94.0",
7
7
  "author": {
8
8
  "name": "Alberto Vioque",
9
9
  "email": "mashware@gmail.com"
@@ -5,6 +5,39 @@ plugin and is what `/flow:news` reads to show you what changed since your previo
5
5
 
6
6
  The canonical, richest notes live in the [GitHub Releases](https://github.com/mashware/flow-workflows/releases).
7
7
 
8
+ ## v0.94.0 — A repo commits the FLOW config for where flow runs, and whoever launches flow picks it · 2026-10-14
9
+
10
+ **In short**
11
+ - **A context layer.** Whoever launches flow sets `FLOW_CONTEXT=<name>` and `FLOW.<name>.md` is read
12
+ between `FLOW.md` and the harness overlay, with the overlay's merge rules. The repo commits it, so
13
+ what only makes sense on a server, in CI or under an orchestrator — how to drive the app in a
14
+ container, a convention for unattended MRs/PRs — is versioned with the code and reaches no laptop
15
+ that does not set the variable. No variable → nothing changes. The name is an ASCII letter or
16
+ digit, then letters, digits, `-` or `_`, taken as written and never a harness name; anything
17
+ else, and a variable naming a file that is not there, is named in the config line and never stops
18
+ a run.
19
+ - **Each FLOW file is found on its own.** From a worktree, a file is read from the worktree, and
20
+ from the main checkout only when that checkout does not track it — the git-ignored personal file
21
+ a worktree never carries. This also fixes a silent miss: a git-ignored overlay beside a committed
22
+ `FLOW.md` used to go unread from a worktree. A committed file the branch deleted is no longer
23
+ borrowed from another branch.
24
+ - **`flow bundle` and `flow tier` say which layers they read**, and where: `FLOW.md (from <main>) +
25
+ FLOW.ci.md [FLOW_CONTEXT=ci] + FLOW.claude.md`. Under `--rev`, a FLOW file the revision holds is
26
+ read from it, so a frozen review reads frozen config too. A base that does not resolve still
27
+ names the config it read.
28
+ - **`/flow:doctor` shows the active context**, the layer each value came from, the context files no
29
+ variable selected (beside the overlay names, so a misspelled overlay is visible), and warns when
30
+ an ignore rule — such as an older `FLOW.*.md` — would keep a context file out of git.
31
+ - **The setup advice ignores the personal files by name** (`/FLOW.md` and the six overlays), never
32
+ `/FLOW.*.md`, which would hide a context file. Existing repos keep their rule until they change it;
33
+ doctor points at it.
34
+ - **The phases read the variable safely**: the look step is a quoted `find` (a zsh glob with no
35
+ match aborted the whole listing) and `printf %q` (a `\r` or a trailing space shows instead of
36
+ passing as a clean name). A value from the context layer is a tracked file of the branch, never a
37
+ quick config line written after the MR/PR.
38
+ - **A committed context file is code**: a branch can change what flow runs under it, as it can its
39
+ Makefile. A runner that must not take that from a branch masks those keys in its overlay.
40
+
8
41
  ## v0.93.0 — A study says which tickets it is waiting for, so whoever builds it waits for them too · 2026-10-13
9
42
 
10
43
  **In short**
@@ -10,15 +10,24 @@ Use this shape for the shared base `FLOW.md` and for an optional sparse harness
10
10
  read after the base. A key present in it replaces the base value; a present-but-empty key masks the
11
11
  base and selects the normal empty fallback; an absent key inherits the base. Lists replace whole
12
12
  lists. `conventions` is additive: overlay lines follow the base lines and both apply. Existing repos
13
- with only `FLOW.md` behave exactly as before. Unknown `FLOW.*.md` names are ignored.
13
+ with only `FLOW.md` behave exactly as before.
14
+
15
+ A third, optional layer depends on **where flow runs** rather than on who runs it: whoever launches
16
+ flow sets `FLOW_CONTEXT=<name>` (`ci`, `server`, `unattended`…) and `FLOW.<name>.md` is read
17
+ between the base and the harness overlay, with the same rules. The repo commits it, so what only
18
+ makes sense on a server or in CI — how to drive the app in a container, a convention for unattended
19
+ MRs/PRs — is versioned with the code and never reaches a laptop that does not set the variable. The
20
+ name is an ASCII letter or digit, then ASCII letters, digits, `-` or `_`, taken as written, and
21
+ never a harness name.
14
22
 
15
23
  Place it at the repo root. This is **personal config, not team config** — it mixes repo facts
16
24
  (tracker, quality commands, conventions) with your own flow preferences (autonomy mode, the
17
25
  tools/agents you have installed, review depth, assignee), so what one developer wants differs from
18
26
  the next and the same file on another machine may point at agents or an MCP that isn't there. It
19
- holds **no secrets** (those stay in your credential store), but **add `/FLOW.md` and
20
- `/FLOW.*.md` to your `.gitignore`** — `/flow:init` offers to do this for you. A team that wants to
21
- share the repo-fact subset can still commit it deliberately.
27
+ holds **no secrets** (those stay in your credential store), but **add `/FLOW.md`, `/FLOW.claude.md`, `/FLOW.codex.md`, `/FLOW.gemini.md`, `/FLOW.hermes.md`, `/FLOW.opencode.md` and `/FLOW.zcode.md` to your
28
+ `.gitignore`**, each by name — `/flow:init` offers to do this for you. Not `/FLOW.*.md`: that would
29
+ also hide a context file the repo means to commit. A team that wants to share the repo-fact subset
30
+ can still commit it deliberately.
22
31
 
23
32
  ## tracker
24
33
  How tickets are identified and read.