Skip to content

Commit 5854316

Browse files
Phase 16: Commit and PR Workflow
1 parent 56cf356 commit 5854316

6 files changed

Lines changed: 222 additions & 1 deletion

File tree

.mythic/memory.sqlite

0 Bytes
Binary file not shown.

mythic/ai/provider_calls.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
{"routing_attempt": true, "from_provider": "copy-paste", "to_provider": "copy-paste", "role": "Companion Shell", "task_type": "conversation", "packet_id": "shell", "succeeded": true, "error": "", "skipped_reason": ""}
77
{"routing_attempt": true, "from_provider": "copy-paste", "to_provider": "copy-paste", "role": "Companion Shell", "task_type": "conversation", "packet_id": "shell", "succeeded": true, "error": "", "skipped_reason": ""}
88
{"routing_attempt": true, "from_provider": "copy-paste", "to_provider": "copy-paste", "role": "Companion Shell", "task_type": "conversation", "packet_id": "shell", "succeeded": true, "error": "", "skipped_reason": ""}
9+
{"routing_attempt": true, "from_provider": "copy-paste", "to_provider": "copy-paste", "role": "Companion Shell", "task_type": "conversation", "packet_id": "shell", "succeeded": true, "error": "", "skipped_reason": ""}

mythic_vibe_cli/app.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,9 @@ def _help(text: str) -> str:
19671967
"""
19681968
Examples:
19691969
mythic-vibe workspace status
1970+
mythic-vibe workspace diff
1971+
mythic-vibe workspace commit --message "Add memory feature" --yes
1972+
mythic-vibe workspace push --yes
19701973
mythic-vibe workspace clone https://github.com/owner/repo --yes
19711974
mythic-vibe workspace open ./repo
19721975
mythic-vibe workspace branch feature/memory --yes
@@ -2021,6 +2024,24 @@ def _help(text: str) -> str:
20212024
workspace_plan.add_argument("--workspace-root", default="")
20222025
add_runtime_options(workspace_plan, json_output=True)
20232026

2027+
workspace_diff = workspace_sub.add_parser("diff", help=_help("Show changes in the current workspace"))
2028+
workspace_diff.add_argument("--path", default=".")
2029+
workspace_diff.add_argument("--workspace-root", default="")
2030+
add_runtime_options(workspace_diff, json_output=True)
2031+
2032+
workspace_commit = workspace_sub.add_parser("commit", help=_help("Commit changes in the current workspace"))
2033+
workspace_commit.add_argument("--path", default=".")
2034+
workspace_commit.add_argument("--message", "-m", default="", help="Commit message")
2035+
workspace_commit.add_argument("--workspace-root", default="")
2036+
workspace_commit.add_argument("--yes", action="store_true", help="Actually run git commit")
2037+
add_runtime_options(workspace_commit, json_output=True)
2038+
2039+
workspace_push = workspace_sub.add_parser("push", help=_help("Push the current branch to origin"))
2040+
workspace_push.add_argument("--path", default=".")
2041+
workspace_push.add_argument("--workspace-root", default="")
2042+
workspace_push.add_argument("--yes", action="store_true", help="Actually run git push")
2043+
add_runtime_options(workspace_push, json_output=True)
2044+
20242045
# --- PH-05 slice 5.5 / 5.6: graph query + visualize ---
20252046
graph_cmd = sub.add_parser(
20262047
"graph",

mythic_vibe_cli/commands.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6897,9 +6897,15 @@ def cmd_workspace_dispatch(args: argparse.Namespace) -> int:
68976897
return cmd_workspace_pr(args)
68986898
if sub == "plan":
68996899
return cmd_workspace_plan(args)
6900+
if sub == "diff":
6901+
return cmd_workspace_diff(args)
6902+
if sub == "commit":
6903+
return cmd_workspace_commit(args)
6904+
if sub == "push":
6905+
return cmd_workspace_push(args)
69006906
write_error(
69016907
f"Unknown workspace subcommand: {sub!r}. "
6902-
"Valid: status | clone | open | branch | track | pr | plan."
6908+
"Valid: status | clone | open | branch | track | pr | plan | diff | commit | push."
69036909
)
69046910
return USER_INPUT_ERROR
69056911

@@ -6928,6 +6934,62 @@ def cmd_workspace_status(args: argparse.Namespace) -> int:
69286934
return SUCCESS
69296935

69306936

6937+
def cmd_workspace_diff(args: argparse.Namespace) -> int:
6938+
from .workspaces.manager import show_diff
6939+
6940+
action = show_diff(
6941+
Path(getattr(args, "path", ".")).resolve(),
6942+
workspace_root=_workspace_root_from_args(args),
6943+
)
6944+
if _flag(args, "json"):
6945+
write_json({"command": "workspace diff", "action": action.to_dict()})
6946+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6947+
_render_workspace_action(action)
6948+
if action.stdout:
6949+
write_line("")
6950+
write_line(action.stdout)
6951+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6952+
6953+
6954+
def cmd_workspace_commit(args: argparse.Namespace) -> int:
6955+
from .workspaces.manager import commit_changes
6956+
6957+
action = commit_changes(
6958+
Path(getattr(args, "path", ".")).resolve(),
6959+
workspace_root=_workspace_root_from_args(args),
6960+
message=str(getattr(args, "message", "") or ""),
6961+
execute=bool(getattr(args, "yes", False)),
6962+
)
6963+
if _flag(args, "json"):
6964+
write_json({"command": "workspace commit", "action": action.to_dict()})
6965+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6966+
_render_workspace_action(action)
6967+
if action.stdout:
6968+
write_line("")
6969+
write_line(action.stdout)
6970+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6971+
6972+
6973+
def cmd_workspace_push(args: argparse.Namespace) -> int:
6974+
from .workspaces.manager import push_branch
6975+
6976+
action = push_branch(
6977+
Path(getattr(args, "path", ".")).resolve(),
6978+
workspace_root=_workspace_root_from_args(args),
6979+
execute=bool(getattr(args, "yes", False)),
6980+
)
6981+
if _flag(args, "json"):
6982+
write_json({"command": "workspace push", "action": action.to_dict()})
6983+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6984+
_render_workspace_action(action)
6985+
if action.stdout:
6986+
write_line("")
6987+
write_line(action.stdout)
6988+
if action.stderr:
6989+
write_line(action.stderr)
6990+
return SUCCESS if action.exit_code == 0 else USER_INPUT_ERROR
6991+
6992+
69316993
def cmd_workspace_clone(args: argparse.Namespace) -> int:
69326994
from .workspaces.manager import clone_repo
69336995

mythic_vibe_cli/repl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@ def _known_command_names(_main: Callable[[list[str]], int]) -> set[str]:
367367
5. EDIT code, run tests, and execute commands to achieve the goal.
368368
6. REMEMBER the results of your actions.
369369
370+
Workspace Safety Protocol:
371+
- Always use `workspace diff` to review your own changes before making a commit.
372+
- You must ask the user for explicit confirmation before executing `workspace commit` or `workspace push`.
373+
370374
Always use your tools when you need to read files, run tests, or modify code.
371375
When the user asks you a question or gives you a task, you can invoke multiple tools in sequence.
372376
Once you have accomplished the goal, provide a conversational summary to the user.

mythic_vibe_cli/workspaces/manager.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,136 @@ def prepare_pr_draft(
452452
)
453453

454454

455+
def show_diff(path: Path, *, workspace_root: Path) -> WorkspaceAction:
456+
repo, branch, remote, dirty = detect_repo(path)
457+
if not repo:
458+
return WorkspaceAction(
459+
action="diff",
460+
workspace_root=str(workspace_root),
461+
target_path=str(path.resolve()),
462+
exit_code=1,
463+
message="No Git repository detected at this path.",
464+
)
465+
command = ("git", "diff", "HEAD")
466+
result = exec_command(command, cwd=repo)
467+
return WorkspaceAction(
468+
action="diff",
469+
workspace_root=str(workspace_root),
470+
target_path=repo,
471+
branch=branch,
472+
executed=True,
473+
command=command,
474+
exit_code=result.code,
475+
stdout=result.stdout,
476+
stderr=result.stderr,
477+
message="Diff generated." if result.code == 0 else "Failed to generate diff.",
478+
)
479+
480+
481+
def commit_changes(path: Path, *, workspace_root: Path, message: str, execute: bool = False) -> WorkspaceAction:
482+
repo, branch, remote, dirty = detect_repo(path)
483+
if not repo:
484+
return WorkspaceAction(
485+
action="commit",
486+
workspace_root=str(workspace_root),
487+
target_path=str(path.resolve()),
488+
exit_code=1,
489+
message="No Git repository detected at this path.",
490+
)
491+
if not dirty:
492+
return WorkspaceAction(
493+
action="commit",
494+
workspace_root=str(workspace_root),
495+
target_path=repo,
496+
branch=branch,
497+
exit_code=0,
498+
message="Working tree is clean, nothing to commit.",
499+
)
500+
if not message:
501+
return WorkspaceAction(
502+
action="commit",
503+
workspace_root=str(workspace_root),
504+
target_path=repo,
505+
branch=branch,
506+
exit_code=1,
507+
message="A commit message is required.",
508+
)
509+
510+
command = ("git", "commit", "-am", message)
511+
if not execute:
512+
return WorkspaceAction(
513+
action="commit",
514+
workspace_root=str(workspace_root),
515+
target_path=repo,
516+
branch=branch,
517+
executed=False,
518+
command=command,
519+
message="Commit prepared; pass --yes to execute.",
520+
)
521+
522+
result = exec_command(command, cwd=repo)
523+
return WorkspaceAction(
524+
action="commit",
525+
workspace_root=str(workspace_root),
526+
target_path=repo,
527+
branch=branch,
528+
executed=True,
529+
command=command,
530+
exit_code=result.code,
531+
stdout=result.stdout,
532+
stderr=result.stderr,
533+
message="Changes committed." if result.code == 0 else "Commit failed.",
534+
metadata={"dirty": False} if result.code == 0 else {"dirty": True},
535+
)
536+
537+
538+
def push_branch(path: Path, *, workspace_root: Path, execute: bool = False) -> WorkspaceAction:
539+
repo, branch, remote, dirty = detect_repo(path)
540+
if not repo:
541+
return WorkspaceAction(
542+
action="push",
543+
workspace_root=str(workspace_root),
544+
target_path=str(path.resolve()),
545+
exit_code=1,
546+
message="No Git repository detected at this path.",
547+
)
548+
if not remote:
549+
return WorkspaceAction(
550+
action="push",
551+
workspace_root=str(workspace_root),
552+
target_path=repo,
553+
branch=branch,
554+
exit_code=1,
555+
message="No remote configured for this repository.",
556+
)
557+
558+
command = ("git", "push", "origin", branch)
559+
if not execute:
560+
return WorkspaceAction(
561+
action="push",
562+
workspace_root=str(workspace_root),
563+
target_path=repo,
564+
branch=branch,
565+
executed=False,
566+
command=command,
567+
message="Push prepared; pass --yes to execute.",
568+
)
569+
570+
result = exec_command(command, cwd=repo)
571+
return WorkspaceAction(
572+
action="push",
573+
workspace_root=str(workspace_root),
574+
target_path=repo,
575+
branch=branch,
576+
executed=True,
577+
command=command,
578+
exit_code=result.code,
579+
stdout=result.stdout,
580+
stderr=result.stderr,
581+
message="Branch pushed successfully." if result.code == 0 else "Push failed.",
582+
)
583+
584+
455585
def propose_workspace_plan(prompt: str, *, workspace_root: Path) -> str:
456586
text = prompt.strip()
457587
repo_url = _extract_repo_url(text)
@@ -505,16 +635,19 @@ def _extract_branch_name(text: str) -> str:
505635
"WorkspaceRecord",
506636
"WorkspaceStatus",
507637
"clone_repo",
638+
"commit_changes",
508639
"create_branch",
509640
"default_workspace_root",
510641
"detect_repo",
511642
"load_registry",
512643
"open_workspace",
513644
"prepare_pr_draft",
514645
"propose_workspace_plan",
646+
"push_branch",
515647
"registry_path",
516648
"resolve_workspace_root",
517649
"save_registry",
650+
"show_diff",
518651
"track_branch",
519652
"workspace_status",
520653
]

0 commit comments

Comments
 (0)