agent-memory-cli 0.1.0__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.0 → agent_memory_cli-0.1.2}/CHANGELOG.md +26 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/PKG-INFO +4 -2
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/README.md +2 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/pyproject.toml +2 -2
- agent_memory_cli-0.1.2/scripts/release_notes.py +78 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/__init__.py +1 -1
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/cli.py +144 -11
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/doctor.py +103 -19
- agent_memory_cli-0.1.2/src/agent_memory/events.py +409 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/layout.py +17 -1
- agent_memory_cli-0.1.2/src/agent_memory/prerequisites.py +151 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/status.py +82 -13
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/sync.py +109 -24
- agent_memory_cli-0.1.2/tests/conftest.py +112 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/golden/canonical_memory_gitignore.txt +2 -2
- agent_memory_cli-0.1.2/tests/golden/event_record.md +14 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_cli.py +3 -2
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_debrief.py +1 -1
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_doctor.py +148 -3
- agent_memory_cli-0.1.2/tests/test_events.py +706 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_layout.py +17 -1
- agent_memory_cli-0.1.2/tests/test_prerequisites.py +161 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_setup.py +1 -1
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_status.py +97 -4
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_sync.py +146 -0
- agent_memory_cli-0.1.0/tests/conftest.py +0 -60
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/.gitignore +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/LICENSE +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/Makefile +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/scripts/check_installed_wheel.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/__main__.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/archive.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/debrief.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/git_runner.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/home.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/org.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/publication.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/setup/__init__.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/setup/claude.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/setup/codex.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/setup/common.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/setup/legacy.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/startup.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/decisions.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/recent.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/org-memory/rules.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/templates/workflow/SKILL.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/src/agent_memory/workflow.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
- {agent_memory_cli-0.1.0 → 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.0 → 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.0 → 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.0 → 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.0 → 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.0 → 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.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
- {agent_memory_cli-0.1.0 → 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.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
- {agent_memory_cli-0.1.0 → 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.0 → 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.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
- {agent_memory_cli-0.1.0 → 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.0 → 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.0 → 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.0 → 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.0 → agent_memory_cli-0.1.2}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/golden/debrief_record.md +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/golden/doctor_memory_0.4.5.json +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/golden/doctor_memory_0.4.5.txt +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_archive.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_home.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_org.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_startup.py +0 -0
- {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.2}/tests/test_workflow.py +0 -0
|
@@ -4,6 +4,32 @@ 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
|
+
|
|
18
|
+
## 0.1.1 - 2026-09-12
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `event write` publishes one org-memory event under `org-memory/events/`, never replacing a published record ([event](docs/commands.md#event-write)).
|
|
23
|
+
- A `v*` tag attaches the built sdist and wheel to the GitHub Release ([releases](https://github.com/kiloloop/agent-memory/releases)).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- A nested `.gitignore` or `.oacp-memory-repo` no longer blocks `pull`; the allowlist anchors both names to the home root ([sync](docs/commands.md#sync)).
|
|
28
|
+
- `enable` retires superseded lines when it updates a home's `.gitignore`, and the receipt names them ([sync](docs/commands.md#sync)).
|
|
29
|
+
- `doctor` scopes its working-tree row to the allowlist: memory changes read DIRTY, and changes elsewhere are named as outside it ([doctor](docs/commands.md#doctor)).
|
|
30
|
+
- `push` and `doctor` keep both paths of a rename git detects in the working tree; the source path was previously misread ([sync](docs/commands.md#sync)).
|
|
31
|
+
- `push` names a credential helper a sandbox blocked as its own failure, with the remedy, instead of reporting it as a rejected push ([sync](docs/commands.md#sync)).
|
|
32
|
+
|
|
7
33
|
## 0.1.0 - 2026-09-06
|
|
8
34
|
|
|
9
35
|
### 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
|
|
@@ -24,7 +24,7 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
26
26
|
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
27
|
-
Requires-Dist: ruff<
|
|
27
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
|
|
28
28
|
Requires-Dist: twine<8,>=7; extra == 'dev'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
@@ -177,6 +177,7 @@ similarity queries, synthesis, or indexing. Recall reads a fixed file set.
|
|
|
177
177
|
| [`capture`][capture-and-recall] | Record a decision. |
|
|
178
178
|
| [`recall`][capture-and-recall] | Read bounded context. |
|
|
179
179
|
| [`debrief write`][debrief-write] | Publish a session summary. |
|
|
180
|
+
| [`event write`][event-write] | Publish an org-memory event. |
|
|
180
181
|
|
|
181
182
|
[status]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#status
|
|
182
183
|
[doctor]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#doctor
|
|
@@ -187,6 +188,7 @@ similarity queries, synthesis, or indexing. Recall reads a fixed file set.
|
|
|
187
188
|
[startup]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#startup
|
|
188
189
|
[capture-and-recall]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#capture-and-recall
|
|
189
190
|
[debrief-write]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#debrief-write
|
|
191
|
+
[event-write]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#event-write
|
|
190
192
|
|
|
191
193
|
## Examples
|
|
192
194
|
|
|
@@ -147,6 +147,7 @@ similarity queries, synthesis, or indexing. Recall reads a fixed file set.
|
|
|
147
147
|
| [`capture`][capture-and-recall] | Record a decision. |
|
|
148
148
|
| [`recall`][capture-and-recall] | Read bounded context. |
|
|
149
149
|
| [`debrief write`][debrief-write] | Publish a session summary. |
|
|
150
|
+
| [`event write`][event-write] | Publish an org-memory event. |
|
|
150
151
|
|
|
151
152
|
[status]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#status
|
|
152
153
|
[doctor]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#doctor
|
|
@@ -157,6 +158,7 @@ similarity queries, synthesis, or indexing. Recall reads a fixed file set.
|
|
|
157
158
|
[startup]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#startup
|
|
158
159
|
[capture-and-recall]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#capture-and-recall
|
|
159
160
|
[debrief-write]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#debrief-write
|
|
161
|
+
[event-write]: https://github.com/kiloloop/agent-memory/blob/main/docs/commands.md#event-write
|
|
160
162
|
|
|
161
163
|
## Examples
|
|
162
164
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.
|
|
2
|
+
requires = ["hatchling>=1.32,<2"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -38,7 +38,7 @@ Changelog = "https://github.com/kiloloop/agent-memory/blob/main/CHANGELOG.md"
|
|
|
38
38
|
dev = [
|
|
39
39
|
"build>=1.2,<2",
|
|
40
40
|
"pytest>=8,<10",
|
|
41
|
-
"ruff>=0.
|
|
41
|
+
"ruff>=0.16,<0.17",
|
|
42
42
|
"twine>=7,<8",
|
|
43
43
|
]
|
|
44
44
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Kiloloop
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Print the changelog section for one release, as GitHub Release notes.
|
|
5
|
+
|
|
6
|
+
The release workflow feeds this to ``gh release create --notes-file``. GitHub's own
|
|
7
|
+
``--generate-notes`` cannot serve here: the public repository receives each version as a
|
|
8
|
+
single squashed commit, so the generated summary is one line no matter how much the
|
|
9
|
+
release contains. The changelog section already says what shipped, one line per change.
|
|
10
|
+
|
|
11
|
+
Two transformations make a changelog section stand alone on a release page:
|
|
12
|
+
|
|
13
|
+
1. Relative links (``docs/commands.md#init``) are rewritten against the tag. A release
|
|
14
|
+
body is not rendered relative to the repository root, so a relative target 404s; and
|
|
15
|
+
pinning to the tag rather than to ``main`` keeps old release notes pointing at the
|
|
16
|
+
documentation as it stood for that version.
|
|
17
|
+
2. The section heading itself is dropped — the release already carries the version.
|
|
18
|
+
|
|
19
|
+
Exits 1 when the section is absent, so the caller can fall back rather than publish a
|
|
20
|
+
release with an empty body.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
LINK = re.compile(r"\]\(([^)]+)\)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract(changelog: str, version: str) -> str | None:
|
|
34
|
+
"""Return the body of ``## <version>``, or None when no such section exists."""
|
|
35
|
+
lines = changelog.split("\n")
|
|
36
|
+
start = None
|
|
37
|
+
for i, line in enumerate(lines):
|
|
38
|
+
# "## 0.1.0 - 2026-09-06" and "## [0.1.0] - ..." both name this version.
|
|
39
|
+
if line.startswith("## ") and re.match(rf"##\s+\[?{re.escape(version)}\]?\b", line):
|
|
40
|
+
start = i + 1
|
|
41
|
+
break
|
|
42
|
+
if start is None:
|
|
43
|
+
return None
|
|
44
|
+
body: list[str] = []
|
|
45
|
+
for line in lines[start:]:
|
|
46
|
+
if line.startswith("## "):
|
|
47
|
+
break
|
|
48
|
+
body.append(line)
|
|
49
|
+
return "\n".join(body).strip() or None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def absolutise(notes: str, repository: str, tag: str) -> str:
|
|
53
|
+
"""Rewrite repo-relative link targets to absolute URLs pinned at ``tag``."""
|
|
54
|
+
base = f"https://github.com/{repository}/blob/{tag}/"
|
|
55
|
+
return LINK.sub(
|
|
56
|
+
lambda m: f"]({m.group(1)})" if m.group(1).startswith("http") else f"]({base}{m.group(1)})",
|
|
57
|
+
notes,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> int:
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
63
|
+
parser.add_argument("--tag", required=True, help="release tag, e.g. v0.1.0")
|
|
64
|
+
parser.add_argument("--repository", required=True, help="owner/repo, for absolute links")
|
|
65
|
+
parser.add_argument("--changelog", default="CHANGELOG.md", type=Path)
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
|
|
68
|
+
version = args.tag[1:] if args.tag.startswith("v") else args.tag
|
|
69
|
+
notes = extract(args.changelog.read_text(encoding="utf-8"), version)
|
|
70
|
+
if notes is None:
|
|
71
|
+
print(f"no changelog section for {version} in {args.changelog}", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
print(absolutise(notes, args.repository, args.tag))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|
|
@@ -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, 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(
|
|
@@ -189,6 +190,38 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
189
190
|
write.add_argument("--oacp-dir", dest="home", metavar="PATH", help=argparse.SUPPRESS)
|
|
190
191
|
write.add_argument("--dry-run", action="store_true", help="validate and compose the record, print it, and write nothing")
|
|
191
192
|
write.add_argument("--json", dest="json_output", action="store_true", help="emit a machine-readable result")
|
|
193
|
+
event_ = commands.add_parser("event", help="Org-memory event commands.", description="Org-memory event commands.")
|
|
194
|
+
event_commands = event_.add_subparsers(dest="event_command", metavar="<command>")
|
|
195
|
+
event_help = (
|
|
196
|
+
"Publish one org-memory event into the home's events store, failure-atomically: the canonical path only ever "
|
|
197
|
+
"holds a complete, verified record, byte-identical to the kernel's write-event script for the same inputs. "
|
|
198
|
+
"Exit 0 published (or an identical record was already there), 1 on a validation error, 2 on a publication "
|
|
199
|
+
"failure."
|
|
200
|
+
)
|
|
201
|
+
event_write = event_commands.add_parser("write", parents=[common], help=event_help, description=event_help)
|
|
202
|
+
event_write.set_defaults(handler=_event_write)
|
|
203
|
+
event_write.add_argument("--agent", required=True, help="agent creating the event")
|
|
204
|
+
event_write.add_argument("--project", required=True, help="originating project")
|
|
205
|
+
event_write.add_argument(
|
|
206
|
+
"--type", dest="event_type", required=True, choices=sorted(events.ALLOWED_TYPES), help="event type"
|
|
207
|
+
)
|
|
208
|
+
event_write.add_argument(
|
|
209
|
+
"--slug", required=True, help="short slug for the filename: lowercase alphanumerics and hyphens, no dots"
|
|
210
|
+
)
|
|
211
|
+
event_body = event_write.add_mutually_exclusive_group()
|
|
212
|
+
event_body.add_argument("--body", help="event body, inline")
|
|
213
|
+
event_body.add_argument("--body-file", help="path to the event body in Markdown, or '-' to read stdin")
|
|
214
|
+
event_write.add_argument("--source-ref", help="provenance id, e.g. the debrief stem the event was folded from")
|
|
215
|
+
event_write.add_argument(
|
|
216
|
+
"--related", help="cross-references: comma-separated or a JSON array (e.g. 'PR #43,issue #10')"
|
|
217
|
+
)
|
|
218
|
+
event_write.add_argument("--supersedes", help="path of the event this entry overrides")
|
|
219
|
+
# The kernel script's home flag; accepted, unadvertised, through v0.1.x.
|
|
220
|
+
event_write.add_argument("--oacp-dir", dest="home", metavar="PATH", help=argparse.SUPPRESS)
|
|
221
|
+
event_write.add_argument(
|
|
222
|
+
"--dry-run", action="store_true", help="validate and compose the record, print it, and write nothing"
|
|
223
|
+
)
|
|
224
|
+
event_write.add_argument("--json", dest="json_output", action="store_true", help="emit a machine-readable result")
|
|
192
225
|
return parser
|
|
193
226
|
|
|
194
227
|
|
|
@@ -203,23 +236,44 @@ def _max_chars(value: str) -> int:
|
|
|
203
236
|
|
|
204
237
|
|
|
205
238
|
def _status(args: argparse.Namespace) -> int:
|
|
206
|
-
|
|
207
|
-
|
|
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()))
|
|
208
251
|
return readout.exit_code
|
|
209
252
|
|
|
210
253
|
|
|
211
254
|
def _doctor(args: argparse.Namespace) -> int:
|
|
212
|
-
|
|
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
|
|
213
264
|
if not home.is_dir():
|
|
214
|
-
|
|
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)
|
|
215
270
|
return EXIT_FAILED
|
|
216
|
-
categories = doctor.
|
|
217
|
-
memory_lint = doctor.find_memory_lint()
|
|
271
|
+
categories, readout = doctor.diagnose(home)
|
|
218
272
|
if args.json_output:
|
|
219
|
-
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))
|
|
220
274
|
else:
|
|
221
275
|
sys.stdout.write(doctor.report(categories, memory_lint=memory_lint))
|
|
222
|
-
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
|
|
223
277
|
|
|
224
278
|
|
|
225
279
|
def _init(args: argparse.Namespace) -> int:
|
|
@@ -414,6 +468,85 @@ def _debrief_write(args: argparse.Namespace) -> int:
|
|
|
414
468
|
return EXIT_OK
|
|
415
469
|
|
|
416
470
|
|
|
471
|
+
def _event_write(args: argparse.Namespace) -> int:
|
|
472
|
+
home = resolve_home(args.home).path
|
|
473
|
+
try:
|
|
474
|
+
body = _event_body(args)
|
|
475
|
+
except OSError as exc:
|
|
476
|
+
print(f"ERROR: cannot read body: {exc}", file=sys.stderr)
|
|
477
|
+
return EXIT_FAILED
|
|
478
|
+
except events.WriterError as exc:
|
|
479
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
480
|
+
return exc.code
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
result = events.write_event(
|
|
484
|
+
home=home,
|
|
485
|
+
agent=args.agent,
|
|
486
|
+
project=args.project,
|
|
487
|
+
event_type=args.event_type,
|
|
488
|
+
slug=args.slug,
|
|
489
|
+
body=body,
|
|
490
|
+
source_ref=args.source_ref,
|
|
491
|
+
related=events.normalize_related(args.related) if args.related else None,
|
|
492
|
+
supersedes=args.supersedes,
|
|
493
|
+
dry_run=args.dry_run,
|
|
494
|
+
)
|
|
495
|
+
except events.WriterError as exc:
|
|
496
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
497
|
+
return exc.code
|
|
498
|
+
except OSError as exc:
|
|
499
|
+
print(f"ERROR: publication failed: {exc}", file=sys.stderr)
|
|
500
|
+
return 2
|
|
501
|
+
|
|
502
|
+
if args.json_output:
|
|
503
|
+
print(
|
|
504
|
+
json.dumps(
|
|
505
|
+
{
|
|
506
|
+
"path": str(result.path),
|
|
507
|
+
"status": result.status,
|
|
508
|
+
"created_at_utc": result.created_at_utc,
|
|
509
|
+
"date": result.created_at_utc[:10],
|
|
510
|
+
"agent": args.agent,
|
|
511
|
+
"project": args.project,
|
|
512
|
+
"type": args.event_type,
|
|
513
|
+
},
|
|
514
|
+
indent=2,
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
print(f"{result.status}: {result.path}")
|
|
519
|
+
print(f"created_at_utc: {result.created_at_utc}")
|
|
520
|
+
|
|
521
|
+
if args.dry_run:
|
|
522
|
+
print("--- record preview (nothing was written) ---", file=sys.stderr)
|
|
523
|
+
sys.stderr.flush()
|
|
524
|
+
sys.stderr.buffer.write(result.record)
|
|
525
|
+
sys.stderr.buffer.flush()
|
|
526
|
+
return EXIT_OK
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _event_body(args: argparse.Namespace) -> bytes:
|
|
530
|
+
"""The body bytes: ``--body-file`` (a path, or ``-`` for stdin), ``--body``, else piped stdin.
|
|
531
|
+
|
|
532
|
+
Trailing newlines are dropped, as the kernel script drops them; the record
|
|
533
|
+
closes the body with exactly one. A body file is read as text, so its
|
|
534
|
+
CRLF and CR line endings become LF (:func:`events.body_from_file`); stdin
|
|
535
|
+
and the inline body are taken as given, both as the script does.
|
|
536
|
+
"""
|
|
537
|
+
if args.body_file is not None and args.body_file != "-":
|
|
538
|
+
return events.body_from_file(Path(args.body_file).expanduser().read_bytes())
|
|
539
|
+
if args.body_file is not None:
|
|
540
|
+
raw = sys.stdin.buffer.read()
|
|
541
|
+
elif args.body is not None:
|
|
542
|
+
raw = args.body.encode("utf-8")
|
|
543
|
+
elif not sys.stdin.isatty():
|
|
544
|
+
raw = sys.stdin.buffer.read()
|
|
545
|
+
else:
|
|
546
|
+
raise events.WriterError("no body provided: use --body, --body-file <path|->, or pipe the body to stdin", 1)
|
|
547
|
+
return raw.rstrip(b"\n")
|
|
548
|
+
|
|
549
|
+
|
|
417
550
|
def _report(outcome: sync.Outcome) -> int:
|
|
418
551
|
if outcome.lines:
|
|
419
552
|
print("\n".join(outcome.lines), file=sys.stdout if outcome.ok else sys.stderr)
|
|
@@ -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:
|
|
@@ -444,20 +477,16 @@ def check_memory_sync(
|
|
|
444
477
|
state = None
|
|
445
478
|
|
|
446
479
|
if state is not None:
|
|
447
|
-
|
|
448
|
-
cat.add(
|
|
449
|
-
"working-tree",
|
|
450
|
-
Severity.warn,
|
|
451
|
-
"working tree — DIRTY memory changes present",
|
|
452
|
-
"Run `agent-memory push` or resolve changes manually",
|
|
453
|
-
)
|
|
454
|
-
else:
|
|
455
|
-
cat.add("working-tree", Severity.ok, "working tree — clean")
|
|
480
|
+
_add_working_tree(cat, home, runner, state)
|
|
456
481
|
|
|
457
482
|
text = f"sync state — {sync_state_text(state)}"
|
|
458
483
|
if not state.has_remote:
|
|
459
484
|
cat.add("sync-state", Severity.ok, text)
|
|
460
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")
|
|
461
490
|
elif state.fetch_failed:
|
|
462
491
|
cat.add("sync-state", Severity.warn, text, "Check network access and remote permissions")
|
|
463
492
|
cat.add("remote", Severity.warn, "remote — not reachable", "Check network access and remote permissions")
|
|
@@ -531,7 +560,42 @@ def check_memory_sync(
|
|
|
531
560
|
else:
|
|
532
561
|
cat.add("memory-overlays", Severity.ok, f"memory .gitignore overlays — {len(overlays)} safe")
|
|
533
562
|
|
|
534
|
-
|
|
563
|
+
fetched = state is not None and state.has_remote
|
|
564
|
+
return cat, prerequisites.assess(git, configured=True, state=state, fetched=fetched)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _add_working_tree(cat: Category, home: Path, runner: Optional[GitRunner], state: GitState) -> None:
|
|
568
|
+
"""The tree row, scoped like the counts above it.
|
|
569
|
+
|
|
570
|
+
Memory changes inside the allowlist are what ``push`` would publish and
|
|
571
|
+
read DIRTY. Changes anywhere else are named as outside the allowlist: not
|
|
572
|
+
memory content, but ``pull`` still refuses a tree that carries them.
|
|
573
|
+
"""
|
|
574
|
+
if not state.dirty:
|
|
575
|
+
cat.add("working-tree", Severity.ok, "working tree — clean")
|
|
576
|
+
return
|
|
577
|
+
try:
|
|
578
|
+
changed = sync.changed_paths(home, runner)
|
|
579
|
+
except sync.SyncError as exc:
|
|
580
|
+
cat.add("working-tree", Severity.warn, f"working tree — DIRTY; the change readout failed: {exc}")
|
|
581
|
+
return
|
|
582
|
+
memory = [path for path in changed if layout.is_allowed_memory_path(path)]
|
|
583
|
+
elsewhere = [path for path in changed if not layout.is_allowed_memory_path(path)]
|
|
584
|
+
if memory:
|
|
585
|
+
text = f"working tree — DIRTY {len(memory)} memory change(s): {_summarize(memory)}"
|
|
586
|
+
if elsewhere:
|
|
587
|
+
text += f"; {len(elsewhere)} change(s) outside the memory allowlist"
|
|
588
|
+
cat.add("working-tree", Severity.warn, text, "Run `agent-memory push` or resolve changes manually")
|
|
589
|
+
elif elsewhere:
|
|
590
|
+
cat.add(
|
|
591
|
+
"working-tree",
|
|
592
|
+
Severity.warn,
|
|
593
|
+
f"working tree — {len(elsewhere)} change(s) outside the memory allowlist: {_summarize(elsewhere)}; "
|
|
594
|
+
"no memory change is pending, but memory pull refuses a dirty tree",
|
|
595
|
+
"Commit, stash or ignore them outside the memory tiers",
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
cat.add("working-tree", Severity.ok, "working tree — clean")
|
|
535
599
|
|
|
536
600
|
|
|
537
601
|
def enclosing_repository(home: Path, runner: Optional[GitRunner] = None) -> Optional[Path]:
|
|
@@ -542,10 +606,30 @@ def enclosing_repository(home: Path, runner: Optional[GitRunner] = None) -> Opti
|
|
|
542
606
|
return root
|
|
543
607
|
|
|
544
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
|
+
|
|
545
626
|
def sync_state_text(state: GitState) -> str:
|
|
546
627
|
"""One phrase for where the repository stands; ``status`` and ``doctor`` share it."""
|
|
547
628
|
if not state.has_remote:
|
|
548
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}"
|
|
549
633
|
if state.fetch_failed:
|
|
550
634
|
return f"remote fetch failed: {state.fetch_output}"
|
|
551
635
|
if not state.has_upstream:
|