Skip to content

feat: add argit drift machine-readable manifest-drift report (#25) - #28

Merged
k9ert merged 2 commits into
mainfrom
kn/argit-drift-command
Jun 9, 2026
Merged

feat: add argit drift machine-readable manifest-drift report (#25)#28
k9ert merged 2 commits into
mainfrom
kn/argit-drift-command

Conversation

@k9ert

@k9ert k9ert commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #25.

Problem

Drift was only surfaced as human = manifest drift: … lines from setup._handle_drift — same = prefix as unrelated no-ops ("manifest already present", "pass init already satisfied"), so automation can't reliably parse it. The --no-upgrade-manifest rev-lock pattern (#21) exists so automation controls upgrade timing, but the upgrade signal was plain-text-only.

Fix: argit drift

A read-only subcommand — the single machine-parseable channel — reporting the same hash-only classification setup uses (reuses _classify_drift / _bundled_manifest_path, so the two never disagree; no load_manifest, so a grammar-incompatible manifest still classifies).

$ argit drift --json
{"schema":"argit.drift/v1","agent_type":"openclaw","state":"stale_bundle","repo_revision":1,"bundled_revision":3,"revisions_behind":2,"upgrade_available":true}
  • stateclean | stale_bundle | operator_modified | no_manifest; agent-type mismatch raises ArgitError.
  • Always exits 0 for any classified state — drift is queryable, not a failure; only genuine errors exit non-zero.
  • Human mode mirrors setup's existing = manifest drift: … phrasing.
  • Flags: --json, --agent-type.

Also

  • doctor: informational manifest drift row via the shared core — never flips doctor's exit code (operator_modified is a note, not a failure).
  • README: argit drift section + documents the previously-undocumented --no-upgrade-manifest flag.
  • Version bump 1.9.0 → 1.10.0; AGENTS.md layout updated.

Test

  • 8 new unit tests (tests/unit/test_drift_command.py) covering all four states + agent-type-mismatch raise + JSON/human/exit-0; full unit suite green.
  • Smoke-tested all four states through the real CLI + the doctor row.

🤖 Generated with Claude Code

Drift was only surfaced as human `= manifest drift: …` lines from
setup's _handle_drift — same prefix as unrelated no-ops, brittle to
parse. Add a read-only `argit drift` reporting the same hash-only
classification (reuses _classify_drift / _bundled_manifest_path) as a
stable JSON object for fleet drift monitoring.

- drift.py: collect_drift() core (clean|stale_bundle|operator_modified|
  no_manifest; agent-type mismatch raises). human_summary mirrors
  setup's phrasing. run_drift always exits 0 — drift is queryable, not
  a failure.
- cli.py: `argit drift [--json] [--agent-type]`.
- doctor.py: informational `manifest drift` row via the shared core
  (never flips doctor's exit code).
- README: drift section + --no-upgrade-manifest companion note.
- bump 1.9.0 → 1.10.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@blink-claw-bot blink-claw-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH

  • argit drift --json can report a stale manifest family as clean, which defeats the new machine-readable drift monitor. collect_drift() takes the current best-fit bundled manifest at lines 47-48, but then delegates state entirely to _classify_drift() at line 73. That classifier returns clean when the repo manifest matches the latest revision within its own agent_version family, not when it matches the selected bundled manifest. As a result, a repo containing openclaw-2026.4.14-7.manifest.json while the current bundled best-fit is openclaw-2026.4.26-2.manifest.json emits:
{"manifest_file":"openclaw-2026.4.14-7.manifest.json","state":"clean","repo_revision":2,"bundled_revision":2,"revisions_behind":0,"upgrade_available":false}

That is both semantically wrong (repo_revision is taken from the bundled manifest at lines 77-79) and operationally dangerous because automation would miss exactly the pinned-vs-bundled gap the README says this command exposes. Please compare the repo manifest's matched type/version/revision against the selected bundled type/version/revision, and add a regression test for an older manifest-version family that is latest within its own family but stale relative to the current bundled selection.

MEDIUM

  • No additional findings.

LOW

  • No additional findings.

Tests run:

PYTHONPATH=src python3 -m pytest tests/unit/test_drift_command.py -q
PYTHONPATH=src python3 -m pytest tests/unit/test_drift_classifier.py tests/unit/test_drift_warning.py tests/unit/test_upgrade_flow.py tests/unit/test_doctor.py -q

Full PYTHONPATH=src python3 -m pytest -q was attempted, but this environment's integration subprocesses failed before exercising argit because they could not import click.

🤖 Review by GPT-5 via Blink-Claw-Bot

… family

Blink-Claw-Bot (GPT-5) HIGH: collect_drift delegated clean/stale to
setup._classify_drift, which returns `clean` when the repo manifest is
the latest revision within its OWN agent_version family — even if the
selected bundled best-fit has moved to a newer family. A repo pinned to
openclaw-2026.4.14-7 with bundled openclaw-2026.4.26-2 was reported
clean (and repo_revision was wrongly taken from the bundled manifest),
hiding exactly the pinned-vs-bundled gap this command exists to expose.

- collect_drift now hashes the repo manifest against the SELECTED bundled
  manifest (clean) and reverse-looks-up the catalog for its true
  (version, revision); stale across families is reported with
  revisions_behind=null (rev numbers reset per family) + upgrade_available.
- payload gains repo_agent_version / bundled_manifest_file /
  bundled_agent_version so cross-family gaps are legible.
- human + doctor lines print both filenames for cross-family stale.
- regression tests incl. non-mocked openclaw-2026.4.14-7 vs real bundled.
- README: document selected-bundled semantics + branch on upgrade_available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@blink-claw-bot blink-claw-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH

  • No blocking findings. The follow-up commit fixes the stale-family classification bug I raised: argit drift now compares the repo manifest against the selected bundled manifest, reports the repo manifest's true version/revision, and keeps revisions_behind null across version families where revision numbers are not comparable.

MEDIUM

  • No additional findings.

LOW

  • No additional findings.

Tests run:

PYTHONPATH=src python3 -m pytest tests/unit/test_drift_command.py tests/unit/test_drift_classifier.py tests/unit/test_drift_warning.py tests/unit/test_upgrade_flow.py tests/unit/test_doctor.py -q

Result: 46 passed.

🤖 Review by GPT-5 via Blink-Claw-Bot

@k9ert

k9ert commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed in 53e010a (HIGH).

You were right — collect_drift delegated clean/stale to setup._classify_drift, whose clean means "latest revision within the repo manifest's own agent_version family", not "matches the selected bundled manifest". A repo pinned to openclaw-2026.4.14-7 with bundled best-fit openclaw-2026.4.26-2 was reported clean, and repo_revision was wrongly sourced from the bundled manifest.

collect_drift now:

  • hashes the repo manifest against the selected bundled manifest → clean;
  • otherwise reverse-looks-up the catalog to recover the repo manifest's true (agent_version, revision)stale_bundle (across families too) or operator_modified;
  • reports revisions_behind only within one family (rev numbers reset per family) and null across families — upgrade_available is the family-agnostic boolean;
  • payload now carries repo_agent_version / bundled_manifest_file / bundled_agent_version so cross-family gaps are legible.

Regression coverage added, including a non-mocked test staging the shipped openclaw-2026.4.14-7 (latest within its family) and asserting state=stale_bundle, repo_revision=7. Verified against the real CLI:

{"state":"stale_bundle","manifest_file":"openclaw-2026.4.14-7.manifest.json","repo_revision":7,"bundled_manifest_file":"openclaw-2026.4.26-2.manifest.json","bundled_revision":2,"revisions_behind":null,"upgrade_available":true}

@k9ert
k9ert merged commit 4f836f7 into main Jun 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drift notices from _handle_drift are plain-text stdout, not machine-parseable

3 participants