agent-memory-cli 0.1.1__tar.gz → 0.1.2__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.2}/CHANGELOG.md +11 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/PKG-INFO +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/pyproject.toml +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/__init__.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/cli.py +33 -11
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/doctor.py +68 -10
- agent_memory_cli-0.1.2/src/agent_memory/prerequisites.py +151 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/status.py +82 -13
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/sync.py +4 -3
- agent_memory_cli-0.1.2/tests/conftest.py +112 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_debrief.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_doctor.py +80 -3
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_events.py +3 -3
- agent_memory_cli-0.1.2/tests/test_prerequisites.py +161 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_setup.py +1 -1
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_status.py +97 -4
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_sync.py +4 -34
- agent_memory_cli-0.1.1/tests/conftest.py +0 -60
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/.gitignore +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/LICENSE +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/Makefile +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/README.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/scripts/check_installed_wheel.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/scripts/release_notes.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/__main__.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/archive.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/debrief.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/events.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/git_runner.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/home.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/layout.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/org.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/publication.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/setup/__init__.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/setup/claude.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/setup/codex.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/setup/common.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/setup/legacy.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/startup.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/decisions.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/recent.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/rules.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/templates/workflow/SKILL.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/src/agent_memory/workflow.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/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.2}/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.2}/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.2}/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.2}/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.2}/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.2}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/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.2}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/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.2}/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.2}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/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.2}/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.2}/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.2}/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.2}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/golden/canonical_memory_gitignore.txt +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/golden/debrief_record.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/golden/doctor_memory_0.4.5.json +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/golden/doctor_memory_0.4.5.txt +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/golden/event_record.md +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_archive.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_cli.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_home.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_layout.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_org.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_startup.py +0 -0
- {agent_memory_cli-0.1.1 → agent_memory_cli-0.1.2}/tests/test_workflow.py +0 -0
|
@@ -4,6 +4,17 @@ 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.2 - 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `status --json` prints the readout as one object, with an `ok` flag and a `prerequisites` block ([status](docs/commands.md#status)).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `status` and `doctor` exit non-zero with a reason code when a prerequisite such as git fails ([prerequisites](docs/commands.md#prerequisites)).
|
|
16
|
+
- `doctor` and `status --fetch` name a sandbox-blocked credential helper, not an unreachable remote ([doctor](docs/commands.md#doctor)).
|
|
17
|
+
|
|
7
18
|
## 0.1.1 - 2026-09-12
|
|
8
19
|
|
|
9
20
|
### 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.2
|
|
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
|
|
@@ -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(
|
|
@@ -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:
|
|
@@ -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)
|
|
@@ -7,20 +7,23 @@ project), the layout (marker, allowlist, tiers), and, when the home is a sync
|
|
|
7
7
|
repository, its :class:`~agent_memory.sync.GitState`. The remote is contacted
|
|
8
8
|
only with ``--fetch``; otherwise ahead/behind count against the last fetched
|
|
9
9
|
upstream. Exit 0 when the tree is clean and not diverged, 1 when it is dirty
|
|
10
|
-
or diverged
|
|
11
|
-
failed: they are what ``push`` and ``pull``
|
|
10
|
+
or diverged, or a prerequisite failed (:mod:`agent_memory.prerequisites`).
|
|
11
|
+
Ahead and behind are reported, not failed: they are what ``push`` and ``pull``
|
|
12
|
+
are for. ``--json`` prints the same readout as data, with ``ok`` true exactly
|
|
13
|
+
when the exit is 0.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
from __future__ import annotations
|
|
15
17
|
|
|
16
18
|
from dataclasses import dataclass
|
|
17
19
|
from pathlib import Path
|
|
18
|
-
from typing import List, Optional
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
19
21
|
|
|
20
|
-
from . import layout, sync
|
|
21
|
-
from .doctor import enclosing_repository, sync_state_text
|
|
22
|
+
from . import layout, prerequisites, sync
|
|
23
|
+
from .doctor import enclosing_repository, sync_state, sync_state_text
|
|
22
24
|
from .git_runner import GitRunner
|
|
23
25
|
from .home import HomeResolution
|
|
26
|
+
from .prerequisites import Prerequisites
|
|
24
27
|
from .sync import GitState
|
|
25
28
|
|
|
26
29
|
EXIT_OK = 0
|
|
@@ -39,13 +42,14 @@ class Status:
|
|
|
39
42
|
gitignore: str = ""
|
|
40
43
|
org_memory: bool = False
|
|
41
44
|
projects: int = 0
|
|
42
|
-
#: ``None`` without the marker; otherwise whether the home is a git repository.
|
|
45
|
+
#: ``None`` without the marker or without git; otherwise whether the home is a git repository.
|
|
43
46
|
repository: Optional[bool] = None
|
|
44
47
|
#: Set when the home is a repository only by sitting inside another one.
|
|
45
48
|
enclosing: Optional[Path] = None
|
|
46
49
|
git: Optional[GitState] = None
|
|
47
50
|
#: Whether the remote was contacted for this readout.
|
|
48
51
|
fetched: bool = False
|
|
52
|
+
prerequisites: Prerequisites = Prerequisites()
|
|
49
53
|
|
|
50
54
|
@property
|
|
51
55
|
def clean(self) -> bool:
|
|
@@ -54,9 +58,13 @@ class Status:
|
|
|
54
58
|
return False
|
|
55
59
|
return self.git is None or not (self.git.dirty or self.git.diverged)
|
|
56
60
|
|
|
61
|
+
@property
|
|
62
|
+
def ok(self) -> bool:
|
|
63
|
+
return self.prerequisites.ok and self.clean
|
|
64
|
+
|
|
57
65
|
@property
|
|
58
66
|
def exit_code(self) -> int:
|
|
59
|
-
return EXIT_OK if self.
|
|
67
|
+
return EXIT_OK if self.ok else EXIT_FAILED
|
|
60
68
|
|
|
61
69
|
def lines(self) -> List[str]:
|
|
62
70
|
lines = [f"home: {self.home}", f"source: {self.source}"]
|
|
@@ -64,7 +72,7 @@ class Status:
|
|
|
64
72
|
lines.append(f"project: {self.project}")
|
|
65
73
|
if not self.exists:
|
|
66
74
|
lines.append("exists: no")
|
|
67
|
-
return lines
|
|
75
|
+
return self._with_failure(lines)
|
|
68
76
|
lines.extend(
|
|
69
77
|
[
|
|
70
78
|
"exists: yes",
|
|
@@ -74,8 +82,10 @@ class Status:
|
|
|
74
82
|
f"projects: {self.projects} with a memory dir",
|
|
75
83
|
]
|
|
76
84
|
)
|
|
77
|
-
if self.
|
|
85
|
+
if not self.marker:
|
|
78
86
|
lines.append("sync: not configured")
|
|
87
|
+
elif self.prerequisites.reason == prerequisites.GIT_MISSING:
|
|
88
|
+
lines.append(f"sync: not read ({prerequisites.GIT_MISSING})")
|
|
79
89
|
elif not self.repository:
|
|
80
90
|
lines.append("sync: marker present, but the home is not a git repository")
|
|
81
91
|
elif self.enclosing is not None:
|
|
@@ -85,26 +95,70 @@ class Status:
|
|
|
85
95
|
if self.git.has_remote:
|
|
86
96
|
lines.append("fetch: done" if self.fetched else "fetch: skipped (pass --fetch to contact the remote)")
|
|
87
97
|
lines.append(f"tree: {'dirty' if self.git.dirty else 'clean'}")
|
|
88
|
-
return lines
|
|
98
|
+
return self._with_failure(lines)
|
|
99
|
+
|
|
100
|
+
def _with_failure(self, lines: List[str]) -> List[str]:
|
|
101
|
+
failure = self.prerequisites.failure
|
|
102
|
+
return lines + [f"prerequisite failed: {failure.line()}"] if failure else lines
|
|
103
|
+
|
|
104
|
+
def to_json(self) -> Dict[str, Any]:
|
|
105
|
+
"""The readout as data: the fields the lines show, and ``ok`` for the exit code."""
|
|
106
|
+
git = self.git
|
|
107
|
+
return {
|
|
108
|
+
"ok": self.ok,
|
|
109
|
+
"home": str(self.home),
|
|
110
|
+
"source": self.source,
|
|
111
|
+
"project": self.project,
|
|
112
|
+
"exists": self.exists,
|
|
113
|
+
"layout": {
|
|
114
|
+
"marker": self.marker,
|
|
115
|
+
"gitignore": self.gitignore,
|
|
116
|
+
"org_memory": self.org_memory,
|
|
117
|
+
"projects": self.projects,
|
|
118
|
+
}
|
|
119
|
+
if self.exists
|
|
120
|
+
else None,
|
|
121
|
+
"sync": {
|
|
122
|
+
"configured": self.marker,
|
|
123
|
+
"repository": self.repository,
|
|
124
|
+
"enclosing": str(self.enclosing) if self.enclosing is not None else None,
|
|
125
|
+
"state": sync_state(git) if git else None,
|
|
126
|
+
"state_text": sync_state_text(git) if git else None,
|
|
127
|
+
"remote": git.has_remote if git else None,
|
|
128
|
+
"upstream": (git.upstream or None) if git else None,
|
|
129
|
+
"fetched": self.fetched,
|
|
130
|
+
"ahead": git.ahead if git else None,
|
|
131
|
+
"behind": git.behind if git else None,
|
|
132
|
+
"dirty": git.dirty if git else None,
|
|
133
|
+
"diverged": git.diverged if git else None,
|
|
134
|
+
}
|
|
135
|
+
if self.exists
|
|
136
|
+
else None,
|
|
137
|
+
"prerequisites": self.prerequisites.to_json(),
|
|
138
|
+
}
|
|
89
139
|
|
|
90
140
|
|
|
91
141
|
def inspect(resolution: HomeResolution, *, fetch: bool = False, runner: Optional[GitRunner] = None) -> Status:
|
|
92
142
|
"""Read the home ``resolution`` names; nothing is changed, and no network without ``fetch``."""
|
|
93
143
|
home = resolution.path
|
|
94
144
|
if not home.is_dir():
|
|
95
|
-
return Status(
|
|
145
|
+
return Status(
|
|
146
|
+
home, resolution.source, resolution.project, exists=False, prerequisites=prerequisites.home_missing(home)
|
|
147
|
+
)
|
|
96
148
|
tiers = layout.allowed_memory_dirs(home)
|
|
97
149
|
org = layout.org_memory_dir(home)
|
|
98
150
|
marker = sync.is_configured(home)
|
|
151
|
+
probe = prerequisites.probe_git(runner)
|
|
99
152
|
repository: Optional[bool] = None
|
|
100
153
|
enclosing: Optional[Path] = None
|
|
101
154
|
git: Optional[GitState] = None
|
|
102
|
-
if marker:
|
|
155
|
+
if marker and probe.present:
|
|
103
156
|
repository = sync.is_git_repo(home, runner)
|
|
104
157
|
if repository:
|
|
105
158
|
enclosing = enclosing_repository(home, runner)
|
|
106
159
|
if repository and enclosing is None:
|
|
107
160
|
git = sync.git_state(home, runner=runner, fetch=fetch)
|
|
161
|
+
fetched = fetch and git is not None and git.has_remote
|
|
108
162
|
return Status(
|
|
109
163
|
home,
|
|
110
164
|
resolution.source,
|
|
@@ -117,10 +171,25 @@ def inspect(resolution: HomeResolution, *, fetch: bool = False, runner: Optional
|
|
|
117
171
|
repository=repository,
|
|
118
172
|
enclosing=enclosing,
|
|
119
173
|
git=git,
|
|
120
|
-
fetched=
|
|
174
|
+
fetched=fetched,
|
|
175
|
+
prerequisites=prerequisites.assess(probe, configured=marker, state=git, fetched=fetched),
|
|
121
176
|
)
|
|
122
177
|
|
|
123
178
|
|
|
179
|
+
def unresolved(error: Exception) -> Dict[str, Any]:
|
|
180
|
+
"""The JSON readout when the resolver itself failed, so there is no home to read."""
|
|
181
|
+
return {
|
|
182
|
+
"ok": False,
|
|
183
|
+
"home": None,
|
|
184
|
+
"source": None,
|
|
185
|
+
"project": None,
|
|
186
|
+
"exists": False,
|
|
187
|
+
"layout": None,
|
|
188
|
+
"sync": None,
|
|
189
|
+
"prerequisites": prerequisites.home_error(error).to_json(),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
124
193
|
def gitignore_state(home: Path) -> str:
|
|
125
194
|
"""How the root ``.gitignore`` relates to the canonical allowlist, in a few words."""
|
|
126
195
|
path = home / layout.GITIGNORE_FILE
|
|
@@ -547,7 +547,7 @@ def _deliver(
|
|
|
547
547
|
receipt: Optional[GitignoreReceipt] = None,
|
|
548
548
|
) -> Outcome:
|
|
549
549
|
if state.fetch_failed:
|
|
550
|
-
if
|
|
550
|
+
if credential_helper_blocked(state.fetch_output):
|
|
551
551
|
return _helper_blocked(lines, published, receipt)
|
|
552
552
|
failure = _fetch_failure(state, "memory push")
|
|
553
553
|
lines.append(f"{failure.lines[0]} The commit remains local.")
|
|
@@ -557,7 +557,7 @@ def _deliver(
|
|
|
557
557
|
return Outcome("up_to_date", True, tuple(lines), (), published.preserved, receipt)
|
|
558
558
|
result = _push_remote(home, state, runner)
|
|
559
559
|
if not result.ok:
|
|
560
|
-
if
|
|
560
|
+
if credential_helper_blocked(result.output):
|
|
561
561
|
return _helper_blocked(lines, published, receipt)
|
|
562
562
|
what = "timed out" if result.timed_out else "was rejected"
|
|
563
563
|
lines.append(f"memory push: the push {what}; the commit remains local. {result.output}".rstrip())
|
|
@@ -575,9 +575,10 @@ def _push_remote(home: Path, state: GitState, runner: Optional[GitRunner]) -> Gi
|
|
|
575
575
|
return _git(home, ["push", "--quiet", "-u", remote, branch], runner, timeout=NETWORK_TIMEOUT_SECONDS)
|
|
576
576
|
|
|
577
577
|
|
|
578
|
-
def
|
|
578
|
+
def credential_helper_blocked(output: str) -> bool:
|
|
579
579
|
"""Does this git failure carry the blocked-credential-helper signature?
|
|
580
580
|
|
|
581
|
+
The one detector: ``doctor`` and ``status`` read fetch output through it too.
|
|
581
582
|
git falls back to prompting when a helper hands it nothing, and under a
|
|
582
583
|
sandbox that prompt has no terminal to read from either. Both halves are
|
|
583
584
|
required, and the second must come from the helper: a prompt failure on
|