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.
- package/README.md +4 -4
- package/adapters/codex/CORE.md +50 -16
- package/adapters/codex/skills/flow-bug-validate/SKILL.md +1 -1
- package/adapters/codex/skills/flow-doctor/SKILL.md +33 -15
- package/adapters/codex/skills/flow-feat-design/SKILL.md +2 -2
- package/adapters/codex/skills/flow-feat-ship/SKILL.md +3 -1
- package/adapters/codex/skills/flow-feat-start/SKILL.md +1 -1
- package/adapters/codex/skills/flow-feat-validate/SKILL.md +1 -1
- package/adapters/codex/skills/flow-init/SKILL.md +12 -8
- package/adapters/codex/skills/flow-next/SKILL.md +5 -4
- package/adapters/codex/skills/flow-work-README/SKILL.md +5 -2
- package/adapters/gemini/CORE.md +50 -16
- package/adapters/gemini/commands/flow/bug/validate.toml +1 -1
- package/adapters/gemini/commands/flow/doctor.toml +33 -15
- package/adapters/gemini/commands/flow/feat/design.toml +2 -2
- package/adapters/gemini/commands/flow/feat/ship.toml +3 -1
- package/adapters/gemini/commands/flow/feat/start.toml +1 -1
- package/adapters/gemini/commands/flow/feat/validate.toml +1 -1
- package/adapters/gemini/commands/flow/init.toml +12 -8
- package/adapters/gemini/commands/flow/next.toml +5 -4
- package/adapters/gemini/commands/flow/work/README.toml +5 -2
- package/adapters/hermes/CORE.md +50 -16
- package/adapters/hermes/skills/flow-bug-validate/SKILL.md +1 -1
- package/adapters/hermes/skills/flow-doctor/SKILL.md +33 -15
- package/adapters/hermes/skills/flow-feat-design/SKILL.md +2 -2
- package/adapters/hermes/skills/flow-feat-ship/SKILL.md +3 -1
- package/adapters/hermes/skills/flow-feat-start/SKILL.md +1 -1
- package/adapters/hermes/skills/flow-feat-validate/SKILL.md +1 -1
- package/adapters/hermes/skills/flow-init/SKILL.md +12 -8
- package/adapters/hermes/skills/flow-next/SKILL.md +5 -4
- package/adapters/hermes/skills/flow-work-README/SKILL.md +5 -2
- package/adapters/opencode/CORE.md +50 -16
- package/adapters/opencode/commands/flow-bug-validate.md +1 -1
- package/adapters/opencode/commands/flow-doctor.md +33 -15
- package/adapters/opencode/commands/flow-feat-design.md +2 -2
- package/adapters/opencode/commands/flow-feat-ship.md +3 -1
- package/adapters/opencode/commands/flow-feat-start.md +1 -1
- package/adapters/opencode/commands/flow-feat-validate.md +1 -1
- package/adapters/opencode/commands/flow-init.md +12 -8
- package/adapters/opencode/commands/flow-next.md +5 -4
- package/adapters/opencode/commands/flow-work-README.md +5 -2
- package/bin/cli.mjs +160 -65
- package/package.json +1 -1
- package/plugins/flow/.claude-plugin/plugin.json +1 -1
- package/plugins/flow/CHANGELOG.md +33 -0
- 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
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
308
|
-
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
const found =
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
338
|
-
// it: a key present in
|
|
339
|
-
function flowConfig(repo, harness) {
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
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: ${
|
|
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
|
|
1148
|
-
runs inside a worktree that does not carry
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
21
|
-
|
|
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.
|