agent-memory-cli 0.1.0__tar.gz → 0.1.1__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.
Files changed (79) hide show
  1. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/CHANGELOG.md +15 -0
  2. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/PKG-INFO +4 -2
  3. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/README.md +2 -0
  4. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/pyproject.toml +1 -1
  5. agent_memory_cli-0.1.1/scripts/release_notes.py +78 -0
  6. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/__init__.py +1 -1
  7. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/cli.py +112 -1
  8. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/doctor.py +35 -9
  9. agent_memory_cli-0.1.1/src/agent_memory/events.py +409 -0
  10. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/layout.py +17 -1
  11. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/sync.py +108 -24
  12. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/golden/canonical_memory_gitignore.txt +2 -2
  13. agent_memory_cli-0.1.1/tests/golden/event_record.md +14 -0
  14. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_cli.py +3 -2
  15. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_doctor.py +68 -0
  16. agent_memory_cli-0.1.1/tests/test_events.py +706 -0
  17. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_layout.py +17 -1
  18. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_sync.py +176 -0
  19. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/.gitignore +0 -0
  20. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/LICENSE +0 -0
  21. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/Makefile +0 -0
  22. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/scripts/check_installed_wheel.py +0 -0
  23. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/__main__.py +0 -0
  24. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/archive.py +0 -0
  25. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/debrief.py +0 -0
  26. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/git_runner.py +0 -0
  27. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/home.py +0 -0
  28. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/org.py +0 -0
  29. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/publication.py +0 -0
  30. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/setup/__init__.py +0 -0
  31. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/setup/claude.py +0 -0
  32. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/setup/codex.py +0 -0
  33. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/setup/common.py +0 -0
  34. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/setup/legacy.py +0 -0
  35. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/startup.py +0 -0
  36. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/status.py +0 -0
  37. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/org-memory/decisions.md +0 -0
  38. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/org-memory/recent.md +0 -0
  39. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/org-memory/rules.md +0 -0
  40. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/project-memory/decision_log.md +0 -0
  41. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/project-memory/known_debt.md +0 -0
  42. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/project-memory/open_threads.md +0 -0
  43. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/project-memory/project_facts.md +0 -0
  44. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/templates/workflow/SKILL.md +0 -0
  45. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/src/agent_memory/workflow.py +0 -0
  46. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/expected.json +0 -0
  47. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/.hidden/2026/08/20260825-alice-abc12345.md +0 -0
  48. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  49. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825-alice-ABCD.md +0 -0
  50. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/08/20260825_alice_abc12345.md +0 -0
  51. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/2026/09/20260825-alice-def12345.md +0 -0
  52. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/bad_layout/org-memory/debriefs/demo-project/20260825-alice-abc12345.md +0 -0
  53. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/empty_store/expected.json +0 -0
  54. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/empty_store/org-memory/debriefs/.gitkeep +0 -0
  55. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/missing_debriefs_dir/expected.json +0 -0
  56. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/missing_debriefs_dir/org-memory/recent.md +0 -0
  57. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/staging_artifact/expected.json +0 -0
  58. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/.stage.20260825-alice-77xx88yy.md.a1b2c3 +0 -0
  59. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/staging_artifact/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  60. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/expected.json +0 -0
  61. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/.gitkeep +0 -0
  62. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/Demo_Project/2026/08/20260825-bob-ops-9f00aa11.md +0 -0
  63. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo-project/2026/08/20260825-alice-1f3a9c2b.md +0 -0
  64. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/demo.project/2026/07/20260701-Alice_2.dev-00aa11bb.md +0 -0
  65. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conformance/org_memory/cases/valid_store/org-memory/debriefs/other-project/2026/12/20261203-bob-9e0d44aa.md +0 -0
  66. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/conftest.py +0 -0
  67. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/fixtures/setup/claude_settings_fleet.json +0 -0
  68. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/fixtures/setup/codex_hooks_kernel.json +0 -0
  69. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/golden/debrief_record.md +0 -0
  70. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/golden/doctor_memory_0.4.5.json +0 -0
  71. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/golden/doctor_memory_0.4.5.txt +0 -0
  72. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_archive.py +0 -0
  73. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_debrief.py +0 -0
  74. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_home.py +0 -0
  75. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_org.py +0 -0
  76. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_setup.py +0 -0
  77. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_startup.py +0 -0
  78. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_status.py +0 -0
  79. {agent_memory_cli-0.1.0 → agent_memory_cli-0.1.1}/tests/test_workflow.py +0 -0
@@ -4,6 +4,21 @@ 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.1 - 2026-09-12
8
+
9
+ ### Added
10
+
11
+ - `event write` publishes one org-memory event under `org-memory/events/`, never replacing a published record ([event](docs/commands.md#event-write)).
12
+ - A `v*` tag attaches the built sdist and wheel to the GitHub Release ([releases](https://github.com/kiloloop/agent-memory/releases)).
13
+
14
+ ### Fixed
15
+
16
+ - 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)).
17
+ - `enable` retires superseded lines when it updates a home's `.gitignore`, and the receipt names them ([sync](docs/commands.md#sync)).
18
+ - `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)).
19
+ - `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)).
20
+ - `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)).
21
+
7
22
  ## 0.1.0 - 2026-09-06
8
23
 
9
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-memory-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
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<1,>=0.11; extra == 'dev'
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
 
@@ -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.11,<1",
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())
@@ -4,6 +4,6 @@
4
4
 
5
5
  from .home import HomeError, HomeResolution, resolve_home
6
6
 
7
- __version__ = "0.1.0"
7
+ __version__ = "0.1.1"
8
8
 
9
9
  __all__ = ["HomeError", "HomeResolution", "__version__", "resolve_home"]
@@ -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, 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
@@ -189,6 +189,38 @@ def build_parser() -> argparse.ArgumentParser:
189
189
  write.add_argument("--oacp-dir", dest="home", metavar="PATH", help=argparse.SUPPRESS)
190
190
  write.add_argument("--dry-run", action="store_true", help="validate and compose the record, print it, and write nothing")
191
191
  write.add_argument("--json", dest="json_output", action="store_true", help="emit a machine-readable result")
192
+ event_ = commands.add_parser("event", help="Org-memory event commands.", description="Org-memory event commands.")
193
+ event_commands = event_.add_subparsers(dest="event_command", metavar="<command>")
194
+ event_help = (
195
+ "Publish one org-memory event into the home's events store, failure-atomically: the canonical path only ever "
196
+ "holds a complete, verified record, byte-identical to the kernel's write-event script for the same inputs. "
197
+ "Exit 0 published (or an identical record was already there), 1 on a validation error, 2 on a publication "
198
+ "failure."
199
+ )
200
+ event_write = event_commands.add_parser("write", parents=[common], help=event_help, description=event_help)
201
+ event_write.set_defaults(handler=_event_write)
202
+ event_write.add_argument("--agent", required=True, help="agent creating the event")
203
+ event_write.add_argument("--project", required=True, help="originating project")
204
+ event_write.add_argument(
205
+ "--type", dest="event_type", required=True, choices=sorted(events.ALLOWED_TYPES), help="event type"
206
+ )
207
+ event_write.add_argument(
208
+ "--slug", required=True, help="short slug for the filename: lowercase alphanumerics and hyphens, no dots"
209
+ )
210
+ event_body = event_write.add_mutually_exclusive_group()
211
+ event_body.add_argument("--body", help="event body, inline")
212
+ event_body.add_argument("--body-file", help="path to the event body in Markdown, or '-' to read stdin")
213
+ event_write.add_argument("--source-ref", help="provenance id, e.g. the debrief stem the event was folded from")
214
+ event_write.add_argument(
215
+ "--related", help="cross-references: comma-separated or a JSON array (e.g. 'PR #43,issue #10')"
216
+ )
217
+ event_write.add_argument("--supersedes", help="path of the event this entry overrides")
218
+ # The kernel script's home flag; accepted, unadvertised, through v0.1.x.
219
+ event_write.add_argument("--oacp-dir", dest="home", metavar="PATH", help=argparse.SUPPRESS)
220
+ event_write.add_argument(
221
+ "--dry-run", action="store_true", help="validate and compose the record, print it, and write nothing"
222
+ )
223
+ event_write.add_argument("--json", dest="json_output", action="store_true", help="emit a machine-readable result")
192
224
  return parser
193
225
 
194
226
 
@@ -414,6 +446,85 @@ def _debrief_write(args: argparse.Namespace) -> int:
414
446
  return EXIT_OK
415
447
 
416
448
 
449
+ def _event_write(args: argparse.Namespace) -> int:
450
+ home = resolve_home(args.home).path
451
+ try:
452
+ body = _event_body(args)
453
+ except OSError as exc:
454
+ print(f"ERROR: cannot read body: {exc}", file=sys.stderr)
455
+ return EXIT_FAILED
456
+ except events.WriterError as exc:
457
+ print(f"ERROR: {exc}", file=sys.stderr)
458
+ return exc.code
459
+
460
+ try:
461
+ result = events.write_event(
462
+ home=home,
463
+ agent=args.agent,
464
+ project=args.project,
465
+ event_type=args.event_type,
466
+ slug=args.slug,
467
+ body=body,
468
+ source_ref=args.source_ref,
469
+ related=events.normalize_related(args.related) if args.related else None,
470
+ supersedes=args.supersedes,
471
+ dry_run=args.dry_run,
472
+ )
473
+ except events.WriterError as exc:
474
+ print(f"ERROR: {exc}", file=sys.stderr)
475
+ return exc.code
476
+ except OSError as exc:
477
+ print(f"ERROR: publication failed: {exc}", file=sys.stderr)
478
+ return 2
479
+
480
+ if args.json_output:
481
+ print(
482
+ json.dumps(
483
+ {
484
+ "path": str(result.path),
485
+ "status": result.status,
486
+ "created_at_utc": result.created_at_utc,
487
+ "date": result.created_at_utc[:10],
488
+ "agent": args.agent,
489
+ "project": args.project,
490
+ "type": args.event_type,
491
+ },
492
+ indent=2,
493
+ )
494
+ )
495
+ else:
496
+ print(f"{result.status}: {result.path}")
497
+ print(f"created_at_utc: {result.created_at_utc}")
498
+
499
+ if args.dry_run:
500
+ print("--- record preview (nothing was written) ---", file=sys.stderr)
501
+ sys.stderr.flush()
502
+ sys.stderr.buffer.write(result.record)
503
+ sys.stderr.buffer.flush()
504
+ return EXIT_OK
505
+
506
+
507
+ def _event_body(args: argparse.Namespace) -> bytes:
508
+ """The body bytes: ``--body-file`` (a path, or ``-`` for stdin), ``--body``, else piped stdin.
509
+
510
+ Trailing newlines are dropped, as the kernel script drops them; the record
511
+ closes the body with exactly one. A body file is read as text, so its
512
+ CRLF and CR line endings become LF (:func:`events.body_from_file`); stdin
513
+ and the inline body are taken as given, both as the script does.
514
+ """
515
+ if args.body_file is not None and args.body_file != "-":
516
+ return events.body_from_file(Path(args.body_file).expanduser().read_bytes())
517
+ if args.body_file is not None:
518
+ raw = sys.stdin.buffer.read()
519
+ elif args.body is not None:
520
+ raw = args.body.encode("utf-8")
521
+ elif not sys.stdin.isatty():
522
+ raw = sys.stdin.buffer.read()
523
+ else:
524
+ raise events.WriterError("no body provided: use --body, --body-file <path|->, or pipe the body to stdin", 1)
525
+ return raw.rstrip(b"\n")
526
+
527
+
417
528
  def _report(outcome: sync.Outcome) -> int:
418
529
  if outcome.lines:
419
530
  print("\n".join(outcome.lines), file=sys.stdout if outcome.ok else sys.stderr)
@@ -444,15 +444,7 @@ def check_memory_sync(
444
444
  state = None
445
445
 
446
446
  if state is not None:
447
- if state.dirty:
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")
447
+ _add_working_tree(cat, home, runner, state)
456
448
 
457
449
  text = f"sync state — {sync_state_text(state)}"
458
450
  if not state.has_remote:
@@ -534,6 +526,40 @@ def check_memory_sync(
534
526
  return cat
535
527
 
536
528
 
529
+ def _add_working_tree(cat: Category, home: Path, runner: Optional[GitRunner], state: GitState) -> None:
530
+ """The tree row, scoped like the counts above it.
531
+
532
+ Memory changes inside the allowlist are what ``push`` would publish and
533
+ read DIRTY. Changes anywhere else are named as outside the allowlist: not
534
+ memory content, but ``pull`` still refuses a tree that carries them.
535
+ """
536
+ if not state.dirty:
537
+ cat.add("working-tree", Severity.ok, "working tree — clean")
538
+ return
539
+ try:
540
+ changed = sync.changed_paths(home, runner)
541
+ except sync.SyncError as exc:
542
+ cat.add("working-tree", Severity.warn, f"working tree — DIRTY; the change readout failed: {exc}")
543
+ return
544
+ memory = [path for path in changed if layout.is_allowed_memory_path(path)]
545
+ elsewhere = [path for path in changed if not layout.is_allowed_memory_path(path)]
546
+ if memory:
547
+ text = f"working tree — DIRTY {len(memory)} memory change(s): {_summarize(memory)}"
548
+ if elsewhere:
549
+ text += f"; {len(elsewhere)} change(s) outside the memory allowlist"
550
+ cat.add("working-tree", Severity.warn, text, "Run `agent-memory push` or resolve changes manually")
551
+ elif elsewhere:
552
+ cat.add(
553
+ "working-tree",
554
+ Severity.warn,
555
+ f"working tree — {len(elsewhere)} change(s) outside the memory allowlist: {_summarize(elsewhere)}; "
556
+ "no memory change is pending, but memory pull refuses a dirty tree",
557
+ "Commit, stash or ignore them outside the memory tiers",
558
+ )
559
+ else:
560
+ cat.add("working-tree", Severity.ok, "working tree — clean")
561
+
562
+
537
563
  def enclosing_repository(home: Path, runner: Optional[GitRunner] = None) -> Optional[Path]:
538
564
  """The worktree root when ``home`` sits inside a repository that is not its own; ``None`` when it is the root."""
539
565
  root = sync.worktree_root(home, runner)