agent-memory-cli 0.1.2__tar.gz → 0.1.3__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.
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/CHANGELOG.md +6 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/PKG-INFO +7 -6
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/README.md +6 -5
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/__init__.py +1 -1
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/cli.py +3 -3
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/startup.py +3 -3
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/sync.py +104 -44
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_startup.py +22 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_sync.py +194 -11
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/.gitignore +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/LICENSE +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/Makefile +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/pyproject.toml +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/scripts/check_installed_wheel.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/scripts/release_notes.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/__main__.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/archive.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/debrief.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/doctor.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/events.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/git_runner.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/home.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/layout.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/org.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/prerequisites.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/publication.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/setup/__init__.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/setup/claude.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/setup/codex.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/setup/common.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/setup/legacy.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/status.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/decisions.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/recent.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/rules.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/workflow/SKILL.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/workflow.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/.hidden/2026/08/20260825-alice-abc12345.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-ABCD.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825_alice_abc12345.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/09/20260825-alice-def12345.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/20260825-alice-abc12345.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/missing_debriefs_dir/org-memory/recent.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/.stage.20260825-alice-77xx88yy.md.a1b2c3 +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/Demo_Project/2026/08/20260825-bob-ops-9f00aa11.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo.project/2026/07/20260701-Alice_2.dev-00aa11bb.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/other-project/2026/12/20261203-bob-9e0d44aa.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/conftest.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/canonical_memory_gitignore.txt +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/debrief_record.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/doctor_memory_0.4.5.json +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/doctor_memory_0.4.5.txt +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/event_record.md +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_archive.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_cli.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_debrief.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_doctor.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_events.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_home.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_layout.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_org.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_prerequisites.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_setup.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_status.py +0 -0
- {agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/test_workflow.py +0 -0
|
@@ -4,6 +4,12 @@ All notable changes to agent-memory are documented in this file. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.1.3 - 2026-09-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `pull`, `push`, and SessionStart sync can resolve homes with local memory changes behind upstream ([sync](docs/commands.md#sync)).
|
|
12
|
+
|
|
7
13
|
## 0.1.2 - 2026-09-17
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-memory-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Cross-session memory for coding agents: plain files, git-native, no server
|
|
5
5
|
Project-URL: Homepage, https://github.com/kiloloop/agent-memory
|
|
6
6
|
Project-URL: Source, https://github.com/kiloloop/agent-memory
|
|
@@ -105,7 +105,7 @@ Python 3.10+ is required; sync needs git 2.25+, hooks use Bash, and
|
|
|
105
105
|
agent-memory enable --remote git@github.com:YOUR_ORG/agent-memory-store.git
|
|
106
106
|
agent-memory push --agent codex
|
|
107
107
|
# After another machine pushes, with a clean local tree:
|
|
108
|
-
agent-memory pull
|
|
108
|
+
agent-memory pull --agent claude
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
Elsewhere, `agent-memory clone <remote-url> --home <local-home>`, then repeat
|
|
@@ -141,13 +141,14 @@ points into a home's `projects/` tree. Project selection uses `--project` or a
|
|
|
141
141
|
matching binding/marker. OACP is not required.
|
|
142
142
|
|
|
143
143
|
Sync makes the home a git repository with an allowlist and `.oacp-memory-repo`
|
|
144
|
-
marker. `push`
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
marker. `push` and `pull --agent NAME` commit selected paths, rebase when behind,
|
|
145
|
+
and deliver. A plain `pull` updates a clean tree and reports unpushed commits.
|
|
146
|
+
Conflicts abort the rebase and retain local commits. Keys and setup receipts
|
|
147
|
+
stay local. Network and rebase operations time out after 30 seconds.
|
|
147
148
|
|
|
148
149
|
`setup` installs a SessionStart hook and memory workflow. `startup` lists
|
|
149
150
|
metadata for the four project files, then the three curated org files; it
|
|
150
|
-
injects no content. `--pull`
|
|
151
|
+
injects no content. `--pull` commits and syncs first under the runtime identity, warning on failure. The workflow
|
|
151
152
|
tells the agent to run `recall` for an 8,000-character bounded read. Raise
|
|
152
153
|
`--max-chars` or read remaining files directly when cut. `capture` records
|
|
153
154
|
decisions during work. At the end, update threads and debt, optionally publish
|
|
@@ -75,7 +75,7 @@ Python 3.10+ is required; sync needs git 2.25+, hooks use Bash, and
|
|
|
75
75
|
agent-memory enable --remote git@github.com:YOUR_ORG/agent-memory-store.git
|
|
76
76
|
agent-memory push --agent codex
|
|
77
77
|
# After another machine pushes, with a clean local tree:
|
|
78
|
-
agent-memory pull
|
|
78
|
+
agent-memory pull --agent claude
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Elsewhere, `agent-memory clone <remote-url> --home <local-home>`, then repeat
|
|
@@ -111,13 +111,14 @@ points into a home's `projects/` tree. Project selection uses `--project` or a
|
|
|
111
111
|
matching binding/marker. OACP is not required.
|
|
112
112
|
|
|
113
113
|
Sync makes the home a git repository with an allowlist and `.oacp-memory-repo`
|
|
114
|
-
marker. `push`
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
marker. `push` and `pull --agent NAME` commit selected paths, rebase when behind,
|
|
115
|
+
and deliver. A plain `pull` updates a clean tree and reports unpushed commits.
|
|
116
|
+
Conflicts abort the rebase and retain local commits. Keys and setup receipts
|
|
117
|
+
stay local. Network and rebase operations time out after 30 seconds.
|
|
117
118
|
|
|
118
119
|
`setup` installs a SessionStart hook and memory workflow. `startup` lists
|
|
119
120
|
metadata for the four project files, then the three curated org files; it
|
|
120
|
-
injects no content. `--pull`
|
|
121
|
+
injects no content. `--pull` commits and syncs first under the runtime identity, warning on failure. The workflow
|
|
121
122
|
tells the agent to run `recall` for an 8,000-character bounded read. Raise
|
|
122
123
|
`--max-chars` or read remaining files directly when cut. `capture` records
|
|
123
124
|
decisions during work. At the end, update threads and debt, optionally publish
|
|
@@ -92,7 +92,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
92
92
|
clone_ = add("clone", _clone, "Clone a memory repository into the home.")
|
|
93
93
|
clone_.add_argument("url", help="git remote URL to clone")
|
|
94
94
|
clone_.add_argument("--force", action="store_true", help="move a non-empty home aside before cloning")
|
|
95
|
-
add("pull", _pull, "
|
|
95
|
+
add("pull", _pull, "Update from upstream; with --agent, commit allowlisted changes and deliver them too.", agent)
|
|
96
96
|
add("push", _push, "Commit the allowlisted memory changes and push them when a remote exists.", agent)
|
|
97
97
|
add("disable", _disable, "Remove the sync marker; the repository stays in place.")
|
|
98
98
|
for name, handler, help_text, positional, positional_help in (
|
|
@@ -121,7 +121,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
121
121
|
"startup",
|
|
122
122
|
_startup,
|
|
123
123
|
"Print the session-start manifest: the memory files to read, in order, with their readability, size and "
|
|
124
|
-
"age, and where the sync stands. Content is never included. With --pull,
|
|
124
|
+
"age, and where the sync stands. Content is never included. With --pull, commit and sync memory under the runtime identity first.",
|
|
125
125
|
)
|
|
126
126
|
startup_.add_argument("--runtime", choices=startup.RUNTIMES, required=True, help="shape the output for this runtime's hook")
|
|
127
127
|
startup_.add_argument("--project", metavar="ID", help="the project tier to list (default: the one the binding or marker names)")
|
|
@@ -297,7 +297,7 @@ def _clone(args: argparse.Namespace) -> int:
|
|
|
297
297
|
|
|
298
298
|
|
|
299
299
|
def _pull(args: argparse.Namespace) -> int:
|
|
300
|
-
return _report(sync.pull(resolve_home(args.home).path))
|
|
300
|
+
return _report(sync.pull(resolve_home(args.home).path, agent=args.agent))
|
|
301
301
|
|
|
302
302
|
|
|
303
303
|
def _push(args: argparse.Namespace) -> int:
|
|
@@ -72,7 +72,7 @@ def build_manifest(
|
|
|
72
72
|
warnings: List[str] = list(notes)
|
|
73
73
|
files: List[Dict[str, Any]] = []
|
|
74
74
|
|
|
75
|
-
sync_info, pull_info = _sync(home, pull, runner, warnings)
|
|
75
|
+
sync_info, pull_info = _sync(home, pull, runner, warnings, runtime)
|
|
76
76
|
if project is not None:
|
|
77
77
|
try:
|
|
78
78
|
layout.validate_project_name(project)
|
|
@@ -154,14 +154,14 @@ def _entry(home: Path, path: Path, tier: str, name: str) -> Dict[str, Any]:
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def _sync(
|
|
157
|
-
home: Path, pull: bool, runner: Optional[GitRunner], warnings: List[str]
|
|
157
|
+
home: Path, pull: bool, runner: Optional[GitRunner], warnings: List[str], runtime: str
|
|
158
158
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
159
159
|
marker = sync.is_configured(home)
|
|
160
160
|
info: Dict[str, Any] = {"marker": marker, "last_commit_at_utc": None}
|
|
161
161
|
pull_info: Dict[str, Any] = {"requested": pull, "status": PULL_NOT_REQUESTED, "ok": True, "lines": []}
|
|
162
162
|
if pull:
|
|
163
163
|
try:
|
|
164
|
-
outcome = sync.pull(home, runner=runner)
|
|
164
|
+
outcome = sync.pull(home, agent=runtime, runner=runner)
|
|
165
165
|
pull_info = {"requested": True, "status": outcome.status, "ok": outcome.ok, "lines": list(outcome.lines)}
|
|
166
166
|
except (sync.SyncError, OSError) as exc:
|
|
167
167
|
pull_info = {"requested": True, "status": PULL_ERROR, "ok": False, "lines": [f"memory pull: {exc}"]}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The engine owns the sync marker, the managed block of the home's
|
|
6
6
|
``.gitignore``, the git state readout and the verbs ``init``, ``clone``,
|
|
7
|
-
``pull``, ``push`` and ``disable``. It never reads memory content
|
|
8
|
-
merges and never touches ``keys/``.
|
|
7
|
+
``pull``, ``push`` and ``disable``. It never reads memory content directly and never touches ``keys/``.
|
|
9
8
|
|
|
10
9
|
The publication boundary
|
|
11
10
|
------------------------
|
|
@@ -27,10 +26,10 @@ except the ones an earlier block carried and the current one retired, and
|
|
|
27
26
|
every write comes back with a before/after receipt that names them.
|
|
28
27
|
|
|
29
28
|
The network verbs (fetch, pull, push, clone) run under a 30 s timeout.
|
|
30
|
-
``
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
``push`` and ``pull --agent`` commit allowlisted changes before fetching,
|
|
30
|
+
rebase when behind, then deliver. A clean ``pull`` without an agent updates
|
|
31
|
+
and reports unpushed commits. Conflicts abort the rebase and keep local
|
|
32
|
+
commits; rejected delivery also leaves them local.
|
|
34
33
|
"""
|
|
35
34
|
|
|
36
35
|
from __future__ import annotations
|
|
@@ -350,39 +349,43 @@ def clone(home: Path, url: str, *, force: bool = False, runner: Optional[GitRunn
|
|
|
350
349
|
)
|
|
351
350
|
|
|
352
351
|
|
|
353
|
-
def pull(
|
|
354
|
-
|
|
352
|
+
def pull(
|
|
353
|
+
home: Path,
|
|
354
|
+
*,
|
|
355
|
+
agent: Optional[str] = None,
|
|
356
|
+
runner: Optional[GitRunner] = None,
|
|
357
|
+
env: Optional[Mapping[str, str]] = None,
|
|
358
|
+
) -> Outcome:
|
|
359
|
+
"""Update the home, publishing dirty memory only with an explicit identity.
|
|
355
360
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
and is not an error.
|
|
361
|
+
Clean pulls keep their read-mostly behavior: fast-forward, or rebase local
|
|
362
|
+
commits and report ahead. An explicit agent permits dirty publication/delivery;
|
|
363
|
+
SessionStart supplies its runtime name through this same entry point.
|
|
360
364
|
"""
|
|
361
365
|
home = _home(home)
|
|
362
366
|
if not is_configured(home):
|
|
363
367
|
return Outcome("not_configured", True)
|
|
364
368
|
_require_repo(home, runner)
|
|
369
|
+
if _operation_in_progress(home, runner):
|
|
370
|
+
return Outcome("operation_in_progress", False, ("memory pull: a git operation is in progress; finish it first.",))
|
|
371
|
+
if changed_paths(home, runner):
|
|
372
|
+
if agent:
|
|
373
|
+
return _sync_changes(home, agent=agent, runner=runner, env=env)
|
|
374
|
+
return Outcome("dirty", False, ("memory pull: uncommitted changes present; pass --agent NAME to commit and sync.",))
|
|
365
375
|
state = git_state(home, runner=runner, fetch=True)
|
|
366
376
|
if not state.has_remote:
|
|
367
377
|
return Outcome("local_only", True, ("memory pull: local-only repository; no remote to pull from.",))
|
|
368
378
|
if state.fetch_failed:
|
|
369
379
|
return _fetch_failure(state, "memory pull")
|
|
370
|
-
if state.dirty:
|
|
371
|
-
return Outcome("dirty", False, ("memory pull: uncommitted changes present; not pulling.",))
|
|
372
|
-
if state.diverged:
|
|
373
|
-
return Outcome("diverged", False, ("memory pull: diverged from upstream; resolve manually.",))
|
|
374
380
|
if not state.has_upstream:
|
|
375
381
|
return Outcome("no_upstream", True, ("memory pull: no upstream branch configured; skipping.",))
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if not result.ok:
|
|
384
|
-
return Outcome("pull_failed", False, (f"memory pull: fast-forward failed: {result.output}",))
|
|
385
|
-
return Outcome("synced", True, (f"memory pull: synced {state.behind} commit(s).",))
|
|
382
|
+
updated = _update(home, state, runner)
|
|
383
|
+
if not updated.ok:
|
|
384
|
+
return updated
|
|
385
|
+
current = git_state(home, runner=runner, fetch=False) if state.behind else state
|
|
386
|
+
if current.ahead:
|
|
387
|
+
return Outcome("ahead", True, (*updated.lines, f"memory pull: {current.ahead} unpushed commit(s); use push to deliver."))
|
|
388
|
+
return updated
|
|
386
389
|
|
|
387
390
|
|
|
388
391
|
def push(
|
|
@@ -392,36 +395,93 @@ def push(
|
|
|
392
395
|
runner: Optional[GitRunner] = None,
|
|
393
396
|
env: Optional[Mapping[str, str]] = None,
|
|
394
397
|
) -> Outcome:
|
|
395
|
-
"""Commit
|
|
396
|
-
|
|
397
|
-
Silent on a home that is not configured for sync. A home that is behind
|
|
398
|
-
or diverged is refused before anything is committed. A commit that could
|
|
399
|
-
not be delivered -- no remote, an unreachable one, a rejected push -- is
|
|
400
|
-
reported as such, distinctly from delivery; a credential helper a sandbox
|
|
401
|
-
blocked is named as its own cause, not as a rejection.
|
|
402
|
-
"""
|
|
398
|
+
"""Commit allowlisted changes, update from upstream, and deliver to the remote."""
|
|
403
399
|
home = _home(home)
|
|
404
400
|
if not is_configured(home):
|
|
405
401
|
return Outcome("not_configured", True)
|
|
406
402
|
_require_repo(home, runner)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
403
|
+
if _operation_in_progress(home, runner):
|
|
404
|
+
return Outcome("operation_in_progress", False, ("memory push: a git operation is in progress; finish it first.",))
|
|
405
|
+
return _sync_changes(home, agent=agent, runner=runner, env=env)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _operation_in_progress(home: Path, runner: Optional[GitRunner]) -> bool:
|
|
409
|
+
# Never abort or publish into an operation started by somebody else.
|
|
410
|
+
for name in ("rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"):
|
|
411
|
+
result = _git(home, ["rev-parse", "--git-path", name], runner)
|
|
412
|
+
if not result.ok:
|
|
413
|
+
raise SyncError(f"cannot inspect git operation state: {result.output}")
|
|
414
|
+
path = result.stdout.strip()
|
|
415
|
+
if path and (home / path).exists():
|
|
416
|
+
return True
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _sync_changes(
|
|
421
|
+
home: Path, *, agent: Optional[str], runner: Optional[GitRunner], env: Optional[Mapping[str, str]]
|
|
422
|
+
) -> Outcome:
|
|
415
423
|
published = _publish(home, agent=agent, runner=runner, env=env)
|
|
416
424
|
if not published.ok:
|
|
417
425
|
return published
|
|
426
|
+
# Commit before fetch: even a network failure leaves a recoverable local commit.
|
|
427
|
+
state = git_state(home, runner=runner, fetch=True)
|
|
418
428
|
lines = list(published.lines)
|
|
419
429
|
if not state.has_remote:
|
|
420
430
|
lines.append("memory push: no remote configured; the commit remains local.")
|
|
421
431
|
return Outcome("local_only", True, tuple(lines), published.committed, published.preserved)
|
|
432
|
+
if not state.fetch_failed and state.has_upstream and state.behind:
|
|
433
|
+
updated = _update(home, state, runner)
|
|
434
|
+
lines.extend(updated.lines)
|
|
435
|
+
if not updated.ok:
|
|
436
|
+
return Outcome(updated.status, False, tuple(lines), published.committed, published.preserved)
|
|
437
|
+
state = git_state(home, runner=runner, fetch=False)
|
|
422
438
|
return _deliver(home, published, state, lines, runner)
|
|
423
439
|
|
|
424
440
|
|
|
441
|
+
def _update(home: Path, state: GitState, runner: Optional[GitRunner]) -> Outcome:
|
|
442
|
+
if not state.behind:
|
|
443
|
+
return Outcome("up_to_date", True, ("memory pull: already synced.",))
|
|
444
|
+
remaining = changed_paths(home, runner)
|
|
445
|
+
if remaining:
|
|
446
|
+
return Outcome("dirty", False, (
|
|
447
|
+
"memory pull: cannot update with remaining uncommitted paths; left untouched: " + ", ".join(remaining),
|
|
448
|
+
))
|
|
449
|
+
if not state.ahead:
|
|
450
|
+
result = _git(home, ["pull", "--ff-only", "--quiet"], runner, timeout=NETWORK_TIMEOUT_SECONDS)
|
|
451
|
+
if result.timed_out:
|
|
452
|
+
return Outcome("pull_timed_out", False, (f"memory pull: {result.output}",))
|
|
453
|
+
if not result.ok:
|
|
454
|
+
return Outcome("pull_failed", False, (f"memory pull: fast-forward failed: {result.output}",))
|
|
455
|
+
return Outcome("synced", True, (f"memory pull: synced {state.behind} commit(s).",))
|
|
456
|
+
merges = _git(home, ["rev-list", "--merges", f"{state.upstream}..HEAD"], runner)
|
|
457
|
+
if not merges.ok or merges.stdout.strip():
|
|
458
|
+
return Outcome("diverged", False, (
|
|
459
|
+
"memory pull: local merge commits cannot be safely rebased automatically; resolve manually."
|
|
460
|
+
if merges.ok else f"memory pull: cannot inspect local history: {merges.output}",
|
|
461
|
+
))
|
|
462
|
+
# Rebase exactly the fetched upstream. Do not stash unrelated edits or reuse
|
|
463
|
+
# a recorded conflict resolution: a conflicting same-path add must surface.
|
|
464
|
+
result = _git(home, ["-c", "rerere.enabled=false", "-c", "rebase.updateRefs=false",
|
|
465
|
+
"rebase", "--no-autostash", state.upstream],
|
|
466
|
+
runner, timeout=NETWORK_TIMEOUT_SECONDS)
|
|
467
|
+
if result.ok:
|
|
468
|
+
return Outcome("synced", True, ("memory pull: rebased local commits onto upstream.",))
|
|
469
|
+
conflicts = _git(home, ["diff", "--name-only", "--diff-filter=U", "-z"], runner)
|
|
470
|
+
paths = [path for path in conflicts.stdout.split("\0") if path] if conflicts.ok else []
|
|
471
|
+
if _operation_in_progress(home, runner):
|
|
472
|
+
aborted = _git(home, ["rebase", "--abort"], runner, timeout=NETWORK_TIMEOUT_SECONDS)
|
|
473
|
+
if not aborted.ok or _operation_in_progress(home, runner):
|
|
474
|
+
return Outcome("rebase_abort_failed", False, (
|
|
475
|
+
f"memory pull: rebase failed and could not be aborted; manual recovery required. {aborted.output}",
|
|
476
|
+
))
|
|
477
|
+
detail = ", ".join(paths) if paths else result.output
|
|
478
|
+
status = "pull_timed_out" if result.timed_out else "diverged"
|
|
479
|
+
return Outcome(status, False, (
|
|
480
|
+
f"memory pull: {'rebase timed out' if result.timed_out else 'rebase failed'}; "
|
|
481
|
+
f"local commits retained. Conflicting paths or git error: {detail}",
|
|
482
|
+
))
|
|
483
|
+
|
|
484
|
+
|
|
425
485
|
def disable(home: Path) -> Outcome:
|
|
426
486
|
"""Remove the marker; the repository and its history stay in place."""
|
|
427
487
|
home = _home(home)
|
|
@@ -552,9 +612,9 @@ def _deliver(
|
|
|
552
612
|
failure = _fetch_failure(state, "memory push")
|
|
553
613
|
lines.append(f"{failure.lines[0]} The commit remains local.")
|
|
554
614
|
return Outcome(failure.status, False, tuple(lines), published.committed, published.preserved, receipt)
|
|
555
|
-
if
|
|
615
|
+
if state.has_upstream and not state.ahead:
|
|
556
616
|
lines.append("memory push: remote already up to date.")
|
|
557
|
-
return Outcome("up_to_date", True, tuple(lines),
|
|
617
|
+
return Outcome("up_to_date", True, tuple(lines), published.committed, published.preserved, receipt)
|
|
558
618
|
result = _push_remote(home, state, runner)
|
|
559
619
|
if not result.ok:
|
|
560
620
|
if credential_helper_blocked(result.output):
|
|
@@ -382,3 +382,25 @@ def test_cli_startup_project_flag_wins_over_the_binding(home: Path, isolated: Pa
|
|
|
382
382
|
manifest = json.loads(capsys.readouterr().out)
|
|
383
383
|
assert manifest["project"] == "other" and manifest["project_source"] == "flag"
|
|
384
384
|
assert [entry["state"] for entry in manifest["files"][:4]] == [MISSING] * 4
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@pytest.mark.parametrize("runtime", ["claude", "codex"])
|
|
388
|
+
def test_session_start_resolves_dirty_behind_home(home: Path, tmp_path: Path, git_env: None, runtime: str) -> None:
|
|
389
|
+
# Realistic sync layout with synthetic files and identities only.
|
|
390
|
+
from conftest import git, write
|
|
391
|
+
remote = tmp_path / "remote.git"
|
|
392
|
+
git("init", "--bare", "--quiet", str(remote), cwd=tmp_path)
|
|
393
|
+
assert sync.init(home, remote=str(remote)).ok
|
|
394
|
+
peer = tmp_path / "peer"
|
|
395
|
+
git("clone", "--quiet", str(remote), str(peer), cwd=tmp_path)
|
|
396
|
+
write(peer / "org-memory/events/peer.md", "peer content\n")
|
|
397
|
+
git("add", "org-memory/events/peer.md", cwd=peer)
|
|
398
|
+
git("commit", "-qm", "peer addition", cwd=peer)
|
|
399
|
+
git("push", cwd=peer)
|
|
400
|
+
write(home / "org-memory/events/local.md", "local content\n")
|
|
401
|
+
manifest = build_manifest(home, runtime=runtime, project="demo", pull=True)
|
|
402
|
+
assert manifest["pull"]["status"] == "pushed" and manifest["pull"]["ok"]
|
|
403
|
+
assert manifest["result"] == "ok"
|
|
404
|
+
assert git("status", "--porcelain", cwd=home) == ""
|
|
405
|
+
assert git("log", "-1", "--format=%s", cwd=home).startswith(f"memory: {runtime}@")
|
|
406
|
+
assert git("--git-dir", str(remote), "show", "main:org-memory/events/local.md", cwd=tmp_path) == "local content"
|
|
@@ -720,7 +720,7 @@ def test_push_with_nothing_new_is_up_to_date(tmp_path: Path) -> None:
|
|
|
720
720
|
assert outcome.committed == ()
|
|
721
721
|
|
|
722
722
|
|
|
723
|
-
def
|
|
723
|
+
def test_push_resolves_a_repository_behind_its_upstream(tmp_path: Path) -> None:
|
|
724
724
|
remote, seed = _create_remote(tmp_path)
|
|
725
725
|
home = tmp_path / "home"
|
|
726
726
|
sync.clone(home, str(remote))
|
|
@@ -730,13 +730,13 @@ def test_push_refuses_a_repository_behind_its_upstream(tmp_path: Path) -> None:
|
|
|
730
730
|
|
|
731
731
|
outcome = sync.push(home)
|
|
732
732
|
|
|
733
|
-
assert outcome.status == "
|
|
734
|
-
assert
|
|
735
|
-
assert
|
|
733
|
+
assert outcome.status == "pushed" and outcome.ok
|
|
734
|
+
assert _commit_count(home) == before + 2
|
|
735
|
+
assert _remote_has(remote, "org-memory/local.md")
|
|
736
736
|
assert (home / "org-memory" / "local.md").read_text(encoding="utf-8") == "local update\n"
|
|
737
737
|
|
|
738
738
|
|
|
739
|
-
def
|
|
739
|
+
def test_push_rebases_existing_and_new_commits(tmp_path: Path) -> None:
|
|
740
740
|
remote, seed = _create_remote(tmp_path)
|
|
741
741
|
home = tmp_path / "home"
|
|
742
742
|
sync.clone(home, str(remote))
|
|
@@ -749,8 +749,9 @@ def test_push_refuses_a_diverged_repository_without_committing(tmp_path: Path) -
|
|
|
749
749
|
|
|
750
750
|
outcome = sync.push(home)
|
|
751
751
|
|
|
752
|
-
assert outcome.status == "
|
|
753
|
-
assert _commit_count(home) == before
|
|
752
|
+
assert outcome.status == "pushed" and outcome.ok
|
|
753
|
+
assert _commit_count(home) == before + 2
|
|
754
|
+
assert _remote_has(remote, "org-memory/uncommitted.md")
|
|
754
755
|
assert (home / "org-memory" / "uncommitted.md").is_file()
|
|
755
756
|
|
|
756
757
|
|
|
@@ -802,7 +803,7 @@ def test_pull_refuses_a_dirty_tree(tmp_path: Path) -> None:
|
|
|
802
803
|
assert (home / "org-memory" / "recent.md").read_text(encoding="utf-8") == "edited locally\n"
|
|
803
804
|
|
|
804
805
|
|
|
805
|
-
def
|
|
806
|
+
def test_pull_rebases_a_diverged_repository_and_reports_ahead(tmp_path: Path) -> None:
|
|
806
807
|
remote, seed = _create_remote(tmp_path)
|
|
807
808
|
home = tmp_path / "home"
|
|
808
809
|
sync.clone(home, str(remote))
|
|
@@ -813,8 +814,8 @@ def test_pull_refuses_a_diverged_repository(tmp_path: Path) -> None:
|
|
|
813
814
|
|
|
814
815
|
outcome = sync.pull(home)
|
|
815
816
|
|
|
816
|
-
assert outcome.status == "
|
|
817
|
-
assert
|
|
817
|
+
assert outcome.status == "ahead" and outcome.ok
|
|
818
|
+
assert (home / "org-memory" / "remote.md").exists()
|
|
818
819
|
|
|
819
820
|
|
|
820
821
|
def test_pull_with_unpushed_commits_is_not_an_error(tmp_path: Path) -> None:
|
|
@@ -828,7 +829,7 @@ def test_pull_with_unpushed_commits_is_not_an_error(tmp_path: Path) -> None:
|
|
|
828
829
|
outcome = sync.pull(home)
|
|
829
830
|
|
|
830
831
|
assert outcome.status == "ahead" and outcome.ok
|
|
831
|
-
assert "1 unpushed commit(s)" in outcome.lines[
|
|
832
|
+
assert "1 unpushed commit(s)" in outcome.lines[-1]
|
|
832
833
|
|
|
833
834
|
|
|
834
835
|
def test_pull_when_already_synced(tmp_path: Path) -> None:
|
|
@@ -998,3 +999,185 @@ def test_push_commits_a_rename_git_detects_in_the_working_tree(tmp_path: Path) -
|
|
|
998
999
|
assert outcome.committed == ("org-memory/b.md",) # HEAD reports a rename by its destination
|
|
999
1000
|
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1000
1001
|
assert _git("show", "--name-status", "--format=", "HEAD", cwd=home) == "R100\torg-memory/a.md\torg-memory/b.md"
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
# Real git shapes; all repository names, identities and contents are synthetic.
|
|
1005
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1006
|
+
@pytest.mark.parametrize("duplicate", [False, True])
|
|
1007
|
+
def test_dirty_behind_commits_rebases_delivers(tmp_path: Path, verb, duplicate: bool) -> None:
|
|
1008
|
+
remote, seed = _create_remote(tmp_path)
|
|
1009
|
+
home = tmp_path / "home"
|
|
1010
|
+
sync.clone(home, str(remote))
|
|
1011
|
+
_commit_and_push(seed, "org-memory/peer.md", "peer content\n")
|
|
1012
|
+
if duplicate:
|
|
1013
|
+
_write(home / "org-memory/peer.md", "peer content\n")
|
|
1014
|
+
_write(home / "org-memory/local.md", "local content\n")
|
|
1015
|
+
outcome = verb(home, agent="test-agent")
|
|
1016
|
+
assert outcome.ok and outcome.status == "pushed"
|
|
1017
|
+
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1018
|
+
assert _git("rev-parse", "HEAD", cwd=home) == _git("rev-parse", "origin/main", cwd=home)
|
|
1019
|
+
assert _remote_has(remote, "org-memory/local.md")
|
|
1020
|
+
assert (home / "org-memory/peer.md").read_text() == "peer content\n"
|
|
1021
|
+
assert _git("log", "-1", "--format=%s", cwd=home).startswith("memory: test-agent@")
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1025
|
+
def test_only_identical_untracked_add_becomes_up_to_date(tmp_path: Path, verb) -> None:
|
|
1026
|
+
remote, seed = _create_remote(tmp_path)
|
|
1027
|
+
home = tmp_path / "home"
|
|
1028
|
+
sync.clone(home, str(remote))
|
|
1029
|
+
_commit_and_push(seed, "org-memory/peer.md", "same\n")
|
|
1030
|
+
_write(home / "org-memory/peer.md", "same\n")
|
|
1031
|
+
outcome = verb(home, agent="test-agent")
|
|
1032
|
+
assert outcome.status == "up_to_date" and outcome.ok
|
|
1033
|
+
assert outcome.committed == ("org-memory/peer.md",)
|
|
1034
|
+
assert _git("rev-parse", "HEAD", cwd=home) == _git("rev-parse", "HEAD", cwd=seed)
|
|
1035
|
+
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1039
|
+
@pytest.mark.parametrize("path", ["org-memory/recent.md", "org-memory/same path.md"])
|
|
1040
|
+
def test_conflict_aborts_and_retains_local_commit(tmp_path: Path, verb, path: str) -> None:
|
|
1041
|
+
remote, seed = _create_remote(tmp_path)
|
|
1042
|
+
home = tmp_path / "home"
|
|
1043
|
+
sync.clone(home, str(remote))
|
|
1044
|
+
_commit_and_push(seed, path, "upstream\n")
|
|
1045
|
+
_write(home / path, "local\n")
|
|
1046
|
+
before = _git("rev-parse", "HEAD", cwd=home)
|
|
1047
|
+
outcome = verb(home, agent="test-agent")
|
|
1048
|
+
assert outcome.status == "diverged" and not outcome.ok
|
|
1049
|
+
assert path in outcome.lines[-1]
|
|
1050
|
+
assert _git("rev-parse", "HEAD^", cwd=home) == before
|
|
1051
|
+
assert (home / path).read_text() == "local\n"
|
|
1052
|
+
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1053
|
+
assert not (home / ".git/rebase-merge").exists()
|
|
1054
|
+
assert not (home / ".git/rebase-apply").exists()
|
|
1055
|
+
assert _git("--git-dir", str(remote), "show", f"main:{path}", cwd=tmp_path) == "upstream"
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1059
|
+
def test_behind_preserves_foreign_index_and_worktree(tmp_path: Path, verb) -> None:
|
|
1060
|
+
remote, seed = _create_remote(tmp_path)
|
|
1061
|
+
home = tmp_path / "home"
|
|
1062
|
+
sync.clone(home, str(remote))
|
|
1063
|
+
_commit_and_push(seed, "org-memory/peer.md", "peer\n")
|
|
1064
|
+
_write(home / RUNTIME_FILE, "staged\n")
|
|
1065
|
+
_git("add", "-f", RUNTIME_FILE, cwd=home)
|
|
1066
|
+
_write(home / RUNTIME_FILE, "unstaged\n")
|
|
1067
|
+
_write(home / "org-memory/local.md", "memory\n")
|
|
1068
|
+
_git("config", "rebase.autoStash", "true", cwd=home)
|
|
1069
|
+
outcome = verb(home, agent="test-agent")
|
|
1070
|
+
assert outcome.status == "dirty" and not outcome.ok
|
|
1071
|
+
assert outcome.preserved == (RUNTIME_FILE,)
|
|
1072
|
+
assert _git("show", f":{RUNTIME_FILE}", cwd=home) == "staged"
|
|
1073
|
+
assert (home / RUNTIME_FILE).read_text() == "unstaged\n"
|
|
1074
|
+
assert not _remote_has(remote, "org-memory/local.md")
|
|
1075
|
+
assert not (home / ".git/rebase-merge").exists()
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1079
|
+
def test_existing_rebase_is_never_published_or_aborted(tmp_path: Path, verb) -> None:
|
|
1080
|
+
remote, seed = _create_remote(tmp_path)
|
|
1081
|
+
home = tmp_path / "home"
|
|
1082
|
+
sync.clone(home, str(remote))
|
|
1083
|
+
_commit_and_push(seed, "org-memory/recent.md", "remote\n")
|
|
1084
|
+
_write(home / "org-memory/recent.md", "local\n")
|
|
1085
|
+
_git("add", "org-memory/recent.md", cwd=home)
|
|
1086
|
+
_git("commit", "-qm", "local", cwd=home)
|
|
1087
|
+
_git("fetch", cwd=home)
|
|
1088
|
+
result = run_git(["rebase", "origin/main"], cwd=home)
|
|
1089
|
+
assert not result.ok
|
|
1090
|
+
before = _git("status", "--porcelain", cwd=home)
|
|
1091
|
+
outcome = verb(home, agent="test-agent")
|
|
1092
|
+
assert outcome.status == "operation_in_progress" and not outcome.ok
|
|
1093
|
+
assert _git("status", "--porcelain", cwd=home) == before
|
|
1094
|
+
assert (home / ".git/rebase-merge").exists()
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1098
|
+
def test_dirty_fetch_timeout_keeps_commit_and_identity(tmp_path: Path, verb) -> None:
|
|
1099
|
+
remote, _ = _create_remote(tmp_path)
|
|
1100
|
+
home = tmp_path / "home"
|
|
1101
|
+
sync.clone(home, str(remote))
|
|
1102
|
+
_write(home / "org-memory/local.md", "local\n")
|
|
1103
|
+
outcome = verb(home, agent="test-agent", runner=FetchTimesOut())
|
|
1104
|
+
assert outcome.status == "fetch_timed_out" and not outcome.ok
|
|
1105
|
+
assert outcome.committed == ("org-memory/local.md",)
|
|
1106
|
+
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1107
|
+
assert _git("log", "-1", "--format=%s", cwd=home).startswith("memory: test-agent@")
|
|
1108
|
+
assert not _remote_has(remote, "org-memory/local.md")
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def test_pull_cli_requires_agent_for_dirty_publication(tmp_path: Path, capsys) -> None:
|
|
1112
|
+
from agent_memory.cli import main
|
|
1113
|
+
remote, seed = _create_remote(tmp_path)
|
|
1114
|
+
home = tmp_path / "home"
|
|
1115
|
+
sync.clone(home, str(remote))
|
|
1116
|
+
_commit_and_push(seed, "org-memory/peer.md", "peer\n")
|
|
1117
|
+
_write(home / "org-memory/local.md", "local\n")
|
|
1118
|
+
assert main(["pull", "--home", str(home)]) == 1
|
|
1119
|
+
assert "--agent" in capsys.readouterr().err
|
|
1120
|
+
assert main(["pull", "--home", str(home), "--agent", "test-agent"]) == 0
|
|
1121
|
+
assert _remote_has(remote, "org-memory/local.md")
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1125
|
+
@pytest.mark.parametrize("abort_fails", [False, True])
|
|
1126
|
+
def test_rebase_timeout_aborts_or_names_recovery_failure(tmp_path: Path, verb, abort_fails: bool) -> None:
|
|
1127
|
+
remote, seed = _create_remote(tmp_path)
|
|
1128
|
+
home = tmp_path / "home"
|
|
1129
|
+
sync.clone(home, str(remote))
|
|
1130
|
+
_commit_and_push(seed, "org-memory/recent.md", "upstream\n")
|
|
1131
|
+
_write(home / "org-memory/recent.md", "local\n")
|
|
1132
|
+
before = _git("rev-parse", "HEAD", cwd=home)
|
|
1133
|
+
|
|
1134
|
+
def runner(args, *, cwd, timeout=None):
|
|
1135
|
+
if "rebase" in args and "--no-autostash" in args:
|
|
1136
|
+
assert timeout == sync.NETWORK_TIMEOUT_SECONDS
|
|
1137
|
+
# A real conflict leaves the same in-progress state a timeout can.
|
|
1138
|
+
assert not run_git(args, cwd=cwd, timeout=timeout).ok
|
|
1139
|
+
return GitResult(EXIT_TIMEOUT, "", "synthetic rebase timeout")
|
|
1140
|
+
if list(args) == ["rebase", "--abort"] and abort_fails:
|
|
1141
|
+
return GitResult(1, "", "synthetic abort failure")
|
|
1142
|
+
return run_git(args, cwd=cwd, timeout=timeout)
|
|
1143
|
+
|
|
1144
|
+
outcome = verb(home, agent="test-agent", runner=runner)
|
|
1145
|
+
assert not outcome.ok
|
|
1146
|
+
if abort_fails:
|
|
1147
|
+
assert outcome.status == "rebase_abort_failed"
|
|
1148
|
+
assert "manual recovery" in outcome.lines[-1]
|
|
1149
|
+
assert (home / ".git/rebase-merge").exists()
|
|
1150
|
+
else:
|
|
1151
|
+
assert outcome.status == "pull_timed_out"
|
|
1152
|
+
assert "org-memory/recent.md" in outcome.lines[-1]
|
|
1153
|
+
assert _git("rev-parse", "HEAD^", cwd=home) == before
|
|
1154
|
+
assert (home / "org-memory/recent.md").read_text() == "local\n"
|
|
1155
|
+
assert _git("status", "--porcelain", cwd=home) == ""
|
|
1156
|
+
assert not (home / ".git/rebase-merge").exists()
|
|
1157
|
+
assert _git("--git-dir", str(remote), "show", "main:org-memory/recent.md", cwd=tmp_path) == "upstream"
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
@pytest.mark.parametrize("verb", [sync.pull, sync.push])
|
|
1161
|
+
def test_local_merge_content_is_not_dropped_by_rebase(tmp_path: Path, verb) -> None:
|
|
1162
|
+
remote, seed = _create_remote(tmp_path)
|
|
1163
|
+
home = tmp_path / "home"
|
|
1164
|
+
sync.clone(home, str(remote))
|
|
1165
|
+
_git("checkout", "-qb", "side", cwd=home)
|
|
1166
|
+
_write(home / "org-memory/side.md", "side\n")
|
|
1167
|
+
_git("add", "org-memory/side.md", cwd=home)
|
|
1168
|
+
_git("commit", "-qm", "side", cwd=home)
|
|
1169
|
+
_git("checkout", "main", cwd=home)
|
|
1170
|
+
_write(home / "org-memory/main.md", "main\n")
|
|
1171
|
+
_git("add", "org-memory/main.md", cwd=home)
|
|
1172
|
+
_git("commit", "-qm", "main", cwd=home)
|
|
1173
|
+
_git("merge", "--no-commit", "side", cwd=home)
|
|
1174
|
+
_write(home / "org-memory/merge-only.md", "merge resolution\n")
|
|
1175
|
+
_git("add", "org-memory/merge-only.md", cwd=home)
|
|
1176
|
+
_git("commit", "-qm", "merge", cwd=home)
|
|
1177
|
+
before = _git("rev-parse", "HEAD", cwd=home)
|
|
1178
|
+
_commit_and_push(seed, "org-memory/upstream.md", "upstream\n")
|
|
1179
|
+
outcome = verb(home, agent="test-agent")
|
|
1180
|
+
assert outcome.status == "diverged" and not outcome.ok
|
|
1181
|
+
assert "merge commits" in outcome.lines[-1]
|
|
1182
|
+
assert _git("rev-parse", "HEAD", cwd=home) == before
|
|
1183
|
+
assert (home / "org-memory/merge-only.md").read_text() == "merge resolution\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/decisions.md
RENAMED
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/recent.md
RENAMED
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/rules.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/src/agent_memory/templates/workflow/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/fixtures/setup/claude_settings_fleet.json
RENAMED
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/fixtures/setup/codex_hooks_kernel.json
RENAMED
|
File without changes
|
{agent_memory_cli-0.1.2 → agent_memory_cli-0.1.3}/tests/golden/canonical_memory_gitignore.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|