Skip to content

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

Merged
ctawiah merged 4 commits into
mainfrom
ctawiah/plan-attachment-rows-1e8a
Oct 22, 2026
Merged

ctawiah merged 4 commits into
mainfrom
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

Note

Overview
Adds scoped sync planning so tools and skills get their own plan rows (not only as embedded variation content), using the same three-way sync.lock rules as prompts.

Scope (prompts, tools, skills, all) on SyncAction limits which kinds are planned; filters validate against the scope’s key kind. CompileAttachments reads managed tool/skill files (with symlink rejection and scope-aware inclusion). planAttachments builds rows for any local file or lock entry; plans sort skill → tool → variation so apply order can publish attachments before variations pin them.

Safety / UX: checkAttachmentBaselines upgrades variation rows to conflict when they would overwrite a tool/skill edit relative to the lock (fixes shared-tool overwrite). checkAttachmentDeletes blocks deleting local tool/skill files while a variation still references them, with re-check after conflict resolution. ValidateCreate pre-validates create keys/names/descriptions. Conflict groups now tie tool/skill rows to variations that share changed attachments; deleted-on-LD tool rows stay out of groups so “use local” cannot recreate them. Filter typo errors point at scope-specific ldcli sync … add commands.

CLI wiring for non-prompt scopes is deferred; default scope keeps sync prompts behavior except for the baseline guard.

Reviewed by Cursor Bugbot for commit 0fd0962. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor
cursor Bot force-pushed the ctawiah/plan-attachment-rows-1e8a branch from 5a7070e to a899488 Compare October 22, 2026 05:24
@ctawiah
ctawiah added this pull request to stack #942 October 22, 2026 13:28
@cursor
cursor Bot force-pushed the ctawiah/plan-attachment-rows-1e8a branch from a899488 to ca26912 Compare October 22, 2026 15:07
@cursor
cursor Bot force-pushed the ctawiah/plan-attachment-rows-1e8a branch from ca26912 to e5d32e6 Compare October 22, 2026 15:15
@ctawiah
ctawiah marked this pull request as ready for review October 22, 2026 15:43
@ctawiah
ctawiah requested a review from a team as a code owner October 22, 2026 15:43
@ctawiah
ctawiah requested review from btesfamariam and nkester-ld and removed request for a team October 22, 2026 15:43
@cursor
cursor Bot force-pushed the ctawiah/plan-attachment-rows-1e8a branch from e5d32e6 to fb8b0d7 Compare October 22, 2026 15:47

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread internal/sync/prompt/state.go
Comment thread internal/sync/prompt/plan.go Outdated

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8d8114a. Configure here.

Comment thread internal/sync/prompt/state.go
Comment thread internal/sync/prompt/acceptance_test.go
Comment thread internal/sync/prompt/plan_test.go
@ctawiah
ctawiah force-pushed the ctawiah/plan-attachment-rows-1e8a branch from 0fd0962 to 87de606 Compare October 22, 2026 19:29
Base automatically changed from ctawiah/sync-target-flags-1e8a to main October 22, 2026 19:33
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.
The error for a missing skill told the user to add upsert: true to the
front matter. A skill file has no upsert key yet, so that edit made the
file fail to parse. The error now gives only the detach step for a
skill. A tool still gets the upsert step.
A delete of a tool or a skill that a local variation still uses becomes
an error. Before, only a pull of a version without the reference kept
the delete. A delete of the variation in the same plan, or a conflict
that LaunchDarkly wins, also made the delete an error, and that error
stopped the whole plan.

A variation delete now keeps the tool or skill delete. A conflict keeps
it if the LaunchDarkly version has no reference. After the conflict
choices, sync checks the deletes again. If the local version of the
variation wins, the delete becomes an error.
A conflict choice applies to every row in the group of the conflict. A
tool or skill delete joined the group of a variation conflict that uses
it. A local choice then made the delete a create, and sync made the
tool again in LaunchDarkly.

A deleted tool or skill now stays out of conflict groups. After the
choice, sync checks the delete again. A local choice makes the delete
an error, and a LaunchDarkly choice keeps the delete.
@ctawiah
ctawiah force-pushed the ctawiah/plan-attachment-rows-1e8a branch from 87de606 to 125ca05 Compare October 22, 2026 19:33
@ctawiah
ctawiah merged commit 6ebe04f into main Oct 22, 2026
9 checks passed
@ctawiah
ctawiah deleted the ctawiah/plan-attachment-rows-1e8a branch October 22, 2026 19:39
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.

2 participants