feat(push): orphan-YAML gate — halt push when local files lack state entries#30
Merged
Conversation
…entries Catches the duplicate-creation patterns surfaced during the gitops-mudflap working session 2026-05-13: - Flow F: user renames a file locally (`mv foo.md bar.md`) then pushes. bar.md has no state entry → engine silently POSTed as a new resource, leaving the original foo's UUID orphaned on the dashboard. - Flow G: dashboard rename → pull writes a new <name>-<uuid8>.md file but leaves the old YAML on disk. Next push iterates the old YAML, POSTs it, creates a duplicate. - Flow M: `npm run apply` (pull → merge → push) compresses Flow G into one click, silently spawning duplicates. The gate is default-on. When it fires, push exits 1 with a verbose error that lists every orphan, pairs orphans with possible state-only rename sources by shared base-slug, and includes an AI-agent-addressed paragraph explicitly asking agents NOT to auto-pass --allow-new-files without confirming with the human. ANSI color when stderr is a TTY; plain text when piped/captured (CI, AI agent stdout capture). Override: --allow-new-files flag bypasses the gate with a one-line notice. Apply propagates the flag through `pushArgs`. Bootstrap mode (empty state) suppresses the gate — genuine first-time pushes are all "new". Selective push (`-- <paths>`) scopes the gate to the selection. Unrelated orphans elsewhere don't block. Reuses audit.ts's `checkOrphanYaml` detection via a shared `findOrphanResourceIds` helper exported from new-file-gate.ts — single source of truth for "what counts as an orphan YAML". Suite: 188/188 tests pass (21 new for this gate).
…y-run verb Addresses three code-review findings on the new-file-gate PR: 1. **M1 — .vapi-ignore now suppresses the gate** for files the customer has explicitly opted out of gitops tracking. Without this, the gate would halt every push for stale dashboard artifacts that customers keep around (silenced via .vapi-ignore) — defeating both the .vapi-ignore workaround and the gate's intent of catching REAL rename / orphan cases. Same `matchesIgnore` helper that audit.ts uses; injected via the gate's DI options for filesystem-free tests. 2. **M2 — `<org>` placeholder in relativePath replaced with real `VAPI_ENV`**. Previously the gate's message rendered `resources/<org>/...` literally. Now renders e.g. `resources/mudflap-prod/...`. Tests updated to use `[^/]+` regex class so they work regardless of the test env name. 3. **M2 — asymmetric `filter.endsWith(relativePath)` direction removed** from `pathMatchesAnyFilter`. The remaining checks (exact match, suffix match, resourceId-with-extension tail match) cover the real cases without over-matching when relativePath is short. Cleanups: - Dead UUID-length conditional in formatGateMessage (real UUIDs are always 36 chars, slice always fires) → unconditional slice. - Duplicate `(possible rename SOURCES):` header in both branches of the rename-sources block → print header once, then either '(none)' or entries. - Dry-run-aware verb in the bypass notice: "would create" vs "creating" based on DRY_RUN flag. New test: pin .vapi-ignore suppression as a regression guard against re-introducing the M1 bug. 189/189 tests pass.
2 tasks
dhruva-reddy
added a commit
that referenced
this pull request
May 13, 2026
…NTS (#31) Followup to PR #30 (orphan-YAML pre-flight gate). The gate landed in the engine but neither README.md nor AGENTS.md mentioned the new default-on behavior or the --allow-new-files override. Agents and human operators reading those docs would hit the gate unexpectedly. README.md: - Updated push command-table row to mention the gate + flag - New "Creating new resources after the first push (orphan-YAML gate)" section under Suggested Workflows, explaining the three cases (new / rename / cruft), the --allow-new-files override, and the AI-agent caveat (do NOT auto-pass without confirming) - Documents the automatic suppressions (--bootstrap, .vapi-ignore, selective-push scope) AGENTS.md: - Common commands table row for "Push with new resources" with the --allow-new-files flag + explicit AI-agent caveat - New "Orphan-YAML gate" subsection under "npm run push" explaining the gate behavior, override flag, AI-agent guidance, and suppression rules. Notes that the same gate fires inside `apply` and the flag propagates through `apply --allow-new-files` - New bash command-block lines for `npm run push --allow-new-files` and `npm run apply --allow-new-files` Docs-only PR. Skipping test-writer and code-reviewer per the always-apply rule for docs-only changes.
This was referenced May 15, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
npm run pushnow refuses to upload local YAML files that have no entry in the state file ("orphan YAMLs"). Without this gate, three distinct workflows have been silently spawning duplicate resources on customer dashboards:mv foo.md bar.md) and pushes.bar.mdhas no state entry → engine silently POSTed as a new resource, leaving the originalfooorphaned on the dashboard.<name>-<uuid8>.mdfile but leaves the old YAML on disk → next push iterates the old YAML and POSTs it as a duplicate.npm run apply(pull → merge → push) compresses Flow G into one click; the user sees no warning.The gate is the structural fix. When triggered, it halts the push and prints a verbose, AI-agent-addressed error message that forces the operator (and any AI in the loop) to explicitly classify each orphan.
The message (when the gate fires)
The "FOR AI AGENTS" paragraph is the load-bearing UX detail — it lives in the error message itself (not in docs) because the AI is going to read whatever the failing command prints. ANSI color codes are emitted only when stderr is a TTY; CI logs and AI agent stdout captures get plain text.
Behavior
--allow-new-filescreating N new resource(s)orwould create N…for--dry-run)--bootstrapflag-- <paths>) with orphan outside the selectionnpm run apply(pull → merge → push) with rename leftover from pull.vapi-ignoreSingle halt across all resource types
Gate runs ONCE for all 9 resource types (tools, structuredOutputs, assistants, squads, personalities, scenarios, simulations, simulationSuites, evals) before any apply phase begins. The operator sees one complete picture of all orphans, not 9 separate halts as types are processed sequentially.
Files changed
src/new-file-gate.tstests/new-file-gate.test.tssrc/audit.tsfindOrphanResourceIdspredicate. PubliccheckOrphanYamlsignature + finding shape unchangedsrc/push.tsmaybeBootstrapState, before apply phases. HonorsBOOTSTRAP_SYNC,APPLY_FILTER.filePaths,ALLOW_NEW_FILESsrc/config.ts--allow-new-filesflag +ALLOW_NEW_FILESexport, added to recognized-flags whitelistsrc/apply.ts--allow-new-filespropagates throughpushArgs(no functional change)Test plan
npm run build(tsc --noEmit) — cleannpm test— 189/189 pass (167 prior + 22 new)npx @biomejs/biome check --write— cleanCode review
Reviewed locally in this branch. Two non-blocking findings addressed in-branch:
.vapi-ignore'd files were tripping the gate. Fixed by passingmatchesIgnorethrough the detection. Added a regression test (detectOrphanYamls: files matched by .vapi-ignore are excluded).<org>placeholder inrelativePathreplaced with actualVAPI_ENV. Asymmetricfilter.endsWith(relativePath)direction removed from path matching (over-matched on short paths).Plus the dry-run-aware bypass message, deduplicated rename-sources header, and dead UUID-length conditional cleaned up.
Residual notes (non-blocking)
npm run audit)" hint as a future UX improvement.toolsmatching an orphan underassistants) are intentionally not surfaced — the signal would be too noisy.Related
npm run auditcommand, merged 2026-05-13)