feature-flow-cli 0.1.4__tar.gz → 0.1.5__tar.gz

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 (57) hide show
  1. {feature_flow_cli-0.1.4/feature_flow_cli.egg-info → feature_flow_cli-0.1.5}/PKG-INFO +2 -2
  2. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/README.md +1 -1
  3. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/adapters/codex/feature-flow/SKILL.md +1 -1
  4. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/__init__.py +1 -1
  5. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/git.py +36 -8
  6. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/install.py +99 -19
  7. feature_flow_cli-0.1.5/feature_flow/released.py +90 -0
  8. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5/feature_flow_cli.egg-info}/PKG-INFO +2 -2
  9. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow_cli.egg-info/SOURCES.txt +1 -0
  10. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/build.md +1 -1
  11. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/skills/feature-flow/SKILL.md +1 -1
  12. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/LICENSE +0 -0
  13. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/MANIFEST.in +0 -0
  14. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
  15. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/agents/feature-planner.md +0 -0
  16. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/agents/plan-reviewer.md +0 -0
  17. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/agents/ticket-builder.md +0 -0
  18. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/agents/ticket-reviewer.md +0 -0
  19. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/__main__.py +0 -0
  20. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/checks.py +0 -0
  21. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/cli.py +0 -0
  22. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/command.py +0 -0
  23. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/conductor.py +0 -0
  24. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/floorguard.py +0 -0
  25. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/gate.py +0 -0
  26. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/prompts.py +0 -0
  27. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/state.py +0 -0
  28. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/status.py +0 -0
  29. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/suggest.py +0 -0
  30. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/tickets.py +0 -0
  31. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow/view.py +0 -0
  32. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
  33. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow_cli.egg-info/entry_points.txt +0 -0
  34. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/feature_flow_cli.egg-info/top_level.txt +0 -0
  35. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/brief.md +0 -0
  36. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/plan-review.md +0 -0
  37. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/plan.md +0 -0
  38. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/review.md +0 -0
  39. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/show.md +0 -0
  40. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/commands.md +0 -0
  41. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/learnings.md +0 -0
  42. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/map.md +0 -0
  43. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/spec.md +0 -0
  44. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/ticket.md +0 -0
  45. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/guides/templates/ui-mockup.md +0 -0
  46. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/install.py +0 -0
  47. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/install.sh +0 -0
  48. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/pyproject.toml +0 -0
  49. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/floor-guard.py +0 -0
  50. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/flow-status.py +0 -0
  51. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/flow-view.html +0 -0
  52. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/flow-view.py +0 -0
  53. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/flow.py +0 -0
  54. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/scripts/gate.py +0 -0
  55. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/setup.cfg +0 -0
  56. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/skills/architect-review/SKILL.md +0 -0
  57. {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.5}/skills/automation-design/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
@@ -53,7 +53,7 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
53
53
 
54
54
  The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
55
55
 
56
- The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
56
+ The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
57
57
 
58
58
  Then, in your repo, in the agent:
59
59
 
@@ -34,7 +34,7 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
34
34
 
35
35
  The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
36
36
 
37
- The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
37
+ The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
38
38
 
39
39
  Then, in your repo, in the agent:
40
40
 
@@ -31,7 +31,7 @@ To add tickets to a plan that exists, edit it by hand following the ticket rules
31
31
 
32
32
  With a plan present, check these before `start`, and stop at the first that fails:
33
33
 
34
- - `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and untracked files listed in `.feature-flow/installed.txt`: those are feature-flow's own install, so never stop for them. Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
34
+ - `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
35
35
  - `python3 scripts/flow-status.py <feature> --check` prints `OK`.
36
36
  - `.agents/flow-roles/ticket-builder.md` and `.agents/flow-roles/ticket-reviewer.md` exist. If not, say to run `uvx feature-flow-cli install . --agent codex` in this repo (or `python3 install.py <repo> --agent codex` from a feature-flow clone).
37
37
 
@@ -1,3 +1,3 @@
1
1
  """Feature Flow: a ticket-graph workflow for coding agents."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
@@ -1,5 +1,6 @@
1
1
  """The few git calls the conductor needs, each one an argument list, never a shell string."""
2
2
 
3
+ import hashlib
3
4
  import subprocess
4
5
  from pathlib import Path
5
6
 
@@ -30,17 +31,30 @@ def head():
30
31
  return _git("rev-parse", "HEAD").stdout.strip()
31
32
 
32
33
 
34
+ # the installer's record of what it wrote: the paths, and the sha256 of each as it wrote it
35
+ INSTALLED = Path(".feature-flow") / "installed.txt"
36
+ HASHES = Path(".feature-flow") / "installed.sha256"
37
+
38
+
39
+ def _read_lines(path):
40
+ try:
41
+ return path.read_text(encoding="utf-8").splitlines()
42
+ except (OSError, UnicodeDecodeError):
43
+ return []
44
+
45
+
33
46
  def changes():
34
47
  """Every changed path, untracked files included, as `git status --porcelain` sees them, except
35
- the untracked files feature-flow's installer wrote (an install not committed yet is not a change)
36
- and untracked Python bytecode, which the flow's own Python runs leave behind."""
37
- installed = set()
48
+ feature-flow's own install: a file still exactly as the installer wrote it, an untracked file it
49
+ lists without a hash (an install from before it kept hashes), and its two record files, which every
50
+ install rewrites; and untracked Python bytecode, which the flow's own Python runs leave behind."""
51
+ listed, hashes = set(), {}
38
52
  top = toplevel()
39
53
  if top is not None:
40
- try:
41
- installed = set((top / ".feature-flow" / "installed.txt").read_text(encoding="utf-8").splitlines())
42
- except (OSError, UnicodeDecodeError):
43
- pass
54
+ listed = set(_read_lines(top / INSTALLED))
55
+ for line in _read_lines(top / HASHES):
56
+ digest, _, name = line.partition(" ")
57
+ hashes[name] = digest
44
58
  entries = _git("status", "--porcelain", "-z", "-uall").stdout.split("\0")
45
59
  found = []
46
60
  i = 0
@@ -52,12 +66,26 @@ def changes():
52
66
  code, path = entry[:2], entry[3:]
53
67
  if code[0] in "RC":
54
68
  i += 1 # the rename's source path follows
55
- if code == "??" and (path in installed or _bytecode(path)):
69
+ if path in (INSTALLED.as_posix(), HASHES.as_posix()):
70
+ continue
71
+ if path in hashes:
72
+ # only a worktree change: a staged version would ride along in the next commit
73
+ if (code[0] in " ?" and code[1] != "T" and top is not None and not (top / path).is_symlink()
74
+ and _sha256(top / path) == hashes[path]):
75
+ continue
76
+ elif code == "??" and (path in listed or _bytecode(path)):
56
77
  continue
57
78
  found.append(path)
58
79
  return found
59
80
 
60
81
 
82
+ def _sha256(path):
83
+ try:
84
+ return hashlib.sha256(path.read_bytes()).hexdigest()
85
+ except OSError:
86
+ return None
87
+
88
+
61
89
  def _bytecode(path):
62
90
  return path.endswith(".pyc") or "__pycache__/" in "/" + path
63
91
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
4
4
  copies the feature-flow skill, its roles, scripts and guides into a repo so the flow works there.
5
- a file that already exists and differs is kept and reported, unless --force is given.
5
+ a file that already exists and differs is kept and reported, unless --force is given or nobody
6
+ edited it since an earlier feature-flow install wrote it (then it is updated).
6
7
  nothing is ever deleted. CLAUDE_HOME overrides ~/.claude and AGENTS_HOME overrides ~/.agents for --user.
7
8
 
8
9
  Files are compared and written as bytes, and the two text transforms (the Codex skill header and
@@ -11,12 +12,15 @@ so the installed tree is the same byte for byte. Files are listed in byte order,
11
12
  with LC_ALL=C.
12
13
  """
13
14
 
15
+ import hashlib
14
16
  import os
15
17
  import re
16
18
  import shutil
17
19
  import subprocess
18
20
  import sys
19
21
 
22
+ from feature_flow.released import RELEASED
23
+
20
24
  # the help text
21
25
  USAGE = """\
22
26
  usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
@@ -30,7 +34,8 @@ copies the feature-flow skill, its roles, scripts and guides into a repo so the
30
34
  --agent all does both.
31
35
  scripts go to <target>/scripts/ and guides, roles and the Python package to <target>/.feature-flow/
32
36
  (always: the skill and scripts/flow.py read them from the repo)
33
- a file that already exists and differs is kept and reported, unless --force is given.
37
+ a file that already exists and differs is kept and reported, unless --force is given or it is
38
+ still exactly what an earlier feature-flow install wrote (then it is updated).
34
39
  nothing is ever deleted. CLAUDE_HOME overrides ~/.claude and AGENTS_HOME overrides ~/.agents for --user.
35
40
  """
36
41
 
@@ -42,6 +47,16 @@ SKILLS = ("feature-flow", "architect-review", "automation-design")
42
47
  # the in-repo files an install wrote, one path per line, relative to the target. The conductor does not
43
48
  # count them as a dirty tree while they are untracked, and `git add --pathspec-from-file` commits them.
44
49
  INSTALLED = ".feature-flow/installed.txt"
50
+ # the sha256 of each of those files as the install wrote it, `<sha256> <path>` per line. A later install
51
+ # replaces a file that still has its recorded bytes: nobody edited it, so it is not the user's version.
52
+ HASHES = ".feature-flow/installed.sha256"
53
+ # the same record for a --user install, in ~/.claude and ~/.agents, paths relative to that folder
54
+ USER_HASHES = "feature-flow.sha256"
55
+
56
+
57
+ def _sha256(data):
58
+ return hashlib.sha256(data).hexdigest()
59
+
45
60
 
46
61
  # an old skill that runs the ticket script (the bash one or the Python one) or ends a review with a verdict
47
62
  LEFTOVER = re.compile(rb"scripts/flow-status\.(?:sh|py)|REVIEW: PASS")
@@ -172,7 +187,14 @@ class Installer:
172
187
  package = os.path.dirname(os.path.abspath(__file__))
173
188
  self.package = package
174
189
  self.added = self.updated = self.same = self.kept = 0
175
- self.written = []
190
+ self.written = {}
191
+ # each folder this installs into, with its hash record; the target's own comes first
192
+ self.records = [(target, HASHES)]
193
+ if user_level and agent in ("claude", "all"):
194
+ self.records.append((self.claude_dir, USER_HASHES))
195
+ if user_level and agent in ("codex", "all"):
196
+ self.records.append((self.agents_dir, USER_HASHES))
197
+ self.recorded = dict((root, self._read_hashes(root + "/" + record)) for root, record in self.records)
176
198
 
177
199
  def place(self, file, dest, data=None):
178
200
  """One file: add it, replace it (--force), leave it alone (same) or keep the user's version.
@@ -181,23 +203,55 @@ class Installer:
181
203
  if not os.path.exists(dest):
182
204
  self.out(" add " + dest)
183
205
  self.added += 1
184
- self.written.append(dest)
206
+ self.written[dest] = new
185
207
  if not self.dry:
186
208
  os.makedirs(os.path.dirname(dest), exist_ok=True)
187
209
  self._copy(file, dest, data)
188
210
  elif new is not None and _read(dest) == new:
189
211
  self.same += 1
190
- self.written.append(dest)
191
- elif self.force:
212
+ self.written[dest] = new
213
+ elif self.force or self.untouched(dest):
192
214
  self.out(" update " + dest)
193
215
  self.updated += 1
194
- self.written.append(dest)
216
+ self.written[dest] = new
195
217
  if not self.dry:
196
218
  self._copy(file, dest, data)
197
219
  else:
198
220
  self.out(" kept " + dest + " (differs from this version, use --force to replace it)")
199
221
  self.kept += 1
200
222
 
223
+ def locate(self, dest):
224
+ """The install folder dest is in and dest relative to it, or (None, None)."""
225
+ for root, _ in sorted(self.records, key=lambda r: -len(r[0])):
226
+ if dest.startswith(root + "/"):
227
+ return root, dest[len(root) + 1:]
228
+ return None, None
229
+
230
+ @staticmethod
231
+ def _read_hashes(path):
232
+ recorded = {}
233
+ try:
234
+ with open(path, encoding="utf-8") as f:
235
+ for line in f:
236
+ digest, _, name = line.rstrip("\n").partition(" ")
237
+ if name:
238
+ recorded[name] = digest
239
+ except (OSError, UnicodeDecodeError):
240
+ pass
241
+ return recorded
242
+
243
+ def untouched(self, dest):
244
+ """dest still holds the bytes an earlier feature-flow install wrote there. Never a symlink or a
245
+ file with other hard links: writing through it would change the file it shares bytes with."""
246
+ if os.path.islink(dest) or os.stat(dest).st_nlink > 1:
247
+ return False
248
+ old = _read(dest)
249
+ if old is None:
250
+ return False
251
+ digest = _sha256(old)
252
+ root, rel = self.locate(dest)
253
+ return digest in RELEASED or self.recorded.get(root, {}).get(rel) == digest
254
+
201
255
  @staticmethod
202
256
  def _copy(file, dest, data):
203
257
  """cp -p: contents, mode and times."""
@@ -243,21 +297,48 @@ class Installer:
243
297
  role_body(data if data is not None else b""))
244
298
 
245
299
  def write_installed(self):
246
- """Add this run's in-repo files to the list, keeping the earlier runs' files that still exist."""
247
- path = self.target + "/" + INSTALLED
300
+ """Add this run's in-repo files to the list, and every file's hash to its folder's record, keeping
301
+ the earlier runs' entries for files that still exist. True when the target's list or record changed."""
248
302
  names = set()
249
303
  try:
250
- with open(path, encoding="utf-8") as old:
304
+ with open(self.target + "/" + INSTALLED, encoding="utf-8") as old:
251
305
  names.update(line.strip() for line in old)
252
306
  except OSError:
253
307
  pass
254
- names = set(n for n in names if n and os.path.isfile(self.target + "/" + n))
255
- prefix = self.target + "/"
256
- names.update(dest[len(prefix):] for dest in self.written if dest.startswith(prefix))
257
- names.add(INSTALLED)
308
+ hashes = dict((root, dict(self.recorded[root])) for root, _ in self.records)
309
+ for dest, data in self.written.items():
310
+ root, rel = self.locate(dest)
311
+ if root == self.target:
312
+ names.add(rel)
313
+ if root is not None and data is not None:
314
+ hashes[root][rel] = _sha256(data)
315
+ names.update((INSTALLED, HASHES))
316
+ names = sorted(n for n in names
317
+ if n in (INSTALLED, HASHES) or (n and os.path.isfile(self.target + "/" + n)))
318
+ changed = self._write(self.target + "/" + INSTALLED, "".join(n + "\n" for n in names))
319
+ for root, record in self.records:
320
+ kept = names if root == self.target else sorted(
321
+ n for n in hashes[root] if os.path.isfile(root + "/" + n))
322
+ text = "".join("%s %s\n" % (hashes[root][n], n) for n in kept if n in hashes[root])
323
+ if self._write(root + "/" + record, text) and root == self.target:
324
+ changed = True
325
+ return changed
326
+
327
+ @staticmethod
328
+ def _write(path, text):
329
+ try:
330
+ with open(path, encoding="utf-8", newline="") as f:
331
+ if f.read() == text:
332
+ return False
333
+ except (OSError, UnicodeDecodeError):
334
+ pass
258
335
  os.makedirs(os.path.dirname(path), exist_ok=True)
259
- with open(path, "w", encoding="utf-8", newline="\n") as new:
260
- new.write("".join(n + "\n" for n in sorted(names)))
336
+ # a new file moved into place: a link at the path is replaced, never written through
337
+ tmp = path + ".tmp"
338
+ with open(tmp, "w", encoding="utf-8", newline="\n") as f:
339
+ f.write(text)
340
+ os.replace(tmp, path)
341
+ return True
261
342
 
262
343
  def report_leftovers(self, dirs):
263
344
  """A skill folder this installer does not own but that drives the flow (it runs the ticket
@@ -294,8 +375,7 @@ class Installer:
294
375
  self.copy_tree(here + "/guides", self.target + "/.feature-flow/guides")
295
376
  self.copy_tree(here + "/agents", self.target + "/.feature-flow/agents")
296
377
  self.copy_tree(self.package, self.target + "/.feature-flow/feature_flow", skip=BUNDLE)
297
- if not self.dry:
298
- self.write_installed()
378
+ listed = not self.dry and self.write_installed()
299
379
 
300
380
  self.out("")
301
381
  verb = "would add" if self.dry else "added"
@@ -311,7 +391,7 @@ class Installer:
311
391
  git = 127
312
392
  if git != 0:
313
393
  self.out("note: %s is not a git repository, and the flow needs one" % self.target)
314
- elif self.added or self.updated:
394
+ elif self.added or self.updated or listed:
315
395
  git_c = ""
316
396
  if os.path.realpath(self.target) != os.path.realpath(os.getcwd()):
317
397
  git_c = '-C "%s" ' % self.target if " " in self.target else "-C %s " % self.target
@@ -0,0 +1,90 @@
1
+ """The sha256 of every file that feature-flow-cli 0.1.0 to 0.1.4 installed (all three --agent
2
+ choices; .feature-flow/installed.txt aside). Those releases kept no record of what they wrote, so an
3
+ install over one of them replaces a file whose bytes are on this list: nobody edited it since. Later
4
+ installs record their own hashes in .feature-flow/installed.sha256, so this list never grows."""
5
+
6
+ RELEASED = frozenset((
7
+ "03111666084be6a375a0e7a42784ea8c5aed7a5a1e71e83cf2ad44fd656f758c",
8
+ "0385988f5d4a23daf13556a391b71bb387a25ab2b7fa8bba81b0276321028e07",
9
+ "0597a52bee02482ee472218b98ee6c507900df02c4f081ca795b0038eebc21bd",
10
+ "08419881ab53935e0a739d8ae7fc6e5cdfaeb597394bdd5c11d538a5a8086091",
11
+ "0b2544121889d10f5b3295700ed49d03940291243edbe6f69b5cc9eb5c2678db",
12
+ "0fbc776b32836e8050c07cd95d50265a415999f558c00bc7131a8b1c8f8dd893",
13
+ "116af2cb64dd22b061e7217a7aec96ecc2fa9193905f17154ce73542b8a3a049",
14
+ "158be4902bdd7a039c9fd844188545c148e289c8ca168a0a93890b9321342252",
15
+ "1598ed3851313756552c258447b74a7b59aca36365760ce66ecd0695a8614021",
16
+ "16e75f565e30ac0d4e418d813f9c399928607a26a7c84f4b983bb1e8fc4e30c1",
17
+ "1bdbe5d780580a9931d123decccdf5c06293b11e3481658dbfe50a8511326b85",
18
+ "1ee3b01e4c92e9fa9f8124a18fa89ea336562d2597325201ec0595ee431f0c09",
19
+ "2a3416153494015130e81876ee94f452a9c94848757cff488309ac056048887a",
20
+ "2a799ddadcfc4744c0837cb0a087f74556cdb88945be93ed8e508a5667c1f65a",
21
+ "2ae68f7df0e92d59a9e69dbd3da5103da9ab3de9b9a2ab7aec3054c410cb5ad1",
22
+ "2c6350ef4162ee3d208145821818c4540a99dcf4bb17915f0fdfca8a89e01bfb",
23
+ "2d2666ff07b3bafa9b257b013a1ef5f889b95c6657cc553f580bc74ef1232038",
24
+ "2f5b589e2acafc5bfefbcbf68d69bc57c028dc509d4766160762695be532aa9b",
25
+ "2f94fd1730077974774efa75d49741a6277e9a060041041c023ff9ff4d6129e7",
26
+ "38901e898c4f78b41527dede20bfca26080de99683b3e5bba0019d87046ae338",
27
+ "38b2d2b4c45e8c76ecd0381ef0820ca4cc516c4ac1f117b2ec99ac6ce3c2e300",
28
+ "39d97eb189726b3c92a399dc60e736d12b12a8252d637588c5b2a31fc2020b5f",
29
+ "3ff57472860da0076ac1d8a8a0537eddd29f3c63989cd5068906e5f31e7abebc",
30
+ "408779e5a420a8b90cf0422830660f1593efdd9c4999665a1beb0097219c2749",
31
+ "447f0c9f2eccb2f5313ca913d91d7fb9c336b30fa03b5d534609cd22de040703",
32
+ "45b62b9d60f88ef8ac8f22f585d662a3004b1dce3875f125b5a1649958511392",
33
+ "45cc664cb168ea82b3d04d16cd96c4392bcc21741f55749306e69fd8f2a0f68c",
34
+ "46ad183898dfa8d860480a123049a74c1bae56d364434ddcd51ed955fec02ca1",
35
+ "4bb17e6f9d9b3c77e858f061b888ab9a7bea9e3ef2cf269230c1c3d42d800a91",
36
+ "52c8362a637ea80835743fd568bce4c05c591d758608c5b89316391779b0166b",
37
+ "569951f6d447ae256d1802ab29ac287af6858e38a34879d3f4534d732097babc",
38
+ "5a2b9d23bbc192412b574d96cc26c09e486173c4ecf3b1d72e4f989f600bbaa9",
39
+ "5b4611c7d419c54d645189cf3e67630678b10d5300f7a6dbe32f75d87876dbf4",
40
+ "5e3895c34a7a0a95cd784c3c99beab4e102bc0a84edd76a9e7586007f55df16b",
41
+ "631fd0f9243caca088bf8ff37a08c1f5e75bcd42589b73a18137936259d211b7",
42
+ "6981a0e0de889d7296bc680f030adf3e354785424d589548257863ca6ae82835",
43
+ "69b13a131466af54a172e7255fade246aad706b177fa3601a843af7a873cb226",
44
+ "6b559ad3ea84b358417700469b7aa0aa48079fc6cddcc9fcbc2dbe312a6ccec8",
45
+ "71c83d16835bf50ee9e78a4a4183a101a1124d5059390e219b7509c96b70488c",
46
+ "71d26991a5263aade812bc9aaf90ddbee44b9c61263302fa5fbcf0093f74be6b",
47
+ "7530aa90149353b607560eb9c16a8ce9be75846f68fb3b71c5a2c9173d21a9b3",
48
+ "761710d8db4ffcb3c9c1a3d941210fa052569994bc2138e974ad44cc6ffe6ba3",
49
+ "774116e163377390b58dc779d008825013d4aff023a2c30332f46db53b473edd",
50
+ "7e42e219a8bb55cad10239ffa290a3d12912350635e8c846c105888dfada5670",
51
+ "800e3a2518755ca20f64ba8518943a11f60e14c8e0ec77bc7a631b7660968861",
52
+ "80535d95ae9fb56ddf746240e9ce6fcf6eaac401e0516ac37584b411113b7017",
53
+ "89522ebb5bf8891a5faab80c86715e3f610808ef588ce0bb261c5ba4bb83c115",
54
+ "8cfd3af473fa146477117cbc406841c542bc137516951fb801204395b444233b",
55
+ "9551f7f94fed0f7e1bb6f35dc6826e2eec37ae18f840ac741fbf369c0fb0bfe7",
56
+ "983395967e1e26bca59cf4da539481f426c3c3d02e67028076cd4d204527ac44",
57
+ "9892816825e0de4f5a40dbaf2506b30d1b3568d0aeadc6597c4a1fc0fd872225",
58
+ "9de945fd57f367ae4db739d8a63d45996912f3797450b53f3ad04f8c43f5cbd5",
59
+ "a0b250ed64f1d38dd9029507a8a34fdbd2bc3aa78a930c439029889f6acfd89f",
60
+ "a666d3eba10f164f50a5005ef70c19b1d5f33c3d43fdf926ac7a9a6815514366",
61
+ "a6d0a0b7ba5c5ccedf2a4d27e0b06e61b8e9563e81fdcd7617da9d4bc3506d71",
62
+ "a7871218beabd7928dc63729cd5141539dc291bb037baa1b9c82e8269c0e272a",
63
+ "aafd905d9d98879c7b6747aa544542df23f6d565cdcef47f7a207808983900d9",
64
+ "ae0790f7fca8c996c3844827e9904ee95b2ced3854277bd440faa59213603935",
65
+ "ae999c50a91a234b4943dff8731d40d30da7d04b2ba4cf13911a6ac6236d2c60",
66
+ "af9a25625f44a64770d6aebce5882c08dbda89b116a25d430a1b7855b8b0d9cf",
67
+ "b922951702a7837923fb52820930ca32fc96c13fc1f0a44650ee135828b4aa80",
68
+ "bc2f066334afbee46bd97168513c36ae129a098179c389c1986b0b7a47f98621",
69
+ "bd24f9b858bab7411493eb97a2c011d906ec56d7a7efb2c7e116cc308b0e4c55",
70
+ "bd7a2653a1015b9e42366c21a2dd60d1c4ea3ca07ca0afee9dddf3d36ec7741e",
71
+ "bda718088ea2b848b000dca2ff78e35795d4400b57ecbc0bfbccdab451245f45",
72
+ "c0a28ab04d02c5eff6f94ed3cdc543257919adcaaf30ec108cd6692a5319081c",
73
+ "c2dbc3162f8bd47a15cb0a10e6cade2cefd3f951ea8c75b175957be1cdceb174",
74
+ "c35e773600d4c3340b5e49eff9cc8eb2682adf7ab8a56599a2ce3fa709c10cbf",
75
+ "c36d2575c40b7f71abc45a58124e6a18b6f423919b9bc1ec69a1a1ff0095589d",
76
+ "c3a0196ebb59f110e968173ff40df3778e73e7eded2afe58deea9dc0734a9e20",
77
+ "cf78e2cfe6e224e3854017787634bffc3e048b48709b767b7ae953e20b707746",
78
+ "d3a2313826088d95dc0bc2625b0222f98223636eda3a59efeac5838f362e5b2b",
79
+ "e0a98bc037bfdef778a0d5d14a4d9abc2b23c4c0ea754491cba307e9c0e2169f",
80
+ "e8194c0aa46b169d8412837f9345c8f32378570b8474a2357c5570080093b7ce",
81
+ "eb346f05cd6359292749a0915fe8b3412b3f99a493a21b294e763a3da0a80852",
82
+ "ee7c8f100721d6ad36ad653a0b82c11eab02217f015344bc707c158f32a08c93",
83
+ "f05d890a79379ee8b89bcef86361acc93ca6b3f7b277500126d058243a60f9f2",
84
+ "f335488f1e89e63bf2f0588a71feccbfece080ef9dcb5fac15f89868576434f0",
85
+ "f34612dfc665fccc1a47f041d096877fa8a4d9c00aad832646d9ab6408305bba",
86
+ "f6352f7ca92571045aa0c7d8ffbe2708f8315c6bd852433e7f32fc8b4af65fdb",
87
+ "f714136a9c949bcb23b67755a3072d0946ca5a6c0a484f6a42f80c23964e5758",
88
+ "f799e5372b598cda13cf2b2d8b888c1703fed71a3af2ec2aa802b838a7de39ba",
89
+ "fded1be6a891c062ad84d14960b47daa0d2424d87ade33922d7f9acfe2b64600",
90
+ ))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feature-flow-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
@@ -53,7 +53,7 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
53
53
 
54
54
  The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
55
55
 
56
- The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
56
+ The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
57
57
 
58
58
  Then, in your repo, in the agent:
59
59
 
@@ -21,6 +21,7 @@ feature_flow/gate.py
21
21
  feature_flow/git.py
22
22
  feature_flow/install.py
23
23
  feature_flow/prompts.py
24
+ feature_flow/released.py
24
25
  feature_flow/state.py
25
26
  feature_flow/status.py
26
27
  feature_flow/suggest.py
@@ -12,7 +12,7 @@ Follow the repo's documented ticket location if it has one. Otherwise they are i
12
12
 
13
13
  ## Step 2: Pre-flight
14
14
 
15
- Run `git status --porcelain`. Untracked files listed in `.feature-flow/installed.txt` are feature-flow's own install and do not count, and neither does untracked Python bytecode (`__pycache__/`, `*.pyc`).
15
+ Run `git status --porcelain`. Files listed in `.feature-flow/installed.txt`, and that list itself, are feature-flow's own install and do not count, and neither does untracked Python bytecode (`__pycache__/`, `*.pyc`).
16
16
 
17
17
  - Clean: continue.
18
18
  - Dirty, manual: show the files and offer to commit, stash, or continue anyway.
@@ -31,7 +31,7 @@ To add tickets to a plan that exists, edit it by hand following the ticket rules
31
31
 
32
32
  With a plan present, check these before `start`, and stop at the first that fails:
33
33
 
34
- - `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and untracked files listed in `.feature-flow/installed.txt`: those are feature-flow's own install, so never stop for them. Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
34
+ - `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
35
35
  - `python3 scripts/flow-status.py <feature> --check` prints `OK`.
36
36
  - `ticket-builder.md` and `ticket-reviewer.md` are in `.claude/agents/`, `~/.claude/agents/`, or `$CLAUDE_HOME/agents/` when `CLAUDE_HOME` is set. If not, say to run `uvx feature-flow-cli install .` in this repo (or `python3 install.py <repo>` from a feature-flow clone).
37
37