Skip to content

feat: plan standalone tool and skill rows by sync scope - #939

Draft
ctawiah wants to merge 1 commit into
ctawiah/sync-target-flags-1e8afrom
ctawiah/plan-attachment-rows-1e8a
Draft

ctawiah wants to merge 1 commit into
ctawiah/sync-target-flags-1e8afrom
ctawiah/plan-attachment-rows-1e8a

Conversation

@ctawiah

@ctawiah ctawiah commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Context

ldcli sync prompts keeps local prompt files in step with the variations of LaunchDarkly AI Configs. A variation can use tools and skills, and it pins one version of each. Locally, a tool is a JSON file and a skill is a Markdown file. The code calls both attachments.

Today the planner plans only variations. It compares a tool or a skill with LaunchDarkly only as content inside the variations that use it. This causes two problems:

  • A tool or skill file that no variation uses is never planned.
  • Two variations that share a tool can undo an edit to that tool. The example below shows the steps.

This PR plans each tool and skill as a row of its own, limited by a scope. Later PRs apply those rows and add the sync all, sync tools, and sync skills commands.

What changes

  • Scope (prompts, tools, skills, or all) sets which kinds a sync plans. No command selects a scope yet, so sync prompts plans the same rows as before, except for the guard below.
  • planAttachments builds one row for each tool or skill that has a local file or a sync.lock row. sync.lock stores the state that a working copy last agreed on with LaunchDarkly. The rules are the same as for a variation.
  • Rows sort skill, then tool, then variation. The apply order follows the plan order, so a tool publishes before the variations that pin it.
  • A tool or skill row and the variations that change it form one conflict choice.
  • A new guard, checkAttachmentBaselines, makes a variation row a conflict when the row will overwrite a tool edit. This fixes the shared-tool case.
  • If LaunchDarkly deleted a tool that a local variation still uses, the delete_local row of the tool becomes an error. The exception is a plan that also pulls that variation without the reference.
  • A create_server row creates a resource in LaunchDarkly. If LaunchDarkly will reject its key, name, or description, the row becomes an error row, so the apply cannot fail halfway.
  • A missing tool without upsert gets a message that names both fixes. Target flag errors name the command of the scope, such as ldcli sync tools add.

The shared-tool case, as it works on main:

1. Edit tools/search.json.
2. Run "sync prompts add" for a second variation that uses search.
3. Run "sync prompts".

Step 3 plans:
  first variation    update_server   publishes the edit to LaunchDarkly
  second variation   update_local    writes the old tool content back over the edit

With this PR, the second row becomes a conflict, and one conflict choice covers both variations.

The message for a missing tool:

tool "refund" does not exist in LaunchDarkly. To create it during sync, add "upsert": true to its file. To stop syncing it, run ldcli sync tools detach --project demo --key refund.

Design notes

  • planAttachments reuses FingerprintAttachment and chooseAction. chooseAction and resolvedConflictAction now use hasLocal() and hasServer(). Without that change, a local conflict choice for a tool always becomes a create.
  • Scope sits on SyncAction next to the Filter from feat: add target flags to sync prompts and read sync flags only from the command line #938, so the sync after attach or link stays a prompts sync.
  • local.CompileAttachments parses only the files that the scope and the filter keep, so an invalid tool file does not break sync skills. It uses the symbolic-link rules of CompileWorkspace.
  • The attachment cache keeps a 404 as "absent". Tool rows share the cache with variation hydration, which reads every tool and skill that a LaunchDarkly variation uses. So the all scope reads a shared tool once.
  • The guard compares the side that a variation row will overwrite with the sync.lock row of the tool. A variation push that creates a new tool with upsert does not change, because LaunchDarkly has no copy on that variation.
  • syncdomain.ValidateCreate copies the create rules of the API. LaunchDarkly stays the authority.
  • The deletion rule reads the users of a tool from the local variations. I did not add a usedBy field to PlannedResource, because only this rule needs it.
  • Left for later: feat: create skills during sync, and read again after a 409 on create #940 adds skill names to the create rules, and feat: apply tool and skill rows in a sync #941 lets executePlan apply tool and skill rows.

Review focus

  • Is the guard right? It is the only change to what sync prompts plans.
  • In the shared cache, does hydrate still fail on a missing attachment while read returns nil?
  • Does the plan order make sure that a tool publishes before every variation that pins it?
  • Do the copied create rules match what LaunchDarkly accepts?

Verification

  • go test ./...
  • golangci-lint run (v2.9.0)
  • fingerprint_golden_test.go and every existing test pass without edits. The one exception is a plain-text reason test that needed a resource ID. TestPromptCreatesMissingToolWhenUpsertIsEnabled still passes.
  • TestCheckAttachmentBaselinesStopsAVariationFromOverwritingAnUnsyncedEdit covers the guard with a tracked edit, an untracked conflict, and a push that adds a tool.
  • TestPromptPlansToolAndSkillRowsByScope runs a dry run in each scope against an API fake that returns 404 for absent tools and skills. TestPromptFiltersToolRowsBeforeAnyRead makes sure that a tool filter reads only its tool.
  • I reproduced the shared-tool case on main with a built binary. With this PR, the plan shows the conflict. --conflict local then publishes the tool once, as version 2, and both variations pin version 2.

Related changes

  1. feat: show a compact sync plan and add a --diff flag #937: show a compact sync plan and add a --diff flag
  2. feat: add target flags to sync prompts and read sync flags only from the command line #938: add target flags, and read sync flags only from the command line
  3. This PR: plan tool and skill rows by sync scope
  4. feat: create skills during sync, and read again after a 409 on create #940: create skills during sync, and read again after a 409 on create
  5. feat: apply tool and skill rows in a sync #941: apply tool and skill rows in a sync

A sync can now plan tools and skills as rows of their own. A scope
selects the kinds to plan: prompts, tools, skills, or all. A tool or
skill row uses the same three-way rules, fingerprints, and sync.lock
rows as a variation, and sorts before the variations that pin it. No
command selects a scope yet, so sync prompts plans the same rows as
before, with one fix: a variation row that would push or pull a tool
or skill past an unsynced change on the other side is now a conflict.
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