Skip to content

feat(agent-sync): genie update converges every detected coding agent - #2541

Merged
namastex888 merged 6 commits into
devfrom
wish/agent-sync
Jul 10, 2026
Merged

namastex888 merged 6 commits into
devfrom
wish/agent-sync

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

  • genie update is now the single canonical updater: an internal agent-sync phase converges every DETECTED coding agent (Claude Code, Codex, Hermes) on every invocation — including the already-at-latest short-circuit. No new command or flag; the internal env GENIE_UPDATE_SYNC_ONLY=1 is the only re-entry contract (post-swap re-exec of the fresh binary + the CC SessionStart hook trigger both use it).
  • Engine (src/lib/agent-sync.ts): manifest-managed dirs (.genie-sync.json, dir-level digest), auto-adopt-with-backup to ~/.genie/state-backups/, managed-orphan removal, staged-rename atomicity, 31 tmpdir-isolated tests. Adapters: claude (skills + council.js stamp), codex (~/.codex/skills/.curated/), hermes (~/.hermes/plugins/genie symlink + enable).
  • CC SessionStart hook now DELEGATES to genie update (6h throttle) with a CLI-less stamp fallback via resolveStampInputs (stable ~/.genie/plugins/genie root — kills the stale-cache downgrade ping-pong); the diverged scripts/smart-install.js fork is deleted.
  • genie doctor gains per-agent freshness; genie uninstall removes only manifest/signature/target-verified assets; docs (plugin README, CLAUDE.md gotchas).
  • Ride-along fix: /council workflow template coerces string args (the Workflow runtime stringifies saved-workflow input — found in live QA).

Reviews

Wish .genie/wishes/agent-sync/WISH.md — plan approved via /plan; per-group execution reviews SHIP (G1 after 1 fix loop killing a HIGH data-loss vector: user <name>.old dirs were destroyed by staging pre-clean, now collision-proof suffixes); final execution review SHIP. bun run check: 806 pass / 1 skip / 0 fail.

Test plan

  • Gates g1/g2/g3 (fail-hard scripts under .genie/wishes/agent-sync/validate/)
  • Full bun run check incl. council-workflow lint
  • Post-release live ritual (user-gated): genie update ×2 on the reference machine → verify ~/.claude/skills + workflows/council.js, ~/.codex/skills/.curated/, ~/.hermes/plugins/genie; then a real /council run

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f7a6624c-861c-4b6f-b5d1-9902eaf83f28

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/agent-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@namastex888
namastex888 merged commit 08b3890 into dev Jul 10, 2026
11 checks passed

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements the agent-sync feature, which converges the genie skill set and the /council workflow stamp across all detected coding agents (Claude Code, Codex, and Hermes) using genie update as the canonical updater. It introduces the core sync engine, integrates triggers into the install and update commands, delegates the SessionStart hook to the CLI, and updates the doctor and uninstall commands to handle synced assets. The review feedback highlights three important improvement opportunities in src/lib/agent-sync.ts: normalizing relative paths to forward slashes in computeDirDigest to prevent platform-dependent digest mismatches, wrapping individual orphan processing in a try-catch block within removeManagedOrphans to ensure resilience against single-file failures, and using directory junctions on Windows in ensureHermesLink to avoid privilege and compatibility issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/lib/agent-sync.ts
): void {
for (const entry of readdirSync(current, { withFileTypes: true })) {
const abs = join(current, entry.name);
const rel = relative(root, abs);

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.

medium

On Windows, relative(root, abs) returns paths with backslashes (\\), whereas on POSIX systems it returns forward slashes (/). This makes the computed directory digest platform-dependent, which can lead to false-positive cache mismatches or 'stale' warnings when manifests are shared or verified across different operating systems. Normalizing the relative path to use forward slashes ensures platform-independent digests.

Suggested change
const rel = relative(root, abs);
const rel = relative(root, abs).replace(/\\/g, '/');

Comment thread src/lib/agent-sync.ts
Comment on lines +354 to +376
function removeManagedOrphans(
ctx: RunContext,
agent: string,
targetParent: string,
sourceNames: Set<string>,
report: AgentReport,
): void {
for (const entry of readdirSync(targetParent, { withFileTypes: true })) {
if (entry.name.endsWith(STAGING_SUFFIX) || entry.name.endsWith(PREV_SUFFIX)) continue;
const dir = join(targetParent, entry.name);
if (classifyEntry(dir, entry) !== 'dir' || sourceNames.has(entry.name)) continue;
const manifest = readManifest(dir);
if (manifest === null) continue;
if (computeDirDigest(dir) === manifest.digest) {
ctx.backupInto(agent, entry.name, dir);
rmSync(dir, { recursive: true, force: true });
report.skills.push({ name: entry.name, action: 'removed' });
} else {
report.skills.push({ name: entry.name, action: 'kept-modified-orphan' });
report.advisories.push(`kept modified orphan ${entry.name} (${agent})`);
}
}
}

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.

medium

If reading the manifest, computing the digest, or deleting/backing up an orphan directory fails (e.g., due to file locks, permission issues, or corrupt files), the entire removeManagedOrphans function will throw and abort. This would prevent other orphans from being cleaned up and abort subsequent sync steps for the agent. Wrapping the individual orphan processing inside the loop in a try-catch block ensures that a failure with one orphan doesn't disrupt the rest of the cleanup process.

function removeManagedOrphans(
  ctx: RunContext,
  agent: string,
  targetParent: string,
  sourceNames: Set<string>,
  report: AgentReport,
): void {
  for (const entry of readdirSync(targetParent, { withFileTypes: true })) {
    if (entry.name.endsWith(STAGING_SUFFIX) || entry.name.endsWith(PREV_SUFFIX)) continue;
    const dir = join(targetParent, entry.name);
    if (classifyEntry(dir, entry) !== 'dir' || sourceNames.has(entry.name)) continue;
    try {
      const manifest = readManifest(dir);
      if (manifest === null) continue;
      if (computeDirDigest(dir) === manifest.digest) {
        ctx.backupInto(agent, entry.name, dir);
        rmSync(dir, { recursive: true, force: true });
        report.skills.push({ name: entry.name, action: 'removed' });
      } else {
        report.skills.push({ name: entry.name, action: 'kept-modified-orphan' });
        report.advisories.push(`kept modified orphan ${entry.name} (${agent})`);
      }
    } catch (err) {
      report.advisories.push(`orphan removal ${entry.name} (${agent}) failed: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
}

Comment thread src/lib/agent-sync.ts
Comment on lines +464 to +484
function ensureHermesLink(
ctx: RunContext,
linkPath: string,
hermesRoot: string,
backupName: string,
report: AgentReport,
): LinkAction {
mkdirSync(dirname(linkPath), { recursive: true });
const stat = lstatSafe(linkPath);
if (stat === null) {
symlinkSync(hermesRoot, linkPath);
report.extras.push({ kind: 'symlink', action: 'created', detail: `${linkPath} -> ${hermesRoot}` });
return 'created';
}
if (stat.isSymbolicLink()) return reconcileExistingSymlink(linkPath, hermesRoot, report);
const backup = ctx.backupInto('hermes', backupName, linkPath);
rmSync(linkPath, { recursive: true, force: true });
symlinkSync(hermesRoot, linkPath);
report.extras.push({ kind: 'symlink', action: 'adopted', detail: `real dir backed up to ${backup}` });
return 'adopted';
}

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.

medium

On Windows, symlinkSync requires a type argument (such as 'dir' or 'junction') when creating a symlink to a directory. If not specified, it may default to a file symlink or fail. Furthermore, creating directory symlinks on Windows typically requires administrator privileges unless Developer Mode is enabled, whereas directory junctions ('junction') do not require special privileges. Since hermesRoot is a directory and we are resolving absolute paths, we should use 'junction' on Windows to ensure maximum compatibility and avoid privilege issues.

function ensureHermesLink(
  ctx: RunContext,
  linkPath: string,
  hermesRoot: string,
  backupName: string,
  report: AgentReport,
): LinkAction {
  mkdirSync(dirname(linkPath), { recursive: true });
  const stat = lstatSafe(linkPath);
  const symlinkType = process.platform === 'win32' ? 'junction' : undefined;
  if (stat === null) {
    symlinkSync(hermesRoot, linkPath, symlinkType);
    report.extras.push({ kind: 'symlink', action: 'created', detail: `${linkPath} -> ${hermesRoot}` });
    return 'created';
  }
  if (stat.isSymbolicLink()) return reconcileExistingSymlink(linkPath, hermesRoot, report);
  const backup = ctx.backupInto('hermes', backupName, linkPath);
  rmSync(linkPath, { recursive: true, force: true });
  symlinkSync(hermesRoot, linkPath, symlinkType);
  report.extras.push({ kind: 'symlink', action: 'adopted', detail: `real dir backed up to ${backup}` });
  return 'adopted';
}

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46cc3fb7ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

collectManagedSkillDirs(join(claudeDir, 'skills'), 'claude', out);
collectManagedSkillDirs(join(codexDir, 'skills', '.curated'), 'codex', out);
collectStampedCouncil(claudeDir, out);
collectHermesLink(hermesHome, genieHome, out);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove Hermes profile links during uninstall

When an active Hermes profile exists, the new sync path writes a managed symlink at ~/.hermes/profiles/<active>/plugins/genie (see ensureStickyProfileLink), but this collection only checks ~/.hermes/plugins/genie. Running genie uninstall then removes ~/.genie while leaving the profile-level symlink behind, so that profile retains a broken/stale Genie plugin entry. Include the profile links that resolve into genieHome in the uninstall asset scan.

Useful? React with 👍 / 👎.

function isStampedCouncil(councilPath: string): boolean {
try {
const content = readFileSync(councilPath, 'utf8');
return content.includes('const LENS_ROOT =') && /name:\s*'council'/.test(content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify council workflow ownership before unlinking

With a user-managed ~/.claude/workflows/council.js that happens to use the same workflow name and a const LENS_ROOT = ... constant (for example a customized copy pointing outside Genie), this predicate treats it as shipped by Genie and removeAgentSyncAssets unlinks it. Since the file has no manifest and lives outside ~/.genie, verify that the stamped root is one of Genie's managed roots (or otherwise mark generated files) before deleting it.

Useful? React with 👍 / 👎.

lirazsiri pushed a commit to lirazsiri/genie that referenced this pull request Jul 28, 2026
…ng INDEX entries

Full wish inventory (2026-07-26) found the INDEX.md ledger current but six
WISH.md Status headers stale, two wishes entirely absent from INDEX, and
one INDEX line (stable-release-security-gate) behind its own WISH header.

Headers reconciled (each keeps its history, gains a dated note):
- plugin-resource-shipping  DRAFT -> EXECUTED (PR automagik-si#2540 merged 07-10)
- codex-plugin-update-handoff  EXECUTED/'merge gate OPEN' -> SHIPPED (PR automagik-si#2617 merged 07-22)
- genie-ui  IN_PROGRESS -> SUPERSEDED (direction moved to genie-ui-dash 07-21)
- agent-sync-hardening  BLOCKED -> SUPERSEDED (gates closed with pr-2545 07-24, F02 excepted)
- agent-sync  EXECUTED -> DONE (merged automagik-si#2541, superseded by current safety contract)
- skills-fable5-revamp  EXECUTED -> DONE (merged automagik-si#2518, 3 LOW remain)

INDEX additions: v4-home-residue-doctor (DRAFT, now the natural vehicle for
issue automagik-si#2450 v4-residue findings) and codex-plugin-dogfood-remediation
(IN_PROGRESS 21/24 — the most active wish had no INDEX entry, invisible to
the jar<->INDEX drift lint). INDEX line for stable-release-security-gate
updated APPROVED -> IN_PROGRESS 4/6 to match its WISH header.
@automagik-genie
automagik-genie deleted the wish/agent-sync branch September 25, 2026 04:51
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.

1 participant