issue-flow 0.5.2__tar.gz → 0.5.4__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.
- {issue_flow-0.5.2 → issue_flow-0.5.4}/PKG-INFO +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/pyproject.toml +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/pyproject.toml.orig +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/agent.py +319 -8
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/cli.py +153 -4
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/config.py +16 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/config_ops.py +1 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/gitutils.py +122 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/history.py +171 -21
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/init.py +22 -13
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/modes.py +26 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/project.py +53 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-cleanup.md.j2 +6 -3
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-close.md.j2 +14 -3
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-pick.md.j2 +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/docs/issue-workflow.md.j2 +5 -3
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/rules/_body.md.j2 +4 -2
- issue_flow-0.5.4/src/issue_flow/templates/skills/_default_sync_recover.md.j2 +11 -0
- issue_flow-0.5.4/src/issue_flow/templates/skills/_worktree_start.md.j2 +8 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_cleanup/SKILL.md.j2 +8 -3
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_close/SKILL.md.j2 +11 -5
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_cycle/SKILL.md.j2 +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_doctor/SKILL.md.j2 +5 -2
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_epic/SKILL.md.j2 +2 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_history_update/SKILL.md.j2 +25 -7
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_init/SKILL.md.j2 +1 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_yolo/SKILL.md.j2 +1 -1
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/user_global.py +2 -6
- issue_flow-0.5.2/src/issue_flow/templates/skills/_worktree_start.md.j2 +0 -7
- {issue_flow-0.5.2 → issue_flow-0.5.4}/LICENSE +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/README.md +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/__init__.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/console_io.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/convert.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/dependencies.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/editors.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/epicplan.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/graphify.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/modes.toml +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/py.typed +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/queueplan.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/readiness.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/skill_ownership.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/step_profiles.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/step_profiles.toml +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/surfaces.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/sync.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/__init__.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/__init__.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-archive.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-auto.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-build.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-capture.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-cycle.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-doctor.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-epic.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-fix.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-graphify.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-init.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-issue.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-ops.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-pause.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-plan.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-pr-sync.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-review.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-setup.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-split.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-status.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow-yolo.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/commands/iflow.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/designs/essential-tests.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/designs/python-quality-tools.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/designs/test-registry.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/docs/__init__.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/docs/this-project.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/rules/AGENTS.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/rules/CLAUDE.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/rules/__init__.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/rules/issueflow-rules.mdc.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_essential_tests_close_sanity.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_essential_tests_review.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_invocation_forms.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_model_directive.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_pstack_LICENSE.txt +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_resolve_project_root.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/_worktree_close_remove.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/caveman/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/gh_ci/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/grill_me/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_archive/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_auto/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_build/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_capture/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_comments/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_fix/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_graphify/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_iflow/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_issue/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_ops/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_pause/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_pick/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_plan/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_pr_sync/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_review/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_setup/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_split/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_status/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/iflow_version_bump/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_blast_radius/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_bro/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_principle_fix_root_causes/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_principle_prove_it_works/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_principle_subtract_before_you_add/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_principle_test_behavior_not_implementation/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_tdd/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_technical_writing/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/skills/pstack_unslop/SKILL.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templates/tools/README.md.j2 +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/templating.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/tracking.py +0 -0
- {issue_flow-0.5.2 → issue_flow-0.5.4}/src/issue_flow/versionplan.py +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
These functions back ``issue-flow status`` (human-facing, top-level) and the
|
|
4
4
|
``issue-flow agent ...`` sub-commands (``state`` / ``preflight`` / ``switchback`` /
|
|
5
|
-
``sync-branch`` / ``branches`` / ``version-plan`` / ``resolve`` /
|
|
6
|
-
``
|
|
7
|
-
``archive`` / ``capture`` / ``sub-issue-add``) that exist so AI agents
|
|
5
|
+
``default-sync`` / ``sync-branch`` / ``branches`` / ``version-plan`` / ``resolve`` /
|
|
6
|
+
``open-workspace`` / ``worktree-add`` / ``worktree-list`` / ``worktree-remove`` /
|
|
7
|
+
``sweep`` / ``archive`` / ``capture`` / ``sub-issue-add``) that exist so AI agents
|
|
8
8
|
can ask the tool for a deterministic answer instead of re-deriving lifecycle
|
|
9
9
|
state by hand on every run.
|
|
10
10
|
|
|
@@ -795,6 +795,7 @@ def run_switchback(project_root: Path, console: Console, as_json: bool) -> int:
|
|
|
795
795
|
"pulled": False,
|
|
796
796
|
"in_worktree": False,
|
|
797
797
|
"dirty_paths": [],
|
|
798
|
+
"default_sync": None,
|
|
798
799
|
"notes": notes,
|
|
799
800
|
}
|
|
800
801
|
|
|
@@ -849,13 +850,24 @@ def run_switchback(project_root: Path, console: Console, as_json: bool) -> int:
|
|
|
849
850
|
|
|
850
851
|
ok, error = gitutils.pull_ff_only(project_root)
|
|
851
852
|
payload["pulled"] = ok
|
|
853
|
+
sync = gitutils.classify_default_sync(
|
|
854
|
+
project_root,
|
|
855
|
+
issueflows_dir=Settings().issueflows_dir,
|
|
856
|
+
default=default,
|
|
857
|
+
fetch=False,
|
|
858
|
+
)
|
|
859
|
+
payload["default_sync"] = sync
|
|
852
860
|
if not ok:
|
|
853
861
|
notes.append(
|
|
854
|
-
f"git pull --ff-only refused: {error} —
|
|
855
|
-
"
|
|
862
|
+
f"git pull --ff-only refused: {error} — do not rebase, force-push, "
|
|
863
|
+
"or push default to skip CI."
|
|
856
864
|
)
|
|
865
|
+
notes.extend(_default_sync_note_lines(sync))
|
|
857
866
|
return emit(1)
|
|
858
867
|
|
|
868
|
+
if sync.get("ahead"):
|
|
869
|
+
notes.extend(_default_sync_note_lines(sync))
|
|
870
|
+
|
|
859
871
|
return emit(0)
|
|
860
872
|
|
|
861
873
|
|
|
@@ -878,6 +890,100 @@ def _render_switchback_text(
|
|
|
878
890
|
console.print(f" [{style}]{escape(note)}[/{style}]")
|
|
879
891
|
|
|
880
892
|
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
# agent default-sync (issue #303)
|
|
895
|
+
# ---------------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def run_default_sync(project_root: Path, console: Console, as_json: bool) -> int:
|
|
899
|
+
"""Classify unique commits on home default vs origin — no mutate."""
|
|
900
|
+
notes: list[str] = []
|
|
901
|
+
if not gitutils.git_available():
|
|
902
|
+
payload = {
|
|
903
|
+
"default_branch": None,
|
|
904
|
+
"ahead": None,
|
|
905
|
+
"behind": None,
|
|
906
|
+
"ff_possible": False,
|
|
907
|
+
"class": "unknown",
|
|
908
|
+
"action": "unknown",
|
|
909
|
+
"commits": [],
|
|
910
|
+
"tree_paths": [],
|
|
911
|
+
"never": list(gitutils.DEFAULT_SYNC_NEVER),
|
|
912
|
+
"notes": ["git is not on PATH"],
|
|
913
|
+
}
|
|
914
|
+
if as_json:
|
|
915
|
+
_emit_json(console, payload)
|
|
916
|
+
else:
|
|
917
|
+
console.print("[red]error[/red] git is not on PATH")
|
|
918
|
+
return 1
|
|
919
|
+
|
|
920
|
+
payload = gitutils.classify_default_sync(
|
|
921
|
+
project_root,
|
|
922
|
+
issueflows_dir=Settings().issueflows_dir,
|
|
923
|
+
)
|
|
924
|
+
notes.extend(payload.get("notes") or [])
|
|
925
|
+
payload["notes"] = notes
|
|
926
|
+
if as_json:
|
|
927
|
+
_emit_json(console, payload)
|
|
928
|
+
return 0
|
|
929
|
+
_render_default_sync_text(console, payload)
|
|
930
|
+
return 0
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _default_sync_note_lines(sync: dict[str, Any]) -> list[str]:
|
|
934
|
+
"""Short notes skills can print after an ff-only refusal or ahead-only home."""
|
|
935
|
+
lines = [
|
|
936
|
+
f"default-sync: action={sync.get('action')} class={sync.get('class')} "
|
|
937
|
+
f"ahead={sync.get('ahead')} behind={sync.get('behind')}"
|
|
938
|
+
]
|
|
939
|
+
for commit in sync.get("commits") or []:
|
|
940
|
+
mark = " (merge)" if commit.get("is_merge") else ""
|
|
941
|
+
paths = ", ".join(commit.get("paths") or []) or "(no paths)"
|
|
942
|
+
lines.append(f" {commit.get('sha')} {commit.get('subject')}{mark} — {paths}")
|
|
943
|
+
tree = sync.get("tree_paths") or []
|
|
944
|
+
if tree:
|
|
945
|
+
lines.append(" tree: " + ", ".join(tree))
|
|
946
|
+
action = sync.get("action")
|
|
947
|
+
if action == "tracking_pr":
|
|
948
|
+
lines.append(
|
|
949
|
+
"offer: merge origin/<default> or cherry-pick onto a chore branch, "
|
|
950
|
+
"then open a tiny PR — never push default directly."
|
|
951
|
+
)
|
|
952
|
+
elif action == "replay_tracking":
|
|
953
|
+
lines.append(
|
|
954
|
+
"offer: replay the tracking commit onto origin/<default> "
|
|
955
|
+
"(chore branch + PR). Do not stack another merge."
|
|
956
|
+
)
|
|
957
|
+
elif action == "stop_product":
|
|
958
|
+
lines.append(
|
|
959
|
+
"stop: unique commits touch product / lock / HISTORY. "
|
|
960
|
+
"User decides. Do not merge onto default."
|
|
961
|
+
)
|
|
962
|
+
elif action == "report_ahead":
|
|
963
|
+
lines.append(
|
|
964
|
+
"home default is ahead of origin; report unique commits. "
|
|
965
|
+
"Do not silent-push default."
|
|
966
|
+
)
|
|
967
|
+
elif action == "ff_only":
|
|
968
|
+
lines.append("ff-only is safe (behind only).")
|
|
969
|
+
never = sync.get("never") or list(gitutils.DEFAULT_SYNC_NEVER)
|
|
970
|
+
lines.append("never: " + "; ".join(never))
|
|
971
|
+
return lines
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _render_default_sync_text(console: Console, payload: dict[str, Any]) -> None:
|
|
975
|
+
console.print(
|
|
976
|
+
f"[bold]action[/bold] {escape(str(payload.get('action')))} "
|
|
977
|
+
f"class={escape(str(payload.get('class')))} "
|
|
978
|
+
f"ahead={payload.get('ahead')} behind={payload.get('behind')} "
|
|
979
|
+
f"ff_possible={payload.get('ff_possible')}"
|
|
980
|
+
)
|
|
981
|
+
for line in _default_sync_note_lines(payload)[1:]:
|
|
982
|
+
console.print(f" {escape(line)}")
|
|
983
|
+
for note in payload.get("notes") or []:
|
|
984
|
+
console.print(f" [dim]{escape(note)}[/dim]")
|
|
985
|
+
|
|
986
|
+
|
|
881
987
|
# ---------------------------------------------------------------------------
|
|
882
988
|
# agent sync-branch
|
|
883
989
|
# ---------------------------------------------------------------------------
|
|
@@ -2809,7 +2915,15 @@ def run_workspace_update(
|
|
|
2809
2915
|
f"`issue-flow workspace init` from the workspace root first."
|
|
2810
2916
|
)
|
|
2811
2917
|
|
|
2812
|
-
|
|
2918
|
+
seen_roots: set[Path] = set()
|
|
2919
|
+
member_pairs: list[tuple[str, Path]] = []
|
|
2920
|
+
for name, root in zip(workspace.members, workspace.member_roots(), strict=True):
|
|
2921
|
+
resolved = root.resolve()
|
|
2922
|
+
if resolved in seen_roots:
|
|
2923
|
+
continue
|
|
2924
|
+
seen_roots.add(resolved)
|
|
2925
|
+
member_pairs.append((name, resolved))
|
|
2926
|
+
member_roots = [root for _, root in member_pairs]
|
|
2813
2927
|
if not member_roots:
|
|
2814
2928
|
return _fail(
|
|
2815
2929
|
f"no scaffolded member repos found under {workspace.root} — run "
|
|
@@ -2844,7 +2958,7 @@ def run_workspace_update(
|
|
|
2844
2958
|
ok_count = 0
|
|
2845
2959
|
fail_count = 0
|
|
2846
2960
|
|
|
2847
|
-
for name, root in
|
|
2961
|
+
for name, root in member_pairs:
|
|
2848
2962
|
entry: dict[str, Any] = {"name": name, "path": str(root)}
|
|
2849
2963
|
try:
|
|
2850
2964
|
_run_member_update(root)
|
|
@@ -2887,6 +3001,202 @@ def run_workspace_update(
|
|
|
2887
3001
|
return 0 if fail_count == 0 else 1
|
|
2888
3002
|
|
|
2889
3003
|
|
|
3004
|
+
# ---------------------------------------------------------------------------
|
|
3005
|
+
# agent apply-changelog
|
|
3006
|
+
# ---------------------------------------------------------------------------
|
|
3007
|
+
|
|
3008
|
+
|
|
3009
|
+
def _status_path_for_issue(
|
|
3010
|
+
project_root: Path, settings: Settings, number: int
|
|
3011
|
+
) -> Path | None:
|
|
3012
|
+
"""``issue<N>_status.md`` in current-issues, then solved (issue #288)."""
|
|
3013
|
+
name = f"issue{number}_status.md"
|
|
3014
|
+
base = project_root / settings.issueflows_dir
|
|
3015
|
+
for folder in (settings.current_issues_folder, settings.solved_folder):
|
|
3016
|
+
path = base / folder / name
|
|
3017
|
+
if path.is_file():
|
|
3018
|
+
return path
|
|
3019
|
+
return None
|
|
3020
|
+
|
|
3021
|
+
|
|
3022
|
+
def run_apply_changelog(
|
|
3023
|
+
project_root: Path,
|
|
3024
|
+
console: Console,
|
|
3025
|
+
issue_number: int,
|
|
3026
|
+
as_json: bool,
|
|
3027
|
+
) -> int:
|
|
3028
|
+
"""Apply a deferred changelog bullet on the default branch (issue #288).
|
|
3029
|
+
|
|
3030
|
+
Reads ``### Deferred changelog`` from ``issue<N>_status.md`` (current, then
|
|
3031
|
+
solved) and writes ``Settings.history_file``. Refuses on an issue branch.
|
|
3032
|
+
No-op when the file is missing, the close step chose ``nohistory``, or the
|
|
3033
|
+
bullet is already present.
|
|
3034
|
+
"""
|
|
3035
|
+
from datetime import date
|
|
3036
|
+
|
|
3037
|
+
settings = Settings()
|
|
3038
|
+
changelog_name = settings.history_file
|
|
3039
|
+
notes: list[str] = []
|
|
3040
|
+
payload: dict[str, Any] = {
|
|
3041
|
+
"ok": False,
|
|
3042
|
+
"action": "none",
|
|
3043
|
+
"reason": None,
|
|
3044
|
+
"issue": issue_number,
|
|
3045
|
+
"history_file": changelog_name,
|
|
3046
|
+
"status_file": None,
|
|
3047
|
+
"bullet": None,
|
|
3048
|
+
"planned_version": None,
|
|
3049
|
+
"branch": None,
|
|
3050
|
+
"default_branch": None,
|
|
3051
|
+
"notes": notes,
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
def emit(exit_code: int) -> int:
|
|
3055
|
+
payload["ok"] = exit_code == 0
|
|
3056
|
+
if as_json:
|
|
3057
|
+
_emit_json(console, payload)
|
|
3058
|
+
else:
|
|
3059
|
+
_render_apply_changelog_text(console, payload, exit_code)
|
|
3060
|
+
return exit_code
|
|
3061
|
+
|
|
3062
|
+
if not gitutils.git_available():
|
|
3063
|
+
notes.append("git is not available.")
|
|
3064
|
+
payload["reason"] = "git_unavailable"
|
|
3065
|
+
payload["action"] = "refused"
|
|
3066
|
+
return emit(1)
|
|
3067
|
+
|
|
3068
|
+
current = gitutils.current_branch(project_root)
|
|
3069
|
+
default = gitutils.default_branch(project_root)
|
|
3070
|
+
payload["branch"] = current
|
|
3071
|
+
payload["default_branch"] = default
|
|
3072
|
+
if current is None or current != default:
|
|
3073
|
+
notes.append(
|
|
3074
|
+
f"apply-changelog runs on the default branch only "
|
|
3075
|
+
f"(now on {current or 'detached'}, default {default})."
|
|
3076
|
+
)
|
|
3077
|
+
payload["reason"] = "not_default_branch"
|
|
3078
|
+
payload["action"] = "refused"
|
|
3079
|
+
return emit(1)
|
|
3080
|
+
|
|
3081
|
+
status_path = _status_path_for_issue(project_root, settings, issue_number)
|
|
3082
|
+
if status_path is None:
|
|
3083
|
+
notes.append(
|
|
3084
|
+
f"no issue{issue_number}_status.md under "
|
|
3085
|
+
f"{settings.current_issues_folder} or {settings.solved_folder}."
|
|
3086
|
+
)
|
|
3087
|
+
payload["reason"] = "no_status_file"
|
|
3088
|
+
payload["action"] = "noop"
|
|
3089
|
+
return emit(0)
|
|
3090
|
+
|
|
3091
|
+
payload["status_file"] = str(
|
|
3092
|
+
status_path.relative_to(project_root)
|
|
3093
|
+
if status_path.is_relative_to(project_root)
|
|
3094
|
+
else status_path
|
|
3095
|
+
)
|
|
3096
|
+
try:
|
|
3097
|
+
status_text = status_path.read_text(encoding="utf-8")
|
|
3098
|
+
except OSError as exc:
|
|
3099
|
+
notes.append(f"could not read status file: {exc}")
|
|
3100
|
+
payload["reason"] = "status_unreadable"
|
|
3101
|
+
payload["action"] = "refused"
|
|
3102
|
+
return emit(1)
|
|
3103
|
+
|
|
3104
|
+
deferred = history.parse_deferred_changelog(status_text)
|
|
3105
|
+
if deferred is None:
|
|
3106
|
+
notes.append("no ### Deferred changelog section.")
|
|
3107
|
+
payload["reason"] = "no_deferred_section"
|
|
3108
|
+
payload["action"] = "noop"
|
|
3109
|
+
return emit(0)
|
|
3110
|
+
if deferred.skipped:
|
|
3111
|
+
notes.append("deferred section is nohistory — skip.")
|
|
3112
|
+
payload["reason"] = "nohistory"
|
|
3113
|
+
payload["action"] = "noop"
|
|
3114
|
+
return emit(0)
|
|
3115
|
+
if not deferred.bullet:
|
|
3116
|
+
notes.append("deferred section has no bullet.")
|
|
3117
|
+
payload["reason"] = "no_bullet"
|
|
3118
|
+
payload["action"] = "noop"
|
|
3119
|
+
return emit(0)
|
|
3120
|
+
|
|
3121
|
+
payload["bullet"] = deferred.bullet
|
|
3122
|
+
payload["planned_version"] = deferred.planned_version
|
|
3123
|
+
|
|
3124
|
+
history_path = project_root / changelog_name
|
|
3125
|
+
if not history_path.is_file():
|
|
3126
|
+
notes.append(f"no {changelog_name} at the project root — skip.")
|
|
3127
|
+
payload["reason"] = "missing_history_file"
|
|
3128
|
+
payload["action"] = "noop"
|
|
3129
|
+
return emit(0)
|
|
3130
|
+
|
|
3131
|
+
try:
|
|
3132
|
+
text = history_path.read_text(encoding="utf-8")
|
|
3133
|
+
except OSError as exc:
|
|
3134
|
+
notes.append(f"could not read {changelog_name}: {exc}")
|
|
3135
|
+
payload["reason"] = "history_unreadable"
|
|
3136
|
+
payload["action"] = "refused"
|
|
3137
|
+
return emit(1)
|
|
3138
|
+
|
|
3139
|
+
if history.changelog_has_bullet(text, deferred.bullet):
|
|
3140
|
+
notes.append("bullet already present.")
|
|
3141
|
+
payload["reason"] = "already_present"
|
|
3142
|
+
payload["action"] = "noop"
|
|
3143
|
+
return emit(0)
|
|
3144
|
+
|
|
3145
|
+
try:
|
|
3146
|
+
updated = history.append_unreleased_bullet(text, deferred.bullet)
|
|
3147
|
+
action = "appended"
|
|
3148
|
+
if deferred.planned_version:
|
|
3149
|
+
updated = history.promote_unreleased(
|
|
3150
|
+
updated, deferred.planned_version, date.today().isoformat()
|
|
3151
|
+
)
|
|
3152
|
+
action = "promoted"
|
|
3153
|
+
except history.MissingUnreleased:
|
|
3154
|
+
notes.append(f"{changelog_name} has no ## [Unreleased] heading.")
|
|
3155
|
+
payload["reason"] = "no_unreleased_section"
|
|
3156
|
+
payload["action"] = "refused"
|
|
3157
|
+
return emit(1)
|
|
3158
|
+
|
|
3159
|
+
if updated == text:
|
|
3160
|
+
notes.append("no change after apply.")
|
|
3161
|
+
payload["reason"] = "unchanged"
|
|
3162
|
+
payload["action"] = "noop"
|
|
3163
|
+
return emit(0)
|
|
3164
|
+
|
|
3165
|
+
try:
|
|
3166
|
+
history_path.write_text(updated, encoding="utf-8")
|
|
3167
|
+
except OSError as exc:
|
|
3168
|
+
notes.append(f"could not write {changelog_name}: {exc}")
|
|
3169
|
+
payload["reason"] = "history_unwritable"
|
|
3170
|
+
payload["action"] = "refused"
|
|
3171
|
+
return emit(1)
|
|
3172
|
+
|
|
3173
|
+
payload["reason"] = action
|
|
3174
|
+
payload["action"] = action
|
|
3175
|
+
notes.append(f"wrote {changelog_name} ({action}).")
|
|
3176
|
+
return emit(0)
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
def _render_apply_changelog_text(
|
|
3180
|
+
console: Console, payload: dict[str, Any], exit_code: int
|
|
3181
|
+
) -> None:
|
|
3182
|
+
action = payload.get("action") or "none"
|
|
3183
|
+
reason = payload.get("reason") or ""
|
|
3184
|
+
issue = payload.get("issue")
|
|
3185
|
+
if exit_code == 0:
|
|
3186
|
+
console.print(
|
|
3187
|
+
f"[green]ok[/green] apply-changelog #{issue} "
|
|
3188
|
+
f"[bold]{escape(str(action))}[/bold]"
|
|
3189
|
+
+ (f" ({escape(str(reason))})" if reason and reason != action else "")
|
|
3190
|
+
)
|
|
3191
|
+
else:
|
|
3192
|
+
console.print(
|
|
3193
|
+
f"[red]refused[/red] apply-changelog #{issue}: "
|
|
3194
|
+
f"{escape(str(reason) or action)}"
|
|
3195
|
+
)
|
|
3196
|
+
for note in payload.get("notes") or []:
|
|
3197
|
+
console.print(f" [dim]{escape(str(note))}[/dim]")
|
|
3198
|
+
|
|
3199
|
+
|
|
2890
3200
|
# ---------------------------------------------------------------------------
|
|
2891
3201
|
# agent sweep
|
|
2892
3202
|
# ---------------------------------------------------------------------------
|
|
@@ -3531,7 +3841,8 @@ def _print_config_guide(console: Console, cfg_path: Path) -> None:
|
|
|
3531
3841
|
"[bold]auto_close[/bold] / "
|
|
3532
3842
|
"[bold]auto_plan[/bold] / [bold]auto_build[/bold] / "
|
|
3533
3843
|
"[bold]early_pr[/bold] / [bold]fix_auto_name[/bold]; "
|
|
3534
|
-
"[bold]confirm_version_bump[/bold] / [bold]confirm_changelog_update[/bold]
|
|
3844
|
+
"[bold]confirm_version_bump[/bold] / [bold]confirm_changelog_update[/bold] / "
|
|
3845
|
+
"[bold]defer_changelog[/bold]; "
|
|
3535
3846
|
"[bold]ruff_autofix[/bold]; [bold]essential_tests[/bold] / "
|
|
3536
3847
|
"[bold]test_runner[/bold] / [bold]essential_marker[/bold] / "
|
|
3537
3848
|
"[bold]essential_review[/bold]: skill-behaviour toggles; re-run "
|
|
@@ -22,10 +22,10 @@ agent_app = typer.Typer(
|
|
|
22
22
|
"Agent-facing helpers that read the .issueflows/ tree and git/gh so "
|
|
23
23
|
"AI agents get deterministic answers instead of re-deriving lifecycle "
|
|
24
24
|
"state by hand. All are read-only except `sweep`, `archive`, `capture`, "
|
|
25
|
-
"`switchback`, `sync-branch`, `pr-sync`, `repair`, `label-apply`, "
|
|
25
|
+
"`switchback`, `sync-branch`, `pr-sync`, `apply-changelog`, `repair`, `label-apply`, "
|
|
26
26
|
"`open-workspace --open`, `worktree-add`, and `worktree-remove`. "
|
|
27
|
-
"`branches` (remote)
|
|
28
|
-
"every delete stays in `/iflow-cleanup
|
|
27
|
+
"`branches` (remote), `local-branches` (local), and `default-sync` only classify: "
|
|
28
|
+
"every delete stays in `/iflow-cleanup`; `default-sync` never mutates."
|
|
29
29
|
),
|
|
30
30
|
)
|
|
31
31
|
|
|
@@ -263,10 +263,26 @@ def update(
|
|
|
263
263
|
json_output: bool = typer.Option(
|
|
264
264
|
False, "--json", help="Emit a machine-readable JSON object (with --all)."
|
|
265
265
|
),
|
|
266
|
+
include_workspace: bool = typer.Option(
|
|
267
|
+
False,
|
|
268
|
+
"--workspace",
|
|
269
|
+
help=(
|
|
270
|
+
"With --all, also union members of the nearest "
|
|
271
|
+
"issueflow-workspace.toml (unique by resolved path). "
|
|
272
|
+
"Without --all this flag is an error."
|
|
273
|
+
),
|
|
274
|
+
),
|
|
266
275
|
) -> None:
|
|
267
276
|
"""Refresh packaged editor commands, rules, and workflow doc from this package."""
|
|
268
277
|
from issue_flow.init import run_update, run_update_all
|
|
269
278
|
|
|
279
|
+
if include_workspace and not all_roots:
|
|
280
|
+
_console.print(
|
|
281
|
+
"[red]error[/red] --workspace requires --all "
|
|
282
|
+
"(union registry + nearest workspace members)."
|
|
283
|
+
)
|
|
284
|
+
raise typer.Exit(code=2)
|
|
285
|
+
|
|
270
286
|
if all_roots:
|
|
271
287
|
raise typer.Exit(
|
|
272
288
|
code=run_update_all(
|
|
@@ -274,6 +290,8 @@ def update(
|
|
|
274
290
|
editors=editor,
|
|
275
291
|
force=force,
|
|
276
292
|
as_json=json_output,
|
|
293
|
+
include_workspace=include_workspace,
|
|
294
|
+
workspace_start=project_dir,
|
|
277
295
|
)
|
|
278
296
|
)
|
|
279
297
|
|
|
@@ -522,6 +540,25 @@ def agent_switchback(
|
|
|
522
540
|
raise typer.Exit(code=run_switchback(project_dir, _console, json_output))
|
|
523
541
|
|
|
524
542
|
|
|
543
|
+
@agent_app.command("default-sync")
|
|
544
|
+
def agent_default_sync(
|
|
545
|
+
project_dir: Path = _PROJECT_DIR_OPTION,
|
|
546
|
+
json_output: bool = typer.Option(
|
|
547
|
+
False, "--json", help="Emit a machine-readable JSON object."
|
|
548
|
+
),
|
|
549
|
+
) -> None:
|
|
550
|
+
"""Classify unique commits on home default vs origin (no mutate).
|
|
551
|
+
|
|
552
|
+
The mechanical half of pick / cleanup / switchback recovery when
|
|
553
|
+
``git pull --ff-only`` cannot reconcile unpushed home commits with a
|
|
554
|
+
squash on origin. Prints ahead/behind, commit onelines + paths, and a
|
|
555
|
+
recommended ``action``. Never rebases, force-pushes, or pushes default.
|
|
556
|
+
"""
|
|
557
|
+
from issue_flow.agent import run_default_sync
|
|
558
|
+
|
|
559
|
+
raise typer.Exit(code=run_default_sync(project_dir, _console, json_output))
|
|
560
|
+
|
|
561
|
+
|
|
525
562
|
@agent_app.command("sync-branch")
|
|
526
563
|
def agent_sync_branch(
|
|
527
564
|
project_dir: Path = _PROJECT_DIR_OPTION,
|
|
@@ -552,6 +589,35 @@ def agent_sync_branch(
|
|
|
552
589
|
raise typer.Exit(code=run_sync_branch(project_dir, _console, strategy, json_output))
|
|
553
590
|
|
|
554
591
|
|
|
592
|
+
@agent_app.command("apply-changelog")
|
|
593
|
+
def agent_apply_changelog(
|
|
594
|
+
issue: int = typer.Option(
|
|
595
|
+
...,
|
|
596
|
+
"--issue",
|
|
597
|
+
"-n",
|
|
598
|
+
help="GitHub issue number whose deferred changelog bullet to apply.",
|
|
599
|
+
),
|
|
600
|
+
project_dir: Path = _PROJECT_DIR_OPTION,
|
|
601
|
+
json_output: bool = typer.Option(
|
|
602
|
+
False, "--json", help="Emit a machine-readable JSON object."
|
|
603
|
+
),
|
|
604
|
+
) -> None:
|
|
605
|
+
"""Apply a deferred changelog bullet on the default branch.
|
|
606
|
+
|
|
607
|
+
Reads ``### Deferred changelog`` from ``issue<N>_status.md`` (current, then
|
|
608
|
+
solved) and writes the project's HISTORY/CHANGELOG file. Refuses unless
|
|
609
|
+
the checkout is on the default branch. No-op when the file is missing,
|
|
610
|
+
the close step chose ``nohistory``, or the bullet is already present.
|
|
611
|
+
Used by ``/iflow-cleanup`` and yolo post-pull when ``defer_changelog`` is
|
|
612
|
+
on (issue #288).
|
|
613
|
+
"""
|
|
614
|
+
from issue_flow.agent import run_apply_changelog
|
|
615
|
+
|
|
616
|
+
raise typer.Exit(
|
|
617
|
+
code=run_apply_changelog(project_dir, _console, issue, json_output)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
555
621
|
@agent_app.command("pr-sync")
|
|
556
622
|
def agent_pr_sync(
|
|
557
623
|
numbers: list[int] = typer.Argument(
|
|
@@ -1201,6 +1267,7 @@ def config_add(
|
|
|
1201
1267
|
``confirm_version_bump``,
|
|
1202
1268
|
``ruff_autofix``, ``auto_close``, ``auto_plan``, ``auto_build``,
|
|
1203
1269
|
``early_pr``, ``fix_auto_name``, ``confirm_changelog_update``,
|
|
1270
|
+
``defer_changelog``,
|
|
1204
1271
|
``essential_tests``, ``test_runner``, ``essential_marker``,
|
|
1205
1272
|
``essential_review``, ``pstack_skills``
|
|
1206
1273
|
— taking each from its ``ISSUEFLOW_*`` env var when set, else the default.
|
|
@@ -1427,7 +1494,10 @@ def workspace_update(
|
|
|
1427
1494
|
def register(
|
|
1428
1495
|
project_dir: Path = typer.Argument(
|
|
1429
1496
|
default=Path("."),
|
|
1430
|
-
help=
|
|
1497
|
+
help=(
|
|
1498
|
+
"Project root to add, or the walk start when ``--discover`` "
|
|
1499
|
+
"is set (defaults to current directory)."
|
|
1500
|
+
),
|
|
1431
1501
|
exists=True,
|
|
1432
1502
|
file_okay=False,
|
|
1433
1503
|
resolve_path=True,
|
|
@@ -1435,10 +1505,89 @@ def register(
|
|
|
1435
1505
|
json_output: bool = typer.Option(
|
|
1436
1506
|
False, "--json", help="Emit a machine-readable JSON object."
|
|
1437
1507
|
),
|
|
1508
|
+
discover: bool = typer.Option(
|
|
1509
|
+
False,
|
|
1510
|
+
"--discover",
|
|
1511
|
+
help=(
|
|
1512
|
+
"Walk PROJECT_DIR for existing issue-flow scaffolds and offer "
|
|
1513
|
+
"to register them. Never runs from update --all / init / "
|
|
1514
|
+
"workspace update."
|
|
1515
|
+
),
|
|
1516
|
+
),
|
|
1517
|
+
yes: bool = typer.Option(
|
|
1518
|
+
False,
|
|
1519
|
+
"--yes",
|
|
1520
|
+
help="With --discover, register candidates without a confirm prompt.",
|
|
1521
|
+
),
|
|
1522
|
+
max_depth: int = typer.Option(
|
|
1523
|
+
4,
|
|
1524
|
+
"--max-depth",
|
|
1525
|
+
help="With --discover, maximum directory depth below PROJECT_DIR.",
|
|
1526
|
+
),
|
|
1438
1527
|
) -> None:
|
|
1439
1528
|
"""Add a project root to the user-global registry for ``update --all``."""
|
|
1529
|
+
from issue_flow.config import Settings
|
|
1530
|
+
from issue_flow.project import discover_issueflow_roots
|
|
1440
1531
|
from issue_flow.user_global import register_root, user_global_registry_path
|
|
1441
1532
|
|
|
1533
|
+
if discover:
|
|
1534
|
+
if max_depth < 0:
|
|
1535
|
+
_console.print("[red]error[/red] --max-depth must be >= 0")
|
|
1536
|
+
raise typer.Exit(code=2)
|
|
1537
|
+
settings = Settings()
|
|
1538
|
+
candidates = discover_issueflow_roots(
|
|
1539
|
+
project_dir,
|
|
1540
|
+
issueflows_dir=settings.issueflows_dir,
|
|
1541
|
+
max_depth=max_depth,
|
|
1542
|
+
)
|
|
1543
|
+
if not yes:
|
|
1544
|
+
_console.print(
|
|
1545
|
+
f"[bold]{len(candidates)} scaffold(s) under {project_dir}[/bold]"
|
|
1546
|
+
)
|
|
1547
|
+
for root in candidates:
|
|
1548
|
+
_console.print(f" {root}")
|
|
1549
|
+
if not candidates:
|
|
1550
|
+
payload = {
|
|
1551
|
+
"ok": True,
|
|
1552
|
+
"discover": True,
|
|
1553
|
+
"candidates": [],
|
|
1554
|
+
"added": [],
|
|
1555
|
+
"already": [],
|
|
1556
|
+
"registry": str(user_global_registry_path()),
|
|
1557
|
+
}
|
|
1558
|
+
if json_output:
|
|
1559
|
+
_console.print_json(data=payload)
|
|
1560
|
+
else:
|
|
1561
|
+
_console.print("[dim]nothing to register[/dim]")
|
|
1562
|
+
return
|
|
1563
|
+
if not typer.confirm("Register these roots?", default=False):
|
|
1564
|
+
raise typer.Exit(code=1)
|
|
1565
|
+
added_paths: list[str] = []
|
|
1566
|
+
already_paths: list[str] = []
|
|
1567
|
+
for root in candidates:
|
|
1568
|
+
if register_root(root):
|
|
1569
|
+
added_paths.append(str(root))
|
|
1570
|
+
else:
|
|
1571
|
+
already_paths.append(str(root))
|
|
1572
|
+
payload = {
|
|
1573
|
+
"ok": True,
|
|
1574
|
+
"discover": True,
|
|
1575
|
+
"candidates": [str(root) for root in candidates],
|
|
1576
|
+
"added": added_paths,
|
|
1577
|
+
"already": already_paths,
|
|
1578
|
+
"registry": str(user_global_registry_path()),
|
|
1579
|
+
}
|
|
1580
|
+
if json_output:
|
|
1581
|
+
_console.print_json(data=payload)
|
|
1582
|
+
return
|
|
1583
|
+
for path in added_paths:
|
|
1584
|
+
_console.print(f"[green]registered[/green] {path}")
|
|
1585
|
+
for path in already_paths:
|
|
1586
|
+
_console.print(f"[dim]already registered[/dim] {path}")
|
|
1587
|
+
if not candidates:
|
|
1588
|
+
_console.print("[dim]nothing to register[/dim]")
|
|
1589
|
+
return
|
|
1590
|
+
|
|
1442
1591
|
added = register_root(project_dir)
|
|
1443
1592
|
payload = {
|
|
1444
1593
|
"ok": True,
|
|
@@ -18,6 +18,7 @@ from issue_flow.modes import (
|
|
|
18
18
|
DEFAULT_AUTO_PLAN,
|
|
19
19
|
DEFAULT_CHECKS_WATCH_MINUTES,
|
|
20
20
|
DEFAULT_CONFIRM_CHANGELOG_UPDATE,
|
|
21
|
+
DEFAULT_DEFER_CHANGELOG,
|
|
21
22
|
DEFAULT_EARLY_PR,
|
|
22
23
|
DEFAULT_FIX_AUTO_NAME,
|
|
23
24
|
DEFAULT_LOCKED,
|
|
@@ -616,6 +617,16 @@ class Settings:
|
|
|
616
617
|
),
|
|
617
618
|
)
|
|
618
619
|
|
|
620
|
+
def resolve_defer_changelog(self, project_root: Path) -> bool:
|
|
621
|
+
"""Resolve whether changelog writes wait until the default branch after merge."""
|
|
622
|
+
persisted = modes_module.read_defer_changelog(self.config_path(project_root))
|
|
623
|
+
if persisted is not None:
|
|
624
|
+
return persisted
|
|
625
|
+
return self.user_global_or(
|
|
626
|
+
"defer_changelog",
|
|
627
|
+
_env_flag("ISSUEFLOW_DEFER_CHANGELOG", default=DEFAULT_DEFER_CHANGELOG),
|
|
628
|
+
)
|
|
629
|
+
|
|
619
630
|
def resolve_essential_tests(self, project_root: Path) -> bool:
|
|
620
631
|
"""Resolve whether essential-tests skill hooks are enabled."""
|
|
621
632
|
persisted = modes_module.read_essential_tests(self.config_path(project_root))
|
|
@@ -828,6 +839,9 @@ class Settings:
|
|
|
828
839
|
"ISSUEFLOW_CONFIRM_CHANGELOG_UPDATE",
|
|
829
840
|
default=DEFAULT_CONFIRM_CHANGELOG_UPDATE,
|
|
830
841
|
),
|
|
842
|
+
"defer_changelog": _env_flag(
|
|
843
|
+
"ISSUEFLOW_DEFER_CHANGELOG", default=DEFAULT_DEFER_CHANGELOG
|
|
844
|
+
),
|
|
831
845
|
"essential_tests": _env_flag(
|
|
832
846
|
"ISSUEFLOW_ESSENTIAL_TESTS", default=DEFAULT_ESSENTIAL_TESTS
|
|
833
847
|
),
|
|
@@ -882,6 +896,7 @@ class Settings:
|
|
|
882
896
|
"confirm_changelog_update": self.resolve_confirm_changelog_update(
|
|
883
897
|
project_root
|
|
884
898
|
),
|
|
899
|
+
"defer_changelog": self.resolve_defer_changelog(project_root),
|
|
885
900
|
"essential_tests": self.resolve_essential_tests(project_root),
|
|
886
901
|
"test_runner": self.resolve_test_runner(project_root),
|
|
887
902
|
"essential_marker": self.resolve_essential_marker(project_root),
|
|
@@ -972,6 +987,7 @@ class Settings:
|
|
|
972
987
|
"confirm_changelog_update": self.resolve_confirm_changelog_update(
|
|
973
988
|
project_root
|
|
974
989
|
),
|
|
990
|
+
"defer_changelog": self.resolve_defer_changelog(project_root),
|
|
975
991
|
"essential_tests": self.resolve_essential_tests(project_root),
|
|
976
992
|
"test_runner": self.resolve_test_runner(project_root),
|
|
977
993
|
"essential_marker": self.resolve_essential_marker(project_root),
|