agsync 0.1.0__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.
- agsync-0.1.0/.github/ISSUE_TEMPLATE/rule-proposal.md +17 -0
- agsync-0.1.0/.github/workflows/ci.yml +38 -0
- agsync-0.1.0/.github/workflows/publish.yml +78 -0
- agsync-0.1.0/.gitignore +17 -0
- agsync-0.1.0/CODE_OF_CONDUCT.md +6 -0
- agsync-0.1.0/CONTRIBUTING.md +67 -0
- agsync-0.1.0/LICENSE +21 -0
- agsync-0.1.0/PKG-INFO +291 -0
- agsync-0.1.0/README.md +269 -0
- agsync-0.1.0/action.yml +42 -0
- agsync-0.1.0/docs/design.md +262 -0
- agsync-0.1.0/pyproject.toml +42 -0
- agsync-0.1.0/src/agsync/__init__.py +24 -0
- agsync-0.1.0/src/agsync/__main__.py +5 -0
- agsync-0.1.0/src/agsync/cli.py +211 -0
- agsync-0.1.0/src/agsync/engine.py +147 -0
- agsync-0.1.0/src/agsync/model.py +138 -0
- agsync-0.1.0/src/agsync/parser.py +310 -0
- agsync-0.1.0/src/agsync/replay.py +524 -0
- agsync-0.1.0/src/agsync/reporters.py +107 -0
- agsync-0.1.0/src/agsync/rules/__init__.py +57 -0
- agsync-0.1.0/src/agsync/rules/decisions.py +112 -0
- agsync-0.1.0/src/agsync/rules/links.py +113 -0
- agsync-0.1.0/src/agsync/rules/tasks.py +168 -0
- agsync-0.1.0/src/agsync/scaffold.py +180 -0
- agsync-0.1.0/tests/fixtures/decayed/AGENTS.md +9 -0
- agsync-0.1.0/tests/fixtures/decayed/memory/decisions.md +28 -0
- agsync-0.1.0/tests/fixtures/decayed/tasks/09-verify-staging-deploy.md +9 -0
- agsync-0.1.0/tests/fixtures/decayed/tasks/25-cache-invalidation.md +5 -0
- agsync-0.1.0/tests/fixtures/decayed/tasks/26-rate-limit-headers.md +6 -0
- agsync-0.1.0/tests/fixtures/decayed/tasks/README.md +14 -0
- agsync-0.1.0/tests/replay_repo.py +133 -0
- agsync-0.1.0/tests/test_agsync.py +420 -0
- agsync-0.1.0/tests/test_replay.py +254 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Rule proposal
|
|
3
|
+
about: Propose a new integrity check
|
|
4
|
+
labels: rule
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**What silently breaks today?**
|
|
8
|
+
Describe the failure as it appeared in a real repo, not in the abstract.
|
|
9
|
+
|
|
10
|
+
**Why can't a human catch it by reading?**
|
|
11
|
+
The best rules catch things that look fine to a careful reader.
|
|
12
|
+
|
|
13
|
+
**Minimal failing example**
|
|
14
|
+
The smallest memory files that reproduce it.
|
|
15
|
+
|
|
16
|
+
**Proposed severity**
|
|
17
|
+
`error` (the memory is now wrong) or `warn` (the memory is fragile).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: ruff check src tests
|
|
23
|
+
- run: pytest -q
|
|
24
|
+
|
|
25
|
+
scaffold:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: pip install -e .
|
|
33
|
+
# A rule that fires on a structure agsync generated itself is a false
|
|
34
|
+
# positive by definition, so `init` output must always lint clean. This
|
|
35
|
+
# repository does not carry its own ledger to check instead: that is
|
|
36
|
+
# generated output, not source.
|
|
37
|
+
- run: agsync init "$RUNNER_TEMP/scaffold"
|
|
38
|
+
- run: agsync check "$RUNNER_TEMP/scaffold" --format github
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
version:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
# The tag names the release; pyproject names what actually gets uploaded.
|
|
16
|
+
# Nothing else ties them together, so a tag cut against the wrong commit
|
|
17
|
+
# publishes a version the tag does not describe — and PyPI never lets that
|
|
18
|
+
# version be replaced. Fails in seconds, in parallel with the suite.
|
|
19
|
+
- name: Tag must match the packaged version
|
|
20
|
+
env:
|
|
21
|
+
TAG: ${{ github.event.release.tag_name }}
|
|
22
|
+
run: |
|
|
23
|
+
packaged=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
|
|
24
|
+
echo "tag=$TAG packaged=$packaged"
|
|
25
|
+
if [ "$TAG" != "$packaged" ] && [ "$TAG" != "v$packaged" ]; then
|
|
26
|
+
echo "::error::release tag $TAG does not match pyproject version $packaged"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
strategy:
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
# The ends of the supported range from pyproject's requires-python.
|
|
36
|
+
# A release that breaks on either end should never reach PyPI.
|
|
37
|
+
python-version: ["3.11", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.python-version }}
|
|
43
|
+
- run: pip install -e ".[dev]"
|
|
44
|
+
- run: ruff check src tests
|
|
45
|
+
- run: pytest -q
|
|
46
|
+
|
|
47
|
+
build:
|
|
48
|
+
needs: [test, version]
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
- uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: "3.12"
|
|
55
|
+
- run: python -m pip install --upgrade build
|
|
56
|
+
- run: python -m build
|
|
57
|
+
- uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
publish:
|
|
63
|
+
needs: build
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
# Trusted Publishing: PyPI mints a short-lived token from this job's OIDC
|
|
66
|
+
# identity, which is why no API token appears anywhere in this file. The
|
|
67
|
+
# identity PyPI checks is (repository, workflow filename, environment), so
|
|
68
|
+
# renaming this file or the environment breaks publishing until the
|
|
69
|
+
# trusted publisher on PyPI is updated to match.
|
|
70
|
+
environment: pypi
|
|
71
|
+
permissions:
|
|
72
|
+
id-token: write
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/download-artifact@v4
|
|
75
|
+
with:
|
|
76
|
+
name: dist
|
|
77
|
+
path: dist/
|
|
78
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
agsync-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.ruff_cache/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# This repository's own agent memory. It is `agsync init` output kept locally:
|
|
12
|
+
# generated files do not belong in the repository that generates them. Paths are
|
|
13
|
+
# root-anchored so the deliberately-broken fixtures under tests/ stay tracked.
|
|
14
|
+
/AGENTS.md
|
|
15
|
+
/.agsync.toml
|
|
16
|
+
/memory/
|
|
17
|
+
/tasks/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
4
|
+
|
|
5
|
+
Report unacceptable behavior by opening a confidential issue or contacting the
|
|
6
|
+
maintainer directly. Reports are handled privately.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
git clone https://github.com/gabrielft-me/agsync
|
|
5
|
+
cd agsync
|
|
6
|
+
pip install -e ".[dev]"
|
|
7
|
+
pytest -q
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
No dependencies in the runtime package. Ever. This runs inside git hooks on
|
|
11
|
+
other people's machines, and a virtualenv surprise there is a broken commit for
|
|
12
|
+
someone who never asked for our opinions about packaging.
|
|
13
|
+
|
|
14
|
+
## Before you start
|
|
15
|
+
|
|
16
|
+
[docs/design.md](docs/design.md) explains why the architecture is the way it is
|
|
17
|
+
— why the parser normalizes instead of demanding, why severity is configuration,
|
|
18
|
+
why fingerprints hash structured fields, why replay clones instead of checking
|
|
19
|
+
out, and why intent is never inferred from a diff. Worth reading if a change
|
|
20
|
+
starts to feel like it is fighting the codebase.
|
|
21
|
+
|
|
22
|
+
## Adding a rule
|
|
23
|
+
|
|
24
|
+
A rule is one function. Start here — it's the easiest contribution and the most
|
|
25
|
+
valuable one.
|
|
26
|
+
|
|
27
|
+
1. Write it in `src/agsync/rules/` (decisions, tasks, or links):
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
@rule("no-undated-decisions", ERROR, "One line: what silently breaks.")
|
|
31
|
+
def no_undated_decisions(memory):
|
|
32
|
+
for decision in memory.decisions.values():
|
|
33
|
+
if "date" not in decision.fields:
|
|
34
|
+
yield Finding("no-undated-decisions", decision.path,
|
|
35
|
+
decision.line, f"{decision.id} has no date")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Add the failing case to `tests/fixtures/decayed/` and a test asserting the
|
|
39
|
+
rule fires with a useful message.
|
|
40
|
+
3. Add a row to the rules table in `README.md`.
|
|
41
|
+
|
|
42
|
+
### What makes a good rule
|
|
43
|
+
|
|
44
|
+
- **It catches something invisible to a careful reader.** A duplicate `D-021`
|
|
45
|
+
fifty lines apart is the archetype.
|
|
46
|
+
- **It has zero false positives on a healthy repo.** Run it against
|
|
47
|
+
`agsync init` output; that must stay clean.
|
|
48
|
+
- **The message says what broke and why it matters**, not just which check
|
|
49
|
+
failed. Compare: `"duplicate ID"` versus `"D-021 is redefined here (first
|
|
50
|
+
defined at line 7); 9 reference(s) are now ambiguous"`.
|
|
51
|
+
- **Default to `warn` if it encodes a style opinion**, `error` only if the
|
|
52
|
+
memory is now factually wrong.
|
|
53
|
+
|
|
54
|
+
## Parser changes
|
|
55
|
+
|
|
56
|
+
The parser normalizes; it never demands. If a real repo uses a format we don't
|
|
57
|
+
read, that's a parser bug, not a user error. Add the shape to
|
|
58
|
+
`tests/fixtures/` and make the parser absorb it.
|
|
59
|
+
|
|
60
|
+
Never make a rule reach into raw markdown. If a rule needs something the model
|
|
61
|
+
doesn't carry, add the field to the model. The reasoning is in
|
|
62
|
+
[docs/design.md](docs/design.md).
|
|
63
|
+
|
|
64
|
+
## Style
|
|
65
|
+
|
|
66
|
+
`ruff check src tests` must pass. Comments explain *why*, not *what* — the code
|
|
67
|
+
already says what.
|
agsync-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Fagundes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
agsync-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agsync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ESLint for your agents' memory — catch decision-log rot before it reaches a new session.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gabrielft-me/agsync
|
|
6
|
+
Project-URL: Issues, https://github.com/gabrielft-me/agsync/issues
|
|
7
|
+
Author: Gabriel Fagundes
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents-md,ai-agents,decision-log,git-hooks,linter,llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# agsync
|
|
24
|
+
|
|
25
|
+
**ESLint for your agents' memory — because the decision log your AI agents write is already lying to them.**
|
|
26
|
+
|
|
27
|
+
[](https://github.com/gabrielft-me/agsync/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/agsync/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
$ agsync check
|
|
33
|
+
AGENTS.md
|
|
34
|
+
6 error protocol-files-exist boot protocol requires 'memory/goal.md', which does not
|
|
35
|
+
exist — this step is unsatisfiable and every session skips it
|
|
36
|
+
memory/decisions.md
|
|
37
|
+
11 error unique-decision-ids D-021 is redefined here (first defined at line 7);
|
|
38
|
+
7 reference(s) to D-021 are now ambiguous
|
|
39
|
+
88 error relations-resolve D-027 supersedes D-019, which is not defined
|
|
40
|
+
tasks/25-search-indexing.md
|
|
41
|
+
3 warn status-not-qualified status is qualified with free text ('(all tiers except the
|
|
42
|
+
edge cache)'); keep the status machine-readable
|
|
43
|
+
tasks/README.md
|
|
44
|
+
- error index-matches-files task 26 exists (tasks/26-webhook-retries.md) but is absent
|
|
45
|
+
from the index — invisible to anyone reading it
|
|
46
|
+
14 error index-matches-files task 25: index says 'todo', file says 'done'
|
|
47
|
+
|
|
48
|
+
27 decisions, 26 tasks, 25 index rows
|
|
49
|
+
9 error(s), 4 warning(s)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## The problem
|
|
55
|
+
|
|
56
|
+
You gave your agents a memory. `AGENTS.md` with a boot protocol, a numbered
|
|
57
|
+
decision log, a task queue. Every session starts by reading it. It is the only
|
|
58
|
+
thing standing between a new agent and a cold start.
|
|
59
|
+
|
|
60
|
+
**Nothing checks that it is true.**
|
|
61
|
+
|
|
62
|
+
Markdown never fails. A decision ID reused for a second, unrelated decision. A
|
|
63
|
+
boot step pointing at a file nobody ever wrote. A status of `done (stub)`. An
|
|
64
|
+
index row that disagrees with the task file it names. Every one of these parses,
|
|
65
|
+
renders, and reviews perfectly well, and every one is read as ground truth by the
|
|
66
|
+
next agent that boots.
|
|
67
|
+
|
|
68
|
+
Four things make the decay invisible, and they compound:
|
|
69
|
+
|
|
70
|
+
- **The reference graph is prose.** `D-021` is a bare token, not a link. Nothing
|
|
71
|
+
resolves it, so nothing notices when a second entry claims the same ID and
|
|
72
|
+
every existing reference to it silently becomes ambiguous.
|
|
73
|
+
- **Contradictions live in separate files.** The index says `todo`, the task file
|
|
74
|
+
says `done`. The protocol says "read this", and the file was never created. No
|
|
75
|
+
single diff contains both halves, so no review can catch it.
|
|
76
|
+
- **Every writer has a partial view.** Each session is a different agent, and
|
|
77
|
+
each edit is locally reasonable. Rot is what locally reasonable edits add up to
|
|
78
|
+
when nothing holds the whole.
|
|
79
|
+
- **A false memory looks exactly like a true one.** No crash, no red test. The
|
|
80
|
+
agent reads a confident, well-formatted, wrong statement and acts on it.
|
|
81
|
+
|
|
82
|
+
The failure is not hypothetical and it is not slow. A repo written by several
|
|
83
|
+
agents over a couple of days will already contain several of these.
|
|
84
|
+
|
|
85
|
+
> A memory system whose integrity depends on being maintained by hand degrades
|
|
86
|
+
> at exactly the speed you use it.
|
|
87
|
+
|
|
88
|
+
The design is not the problem — `AGENTS.md` plus a decision log is the right
|
|
89
|
+
shape. The problem is that nothing enforces it.
|
|
90
|
+
|
|
91
|
+
## The fix is not a smarter agent
|
|
92
|
+
|
|
93
|
+
Agents self-report badly. They tell you what they *intended* to do, not what
|
|
94
|
+
they did, and every harness writes in a different dialect. "Be more careful" does
|
|
95
|
+
not survive a new agent joining.
|
|
96
|
+
|
|
97
|
+
`git push` is a choke point nobody can skip. Put the validation there.
|
|
98
|
+
|
|
99
|
+
## Install
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uvx agsync check # try it, no install
|
|
103
|
+
pipx install agsync # keep it
|
|
104
|
+
pip install agsync
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Python 3.11+. **Zero dependencies** — the whole thing is stdlib, so it runs in a
|
|
108
|
+
git hook without a virtualenv surprise.
|
|
109
|
+
|
|
110
|
+
## Use
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
agsync check # lint the repo
|
|
114
|
+
agsync check --warn-only # first run: see everything, fail nothing
|
|
115
|
+
agsync check --baseline # accept today's mess, fail on new mess
|
|
116
|
+
agsync check --no-baseline # what an agent runs at boot: everything, unsuppressed
|
|
117
|
+
agsync check --format github # inline annotations on the PR diff
|
|
118
|
+
agsync check --format json # for your own tooling
|
|
119
|
+
agsync replay <repo> # how many past pushes the gate would have stopped
|
|
120
|
+
agsync rules # what it checks and why
|
|
121
|
+
agsync init # scaffold a memory structure from scratch
|
|
122
|
+
agsync install-hooks # gate commits locally
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Adopting an existing repo
|
|
126
|
+
|
|
127
|
+
Do not turn everything on at once — a linter that rejects most of your pushes on
|
|
128
|
+
day one gets uninstalled on day one.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
agsync check --warn-only # 1. look at the damage
|
|
132
|
+
agsync check --baseline # 2. freeze it; new violations now fail
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The baseline records a fingerprint per violation that survives line shifts, so
|
|
136
|
+
unrelated edits above a known problem don't resurrect it as "new". Delete
|
|
137
|
+
entries from `.agsync-baseline.json` as you fix them.
|
|
138
|
+
|
|
139
|
+
**The baseline governs the gate, and never the reader.** It records what should
|
|
140
|
+
stop a push — a decision about your rollout, not a statement about what is true.
|
|
141
|
+
An agent about to act on this memory needs the whole picture, including the parts
|
|
142
|
+
you chose not to block, so the boot protocol runs:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
agsync check --no-baseline
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Suppressing a finding is how you keep working. Believing it was suppressed
|
|
149
|
+
because it was fixed is how a new session acts on something false.
|
|
150
|
+
|
|
151
|
+
### Replay: how bad is it already?
|
|
152
|
+
|
|
153
|
+
`agsync replay` runs the same check engine at every commit in a repository's
|
|
154
|
+
history and counts the pushes the gate would have stopped. Point it at a repo
|
|
155
|
+
that has been running on hand-maintained memory for a while, and it will tell you
|
|
156
|
+
when each violation started and how long it has been sitting there.
|
|
157
|
+
|
|
158
|
+
```console
|
|
159
|
+
$ agsync replay . --ref main --first-seen
|
|
160
|
+
sha date errors rules
|
|
161
|
+
3ad9e51 2026-03-02 0 —
|
|
162
|
+
...
|
|
163
|
+
b7e2d13 2026-03-14 4 index-matches-files, links-resolve, unique-decision-ids
|
|
164
|
+
|
|
165
|
+
18 of 42 pushes would have been rejected
|
|
166
|
+
|
|
167
|
+
rule first failed survived
|
|
168
|
+
protocol-files-exist 9f1c0a4 2026-03-04 31 commits, 9 days (still failing at HEAD)
|
|
169
|
+
unique-decision-ids c40b8f2 2026-03-09 14 commits, 4 days (still failing at HEAD)
|
|
170
|
+
index-matches-files c40b8f2 2026-03-09 14 commits, 4 days (still failing at HEAD)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The second table is the uncomfortable one. A broken boot step usually dates from
|
|
174
|
+
the commit that introduced the protocol and is usually still broken at HEAD,
|
|
175
|
+
because nothing has ever checked it.
|
|
176
|
+
|
|
177
|
+
Replay clones the target to a temp directory and moves HEAD only inside that
|
|
178
|
+
clone, so it cannot leave your repository detached at an old commit. It takes a
|
|
179
|
+
path or a clone URL, exits 0 unless the run itself fails — it reports on history,
|
|
180
|
+
it does not gate it — and ignores any baseline, because the question is what the
|
|
181
|
+
rules would have caught, not what someone had already told the gate to overlook.
|
|
182
|
+
|
|
183
|
+
One caveat worth stating out loud: replay judges old commits by today's rules.
|
|
184
|
+
The claim is "these would be rejected now", not "these were rejected then".
|
|
185
|
+
|
|
186
|
+
## Rules
|
|
187
|
+
|
|
188
|
+
| Rule | Default | Catches |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `unique-decision-ids` | error | Two ledger entries sharing an ID, making every reference ambiguous |
|
|
191
|
+
| `decision-refs-resolve` | error | A `D-xxx` cited anywhere that the ledger never defines |
|
|
192
|
+
| `relations-resolve` | error | `(supersedes D-019)` where `D-019` doesn't exist |
|
|
193
|
+
| `no-self-reference` | error | A decision superseding itself |
|
|
194
|
+
| `decision-has-date` | error | An entry with no date, so the ledger can't be ordered |
|
|
195
|
+
| `protocol-files-exist` | error | A boot instruction pointing at a file that isn't there |
|
|
196
|
+
| `links-resolve` | error | Any relative markdown link with no target |
|
|
197
|
+
| `status-in-enum` | error | A status outside `todo/in-progress/done/superseded` |
|
|
198
|
+
| `status-not-qualified` | warn | `done (stub)` — not machine-readable |
|
|
199
|
+
| `index-matches-files` | error | Index and task files disagreeing, or a task missing entirely |
|
|
200
|
+
| `task-refs-resolve` | error | A dependency on a task that doesn't exist |
|
|
201
|
+
| `no-dependency-cycles` | error | Tasks that can never be executed in any order |
|
|
202
|
+
| `blocked-task-not-done` | warn | A task marked done while its dependency is still open |
|
|
203
|
+
| `no-orphan-memory-files` | warn | A memory file nothing references, so no agent reads it |
|
|
204
|
+
|
|
205
|
+
## Configuration
|
|
206
|
+
|
|
207
|
+
`.agsync.toml`. Severity is configuration; rule logic is not.
|
|
208
|
+
|
|
209
|
+
```toml
|
|
210
|
+
exclude = ["archive/"]
|
|
211
|
+
|
|
212
|
+
[rules]
|
|
213
|
+
status-not-qualified = "warn"
|
|
214
|
+
decision-has-date = "off"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Also readable from `[tool.agsync]` in `pyproject.toml`.
|
|
218
|
+
|
|
219
|
+
## CI
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
- uses: gabrielft-me/agsync@v1
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or directly:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
- run: pipx install agsync && agsync check --format github
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Findings render inline on the pull request diff. Pair it with branch protection
|
|
232
|
+
and the memory can't rot through a merge.
|
|
233
|
+
|
|
234
|
+
## How it works
|
|
235
|
+
|
|
236
|
+
**Normalize, don't demand.** Real memory repos are written by several agents over
|
|
237
|
+
weeks and the format drifts: field sets vary between entries, values wrap across
|
|
238
|
+
lines, index tables use different column orders in the same file. The parser
|
|
239
|
+
absorbs all of it into a graph. Rules only ever see the graph.
|
|
240
|
+
|
|
241
|
+
**Rules are tiny and isolated.** Each is a function taking the parsed memory and
|
|
242
|
+
yielding findings. No shared state, no ordering, no knowledge of each other.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from agsync.rules import rule
|
|
246
|
+
from agsync.model import ERROR, Finding
|
|
247
|
+
|
|
248
|
+
@rule("no-undated-tasks", ERROR, "A task file carries no date.")
|
|
249
|
+
def no_undated_tasks(memory):
|
|
250
|
+
for task in memory.tasks.values():
|
|
251
|
+
if "date" not in task.status_raw:
|
|
252
|
+
yield Finding("no-undated-tasks", task.path, 1, "no date")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**One report contract.** Every finding is `{rule, path, line, message, severity}`.
|
|
256
|
+
Text output, JSON, and GitHub annotations all derive from it, so a new output
|
|
257
|
+
format never touches a rule.
|
|
258
|
+
|
|
259
|
+
## Roadmap
|
|
260
|
+
|
|
261
|
+
- [x] Linter, config, baseline, JSON/GitHub output
|
|
262
|
+
- [x] `init` scaffold, local hooks with chaining
|
|
263
|
+
- [x] `replay`: measure the decay across a repository's whole history
|
|
264
|
+
- [ ] `--fix`: regenerate the task index; interactive ID renumbering
|
|
265
|
+
- [ ] YAML front matter schema — make the D-ID ↔ task graph structured instead of prose
|
|
266
|
+
- [ ] Harness adapters (Claude Code, Cursor) writing commit trailers at commit time
|
|
267
|
+
- [ ] `agsync draft`: turn captured trailers into a ledger entry, BYO key, opened as a PR
|
|
268
|
+
- [ ] Single static binary
|
|
269
|
+
|
|
270
|
+
### What this will never be
|
|
271
|
+
|
|
272
|
+
**An intent-inference engine.** A `post-receive` hook fires *after* the push —
|
|
273
|
+
the agent's turn is over and its context is gone. All the server sees is a diff
|
|
274
|
+
and a message, strictly less than the agent had. The reason a project pivoted
|
|
275
|
+
from one approach to another is not in the diff and no model recovers it from
|
|
276
|
+
there.
|
|
277
|
+
|
|
278
|
+
So: **capture at commit time, enforce at push time.** When the ledger entry
|
|
279
|
+
lands, the model formats a *why* that a human or agent already stated. It never
|
|
280
|
+
invents one. If nobody supplied a reason, the right outcome is a rejected push —
|
|
281
|
+
not a plausible-looking fabrication. A ledger that looks trustworthy and isn't is
|
|
282
|
+
worse than no ledger.
|
|
283
|
+
|
|
284
|
+
## Contributing
|
|
285
|
+
|
|
286
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). New rules are welcome and are the easiest
|
|
287
|
+
place to start — one function, one test, one row in the table above.
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT.
|