Skip to content

feat(agent-yaml): YAML schema + parse/write library (wish dir-sync-frontmatter-refresh group 1) - #1191

Merged
automagik-genie merged 3 commits into
devfrom
feat/dir-sync-frontmatter-refresh-group1
Apr 18, 2026
Merged

automagik-genie merged 3 commits into
devfrom
feat/dir-sync-frontmatter-refresh-group1

Conversation

@automagik-genie

Copy link
Copy Markdown
Contributor

Wave 1 / Group 1 of wish `dir-sync-frontmatter-refresh` — the foundation every downstream group imports from. No behavior changes in this PR; only introduces the shared primitive.

Summary

  • New `src/lib/agent-yaml.ts` — single owner of the `agent.yaml` schema + IO.
    • `AgentConfigSchema` (Zod) mirrors `DirectoryEntry` field-for-field, `.strict()` at every object level. Unknown top-level keys (including out-of-scope `skill` and `extraArgs` which belong to the `agent_templates` PG row) are rejected with a field-named error.
    • `parseAgentYaml(path)` — read → js-yaml → Zod. Descriptive errors on any failure.
    • `writeAgentYaml(path, config)` — strips derived fields (`name`/`dir`/`registeredAt`), serializes, writes atomically (`${path}.tmp` + `rename`) under `acquireLock`.
    • `extractFrontmatterFromAgentsMd(content)` — pure helper, byte-for-byte body preservation.
  • New `src/lib/lockfile.ts` — promoted the PID-based `acquireLock`/`releaseLock` helpers out of `claude-native-teams.ts` into a shared module. Importer change only; zero behavior change (claude-native-teams test suite stays green).
  • New `src/lib/agent-yaml.test.ts` — 25 cases pinning every acceptance criterion from the wish: round-trip (populated + empty), strict schema (unknown keys, out-of-scope `skill`/`extraArgs`), nested field-name correctness (`permissions.bashAllowPatterns`, not `bashAllow`), frontmatter-splitter purity (CRLF, trailing newlines, Unicode), 2- and 8-way concurrent `writeAgentYaml` fidelity, malformed YAML / missing file / top-level array / empty file error paths.

Diff

File Delta
`src/lib/agent-yaml.ts` +432 (new)
`src/lib/agent-yaml.test.ts` +362 (new, 25 cases)
`src/lib/lockfile.ts` +104 (new)
`src/lib/claude-native-teams.ts` −70 (imports from lockfile.ts now)

No caller updates in this PR

By design: no caller is rewired to consume `agent-yaml`. Groups 2-6 of the wish do the migration + `dir sync`/`edit`/`add` refactors on top of this foundation, each as its own PR.

Test plan

  • `bun test src/lib/agent-yaml.test.ts src/lib/claude-native-teams.test.ts` → 67 pass, 0 fail
  • `bun run typecheck` clean
  • `bunx biome check` clean on touched files
  • Pre-push full suite: 2659 pass, 0 fail
  • CI Quality Gate green
  • External GitGuardian green (no secrets in diff)

Wish authority trail

  • Wish: `.genie/wishes/dir-sync-frontmatter-refresh/WISH.md` (WRS 100, design in `.genie/brainstorms/dir-sync-frontmatter-refresh/DESIGN.md`)
  • Parent problem: `genie dir sync` reports "Unchanged" on agents whose AGENTS.md frontmatter changed, silently dropping the edit on the floor. This wish moves every agent-config field into a dedicated `agents//agent.yaml`; downstream groups migrate + rewire callers.

🤖 Generated with Claude Code

Genie (env-specialist) added 3 commits April 18, 2026 14:01
…ile.ts

The PID-based lockfile helpers were private to claude-native-teams.ts and
unusable by any other module that needs safe concurrent writes. Promote them
to a shared module with named exports — preserves the existing semantics
(5s timeout, 50ms poll with jitter, stale-PID detection via kill(pid, 0),
force-acquire with warning after timeout) so claude-native-teams.test.ts
passes unchanged.

Prepares the ground for agent-yaml.ts (next commit) which needs the same
lock semantics for atomic writes to agent.yaml.
Ships the Wave-1 module that every downstream group in the
dir-sync-frontmatter-refresh wish imports from:

- `AgentConfigSchema` — Zod schema mirroring DirectoryEntry field-for-field
  with `.strict()` at every object level. Unknown top-level keys (including
  out-of-scope `skill` and `extraArgs` that belong to the agent_templates
  PG row, not the yaml file) fail with a field-named error.
- `parseAgentYaml(path)` — reads, parses via js-yaml, validates via Zod,
  surfaces a descriptive Error on any failure.
- `writeAgentYaml(path, config)` — strips derived fields (name/dir/
  registeredAt) before serialization, writes atomically (`${path}.tmp` +
  rename) under `acquireLock` so concurrent writers and readers never
  observe a partial/spliced file.
- `extractFrontmatterFromAgentsMd(content)` — pure helper that splits the
  `---\n...\n---\n` fence off the top of an AGENTS.md string, preserving
  the body byte-for-byte (CRLF, trailing newlines, Unicode).

No caller is updated in this commit — Groups 2+ consume the module.
25 cases pinning every acceptance criterion from the wish:

- Round-trip: fully-populated config + empty config both survive
  writeAgentYaml → parseAgentYaml untouched; derived fields are stripped
  from the on-disk YAML and absent on re-parse.
- Strict schema / scope guard: unknown top-level keys (`bogus`, `skill`,
  `extraArgs`) are rejected with the key named in the error.
- Nested: `permissions.bashAllowPatterns` parses; `permissions.bashAllow`
  (wrong name) throws an unknown-field error pinned to `permissions`.
- `extractFrontmatterFromAgentsMd` purity: CRLF preserved, trailing newlines
  preserved, Unicode preserved, missing/unclosed fence returns the full body,
  call is pure (no mutation).
- Concurrency: two and eight parallel `writeAgentYaml` calls on the same
  path always leave the file parseable as EXACTLY one of the inputs —
  never a splice.
- Malformed YAML / missing file / top-level array / empty file all
  produce clear errors (or an empty config for the empty-file case).
@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: c14e0124-a21f-409b-9e0a-e0ac647d9bd1

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-group1

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 introduces a new agent-yaml module for robust agent.yaml schema validation, parsing, and atomic writing, alongside a shared lockfile utility extracted from existing logic. Comprehensive unit tests are added to verify round-trip consistency, frontmatter extraction, and concurrent write safety. Feedback identifies a high-severity issue in the PID liveness check where permission errors are incorrectly handled, which could lead to premature lock deletion. Additionally, the reviewer suggests improving the robustness of the lock acquisition logic by treating corrupt or empty lock files as stale rather than polling until timeout.

Comment thread src/lib/lockfile.ts
Comment on lines +39 to +46
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}

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 isPidAlive function incorrectly returns false if process.kill(pid, 0) throws an error with code EPERM. An EPERM error indicates that the process exists but the current user does not have permission to signal it. In this case, the process is alive, and the lock should not be considered stale. Deleting a lock held by an active process can lead to race conditions and file corruption.

Suggested change
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (err) {
return err instanceof Error && (err as any).code === 'EPERM';
}
}

Comment thread src/lib/lockfile.ts
Comment on lines +71 to +73
const holderPid = Number.parseInt(content.trim(), 10);
if (!Number.isNaN(holderPid) && !isPidAlive(holderPid)) {
// Holder is dead — remove stale lock and retry immediately

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 the lock file is empty or contains non-numeric data, parseInt returns NaN. The current logic skips the stale lock check in this case, causing the process to poll for the full LOCK_TIMEOUT_MS before force-acquiring. It is more robust to treat a corrupt or empty lock file as stale and attempt to clean it up immediately.

Suggested change
const holderPid = Number.parseInt(content.trim(), 10);
if (!Number.isNaN(holderPid) && !isPidAlive(holderPid)) {
// Holder is dead — remove stale lock and retry immediately
const holderPid = Number.parseInt(content.trim(), 10);
if (Number.isNaN(holderPid) || !isPidAlive(holderPid)) {
// Holder is dead or lock file is corrupt — remove stale lock and retry immediately

@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: ae4b9110cd

ℹ️ 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/lib/agent-yaml.ts
Comment on lines +413 to +415
if (after.startsWith('---')) {
frontmatter = '';
afterClose = after.slice(3);

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 Validate close fence before empty-frontmatter fast path

extractFrontmatterFromAgentsMd currently treats any after.startsWith('---') as a valid empty frontmatter close, so content like ---\n---not-a-fence\n# body is misparsed as empty frontmatter and a truncated body instead of frontmatter: null. That mutates byte content for malformed or non-frontmatter files and can silently lose text when callers later rewrite AGENTS.md; require the close marker to be an exact fence line (--- followed by newline or EOF) before taking this branch.

Useful? React with 👍 / 👎.

Comment thread src/lib/lockfile.ts
Comment on lines +92 to +94
// Timeout — force acquire (likely stale lock)
console.warn(`[lockfile] Force-acquiring stale lock: ${lock}`);
await writeFile(lock, String(process.pid));

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 overwrite a live lock when timeout expires

acquireLock force-acquires with writeFile(lock, ...) after 5 seconds even if the existing lock holder is still alive, which allows multiple writers into the critical section simultaneously. In writeAgentYaml that means concurrent processes can race on the same ${path}.tmp and rename/unlink out from under each other, producing lost or corrupted writes on slow filesystems; timeout handling should fail or only break verified-stale locks, not clobber a live one.

Useful? React with 👍 / 👎.

@automagik-genie
automagik-genie merged commit 5f247e4 into dev Apr 18, 2026
6 checks passed
automagik-genie pushed a commit that referenced this pull request Apr 18, 2026
…ation

Wave 2 / Group 2 of wish dir-sync-frontmatter-refresh. Depends on Group 1
(#1191, already on dev) which shipped the agent-yaml schema + IO library.

- `migrateAgentToYaml(agentDir, dbRow?)` is idempotent: a second call on
  an already-migrated agent returns `{ migrated: false, reason:
  'already-migrated' }` and makes zero disk writes.
- Extracts frontmatter via `extractFrontmatterFromAgentsMd`, parses it
  as YAML, strips derived/DB-only fields, merges with `dbRow` (frontmatter
  wins on conflict — source-of-truth principle), validates the result
  via `AgentConfigSchema`, writes `agent.yaml` atomically, saves an
  `AGENTS.md.bak` byte-for-byte, and rewrites `AGENTS.md` with the
  post-frontmatter body only.
- Malformed frontmatter throws a descriptive error BEFORE any disk write,
  so no partial state is ever left behind.
- The YAML_ALLOWED_KEYS set is derived from AgentConfigSchema's shape at
  module-load time, so this file never drifts from Group 1's source of
  truth.

Tests: 14 cases pinning every acceptance criterion — idempotency, body
preservation (plain / CRLF / Unicode), .bak byte-equality, dbRow fills
missing fields, frontmatter wins on conflict, DB-only fields (skill,
extra_args, id) never reach agent.yaml, nested permissions survive from
dbRow, no-frontmatter path, malformed-frontmatter error leaves files
untouched, empty frontmatter mapping parses as valid, return-shape
invariants.

No caller is rewired here. Groups 3+ consume the migrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@automagik-genie
automagik-genie deleted the feat/dir-sync-frontmatter-refresh-group1 branch September 25, 2026 04:49
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