agent-memory-cli 0.1.1__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.1 → agent_memory_cli-0.1.3}/CHANGELOG.md +17 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/PKG-INFO +7 -6
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/README.md +6 -5
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/pyproject.toml +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/__init__.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/cli.py +36 -14
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/doctor.py +68 -10
- agent_memory_cli-0.1.3/src/agent_memory/prerequisites.py +151 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/startup.py +3 -3
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/status.py +82 -13
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/sync.py +108 -47
- agent_memory_cli-0.1.3/tests/conftest.py +112 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_debrief.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_doctor.py +80 -3
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_events.py +3 -3
- agent_memory_cli-0.1.3/tests/test_prerequisites.py +161 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_setup.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_startup.py +22 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_status.py +97 -4
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_sync.py +198 -45
- agent_memory_cli-0.1.1/tests/conftest.py +0 -60
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/.gitignore +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/LICENSE +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/Makefile +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/scripts/check_installed_wheel.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/scripts/release_notes.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/__main__.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/archive.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/debrief.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/events.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/git_runner.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/home.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/layout.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/org.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/publication.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/setup/__init__.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/setup/claude.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/setup/codex.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/setup/common.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/setup/legacy.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/decisions.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/recent.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/org-memory/rules.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/templates/workflow/SKILL.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/src/agent_memory/workflow.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
- {agent_memory_cli-0.1.1 → 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.1 → 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.1 → 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.1 → 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.1 → 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.1 → 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.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
- {agent_memory_cli-0.1.1 → 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.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
- {agent_memory_cli-0.1.1 → 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.1 → 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.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.1 → 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.1 → 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.1 → 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.1 → 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.1 → agent_memory_cli-0.1.3}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/golden/canonical_memory_gitignore.txt +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/golden/debrief_record.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/golden/doctor_memory_0.4.5.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/golden/doctor_memory_0.4.5.txt +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/golden/event_record.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_archive.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_cli.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_home.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_layout.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_org.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.3}/tests/test_workflow.py +0 -0
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
13
|
+
## 0.1.2 - 2026-09-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `status --json` prints the readout as one object, with an `ok` flag and a `prerequisites` block ([status](docs/commands.md#status)).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `status` and `doctor` exit non-zero with a reason code when a prerequisite such as git fails ([prerequisites](docs/commands.md#prerequisites)).
|
|
22
|
+
- `doctor` and `status --fetch` name a sandbox-blocked credential helper, not an unreachable remote ([doctor](docs/commands.md#doctor)).
|
|
23
|
+
|
|
7
24
|
## 0.1.1 - 2026-09-12
|
|
8
25
|
|
|
9
26
|
### 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
|
|
@@ -10,7 +10,7 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Callable, List, Optional, Sequence, Tuple
|
|
12
12
|
|
|
13
|
-
from . import __version__, archive, debrief, doctor, events, org, setup, startup, status, sync, workflow
|
|
13
|
+
from . import __version__, archive, debrief, doctor, events, org, prerequisites, setup, startup, status, sync, workflow
|
|
14
14
|
from .home import BINDING_FILE, HomeError, HomeResolution, find_project, resolve_home
|
|
15
15
|
|
|
16
16
|
EXIT_OK = 0
|
|
@@ -51,16 +51,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
51
51
|
"status",
|
|
52
52
|
_status,
|
|
53
53
|
"Show which home resolves, which rule chose it, whether the layout is in place, and where the sync stands. "
|
|
54
|
-
"Exit 1 when the tree is dirty or diverged.",
|
|
54
|
+
"Exit 1 when the tree is dirty or diverged, or a prerequisite (git, the home, the credential helper) failed.",
|
|
55
55
|
)
|
|
56
56
|
status_.add_argument(
|
|
57
57
|
"--fetch", action="store_true", help="contact the remote before counting ahead/behind (no network otherwise)"
|
|
58
58
|
)
|
|
59
|
+
status_.add_argument("--json", dest="json_output", action="store_true", help="emit the readout as JSON")
|
|
59
60
|
doctor_ = add(
|
|
60
61
|
"doctor",
|
|
61
62
|
_doctor,
|
|
62
63
|
"Check the home's setup and health: the debrief store's layout and the sync repository. "
|
|
63
|
-
"Reads no memory content; repairs nothing. Exit 1 on an error row.",
|
|
64
|
+
"Reads no memory content; repairs nothing. Exit 1 on an error row or a failed prerequisite.",
|
|
64
65
|
)
|
|
65
66
|
doctor_.add_argument("--json", dest="json_output", action="store_true", help="emit the report as JSON")
|
|
66
67
|
init_ = add(
|
|
@@ -91,7 +92,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
91
92
|
clone_ = add("clone", _clone, "Clone a memory repository into the home.")
|
|
92
93
|
clone_.add_argument("url", help="git remote URL to clone")
|
|
93
94
|
clone_.add_argument("--force", action="store_true", help="move a non-empty home aside before cloning")
|
|
94
|
-
add("pull", _pull, "
|
|
95
|
+
add("pull", _pull, "Update from upstream; with --agent, commit allowlisted changes and deliver them too.", agent)
|
|
95
96
|
add("push", _push, "Commit the allowlisted memory changes and push them when a remote exists.", agent)
|
|
96
97
|
add("disable", _disable, "Remove the sync marker; the repository stays in place.")
|
|
97
98
|
for name, handler, help_text, positional, positional_help in (
|
|
@@ -120,7 +121,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
120
121
|
"startup",
|
|
121
122
|
_startup,
|
|
122
123
|
"Print the session-start manifest: the memory files to read, in order, with their readability, size and "
|
|
123
|
-
"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.",
|
|
124
125
|
)
|
|
125
126
|
startup_.add_argument("--runtime", choices=startup.RUNTIMES, required=True, help="shape the output for this runtime's hook")
|
|
126
127
|
startup_.add_argument("--project", metavar="ID", help="the project tier to list (default: the one the binding or marker names)")
|
|
@@ -235,23 +236,44 @@ def _max_chars(value: str) -> int:
|
|
|
235
236
|
|
|
236
237
|
|
|
237
238
|
def _status(args: argparse.Namespace) -> int:
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
try:
|
|
240
|
+
resolution = resolve_home(args.home)
|
|
241
|
+
except HomeError as exc:
|
|
242
|
+
if not args.json_output:
|
|
243
|
+
raise
|
|
244
|
+
print(json.dumps(status.unresolved(exc), indent=2))
|
|
245
|
+
return EXIT_USAGE
|
|
246
|
+
readout = status.inspect(resolution, fetch=args.fetch)
|
|
247
|
+
if args.json_output:
|
|
248
|
+
print(json.dumps(readout.to_json(), indent=2))
|
|
249
|
+
else:
|
|
250
|
+
print("\n".join(readout.lines()))
|
|
240
251
|
return readout.exit_code
|
|
241
252
|
|
|
242
253
|
|
|
243
254
|
def _doctor(args: argparse.Namespace) -> int:
|
|
244
|
-
|
|
255
|
+
memory_lint = doctor.find_memory_lint()
|
|
256
|
+
try:
|
|
257
|
+
home = resolve_home(args.home).path
|
|
258
|
+
except HomeError as exc:
|
|
259
|
+
if not args.json_output:
|
|
260
|
+
raise
|
|
261
|
+
unresolved = prerequisites.home_error(exc)
|
|
262
|
+
print(json.dumps(doctor.to_json([], memory_lint=memory_lint, prerequisites=unresolved), indent=2))
|
|
263
|
+
return EXIT_USAGE
|
|
245
264
|
if not home.is_dir():
|
|
246
|
-
|
|
265
|
+
missing = prerequisites.home_missing(home)
|
|
266
|
+
if args.json_output:
|
|
267
|
+
print(json.dumps(doctor.to_json([], memory_lint=memory_lint, prerequisites=missing), indent=2))
|
|
268
|
+
elif missing.failure:
|
|
269
|
+
print(f"agent-memory: error: {missing.failure.line()}", file=sys.stderr)
|
|
247
270
|
return EXIT_FAILED
|
|
248
|
-
categories = doctor.
|
|
249
|
-
memory_lint = doctor.find_memory_lint()
|
|
271
|
+
categories, readout = doctor.diagnose(home)
|
|
250
272
|
if args.json_output:
|
|
251
|
-
print(json.dumps(doctor.to_json(categories, memory_lint=memory_lint), indent=2))
|
|
273
|
+
print(json.dumps(doctor.to_json(categories, memory_lint=memory_lint, prerequisites=readout), indent=2))
|
|
252
274
|
else:
|
|
253
275
|
sys.stdout.write(doctor.report(categories, memory_lint=memory_lint))
|
|
254
|
-
return EXIT_FAILED if doctor.has_errors(categories) else EXIT_OK
|
|
276
|
+
return EXIT_FAILED if doctor.has_errors(categories) or not readout.ok else EXIT_OK
|
|
255
277
|
|
|
256
278
|
|
|
257
279
|
def _init(args: argparse.Namespace) -> int:
|
|
@@ -275,7 +297,7 @@ def _clone(args: argparse.Namespace) -> int:
|
|
|
275
297
|
|
|
276
298
|
|
|
277
299
|
def _pull(args: argparse.Namespace) -> int:
|
|
278
|
-
return _report(sync.pull(resolve_home(args.home).path))
|
|
300
|
+
return _report(sync.pull(resolve_home(args.home).path, agent=args.agent))
|
|
279
301
|
|
|
280
302
|
|
|
281
303
|
def _push(args: argparse.Namespace) -> int:
|
|
@@ -36,8 +36,9 @@ from enum import Enum
|
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
38
38
|
|
|
39
|
-
from . import layout, sync
|
|
39
|
+
from . import layout, prerequisites, sync
|
|
40
40
|
from .git_runner import GitResult, GitRunner, run_git
|
|
41
|
+
from .prerequisites import Prerequisites
|
|
41
42
|
from .sync import GitState
|
|
42
43
|
|
|
43
44
|
#: A last commit older than this many days is reported stale.
|
|
@@ -109,7 +110,15 @@ def run_doctor(
|
|
|
109
110
|
home: Path, *, runner: Optional[GitRunner] = None, now: Optional[dt.datetime] = None
|
|
110
111
|
) -> List[Category]:
|
|
111
112
|
"""Both memory categories for ``home``, in report order."""
|
|
112
|
-
return
|
|
113
|
+
return diagnose(home, runner=runner, now=now)[0]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def diagnose(
|
|
117
|
+
home: Path, *, runner: Optional[GitRunner] = None, now: Optional[dt.datetime] = None
|
|
118
|
+
) -> Tuple[List[Category], Prerequisites]:
|
|
119
|
+
"""Both memory categories in report order, and the prerequisite readout the sync checks took."""
|
|
120
|
+
memory_sync, readout = _memory_sync(home, runner=runner, now=now)
|
|
121
|
+
return [check_org_memory(home), memory_sync], readout
|
|
113
122
|
|
|
114
123
|
|
|
115
124
|
def find_memory_lint(which: Callable[[str], Optional[str]] = shutil.which) -> Optional[str]:
|
|
@@ -135,10 +144,16 @@ def report(categories: Sequence[Category], *, memory_lint: Optional[str] = None)
|
|
|
135
144
|
return "\n".join(lines) + "\n"
|
|
136
145
|
|
|
137
146
|
|
|
138
|
-
def to_json(
|
|
139
|
-
|
|
147
|
+
def to_json(
|
|
148
|
+
categories: Sequence[Category],
|
|
149
|
+
*,
|
|
150
|
+
memory_lint: Optional[str] = None,
|
|
151
|
+
prerequisites: Optional[Prerequisites] = None,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
"""The report as data, in the same order as the text; ``prerequisites`` goes last, so the
|
|
154
|
+
rows before it read as they did without it."""
|
|
140
155
|
output: Dict[str, Any] = {
|
|
141
|
-
"has_errors": has_errors(categories),
|
|
156
|
+
"has_errors": has_errors(categories) or (prerequisites is not None and not prerequisites.ok),
|
|
142
157
|
"memory_lint": memory_lint,
|
|
143
158
|
"categories": [],
|
|
144
159
|
}
|
|
@@ -152,6 +167,8 @@ def to_json(categories: Sequence[Category], *, memory_lint: Optional[str] = None
|
|
|
152
167
|
output["categories"].append(
|
|
153
168
|
{"name": category.name, "worst_severity": category.worst_severity.value, "results": rows}
|
|
154
169
|
)
|
|
170
|
+
if prerequisites is not None:
|
|
171
|
+
output["prerequisites"] = prerequisites.to_json()
|
|
155
172
|
return output
|
|
156
173
|
|
|
157
174
|
|
|
@@ -325,14 +342,21 @@ def check_memory_sync(
|
|
|
325
342
|
home: Path, *, runner: Optional[GitRunner] = None, now: Optional[dt.datetime] = None
|
|
326
343
|
) -> Category:
|
|
327
344
|
"""The sync setup and the repository's state, through git alone; nothing is changed."""
|
|
345
|
+
return _memory_sync(home, runner=runner, now=now)[0]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _memory_sync(
|
|
349
|
+
home: Path, *, runner: Optional[GitRunner], now: Optional[dt.datetime]
|
|
350
|
+
) -> Tuple[Category, Prerequisites]:
|
|
328
351
|
cat = Category("Memory Sync")
|
|
329
352
|
marker = layout.MARKER_FILE
|
|
353
|
+
git = prerequisites.probe_git(runner)
|
|
330
354
|
|
|
331
355
|
try:
|
|
332
356
|
configured = _is_file(sync.marker_path(home))
|
|
333
357
|
except OSError as exc:
|
|
334
358
|
cat.add("memory-marker", Severity.warn, f"{marker} — {_not_inspected(exc)}")
|
|
335
|
-
return cat
|
|
359
|
+
return cat, prerequisites.assess(git, configured=False)
|
|
336
360
|
if not configured:
|
|
337
361
|
cat.add(
|
|
338
362
|
"memory-marker",
|
|
@@ -340,9 +364,18 @@ def check_memory_sync(
|
|
|
340
364
|
f"{marker} — not configured; memory sync hooks are disabled",
|
|
341
365
|
"Run: agent-memory enable [--remote URL]",
|
|
342
366
|
)
|
|
343
|
-
return cat
|
|
367
|
+
return cat, prerequisites.assess(git, configured=False)
|
|
344
368
|
cat.add("memory-marker", Severity.ok, f"{marker} — present")
|
|
345
369
|
|
|
370
|
+
if not git.present:
|
|
371
|
+
# Without git every readout below fails, and "not a git repository" would be the wrong finding.
|
|
372
|
+
cat.add(
|
|
373
|
+
"memory-git",
|
|
374
|
+
Severity.error,
|
|
375
|
+
f"{marker} — present, but {prerequisites.GIT_MISSING_TEXT}",
|
|
376
|
+
prerequisites.GIT_MISSING_REMEDY,
|
|
377
|
+
)
|
|
378
|
+
return cat, prerequisites.assess(git, configured=True)
|
|
346
379
|
if not sync.is_git_repo(home, runner):
|
|
347
380
|
cat.add(
|
|
348
381
|
"memory-git",
|
|
@@ -350,7 +383,7 @@ def check_memory_sync(
|
|
|
350
383
|
f"{marker} — present, but {home} is not a git repository",
|
|
351
384
|
"Run `agent-memory enable`, or `agent-memory disable` to remove the marker",
|
|
352
385
|
)
|
|
353
|
-
return cat
|
|
386
|
+
return cat, prerequisites.assess(git, configured=True)
|
|
354
387
|
enclosing = enclosing_repository(home, runner)
|
|
355
388
|
if enclosing is not None:
|
|
356
389
|
# The sync verbs refuse this home; reading the enclosing repository's state as the home's would be wrong.
|
|
@@ -361,7 +394,7 @@ def check_memory_sync(
|
|
|
361
394
|
"a memory home must be the root of its own repository",
|
|
362
395
|
"Move the home out of the enclosing repository, or `agent-memory disable` to remove the marker",
|
|
363
396
|
)
|
|
364
|
-
return cat
|
|
397
|
+
return cat, prerequisites.assess(git, configured=True)
|
|
365
398
|
|
|
366
399
|
root_gitignore = home / layout.GITIGNORE_FILE
|
|
367
400
|
try:
|
|
@@ -450,6 +483,10 @@ def check_memory_sync(
|
|
|
450
483
|
if not state.has_remote:
|
|
451
484
|
cat.add("sync-state", Severity.ok, text)
|
|
452
485
|
cat.add("remote", Severity.skip, "remote — skipped; local-only memory repo")
|
|
486
|
+
elif prerequisites.helper_blocked(state):
|
|
487
|
+
# Not the network and not the remote's permissions: git never had credentials to try with.
|
|
488
|
+
cat.add("sync-state", Severity.error, text, prerequisites.HELPER_BLOCKED_REMEDY)
|
|
489
|
+
cat.add("remote", Severity.skip, "remote — not checked; the sandbox blocked the git credential helper")
|
|
453
490
|
elif state.fetch_failed:
|
|
454
491
|
cat.add("sync-state", Severity.warn, text, "Check network access and remote permissions")
|
|
455
492
|
cat.add("remote", Severity.warn, "remote — not reachable", "Check network access and remote permissions")
|
|
@@ -523,7 +560,8 @@ def check_memory_sync(
|
|
|
523
560
|
else:
|
|
524
561
|
cat.add("memory-overlays", Severity.ok, f"memory .gitignore overlays — {len(overlays)} safe")
|
|
525
562
|
|
|
526
|
-
|
|
563
|
+
fetched = state is not None and state.has_remote
|
|
564
|
+
return cat, prerequisites.assess(git, configured=True, state=state, fetched=fetched)
|
|
527
565
|
|
|
528
566
|
|
|
529
567
|
def _add_working_tree(cat: Category, home: Path, runner: Optional[GitRunner], state: GitState) -> None:
|
|
@@ -568,10 +606,30 @@ def enclosing_repository(home: Path, runner: Optional[GitRunner] = None) -> Opti
|
|
|
568
606
|
return root
|
|
569
607
|
|
|
570
608
|
|
|
609
|
+
def sync_state(state: GitState) -> str:
|
|
610
|
+
"""Where the repository stands, as the code ``status --json`` carries beside :func:`sync_state_text`."""
|
|
611
|
+
if not state.has_remote:
|
|
612
|
+
return "local_only"
|
|
613
|
+
if state.fetch_failed:
|
|
614
|
+
return "fetch_failed"
|
|
615
|
+
if not state.has_upstream:
|
|
616
|
+
return "no_upstream"
|
|
617
|
+
if state.diverged:
|
|
618
|
+
return "diverged"
|
|
619
|
+
if state.behind:
|
|
620
|
+
return "behind"
|
|
621
|
+
if state.ahead:
|
|
622
|
+
return "ahead"
|
|
623
|
+
return "synced"
|
|
624
|
+
|
|
625
|
+
|
|
571
626
|
def sync_state_text(state: GitState) -> str:
|
|
572
627
|
"""One phrase for where the repository stands; ``status`` and ``doctor`` share it."""
|
|
573
628
|
if not state.has_remote:
|
|
574
629
|
return "local-only; no remote configured"
|
|
630
|
+
if prerequisites.helper_blocked(state):
|
|
631
|
+
# The helper's own output names the secret it was after; the phrase names the cause instead.
|
|
632
|
+
return f"remote fetch failed: {prerequisites.HELPER_BLOCKED_TEXT}"
|
|
575
633
|
if state.fetch_failed:
|
|
576
634
|
return f"remote fetch failed: {state.fetch_output}"
|
|
577
635
|
if not state.has_upstream:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Kiloloop
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""What ``status`` and ``doctor`` need before their readout means anything.
|
|
4
|
+
|
|
5
|
+
Three prerequisite failures, each with a stable reason code. The JSON carries
|
|
6
|
+
the code and the message, the table prints the same message, and either verb
|
|
7
|
+
exits non-zero on any of them:
|
|
8
|
+
|
|
9
|
+
* ``home_unresolved``: no memory home. The resolver failed, or the path it
|
|
10
|
+
chose is not a directory.
|
|
11
|
+
* ``git_missing``: git does not run, on a home that carries the sync marker. A
|
|
12
|
+
home without the marker needs no git, so there its absence is reported, not
|
|
13
|
+
failed. Without git a marked home would read as "not a git repository",
|
|
14
|
+
which is a layout finding it is not.
|
|
15
|
+
* ``credential_helper_blocked``: a fetch failed with the signature
|
|
16
|
+
:func:`agent_memory.sync.credential_helper_blocked` detects, a git
|
|
17
|
+
credential helper that a sandbox stopped. The helper runs only when git has
|
|
18
|
+
no cached credential for the remote, so the failure needs a cold cache as
|
|
19
|
+
well as the sandbox.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, Optional
|
|
28
|
+
|
|
29
|
+
from . import sync
|
|
30
|
+
from .git_runner import EXIT_NOT_FOUND, GitRunner, run_git
|
|
31
|
+
from .sync import GitState
|
|
32
|
+
|
|
33
|
+
HOME_UNRESOLVED = "home_unresolved"
|
|
34
|
+
GIT_MISSING = "git_missing"
|
|
35
|
+
CREDENTIAL_HELPER_BLOCKED = "credential_helper_blocked"
|
|
36
|
+
#: Every reason code, in the order the checks run.
|
|
37
|
+
REASONS = (HOME_UNRESOLVED, GIT_MISSING, CREDENTIAL_HELPER_BLOCKED)
|
|
38
|
+
|
|
39
|
+
GIT_MISSING_TEXT = "git could not be run from PATH"
|
|
40
|
+
GIT_MISSING_REMEDY = "Install git, or put it on PATH"
|
|
41
|
+
HELPER_BLOCKED_TEXT = (
|
|
42
|
+
"the git credential helper could not run under the sandbox, so git had no credentials for the remote"
|
|
43
|
+
)
|
|
44
|
+
HELPER_BLOCKED_REMEDY = "Rerun with the sandbox off"
|
|
45
|
+
HOME_MISSING_REMEDY = "Pass --home or set $AGENT_MEMORY_HOME to an existing home, or create one with `agent-memory init`"
|
|
46
|
+
HOME_ERROR_REMEDY = "Fix the binding or path it names, or pass --home"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Failure:
|
|
51
|
+
"""One failed prerequisite: its reason code, what happened, and what to do."""
|
|
52
|
+
|
|
53
|
+
reason: str
|
|
54
|
+
message: str
|
|
55
|
+
remedy: str
|
|
56
|
+
|
|
57
|
+
def line(self) -> str:
|
|
58
|
+
return f"{self.reason} — {self.message}. {self.remedy}."
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class GitProbe:
|
|
63
|
+
"""Whether git runs, and the version it reports (empty when it reported none)."""
|
|
64
|
+
|
|
65
|
+
present: bool
|
|
66
|
+
version: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Prerequisites:
|
|
71
|
+
"""The prerequisite readout both verbs print."""
|
|
72
|
+
|
|
73
|
+
#: ``None`` when git was not probed, because there was no home to probe it in.
|
|
74
|
+
git: Optional[GitProbe] = None
|
|
75
|
+
#: Whether a fetch ran, the only time the credential helper can be judged.
|
|
76
|
+
helper_checked: bool = False
|
|
77
|
+
helper_blocked: bool = False
|
|
78
|
+
failure: Optional[Failure] = None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def ok(self) -> bool:
|
|
82
|
+
return self.failure is None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def reason(self) -> Optional[str]:
|
|
86
|
+
return self.failure.reason if self.failure else None
|
|
87
|
+
|
|
88
|
+
def to_json(self) -> Dict[str, Any]:
|
|
89
|
+
failure = self.failure
|
|
90
|
+
return {
|
|
91
|
+
"ok": self.ok,
|
|
92
|
+
"reason": failure.reason if failure else None,
|
|
93
|
+
"message": failure.message if failure else None,
|
|
94
|
+
"remedy": failure.remedy if failure else None,
|
|
95
|
+
"git": {
|
|
96
|
+
"present": self.git.present if self.git else None,
|
|
97
|
+
"version": (self.git.version or None) if self.git else None,
|
|
98
|
+
},
|
|
99
|
+
"credential_helper": {
|
|
100
|
+
"checked": self.helper_checked,
|
|
101
|
+
"blocked": self.helper_blocked if self.helper_checked else None,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
#: Where the probe runs git. ``git --version`` reads nothing from its working directory, and the
|
|
107
|
+
#: filesystem root is always there, so a missing or unreadable home cannot pass for a missing git.
|
|
108
|
+
_PROBE_CWD = Path(os.path.abspath(os.sep))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def probe_git(runner: Optional[GitRunner] = None) -> GitProbe:
|
|
112
|
+
"""Run ``git --version``. A git absent from PATH, or one the system will not execute, does not run."""
|
|
113
|
+
try:
|
|
114
|
+
result = (runner or run_git)(["--version"], cwd=_PROBE_CWD)
|
|
115
|
+
except OSError:
|
|
116
|
+
# The runner reports an absent git itself; this is a git it found and could not execute.
|
|
117
|
+
return GitProbe(False)
|
|
118
|
+
if result.returncode == EXIT_NOT_FOUND:
|
|
119
|
+
return GitProbe(False)
|
|
120
|
+
words = result.stdout.split()
|
|
121
|
+
version = words[2] if result.ok and words[:2] == ["git", "version"] and len(words) > 2 else ""
|
|
122
|
+
return GitProbe(True, version)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def helper_blocked(state: Optional[GitState]) -> bool:
|
|
126
|
+
"""Did this readout's fetch fail because a sandbox blocked the credential helper?"""
|
|
127
|
+
return state is not None and state.fetch_failed and sync.credential_helper_blocked(state.fetch_output)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def home_missing(home: Path) -> Prerequisites:
|
|
131
|
+
return Prerequisites(failure=Failure(HOME_UNRESOLVED, f"{home} is not a directory", HOME_MISSING_REMEDY))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def home_error(error: Exception) -> Prerequisites:
|
|
135
|
+
return Prerequisites(
|
|
136
|
+
failure=Failure(HOME_UNRESOLVED, f"the memory home could not be resolved: {error}", HOME_ERROR_REMEDY)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def assess(
|
|
141
|
+
git: GitProbe, *, configured: bool, state: Optional[GitState] = None, fetched: bool = False
|
|
142
|
+
) -> Prerequisites:
|
|
143
|
+
"""The readout for a home that exists. Git is required once the home carries the sync
|
|
144
|
+
marker; the credential helper is judged only when ``fetched``."""
|
|
145
|
+
blocked = fetched and helper_blocked(state)
|
|
146
|
+
failure: Optional[Failure] = None
|
|
147
|
+
if configured and not git.present:
|
|
148
|
+
failure = Failure(GIT_MISSING, GIT_MISSING_TEXT, GIT_MISSING_REMEDY)
|
|
149
|
+
elif blocked:
|
|
150
|
+
failure = Failure(CREDENTIAL_HELPER_BLOCKED, HELPER_BLOCKED_TEXT, HELPER_BLOCKED_REMEDY)
|
|
151
|
+
return Prerequisites(git, fetched, blocked, failure)
|
|
@@ -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}"]}
|