Skip to content

feat: add zcode agent preset and hook installer - #2166

Open
Small-Sagittarius wants to merge 1 commit into
git-ai-project:mainfrom
Small-Sagittarius:feat/zcode-preset
Open

feat: add zcode agent preset and hook installer#2166
Small-Sagittarius wants to merge 1 commit into
git-ai-project:mainfrom
Small-Sagittarius:feat/zcode-preset

Conversation

@Small-Sagittarius

@Small-Sagittarius Small-Sagittarius commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Add first-class support for ZCode (a Claude Code-compatible CLI coding agent) to git-ai:

  • git-ai checkpoint zcode parses ZCode's hook payload. ZCode's hook protocol is identical to Claude Code's (same JSON shape, capitalized Write/Edit/Bash tool names, Claude-format transcript), so the new preset mirrors the existing Claude preset and attributes work to zcode instead of claude.
  • git ai install-hooks now supports ZCode via a new installer that writes the process-style checkpoint hook into ~/.zcode/cli/config.json (hooks.events.PreToolUse/PostToolUse, matcher Edit|Write|Bash, type: "process" with checkpoint args in the args array). It preserves existing top-level config keys (mcp, plugins, ...) and migrates an existing checkpoint claude hook to checkpoint zcode.

Changes

  • src/commands/checkpoint_agent/presets/zcode.rs — new ZcodePreset (mirrors claude.rs, agent id zcode) + unit tests
  • src/commands/checkpoint_agent/presets/mod.rs — register the zcode preset
  • src/mdm/agents/zcode.rs — new ZcodeInstaller (writes/updates/removes the ZCode hook config) + unit tests
  • src/mdm/agents/mod.rs — register ZcodeInstaller

Testing

  • Unit tests for the preset (parse events, session id derivation, skip guards) and the installer (fresh install, idempotency, claude->zcode migration, top-level key preservation, uninstall, hook status)
  • CI: Lint & Format (fmt/clippy/doc) green; Test core jobs green on ubuntu/windows/macos (all zcode tests pass); Ubuntu integration green

Notes

  • ZCode's hook config file is ~/.zcode/cli/config.json and uses a different hook schema than Claude Code (process type + args array), which is why a dedicated installer is needed.
  • Transcript/model extraction reuses the existing Claude JSONL stream format.

Open in Devin Review

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


pengzhou seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

devin-ai-integration[bot]

This comment was marked as resolved.

@Small-Sagittarius
Small-Sagittarius force-pushed the feat/zcode-preset branch 2 times, most recently from fa81850 to dd1a158 Compare August 18, 2026 15:36

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread src/commands/checkpoint_agent/presets/claude.rs
Comment thread src/mdm/agents/zcode.rs
Comment on lines +145 to +158
// Keep user hooks, drop any duplicate git-ai entries, then ensure
// exactly one git-ai zcode hook remains (migrates an existing
// claude-preset hook to zcode).
let mut hooks_array = blocks[block_idx]
.get("hooks")
.and_then(|h| h.as_array())
.cloned()
.unwrap_or_default();

hooks_array.retain(|hook| !Self::is_git_ai_process_hook(hook));
hooks_array.push(Self::desired_hook(params));

if let Some(block) = blocks[block_idx].as_object_mut() {
block.insert("hooks".to_string(), Value::Array(hooks_array));

@devin-ai-integration devin-ai-integration Bot Aug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Old git-ai hook can be left behind when it sits under a different matcher, causing duplicate checkpoints

When updating the ZCode hook config, existing git-ai checkpoint entries are only cleaned out of the block whose matcher exactly equals Edit|Write|Bash (hooks_array.retain(...) at src/mdm/agents/zcode.rs:148-155), so an older entry stored under any other matcher stays and a second entry is added alongside it.
Impact: A user whose ZCode config used a different tool matcher ends up with the same edit reported twice, producing duplicate/misattributed sessions.

Mechanism: matcher-scoped dedup vs. config-wide detection

install_hooks_at locates (or creates) the block with matcher == ZCODE_MATCHER (src/mdm/agents/zcode.rs:129-143) and only strips git-ai hooks from that block's hooks array before pushing the desired hook. A pre-existing git-ai hook under e.g. matcher "*" or "Write|Edit|Bash" is untouched, so both fire on every Write/Edit/Bash tool call — one as checkpoint claude, one as checkpoint zcode.

Note the asymmetry: hook_status (src/mdm/agents/zcode.rs:51-77) scans every block, so such a config is reported as installed/up-to-date, while uninstall_hooks_at (src/mdm/agents/zcode.rs:207-223) does remove git-ai hooks from every block. The Droid installer handles exactly this case by stripping git-ai from all non-target matcher blocks before inserting into the canonical block (src/mdm/agents/droid.rs:138-166).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@Small-Sagittarius
Small-Sagittarius force-pushed the feat/zcode-preset branch 6 times, most recently from 136fa22 to ad035e5 Compare August 18, 2026 17:07
devin-ai-integration[bot]

This comment was marked as resolved.

span: checkpoint preset + install-hooks
fid: none
level: none
test: pass
influence: 新增 zcode agent 一等公民支持:git-ai checkpoint zcode 解析 ZCode hook payload(协议与 Claude Code 一致,复用 ClaudeJsonl transcript),git ai install-hooks 将 process 型 checkpoint hook 写入 ~/.zcode/cli/config.json(保留 mcp/plugins 等既有配置)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +56 to +96
let commit = repo.stage_all_and_commit("Add AI lines").unwrap();

// Dump the checkpoint debug log to stderr so CI failures show exactly what
// the checkpoint command emitted (preset_name / event_count / requests).
let log_dir = repo
.test_home_path()
.join(".git-ai")
.join("internal")
.join("checkpoint-debug-logs");
if let Ok(entries) = fs::read_dir(&log_dir) {
for entry in entries.flatten() {
if let Ok(content) = fs::read_to_string(entry.path()) {
eprintln!(
"=== checkpoint debug log: {} ===\n{}",
entry.path().display(),
content
);
}
}
}

// We should have exactly one session record keyed by the zcode agent_id
assert_eq!(
commit.authorship_log.metadata.sessions.len(),
1,
"Expected a single session record"
);
let session_record = commit
.authorship_log
.metadata
.sessions
.values()
.next()
.expect("Session record should exist");

assert_eq!(session_record.agent_id.tool, "zcode");
// Model is extracted from the real transcript fixture copied in the second checkpoint
assert_eq!(
session_record.agent_id.model, "claude-sonnet-4-20250514",
"Session record model should come from the latest checkpoint's transcript"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 New end-to-end test never checks that AI lines are actually credited to the AI

The new end-to-end test commits twice (stage_all_and_commit("Add AI lines") at tests/integration/zcode.rs:56) but never asserts the per-line credit for the committed file, which the repository's mandatory testing rules require after every commit.
Impact: The test can pass even if the newly added agent credits its edits to a human or leaves them uncredited, so a real attribution regression would ship unnoticed.

AGENTS.md mandates line-level assertions after every commit

AGENTS.md states: "ALWAYS use the helper to assert the lines post-commit AND make sure to always assert line-level after EVERY commit for EVERY test you EVER right. This is CRUCIAL." and lists assert_committed_lines / assert_lines_and_blame as the helpers.

tests/integration/zcode.rs:8-97 performs repo.stage_all_and_commit("Initial commit") (tests/integration/zcode.rs:26) and repo.stage_all_and_commit("Add AI lines") (tests/integration/zcode.rs:56), then only asserts on commit.authorship_log.metadata.sessions (session count, agent_id.tool, agent_id.model). No assert_committed_lines/assert_lines_and_blame call exists anywhere in the file, so the actual AI-vs-human line attribution produced by the new zcode preset is never validated.

A fix would add repo.filename("src/main.rs").assert_committed_lines(...) after each commit, expecting "fn main() {}".unattributed_human() for the pre-existing line and .ai() for the two lines written between the checkpoints.

Prompt for agents
AGENTS.md requires that every integration test assert line-level attribution after EVERY commit using the TestRepo helpers (assert_committed_lines / assert_lines_and_blame). The new test test_zcode_e2e_prefers_latest_checkpoint_for_prompts in tests/integration/zcode.rs performs two commits ("Initial commit" and "Add AI lines") but only asserts on the authorship log's session metadata (session count, agent_id.tool, agent_id.model). It never verifies that the lines written between the zcode pre/post checkpoints are actually attributed to AI, which is the core behavior the new preset is supposed to deliver. Add line-level assertions after each commit: after "Initial commit", src/main.rs should have its single line as unattributed_human; after "Add AI lines", the two added comment lines should be attributed to AI. Use repo.filename("src/main.rs") and the lines! macro with .ai() / .unattributed_human() as described in AGENTS.md.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@Small-Sagittarius

Copy link
Copy Markdown
Author

All Devin Review findings have been addressed in the latest commit (a211c95):

  1. 🔴 Transcript ingestion — resolved: src/streams/agent.rs maps zcode to ClaudeAgent in get_agent (kept out of ALL_AGENT_TYPES so the Claude sweep does not mis-discover ~/.claude transcripts under the zcode name); ZCode transcripts are ingested incrementally from each checkpoint's stream path.
  2. 🟡 check_hooks detection — resolved: tool detection now mirrors the other installers (binary_exists("zcode") || ~/.zcode exists) so a fresh ZCode install gets hooks set up.
  3. 🟡 Preset duplication — resolved: parsing is extracted into parse_claude_like(hook_input, trace_id, tool, display_name) in claude.rs; ZcodePreset is now a thin wrapper.
  4. 🟡 Missing integration tests — resolved: added tests/integration/zcode.rs with a TestRepo-based E2E (checkpoint -> commit -> session record with agent_id.tool == "zcode" and model from transcript) plus preset-level tests.
  5. 🟡 (follow-up) Unexpected config shape — resolved: the installer now starts from an empty object when the config's top level or hooks value is not a JSON object, with a regression test.

Fork CI is green: Lint & Format (fmt/clippy/doc), Test core on ubuntu/windows/macos, and both Ubuntu integration shards.

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.

2 participants