Skip to content

feat(dir-edit): write agent.yaml first, then sync (wish dir-sync-frontmatter-refresh group 4) - #1197

Merged
automagik-genie merged 1 commit into
devfrom
feat/dir-sync-frontmatter-refresh-group4
Apr 18, 2026
Merged

automagik-genie merged 1 commit into
devfrom
feat/dir-sync-frontmatter-refresh-group4

Conversation

@automagik-genie

Copy link
Copy Markdown
Contributor

Wave 3 / Group 4 of wish `dir-sync-frontmatter-refresh`. Depends on Groups 1-3 (#1191 / #1193 / #1196 — all on dev).

Behavior change

`genie dir edit` now honors the "files are source of truth" principle from the wish: every flag writes to `agents//agent.yaml` FIRST, then triggers a single-agent sync to propagate into PG. No more direct `agent_templates` / `agents` writes from the edit handler.

Flow:

  1. Resolve the agent → locate its on-disk dir.
  2. Migrate via `migrateAgentToYaml` if `agent.yaml` doesn't yet exist (covers agents that pre-date the migration).
  3. `parseAgentYaml` → apply the flag updates → `writeAgentYaml` (atomic, lock-protected).
  4. `syncSingleAgentByName` propagates the yaml into the DB via the G3 sync path.
  5. Print refreshed entry.

If not in a workspace: yaml is still written; warning printed advising manual `genie dir sync`.

What's new

  • `src/term-commands/dir.ts` — `handleEdit` rewritten to the yaml-first flow. New imports: `migrateAgentToYaml`, `parseAgentYaml`, `writeAgentYaml`, `syncSingleAgentByName`, `join`, `existsSync`.
  • `src/lib/agent-sync.ts` — `syncSingleAgentByName` is now exported (was private; used by the new edit flow and still by the file watcher).
  • `src/lib/agent-directory.ts` — `syncFrontmatterToDisk` deleted. Its job was to mirror DB edits back into AGENTS.md frontmatter — but post-migration AGENTS.md is body-only, so the function was strictly obsolete. `FRONTMATTER_KEYS` constant + associated imports (`writeFrontmatter`, `serializeSdkConfig`) cleaned up. Knip confirmed zero callers before removal.

Tests

New: `src/term-commands/dir-edit.test.ts` — 7 integration tests pinning every acceptance criterion:

  • `--model opus` lands in yaml AND in the DB projection
  • `--allow Read,Glob,Grep` serializes to nested `permissions.allow` array
  • `--permission-preset read-only` serializes to `permissions.preset`
  • `--sdk-permission-mode auto` serializes to `sdk.permissionMode`
  • yaml write precedes DB write (file on disk is correct even with sync skipped)
  • 8-way concurrent `writeAgentYaml` calls leave a parseable complete config (never a splice — the lockfile holds)
  • combined flags all land in a single yaml write

Scope-guard check

```
grep 'UPDATE agent_templates' src/term-commands/dir.ts → zero hits
```

Test plan

  • `bun test src/term-commands/dir-edit.test.ts` → 7 pass, 0 fail
  • `bun run typecheck` clean
  • `bunx knip` clean (no unused exports — `syncFrontmatterToDisk` removal was captured)
  • `bunx biome check` modulo pre-existing complexity warning on `syncSingleAgent` carried forward from G3
  • Pre-push full suite: 2698 pass, 0 fail
  • CI Quality Gate green
  • External GitGuardian green

What does NOT change

  • `genie dir add` still uses the legacy flow → Group 5.
  • `genie doctor` has no divergence warning yet → Group 6.
  • `team` is still not propagated by `dir edit` through to the DB (pre-existing gap; not in scope).

🤖 Generated with Claude Code

…tmatter-refresh group 4)

Wave 3 / Group 4. Makes `genie dir edit` honor the "files are source of
truth" principle established in Groups 1-3. Depends on G1/G2/G3, all
already on dev.

Behavior change:

- `dir edit` now writes every flag into `agents/<name>/agent.yaml`
  FIRST, then triggers a single-agent sync so the DB picks up the new
  values via the refactored sync path from G3.
- No direct `agent_templates` / `agents` writes from the edit handler
  anymore. The yaml file is authoritative; the DB is a projection.
- If the agent hasn't been migrated yet (no `agent.yaml` on disk),
  `handleEdit` triggers migration first so the file exists before the
  write+sync cycle.
- The legacy `directory.syncFrontmatterToDisk` call is gone — there's
  no frontmatter to sync back to post-migration (AGENTS.md is
  body-only).

Code surface:

- `src/term-commands/dir.ts` — rewritten `handleEdit` per the flow
  above. New imports: `migrateAgentToYaml`, `parseAgentYaml`,
  `writeAgentYaml`, `syncSingleAgentByName`.
- `src/lib/agent-sync.ts` — export `syncSingleAgentByName` (was
  private). Used by the new edit flow. Watcher/file-sync call sites
  unchanged.

Tests (`src/term-commands/dir-edit.test.ts` — new, 7 cases):

- `--model` lands in yaml and DB
- `--allow <tools>` → nested `permissions.allow` array in yaml
- `--permission-preset` → `permissions.preset` in yaml
- `--sdk-permission-mode` → `sdk.permissionMode` in yaml
- yaml write precedes DB write (file on disk is correct even if sync
  were skipped)
- 8-way concurrent `writeAgentYaml` calls leave a parseable, complete
  config (never a truncation)
- combined flags all land in a single write

Wish scope-guard verified: `grep 'UPDATE agent_templates'
src/term-commands/dir.ts` returns zero hits.

Validation: 7/7 new tests pass, typecheck clean, biome clean modulo
the same pre-existing complexity warning on `syncSingleAgent` carried
forward from G3. Pre-push full suite: see CI on this PR.

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

coderabbitai Bot commented Apr 18, 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: fde3a6f3-9fd4-44bf-8a63-af6b479805e2

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 feat/dir-sync-frontmatter-refresh-group4

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 and usage tips.

@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 refactors the dir edit command to follow a 'file-first' synchronization flow, where updates are written to an agent.yaml file before being synchronized to the database. It includes the removal of the syncFrontmatterToDisk function, updates to syncSingleAgentByName to support this flow, and a new integration test suite. Feedback highlights that the --dir and --repo flags may now be non-functional or inconsistent, and that the new logic introduces a regression for global agents edited outside of a workspace. Additionally, it is recommended to reuse the dbRowFromEntry helper to avoid manual object construction during migration.

Comment thread src/term-commands/dir.ts
Comment on lines 234 to 235
if (options.dir) updates.dir = resolvePath(options.dir);
if (options.repo) updates.repo = resolvePath(options.repo);

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.

high

The --dir and --repo flags are now effectively non-functional or inconsistent in this new flow:

  1. --dir: Since dir is a derived field stripped from agent.yaml (per agent-yaml.ts), and the code writes to the existing agentDir (line 269), providing a new path via --dir will not update the agent's location in the database or move the files.
  2. --repo: While this is saved to agent.yaml, the sync process (specifically readYamlAsConfigFields in agent-sync.ts) does not currently read the repo field from YAML, instead overwriting the database entry with values derived from the git remote during syncSingleAgentByName.

If these flags are no longer intended to be editable via this command, they should be removed or the implementation should be adjusted to handle them (e.g., as a move operation for --dir).

Comment thread src/term-commands/dir.ts
Comment on lines +294 to +300
const ws = findWorkspace();
if (ws) {
await syncSingleAgentByName(ws.root, name);
} else {
// Not in a genie workspace — skip sync, but the yaml write stands.
console.warn('Not in a genie workspace — agent.yaml updated on disk; run `genie dir sync` manually to propagate.');
}

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.

high

This logic introduces a regression for "global" agents that are not located within a workspace. Previously, directory.edit updated the database directly regardless of the workspace context. In the new flow, if findWorkspace() returns null, the sync step is skipped. While the agent.yaml is updated on disk, the database (which is used for agent resolution at runtime) remains stale, meaning the edit will have no effect for these agents until a manual sync is performed—which itself requires a workspace.

Comment thread src/term-commands/dir.ts
Comment on lines +271 to +283
const dbRow = {
team: resolved.entry.team,
model: resolved.entry.model,
description: resolved.entry.description,
color: resolved.entry.color,
provider: resolved.entry.provider,
promptMode: resolved.entry.promptMode,
permissions: resolved.entry.permissions,
disallowedTools: resolved.entry.disallowedTools,
omniScopes: resolved.entry.omniScopes,
hooks: resolved.entry.hooks,
sdk: resolved.entry.sdk as unknown as AgentConfig['sdk'],
};

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

This block manually constructs a dbRow for migration, which duplicates the logic already present in the dbRowFromEntry helper function in src/lib/agent-sync.ts. To improve maintainability and ensure consistency, consider exporting dbRowFromEntry and reusing it here. If importing this helper creates a circular dependency cycle, use dynamic imports (require() or import()) to break it at module load time.

References
  1. Use dynamic imports (require() or import()) to break circular dependency cycles that would otherwise occur at module load time.

@automagik-genie
automagik-genie merged commit ad34e3c into dev Apr 18, 2026
6 checks passed

@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: 7660369cc9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/term-commands/dir.ts
Comment on lines +287 to +289
// Read the current yaml, apply the updates, write atomically via the lock.
const current = await parseAgentYaml(yamlPath);
const next: AgentConfig = { ...current, ...updates };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard parseAgentYaml when migration skips no-frontmatter

When agent.yaml is missing, this path always calls migrateAgentToYaml(...) and then immediately parses agent.yaml, but migrateAgentToYaml returns reason: 'no-frontmatter' without creating the file for agents that have body-only AGENTS.md. In that valid legacy/minimal state, parseAgentYaml throws ENOENT and genie dir edit fails instead of updating the agent, which is a regression from the previous DB-edit flow.

Useful? React with 👍 / 👎.

Comment thread src/term-commands/dir.ts
Comment on lines +294 to +296
const ws = findWorkspace();
if (ws) {
await syncSingleAgentByName(ws.root, name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve --repo edits instead of overwriting on sync

After writing YAML, the handler always calls syncSingleAgentByName, whose sync path computes repo from git/dir discovery (repoPath) rather than from agent.yaml fields; as a result, a --repo change is immediately overwritten in DB projection and users see a successful edit that did not actually apply. This makes genie dir edit --repo ... a no-op in normal workspace runs.

Useful? React with 👍 / 👎.

Comment thread src/term-commands/dir.ts
Comment on lines +289 to +290
const next: AgentConfig = { ...current, ...updates };
await writeAgentYaml(yamlPath, next);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not silently drop --dir updates in yaml-first edit

next includes updates.dir, but writeAgentYaml strips derived keys like dir before serialization, so the new directory path is never persisted to disk; then sync re-derives dir from discovery and leaves the stored entry unchanged. Keeping --dir enabled with this flow reports success while discarding the user’s requested path change.

Useful? React with 👍 / 👎.

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